Skip to content

Commit

Permalink
Fix: keep external media on destroy (katspaugh#3233)
Browse files Browse the repository at this point in the history
* Fix: keep external media on destroy

* Lint

* Prettier
  • Loading branch information
katspaugh committed Oct 4, 2023
1 parent bac048c commit 421a0b6
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 25 deletions.
22 changes: 11 additions & 11 deletions cypress/e2e/basic.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,26 +148,26 @@ describe('WaveSurfer basic tests', () => {
})
expect(peaks.length).to.equal(1) // the file is mono
expect(peaks[0].length).to.equal(1000)
expect(peaks[0][0]).to.equal(0)
expect(peaks[0][99]).to.equal(0.07)
expect(peaks[0][100]).to.equal(-0.15)
expect(peaks[0][0]).to.equal(0.01)
expect(peaks[0][99]).to.equal(0.3)
expect(peaks[0][100]).to.equal(0.31)

const peaksB = win.wavesurfer.exportPeaks({
maxLength: 1000,
precision: 1000,
})
expect(peaksB.length).to.equal(1)
expect(peaksB[0].length).to.equal(1000)
expect(peaksB[0][0]).to.equal(0)
expect(peaksB[0][99]).to.equal(0.072)
expect(peaksB[0][100]).to.equal(-0.151)
expect(peaksB[0][0]).to.equal(0.015)
expect(peaksB[0][99]).to.equal(0.296)
expect(peaksB[0][100]).to.equal(0.308)

const peaksC = win.wavesurfer.exportPeaks()
expect(peaksC.length).to.equal(1)
expect(peaksC[0].length).to.equal(8000)
expect(peaksC[0][0]).to.equal(0)
expect(peaksC[0][99]).to.equal(-0.0024)
expect(peaksC[0][100]).to.equal(0.0048)
expect(peaksC[0][0]).to.equal(0.0117)
expect(peaksC[0][99]).to.equal(0.01)
expect(peaksC[0][100]).to.equal(0.0161)
})
})

Expand All @@ -180,9 +180,9 @@ describe('WaveSurfer basic tests', () => {
it('should set media without errors', () => {
cy.window().then((win) => {
const media = document.createElement('audio')
media.id = "new-media"
media.id = 'new-media'
win.wavesurfer.setMediaElement(media)
expect(win.wavesurfer.getMediaElement().id).to.equal("new-media")
expect(win.wavesurfer.getMediaElement().id).to.equal('new-media')
})
})
})
143 changes: 143 additions & 0 deletions examples/react-global-player.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// React example

/*
<html>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</html>
*/

// Import React hooks
const { useRef, useState, useEffect, useCallback, memo } = React

// Import WaveSurfer
import WaveSurfer from 'https://unpkg.com/wavesurfer.js@7/dist/wavesurfer.esm.js'

// WaveSurfer hook
const useWavesurfer = (containerRef, options) => {
const [wavesurfer, setWavesurfer] = useState(null)

// Initialize wavesurfer when the container mounts
// or any of the props change
useEffect(() => {
if (!containerRef.current) return

const ws = WaveSurfer.create({
...options,
container: containerRef.current,
})

setWavesurfer(ws)

return () => {
ws.destroy()
}
}, [options, containerRef])

return wavesurfer
}

// Create a React component that will render wavesurfer.
// Props are wavesurfer options.
const WaveSurferPlayer = memo((props) => {
const containerRef = useRef()
const [isPlaying, setIsPlaying] = useState(false)
const wavesurfer = useWavesurfer(containerRef, props)
const { onPlay, onReady } = props

// On play button click
const onPlayClick = useCallback(() => {
wavesurfer.playPause()
}, [wavesurfer])

// Initialize wavesurfer when the container mounts
// or any of the props change
useEffect(() => {
if (!wavesurfer) return

const getPlayerParams = () => ({
media: wavesurfer.getMediaElement(),
peaks: wavesurfer.exportPeaks(),
})

const subscriptions = [
wavesurfer.on('ready', () => {
onReady && onReady(getPlayerParams())

setIsPlaying(wavesurfer.isPlaying())
}),
wavesurfer.on('play', () => {
onPlay &&
onPlay((prev) => {
const newParams = getPlayerParams()
if (!prev || prev.media !== newParams.media) {
if (prev) {
prev.media.pause()
prev.media.currentTime = 0
}
return newParams
}
return prev
})

setIsPlaying(true)
}),
wavesurfer.on('pause', () => setIsPlaying(false)),
]

return () => {
subscriptions.forEach((unsub) => unsub())
}
}, [wavesurfer, onPlay, onReady])

return (
<div style={{ display: 'flex', gap: '1em', marginBottom: '1em' }}>
<button onClick={onPlayClick}>{isPlaying ? '⏸️' : '▶️'}</button>

<div ref={containerRef} style={{ minWidth: '200px' }} />
</div>
)
})

const Playlist = memo(({ urls, setCurrentPlayer }) => {
return urls.map((url, index) => (
<WaveSurferPlayer
key={url}
height={100}
waveColor="rgb(200, 0, 200)"
progressColor="rgb(100, 0, 100)"
url={url}
onPlay={setCurrentPlayer}
onReady={index === 0 ? setCurrentPlayer : undefined}
/>
))
})

const audioUrls = ['/examples/audio/audio.wav', '/examples/audio/demo.wav', '/examples/audio/stereo.mp3']

const App = () => {
const [currentPlayer, setCurrentPlayer] = useState()

return (
<>
<p>Playlist</p>
<Playlist urls={audioUrls} setCurrentPlayer={setCurrentPlayer} />

<p>Global player</p>
{currentPlayer && (
<WaveSurferPlayer
height={50}
waveColor="blue"
progressColor="purple"
media={currentPlayer.media}
peaks={currentPlayer.peaks}
/>
)}
</>
)
}

// Create a React root and render the app
const root = ReactDOM.createRoot(document.body)
root.render(<App />)
7 changes: 6 additions & 1 deletion src/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ type PlayerOptions = {

class Player<T extends GeneralEventTypes> extends EventEmitter<T> {
protected media: HTMLMediaElement
private isExternalMedia = false

constructor(options: PlayerOptions) {
super()

if (options.media) {
this.media = options.media
this.isExternalMedia = true
} else {
this.media = document.createElement('audio')
}
Expand Down Expand Up @@ -50,7 +52,7 @@ class Player<T extends GeneralEventTypes> extends EventEmitter<T> {
return this.onMediaEvent(event, callback, { once: true })
}

private getSrc() {
protected getSrc() {
return this.media.currentSrc || this.media.src || ''
}

Expand All @@ -72,6 +74,9 @@ class Player<T extends GeneralEventTypes> extends EventEmitter<T> {

protected destroy() {
this.media.pause()

if (this.isExternalMedia) return
this.media.remove()
this.revokeSrc()
this.media.src = ''
// Load resets the media element to its initial state
Expand Down
25 changes: 12 additions & 13 deletions src/wavesurfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ class WaveSurfer extends Player<WaveSurferEvents> {
private async loadAudio(url: string, blob?: Blob, channelData?: WaveSurferOptions['peaks'], duration?: number) {
this.emit('load', url)

if (this.isPlaying()) this.pause()
if (!this.options.media && this.isPlaying()) this.pause()

this.decodedData = null

Expand Down Expand Up @@ -379,23 +379,22 @@ class WaveSurfer extends Player<WaveSurferEvents> {
}

/** Get decoded peaks */
public exportPeaks({ channels = 1, maxLength = 8000, precision = 10_000 } = {}): Array<number[]> {
public exportPeaks({ channels = 2, maxLength = 8000, precision = 10_000 } = {}): Array<number[]> {
if (!this.decodedData) {
throw new Error('The audio has not been decoded yet')
}
const channelsLen = Math.min(channels, this.decodedData.numberOfChannels)
const maxChannels = Math.min(channels, this.decodedData.numberOfChannels)
const peaks = []
for (let i = 0; i < channelsLen; i++) {
const data = this.decodedData.getChannelData(i)
const length = Math.min(data.length, maxLength)
const scale = data.length / length
const sampledData = []
for (let j = 0; j < length; j++) {
const n = Math.round(j * scale)
const val = data[n]
sampledData.push(Math.round(val * precision) / precision)
for (let i = 0; i < maxChannels; i++) {
const channel = this.decodedData.getChannelData(i)
const data = []
const sampleSize = Math.round(channel.length / maxLength)
for (let i = 0; i < maxLength; i++) {
const sample = channel.slice(i * sampleSize, (i + 1) * sampleSize)
const max = Math.max(...sample)
data.push(Math.round(max * precision) / precision)
}
peaks.push(sampledData)
peaks.push(data)
}
return peaks
}
Expand Down

0 comments on commit 421a0b6

Please sign in to comment.