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 277 forks source link

Uncaught DOMException: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The image argument is a canvas element with a width or height of 0 #433

Closed prodbyola closed 2 years ago

prodbyola commented 2 years ago

I'm trying to use peak.js in my Vue 3 project and I followed the documentation to load waveform data from the server. I can confirm the data was loaded successfully as I can see the "peaks" logged in the Peaks.init() function. However, the waveform is not displayed. Instead, I get the error stated above.

jamesb93 commented 2 years ago

Try setting a fixed width and height on your image component? Either in js or css.

<canvas id='sketch' />
style {
    #sketch {
        width: 400px;
        height: 400px;
    }
}

}

prodbyola commented 2 years ago

Try setting a fixed width and height on your image component? Either in js or css.

<canvas id='sketch' />
style {
    #sketch {
        width: 400px;
        height: 400px;
    }
}

}

@jamesb93 thanks for the reply. I'm working with the example code on the homepage of this repo. HTML like


<div id="overview-container"></div>
<audio>
  <source src="sample.mp3" type="audio/mpeg">
  <source src="sample.ogg" type="audio/ogg">
</audio> ```

and in JS:
 ```js
import Peaks from 'peaks.js';

const options = {
  zoomview: {
    container: document.getElementById('zoomview-container')
  },
  overview: {
    container: document.getElementById('overview-container')
  },
  mediaElement: document.querySelector('audio'),
  dataUri: {
    arraybuffer: 'sample.dat' // or json: 'sample.json'
  }
};

Peaks.init(options, function(err, peaks) {
  console.log(err, peaks) // 
}); ```

So are you suggesting I manually add a canvas within the ***zoomview*** and ***overview*** containers? I just wanted to be clear about your response. Thank you!
chrisn commented 2 years ago

The library includes a check that the container element width is non-zero, so I'm not sure why you're getting this error. Can you share a complete minimal example to help figure out what's happening?

So are you suggesting I manually add a canvas within the zoomview and overview containers?

There's no need to create your own canvas, Peaks.js will create those itself within your container div.

jamesb93 commented 2 years ago

This is an example of mine (albeit in SvelteKit). It's a bit busy but it shows you the necessary js and HTML to get to something that works in a component.

<script>
    import { onMount } from "svelte";
    import Container from '$lib/components/Container.svelte';
    import Button from '$lib/components/Button.svelte';
    import { noext, cloudPrefix } from '$lib/utility/paths.js';

    export let segments;
    export let points;
    export let title = "title";
    export let caption = "";
    export let file;
    export let peaks;
    export let id = "";

    const noExtension = noext(file);
    const lossless = cloudPrefix + noExtension + '.wav'

    // Form path to lossless download
    let Peaks, instance, overview, zoom, audio, controls, peaksControls;
    let lastSelected = 0;

    const convert = (time) => {
        const date = new Date(time * 1000).toISOString().substr(11, 8)
        return date.toString().substr(3);
    }

    onMount (async()=>{
        const module = await import("peaks.js");
        Peaks = module.default;
        const options = {
            containers: {
                zoomview: zoom,
                overview: overview
            },
            dataUri: { 
                arraybuffer: peaks 
            },
            mediaElement: audio,
            zoomWaveformColor: 'rgba(0, 30, 128, 0.61)',
            overviewWaveformColor: 'rgba(0, 15, 100, 0.3)',
            overviewHighlightColor: 'grey',
            playheadColor: 'rgba(0, 0, 0, 1)',
            playheadTextColor: '#aaa',
            showPlayheadTime: false,
            pointMarkerColor: '#FF0000',
            axisGridlineColor: '#ccc',
            axisLabelColor: '#aaa',
            randomizeSegmentColor: true,
            segments: segments,
            points: points
        }
        instance = Peaks.init(options, (err, p) => {
            if (err) {
                console.log(err)
            } else {
                instance = p
                instance.views.getView('overview').fitToContainer();
            }
        });
    });

    function clickHandler(segment, i) {
        instance.player.seek(segment.startTime)
        lastSelected = i
    }
</script>

<Container id={id}>
    <div class="horizontal">
        <span id="title">{title}</span>
        <span id="caption">{caption}</span>
    </div>

    <div class="vis">
        <div id='waveform-overview' bind:this={overview} />
        <div id='waveform-zoom' bind:this={zoom} />
    </div>

    <div class="peaks-controls" bind:this={peaksControls}>
        <audio controls bind:this={audio}>
            <source src={file} type="audio/mp3">
            <track kind="captions">
        </audio>
        <div bind:this={controls} class="audio-controls">
            <Button clickHandler={ () => instance.zoom.zoomIn() } text="+" />
            <Button clickHandler={ () => instance.zoom.zoomOut() } text="-" />
        </div>
    </div>
    {#if segments}
    <ul class="segments">
        <span id="timecodes">List of referenced time codes</span>
        {#each segments as segment, i}
            <div class:selected={ lastSelected === i } class='timecode' on:click={ () => clickHandler(segment, i) }>
                <span id='time'>{convert(segment.startTime)} - {convert(segment.endTime)}</span>
                <span id="label">{segment.labelText}</span>
            </div>
        {/each}
    </ul>
    {/if}

    <div id='lossless'>
        <a rel='external' id='lossless-link' href={lossless} download>
            Download Lossless Version
        </a>
    </div>
</Container>

<style>

    #waveform-overview {
        height: 65px;
    }

    #lossless {
        margin-top: 10px;
    }
    #lossless-link {
        font-style: italic;
        text-align: left;
        color: rgb(96, 96, 96);
    }

    #lossless-link:hover {
        background-color: var(--dark-blue);
        color: white;
    }

    .audio-controls {
        display: flex;
        flex-direction: row;
    }

    #title {
        text-align: left;
        font-weight: bold;
    }

    .timecode {
        display: flex;
        flex-direction: row;
        justify-content: space-between;
        word-wrap: none;
        border-top: 1px rgb(197, 197, 197) solid;
        gap: 3%;
    }

    .timecode:hover {
        background-color: rgb(240, 240, 240);
    }

    .timecode:active {
        background-color: rgb(199, 199, 199);
    }

    #time {
        white-space: nowrap;
    }

    #label {
        color: grey;
        text-align: right;
    }

    #caption{
        font-style: italic;
        min-width: max-content;
    }

    .horizontal {
        display: flex;
        flex-direction: row;
        justify-content: space-between;
        gap: 10%;
        padding-bottom: 15px;
    }

    .vis {
        padding-bottom: 5px;
        display: flex;
        flex-direction: column;
        gap: 5px;
    }

    .peaks-controls {
        display: flex;
        justify-content: space-between;
        padding-top: 3px;
        align-items: center;
    }

    a { 
        color: black
    }

    a:hover {
        background-color: inherit;
        text-decoration: none;
    }

    .selected {
        font-weight: bold;
    }

    .segments {
        padding-top: 5px;
        margin-top: 1em;
        padding-left: 1em;
        padding-right: 1em;
        border-top: 1px rgb(197, 197, 197) solid;
    }
</style>
prodbyola commented 2 years ago

@jamesb93 and @chrisn thanks for the help. It actually did work after I created the canvas manually!

chrisn commented 2 years ago

Thanks, I'll close this. If anyone else finds this error, please re-open and we can investigate.

ChrisMorrisDev commented 1 year ago

I just ran into this issue. Trying to render a simple example resulted in the error. Adding <canvas> manually to the zoomview-container and overview-container fixed the issue.

Testing in Chrome Version 104.0.5112.101.

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Peaks.js Demo Page</title>
    <link rel="stylesheet" type="text/css" href="style.css">
</head>

<body>
    <h1>Test Audio</h1>
    <div id="zoomview-container"><canvas></canvas></div>
    <div id="overview-container"><canvas></canvas></div>
    <audio>
        <source src="sample.mp3" type="audio/mpeg">
        <source src="sample.ogg" type='audio/ogg codecs="vorbis"'>
    </audio>

</body>
<script src="index.js" type="module"></script>
</html>

index.js

import Peaks from 'peaks.js';

const audioContext = new AudioContext();

const options = {
  zoomview: {
    container: document.getElementById('zoomview-container')
  },
  overview: {
    container: document.getElementById('overview-container')
  },
  mediaElement: document.querySelector('audio'),
  webAudio: {
    audioContext: audioContext,
    scale: 128,
    multiChannel: true
  }
};

Peaks.init(options, function(err, peaks) {
  // Do something when the waveform is displayed and ready
  console.log("Ready to create!")
});

package.json

{
  "name": "test-audio",
  "version": "1.0.0",
  "description": "Test Waveform App",
  "source": "src/index.html",
  "browserslist": "> 0.5%, last 2 versions, not dead",
  "scripts": {
    "start": "parcel src/index.html -p 3000 --open",
    "build:parcel": "parcel build ./src/index.html --public-url ./ --no-source-maps",
    "build": "npm run clean && npm run build:parcel",
    "clean": "rm -rf dist/*"
  },
  "keywords": [
    "audio",
    "webaudio"
  ],
  "author": "Chris Morris",
  "license": "MIT",
  "devDependencies": {
    "parcel": "^2.7.0"
  },
  "dependencies": {
    "konva": "^8.3.11",
    "peaks.js": "^2.0.5",
    "waveform-data": "^4.3.0"
  }
}
chrisn commented 1 year ago

Thanks @ChrisMorrisDev, your example works fine for me (with an empty style.css file), using Chrome 104.0.5112.102. You shouldn't need to create a canvas element. Do your container divs have non-zero width and height at the time you call Peaks.init()?

chrisn commented 1 year ago

Ignore the above, removing the canvas elements causes the error. I'll investigate...

chrisn commented 1 year ago

Peaks.init() was only checking the container element width, so I have just changed it to also check the height. This means you will now get a more meaningful error message, provided you check the err value in the callback as it won't appear as an uncaught exception.

The reason adding a canvas element inside the container works, is that the default height of a canvas element is 150, which forces the container to have non-zero height.

Thank you! Your code example helped identify what was happening.

ChrisMorrisDev commented 1 year ago

Makes perfect sense. Thank you!

vivekd95 commented 1 year ago

Hey, I am having a hard time rendering the waveform in react project. Sometimes the same error pops up and sometimes nothing comes up only the blank space where waveform should render. I have attached the screenshot of the error. canvas issue

Any advice/help?

chrisn commented 1 year ago

Can you post a minimal code example somewhere to help find the cause of the issue? Also, have you seen this example of how to use with React?

As an aside, I notice the error is triggered from _onResize. This isn't the best design: resizing of the container element may or may not be triggered by resizing the window.