This commit is contained in:
louiscklaw
2025-02-01 01:16:09 +08:00
commit 91fab4a5d5
4178 changed files with 407527 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
node_modules
.cache
dist

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Playground</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
</head>
<body>
<div id="root"></div>
<script src="./src/index.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,31 @@
{
"name": "example",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"browserslist": [
"last 1 Chrome version"
],
"scripts": {
"start": "parcel index.html",
"build": "parcel build.html"
},
"dependencies": {
"react-app-polyfill": "^1.0.0",
"react-router-dom": "^6.11.0"
},
"alias": {
"react": "../node_modules/react",
"react-dom": "../node_modules/react-dom",
"scheduler/tracing": "../node_modules/scheduler/tracing-profiling",
"react-use-audio-player": "../src",
"typescript": "../node_modules/typescript"
},
"devDependencies": {
"@types/react": "^16.9.11",
"@types/react-dom": "^16.8.4",
"parcel": "^1.12.3",
"parcel-plugin-static-files-copy": "^2.2.1",
"sass": "^1.26.3"
}
}

View File

@@ -0,0 +1,44 @@
import React from "react"
import { BrowserRouter, Link, Route, Routes } from "react-router-dom"
import { BasicExample } from "./GlobalAudioSource/AudioPlayerState"
import { Spotifyish } from "./GlobalAudioSource/SoundLibrary"
import { AutoPlayNextSound } from "./GlobalAudioSource/AutoPlayNextSound"
import { MultipleSounds } from "./MultipleSounds"
import { GlobalAudioSource } from "./GlobalAudioSource"
import "./app.scss"
import { Streaming } from "./Streaming"
function ExampleSelect() {
return (
<div className="page">
<h3>Examples</h3>
<Link to="/globalAudio">useGlobalAudioPlayer examples</Link>
<Link to="/multipleSounds">Multiple sound sources</Link>
<Link to="/streaming">Streaming example</Link>
</div>
)
}
function App() {
return (
<div className="app">
<BrowserRouter>
<Routes>
<Route index path="/" element={<ExampleSelect />} />
<Route path="globalAudio" element={<GlobalAudioSource />}>
<Route path="state" element={<BasicExample />} />
<Route path="library" element={<Spotifyish />} />
<Route
path="playNextSound"
element={<AutoPlayNextSound />}
/>
</Route>
<Route path="multipleSounds" element={<MultipleSounds />} />
<Route path="streaming" element={<Streaming />} />
</Routes>
</BrowserRouter>
</div>
)
}
export default App

View File

@@ -0,0 +1,10 @@
.audioSeekBar {
cursor: pointer;
background-color: white;
overflow: hidden;
&__tick {
background-color: #a1d0d1;
height: 100%;
}
}

View File

@@ -0,0 +1,67 @@
import React, {
useCallback,
useEffect,
useRef,
useState,
FunctionComponent,
MouseEvent
} from "react"
import { useGlobalAudioPlayer } from "react-use-audio-player"
import "./AudioSeekBar.scss"
interface AudioSeekBarProps {
className?: string
}
export const AudioSeekBar: FunctionComponent<AudioSeekBarProps> = props => {
const { className = "" } = props
const { playing, getPosition, duration, seek } = useGlobalAudioPlayer()
const [pos, setPos] = useState(0)
const frameRef = useRef<number>()
const seekBarElem = useRef<HTMLDivElement>(null)
useEffect(() => {
const animate = () => {
setPos(getPosition())
frameRef.current = requestAnimationFrame(animate)
}
frameRef.current = window.requestAnimationFrame(animate)
return () => {
if (frameRef.current) {
cancelAnimationFrame(frameRef.current)
}
}
}, [])
const goTo = useCallback(
(event: MouseEvent) => {
const { pageX: eventOffsetX } = event
if (seekBarElem.current) {
const elementOffsetX = seekBarElem.current.offsetLeft
const elementWidth = seekBarElem.current.clientWidth
const percent = (eventOffsetX - elementOffsetX) / elementWidth
seek(percent * duration)
}
},
[duration, playing, seek]
)
if (duration === Infinity) return null
return (
<div
className={`audioSeekBar ${className} `}
ref={seekBarElem}
onClick={goTo}
>
<div
style={{ width: `${(pos / duration) * 100}%` }}
className="audioSeekBar__tick"
/>
</div>
)
}

View File

@@ -0,0 +1,10 @@
import React, { FunctionComponent } from "react"
import { Link } from "react-router-dom"
interface BackToHomeProps {
className?: string
}
export const BackToHome: FunctionComponent<BackToHomeProps> = (props) => {
return <Link className={ props.className || "" } to="/">{"< -"} Example Select</Link>
}

View File

@@ -0,0 +1,26 @@
import React, { useEffect, useState, FunctionComponent } from "react"
import { useGlobalAudioPlayer } from "react-use-audio-player"
export const AudioControls: FunctionComponent<{}> = () => {
const {
play,
pause,
stop,
mute,
muted,
playing,
loop,
looping
} = useGlobalAudioPlayer()
return (
<div>
<button onClick={() => (playing ? pause() : play())}>
{playing ? "pause" : "play"}
</button>
<button onClick={() => stop()}>stop</button>
<button onClick={() => mute(!muted)}>toggle mute</button>
<button onClick={() => loop(!looping)}>toggle loop</button>
</div>
)
}

View File

@@ -0,0 +1,49 @@
import React, {
useState,
FunctionComponent,
ChangeEvent,
useEffect
} from "react"
import { AudioLoadOptions, useGlobalAudioPlayer } from "react-use-audio-player"
export const FileLoader: FunctionComponent = () => {
const [audioFile, setAudioFile] = useState("/audio.mp3")
const { load, isReady, error } = useGlobalAudioPlayer()
useEffect(() => {
const loadOptions: AudioLoadOptions = { initialVolume: 0.5 }
if (audioFile.includes("stream")) {
loadOptions.html5 = true
loadOptions.format = "mp3"
}
load(audioFile, loadOptions)
}, [audioFile, load])
const selectAudioFile = (e: ChangeEvent<HTMLSelectElement>) => {
const { value } = e.target
if (value) {
setAudioFile(value)
}
}
return (
<div>
<h5>select audio file:</h5>
<select onChange={selectAudioFile} value={audioFile}>
<option value="/audio.mp3">audio</option>
<option value="/cats.mp3">cats</option>
<option value="/dog.mp3">dog</option>
<option value="/ch_tunes - baby_seal.wav">
ch_tunes - baby_seal
</option>
<option value="/doesntExist.mp3">does not exist</option>
<option value="https://stream.toohotradio.net/128">
streaming internet radio
</option>
</select>
{!isReady && !error && <p>Fetching audio file...</p>}
{error && <p className="errorMessage">Failed to load</p>}
</div>
)
}

View File

@@ -0,0 +1,53 @@
import React, { FunctionComponent } from "react"
import { useGlobalAudioPlayer } from "react-use-audio-player"
import { FileLoader } from "./FileLoader"
import { AudioControls } from "./AudioControls"
import { AudioSeekBar } from "../../AudioSeekBar"
import { TimeLabel } from "../../TimeLabel"
import "./styles.scss"
const Player = () => {
const state = useGlobalAudioPlayer()
if (state.isReady) {
return (
<div className="player">
<AudioControls />
<AudioSeekBar className="player__seek" />
<TimeLabel />
<br />
<p>
Below are all the fields available on the result of
useGlobalAudioPlayer/useAudioPlayer which can be used to
help build user interfaces for your sounds
</p>
<h3>Audio state:</h3>
<table>
<tbody>
{Object.entries(state).map(([k, v]) => {
if (typeof v === "function") return null
return (
<tr key={k}>
<td>{k}</td>
<td>{v?.toString() ?? "--"}</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}
return null
}
export const BasicExample: FunctionComponent = () => {
return (
<div className="basicExample">
<FileLoader />
<br />
<Player />
</div>
)
}

View File

@@ -0,0 +1,20 @@
.basicExample {
padding: 20px;
}
.errorMessage {
color: red;
font-weight: bold;
}
.player {
&__seek {
width: 400px;
height: 15px;
}
td {
border: solid 1px white;
padding: 5px;
}
}

View File

@@ -0,0 +1,74 @@
import React, { useEffect, useState } from "react"
import { useGlobalAudioPlayer } from "react-use-audio-player"
import { TimeLabel } from "../../TimeLabel"
import "./styles"
import { Howl } from "howler"
const songs = [
"/ch_tunes - jam_10.wav",
"/ch_tunes - baby_seal.wav",
"/ch_tunes - jam_4.wav"
]
export function AutoPlayNextSound() {
const [songIndex, setSongIndex] = useState(0)
const {
togglePlayPause,
isReady,
load,
seek,
duration,
playing
} = useGlobalAudioPlayer()
useEffect(() => {
load(songs[songIndex], {
autoplay: true,
onend: () => {
setSongIndex(index => {
if (index === songs.length - 1) {
return 0
}
return index + 1
})
}
})
}, [load, songIndex])
if (!isReady) return <h1>audio {songs[songIndex]} is loading</h1>
return (
<div className="page autoPlayNextSound">
<div className="autoPlayNextSound__player">
<div className="trackList">
<div className="trackList__title">Now playing...</div>
{songs.map((s, i) => (
<div
key={s}
className={`trackList__song${
i === songIndex ? "--selected" : ""
}`}
>
{s}
</div>
))}
</div>
<div className="autoPlayNextSound__controls">
<button onClick={togglePlayPause}>
{playing ? "pause" : "play"}
</button>
<button onClick={() => seek(duration * 0.99)}>
skip to end
</button>
</div>
<div>
<TimeLabel />
</div>
</div>
</div>
)
}
export default AutoPlayNextSound

View File

@@ -0,0 +1,44 @@
.autoPlayNextSound {
display: flex;
flex-direction: column;
padding: 20px;
}
.autoPlayNextSound a {
margin-bottom: 25px;
}
.autoPlayNextSound__player {
display: flex;
flex-direction: column;
align-items: flex-start;
}
button {
padding: 8px;
margin-right: 4px;
}
.trackList {
border: 1px solid lightblue;
border-radius: 8px;
display: flex;
flex-direction: column;
padding: 8px;
margin-bottom: 16px;
}
.trackList__title {
margin-bottom: 4px;
}
.trackList__song--selected {
font-weight: bolder;
color: lightcyan;
text-decoration: underline;
}
.autoPlayNextSound__controls {
display: flex;
margin-bottom: 16px;
}

View File

@@ -0,0 +1,52 @@
import React from "react"
import { useGlobalAudioPlayer } from "react-use-audio-player"
import { AudioSeekBar } from "../AudioSeekBar"
import { TimeLabel } from "../TimeLabel"
import { VolumeControl } from "../VolumeControl"
export const PlayBar = () => {
const {
togglePlayPause,
playing,
isReady,
setRate,
rate,
src
} = useGlobalAudioPlayer()
return (
<div className="playBar">
<div className="playBar__track">Track: {src}</div>
<div className="playBar__mainControls">
<button
className="playBar__playButton"
onClick={togglePlayPause}
disabled={!isReady}
>
<i className={`fa ${playing ? "fa-pause" : "fa-play"}`} />
</button>
<div className="playBar__timeStuff">
<AudioSeekBar className="playBar__seek" />
<TimeLabel />
</div>
</div>
<div>
<div className="playBar__rateControl">
<span>Playback Speed:</span>
<select
className="playBar__rateSelect"
name="rateSelect"
id="rate"
value={rate}
onChange={e => setRate(Number(e.target.value))}
>
<option value="0.5">1/2x</option>
<option value="1">1x</option>
<option value="2">2x</option>
</select>
</div>
<VolumeControl />
</div>
</div>
)
}

View File

@@ -0,0 +1,43 @@
import React from "react"
import { useGlobalAudioPlayer } from "react-use-audio-player"
import "./styles.scss"
export const Spotifyish = () => {
const sounds = [
"/audio.mp3",
"/cats.mp3",
"/dog.mp3",
"/ch_tunes - baby_seal.wav",
"/ch_tunes - jam_2.wav",
"/ch_tunes - jam_4.wav",
"/ch_tunes - jam_8.wav",
"/ch_tunes - jam_10.wav"
]
const { load, src: loadedSrc } = useGlobalAudioPlayer()
return (
<div className="soundLibrary page">
<div className="page__title">Sound Library</div>
<p>
This page lists the full set of sounds available in the demos.
</p>
<div className="soundLibrary__sounds">
{sounds.map((src, i) => {
return (
<div
key={i}
className={`track ${
src === loadedSrc ? "track--playing" : ""
}`}
onClick={() => load(src, { autoplay: true })}
>
<i className="fa fa-music track__icon" />
<div className="track__title">
{src.slice(1, src.indexOf("."))}
</div>
</div>
)
})}
</div>
</div>
)
}

View File

@@ -0,0 +1,36 @@
.soundLibrary {
&__sounds {
display: flex;
flex-direction: column;
}
}
.track {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 10px 10px;
margin-bottom:25px;
border: solid 1px white;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: white;
color: black;
}
&--playing {
background-color: white;
color: black;
}
&__icon {
font-size: medium;
margin-right: 15px;
}
&__title {
font-size: medium;
}
}

View File

@@ -0,0 +1,42 @@
import React from "react"
import { Link, Outlet, useMatch } from "react-router-dom"
import "./styles.scss"
import { PlayBar } from "./PlayBar"
const ExampleSelect = () => {
return (
<>
<Link to="..">go back</Link>
<p>
The following examples leverage useGlobalAudioPlayer to control
a single, global audio source. At any time, this audio can be
managed by the playbar at the bottom of the page.
</p>
<h4>Examples</h4>
<Link to="state">Audio Player State</Link>
<Link to="library">Full Sound Library</Link>
<Link to="playNextSound">Auto Play Next Example</Link>
</>
)
}
const ExitExample = () => {
return (
<Link to=".." relative="path">
Exit Example
</Link>
)
}
export const GlobalAudioSource = () => {
const matches = useMatch("/globalAudio")
return (
<div className="page globalAudioSourceExamples">
<div className="globalAudioSourceExamples__header">
{matches !== null ? <ExampleSelect /> : <ExitExample />}
</div>
<Outlet />
<PlayBar />
</div>
)
}

View File

@@ -0,0 +1,70 @@
.globalAudioSourceExamples {
}
.globalAudioSourceExamples__header {
}
.playBar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background-color: #1c192f;
display: flex;
align-items: flex-start;
padding: 25px 50px;
&__track {
align-self: flex-start;
max-width: 150px;
}
&__mainControls {
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
&__playButton {
width: 30px;
height: 30px;
border-radius: 50%;
padding-top: 5px;
cursor: pointer;
outline: none;
margin-bottom: 10px;
}
&__timeStuff {
display: flex;
flex-direction: column;
align-items: center;
}
&__seek {
width: 400px;
height: 10px;
border-radius: 12px;
margin-right: 10px;
}
&__rateControl {
display: flex;
align-items: center;
margin-bottom: 5px;
}
&__rateControl span {
padding-right: 10px;
}
&__rateSelect {
width: 100px;
}
}

View File

@@ -0,0 +1,75 @@
import React, { useState, useCallback, useEffect } from "react"
import { Link } from "react-router-dom"
import { useAudioPlayer } from "react-use-audio-player"
import "./styles.scss"
export const MultipleSounds = () => {
const [xFadeValue, setXFadeValue] = useState(50)
const song1 = useAudioPlayer()
const song2 = useAudioPlayer()
useEffect(() => {
song1.load("/ch_tunes - jam_4.wav", { autoplay: false })
song2.load("/ch_tunes - baby_seal.wav", { autoplay: false })
}, [])
const togglePlayingBoth = useCallback(() => {
song1.togglePlayPause()
song2.togglePlayPause()
}, [song1.togglePlayPause, song2.togglePlayPause])
const handleFade = useCallback(e => {
setXFadeValue(e.target.value)
const ratio = e.target.value / 100
song1.setVolume(1 - ratio)
song2.setVolume(ratio)
}, [])
return (
<div className="page multipleSounds">
<Link
to=".."
onClick={() => {
song1.cleanup()
song2.cleanup()
}}
>
go back
</Link>
<div className="multipleSounds__tracks">
<div className="multipleSounds__track">
<p>{song1.src}</p>
<div className="hstack">
<label htmlFor="song1vol">volume</label>
<input
name="song1vol"
type="range"
readOnly
value={Number(song1.volume.toFixed(2)) * 100}
/>
</div>
</div>
<div className="multipleSounds__track">
<p>{song2.src}</p>
<div className="hstack">
<label htmlFor="song2vol">volume</label>
<input
name="song2vol"
type="range"
readOnly
value={Number(song2.volume.toFixed(2)) * 100}
/>
</div>
</div>
</div>
<div className="multipleSounds__controls">
<button onClick={togglePlayingBoth}>
{song1.playing ? "Pause" : "Play"}
</button>
<input type="range" value={xFadeValue} onChange={handleFade} />
<p>Adjust the slider to fade between the two songs</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,29 @@
.multipleSounds {
&__tracks {
display: flex;
justify-content: space-between;
}
&__track {
}
&__controls {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10px;
margin-top: 50px;
input[type="range"] {
margin: 10px 0;
width: 500px;
}
p {
margin: 0;
}
}
}

View File

@@ -0,0 +1,39 @@
import React, { useEffect } from "react"
import { useAudioPlayer } from "react-use-audio-player"
import "./styles.scss"
export function Streaming() {
const player = useAudioPlayer()
useEffect(() => {
player.load("http://mp3-128.streamthejazzgroove.com", {
html5: true,
format: "mp3"
})
}, [])
return (
<div>
<div>
<table>
<tbody>
{Object.entries(player).map(([k, v]) => {
if (typeof v === "function") return null
return (
<tr key={k}>
<td>{k}</td>
<td>{v?.toString() ?? "--"}</td>
</tr>
)
})}
</tbody>
</table>
</div>
<div id="stateupdate"></div>
<button onClick={() => player.togglePlayPause()}>
{player.playing ? "pause" : "play"}
</button>
</div>
)
}

View File

@@ -0,0 +1,32 @@
td {
border: solid 1px white;
padding: 5px;
}
.wave {
width: 100%;
height: 200px;
background-color: #ccc;
position: relative;
overflow: hidden;
}
.wave::before {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #fff;
animation: waveAnimation 4s linear infinite;
}
@keyframes waveAnimation {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-100%);
}
}

View File

@@ -0,0 +1,33 @@
import React, { useEffect, useState } from "react"
import { useGlobalAudioPlayer } from "react-use-audio-player"
const formatTime = (seconds: number) => {
if (seconds === Infinity) {
return "--"
}
const floored = Math.floor(seconds)
let from = 14
let length = 5
// Display hours only if necessary.
if (floored >= 3600) {
from = 11
length = 8
}
return new Date(floored * 1000).toISOString().substr(from, length)
}
export const TimeLabel = () => {
const [pos, setPos] = useState(0)
const { duration, getPosition, playing } = useGlobalAudioPlayer()
useEffect(() => {
const i = setInterval(() => {
setPos(getPosition())
}, 500)
return () => clearInterval(i)
}, [getPosition])
return <div>{`${formatTime(pos)} / ${formatTime(duration)}`}</div>
}

View File

@@ -0,0 +1,9 @@
.volumeControl {
display: flex;
align-items: center;
&__icon {
color: white;
margin-left: 5px;
}
}

View File

@@ -0,0 +1,30 @@
import React, { ChangeEvent, useCallback } from "react"
import { useGlobalAudioPlayer } from "react-use-audio-player"
import "./VolumeControl.scss"
export const VolumeControl = () => {
const { setVolume, volume } = useGlobalAudioPlayer()
const handleChange = useCallback(
(slider: ChangeEvent<HTMLInputElement>) => {
const volValue = parseFloat(
(Number(slider.target.value) / 100).toFixed(2)
)
return setVolume(volValue)
},
[setVolume]
)
return (
<div className="volumeControl">
<input
type="range"
min={0}
max={100}
onChange={handleChange}
value={volume * 100}
/>
<i className="fa fa-volume-up volumeControl__icon" />
</div>
)
}

View File

@@ -0,0 +1,77 @@
* {
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
background-color: #272a51;
color: #e1fdff;
font-weight: 400;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
display: block;
color: #79b7c0;
}
a:hover {
color: #5593aa;
}
h4, h5, h6 {
margin-bottom: 0;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
#root {
width: 100%;
height: 100%;
}
.app {
width: 100%;
height: 100%;
}
.page {
width: 100%;
height: calc(100% - 125px);
padding: 25px;
&__title {
font-size: xx-large;
font-weight: 200;
margin-bottom: 50px;
}
}
.hstack {
display:flex;
align-items: center;
}
/*
#e1fdff
#e5f9f9
#e9f3f3
#ddeaea
#c4dfde
#a1d0d1
#79b7c0
#5593aa
#3a6891
#324672
#272a51
#1c192f
*/

View File

@@ -0,0 +1,19 @@
import React from "react"
import ReactDOM from "react-dom/client"
import "./app.scss"
import App from "./App"
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)
// ReactDOM.render(
// <React.StrictMode>
// <App />
// </React.StrictMode>,
// document.getElementById("root")
// )

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"target": "es5",
"module": "commonjs",
"jsx": "react",
"moduleResolution": "node",
"noImplicitAny": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"removeComments": true,
"strictNullChecks": true,
"preserveConstEnums": true,
"sourceMap": true,
"lib": ["es2021", "dom"],
"baseUrl": ".",
"types": ["node"],
"paths": {
"react-use-audio-player": ["../src"]
}
}
}

File diff suppressed because it is too large Load Diff