bbc / peaks.js

JavaScript UI component for interacting with audio waveforms
https://waveform.prototyping.bbc.co.uk
GNU Lesser General Public License v3.0
3.16k stars 275 forks source link

drawimage error. Is there a way to Unmount/Kill all processes when leaving peaksjs component #484

Closed sshadmand closed 1 year ago

sshadmand commented 1 year ago

Thank you for the awesome library! Everything works wondefully, but there is this one issue I haven't been able to resolve. I am using a React app, and I load the page that contains the PeaksJS audio waveform. If I leave the page for any reason BEFORE the wave form fully loads, then I get the following error. Otherwise no issue. I have tried all the suggestion here (added canvas tags with heights etc) but they don't resolve the issue https://github.com/bbc/peaks.js/issues/433

It seems like if I had a way to unmount the peaks instance or kill it completely on unmount in my React.useEffect it could be avoided.

Using the "destroy" doesn't seem to do it either.

  useEffect(() => {
    console.log('loaded')
    mounted.current = true
    return () => {
      console.log('unload')
      peaksInstanceRef.current.destroy()
      mounted.current = false
      peaksInstanceRef.current = null
    }
  }, [])
Uncaught DOMException: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The image argument is a canvas element with a width or height of 0.
at
SceneContext.drawImage(http://localhost:3001/static/js/9.chunk.js:5194:18)
at Rect.drawScene (http://localhost: 3001/static/js/9.chunk.js: 8622:17)
at http://localhost:3001/static/js/9.chunk.js: 4842:26 at Array.Torcach (canonymous?)
at Layer._drawChildren (http://localhost: 3001/static/is/9.chunk.js: 4841:68)
at Layer. drawScene (http://localhost:3001/static/js/9.chunk.js: 4783:14) at Layer. drawScene (http://localhost:3001/static/js/9.chunk.js: 6397:49)
at Layer.draw (http://localhost:3001/static/js/9.chunk.js:7933:12)
at http://localhost:3001/static/js/9.chunk.js:6325:18
at http://localhost:3001/static/is/9.chunk.js:10613:11
at Array. forEach (anonymous>)
at http://localhost:3001/static/is/9.chunk.js: 10612:15
au sentrywrapped (http://localhost:3001/static/js/8.chunk.js: 116157:17)

Screenshot 2023-04-27 at 2 55 25 PM

chrisn commented 1 year ago

Thank you. Are you using peaksInstance.setSource()?

I can see it's possible for the container element to be removed from the page or resized to zero width/height while Peaks.js is fetching the waveform or audio file, during Peaks.init() or peaks.Instance.setSource() - and we only check that the element is valid in Peaks.init(), before the waveform is fetched.

I'll think about how to fix this. I guess ideally we'd also want to abort the fetch.

sshadmand commented 1 year ago

I use Peaks.init() like so.

useEffect(() => {
    if (mounted.current === false) return console.log('No longer mounted')
    const options = {
      containers: {
        zoomview: document.getElementById('zoomview-container'),
        overview: document.getElementById('overview-container')
      },
      emitCueEvents: true,
      showPlayheadTime: true,
      overviewHighlightOffset: 0,
      fontSize: 8,
      zoomLevels: [256],
      zoomWaveformColor: 'rgba(0,0,200,0.2)', 
      height: 80,
      mediaElement: document.getElementById('audio')
    }
    if (peaks?.data?.length > 0) {
      const max = peaks.data.reduce((max, el) => (el > max ? el : max), -100)
      const denom = max > 125 ? 3 : 1
      const normalizedPeaks = peaks.data.map(el => el / denom)
      options.waveformData = {json: {...peaks, data: normalizedPeaks}}
    } else {
      const AudioContext = window.AudioContext || window.webkitAudioContext
      const audioContext = new AudioContext()
      options.webAudio = {audioContext}
    }

    Peaks.init(options, (err, peaksInstance) => {
      err && console.log(err)
      if (mounted.current === false) return console.log('No longer mounted')
      if (!peaksInstance) return
      peaksInstance.off('player.seeked', handleSeekEvent)
      peaksInstance.on('player.seeked', handleSeekEvent)
      onReady()
      setLoading(false)
      peaksInstanceRef.current = peaksInstance
    })

  }, [peaksInit]) // peaks wave form data

....

return (
<div>
      <div>
        <div id='waveform-container' >
          <div id='zoomview-container' />
          <div id='overview-container' />
        </div>
        <audio id='audio' >
          <source src={url} type='audio/mpeg' />
          <track src={subtitles} kind="captions" srcLang={languageCode} label={`${languageCode}_captions`} />
          Your browser does not support the audio element.
        </audio>
      </div>
    </div>
)