Different behaviour of WebAudio API on Google Chrome - javascript

I'm doing a sound player with a spectrum visualizer. On Firefox works very well but in Google Chrome I'm getting problems. I came from this question I made the other day My previous question
On Firefox I can go forward or go previous on the track list all the times I want without a problem, but in Google Chrome I get this error when I press "next/previous"
Failed to execute 'createMediaElementSource' on 'BaseAudioContext':
HTMLMediaElement already connected previously to a different MediaElementSourceNode.
I don't know why Google Chrome complains and Firefox doesn't. The code of the visualizer it's:
function visualizer(audio) {
closeAudioContext = true;
let src = context.createMediaElementSource(audio); // Here fails on Google Chrome
let analyser = context.createAnalyser();
let canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let ctx = canvas.getContext("2d");
src.connect(analyser);
analyser.connect(context.destination);
analyser.fftSize = 2048;
let bufferLength = analyser.frequencyBinCount;
let dataArray = new Uint8Array(bufferLength);
let WIDTH = ctx.canvas.width;
let HEIGHT = ctx.canvas.height;
let barWidth = (WIDTH / bufferLength) * 1.5;
let barHeight;
let x = 0;
let color = randomColor();
function renderFrame() {
requestAnimationFrame(renderFrame);
x = 0;
analyser.getByteFrequencyData(dataArray);
ctx.clearRect(0, 0, WIDTH, HEIGHT);
for (let i = 0; i < bufferLength; i++) {
barHeight = dataArray[i];
ctx.fillStyle = color;
ctx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
}
renderFrame();
}
And after press the "next/previous" button I do immediately:
if (closeAudioContext && context !== undefined) {
context.close();
}
context = new AudioContext();
And then:
visualizer(audio);
musicPlay();
So my question is, why in Firefox the audio player works fine but in Google Chrome crashes?
I'm using Bowser to check what browser the user is using because as the new policy of Chrome to mute all sounds if its activated the autoplay (in this case I set the autoPlay to false and if the user press play the sound it's not muted). So, if I have to make a different code for Google Chrome I can make an "if" with that code.
Regards.

That's a bug in Chrome; you can't cannot attach an HTMLMediaElement to another MediaElementAudioSourceNode. But since you close the context and create a new one, that's another different bug in Chrome.

Related

Canvas Flickering in Chrome and OBS

I think this is a chrome issue as it only seems to Chrome, Edge, and OBS. However, that is a problem because that is most of my intended audience. There is no flicker in Firefox so I believe I just need a work around for Chromium based browsers.
The flickering only seems to last for 1 rotation through the animation, however, it will occasionally come back at random times. It also comes back whenever we change the sprite animation
Prior to the below code running we have already imported the image as a Javascript Object. Each image has multiple states we can cycle between. Each state has 10 frames which are loaded as DataURLs and switched between so the flickering would not be caused by loading the images.
Currently we are testing with very few images so the total object size is only a few Kb.
//Set up Canvas Javascript Elements
const canvas = document.getElementById('mainCanvas');
const bufferCanvas = document.getElementById('bufferCanvas');
//Get Canavas 2d context
const ctx = canvas.getContext('2d');
const bctx = bufferCanvas.getContext('2d');
//Set Canvas Size - Broken up into multiple lines to more easily enable and disable elements during testing
const CANVAS_WIDTH = 364;
const CANVAS_HEIGHT = 444;
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
bufferCanvas.width = CANVAS_WIDTH;
bufferCanvas.height = CANVAS_HEIGHT;
//Sets current animation, initial frame, and cycle count
var currentAnimation = 'idle'
var currentFrame = 0;
var cycle = 0;
function animate(){
//Only incrementing frames when the cycle mod 10 is 0 slows it down
if(cycle % 10 == 0){
currentFrame++;
if(currentFrame >= currentImage[currentAnimation].length){
currentFrame = 0;
}
playerImage.src = currentImage[currentAnimation][currentFrame];
}
//Draws the buffer canvas prior to clearing the main canvas
bctx.drawImage(playerImage, 0, 0);
//Clears main Canvas
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
//Draws new image to main Canvas
ctx.drawImage(bufferCanvas, 0, 0);
//Clears buffer canvas once main canvas has been drawn
bctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
//Resets cycle to maintain a manageable number
cycle++;
if(cycle >= 10000){
cycle = 0;
}
//Calls itself to loop
requestAnimationFrame(animate);
}

Use GPU to draw on HTML5 Canvas on Google Chrome

I'm using a canvas to draw a marker (in SVG) hundreds (sometimes thousands) of times. The size of the canvas is 300x300 pixels and the SVG is 18x25 pixels.
The code is quite straigth forward, I have a for loop where I draw the markers on the canvas:
drawNewTile = (canvas, points) => {
const drawn = {};
const context = canvas.getContext('2d');
if (points.length === 0) return;
for (let i = points.length; i -= 1;) {
const [x, y] = points[i];
if (!drawn[`${x}:${y}`]) {
drawn[`${x}:${y}`] = true;
this.drawMarker(context, x, y);
}
}
};
drawMarker = (context, x, y) => {
const x_ = Math.floor(x - this.MARKER_WIDTH / 2 + this.MAX_DIMENSION_OF_MARKER);
const y_ = Math.floor(y - this.MARKER_HEIGHT + this.MAX_DIMENSION_OF_MARKER);
context.drawImage(this.marker, x_, y_, this.MARKER_WIDTH, this.MARKER_HEIGHT);
};
I have already put in place some optimizations: like the for loop, only draw those points which are not already drawn, use integer coordinates, etc.
After that, I have some good results, but my page it gets a little bit stuck on Google Chrome. Nonetheless, to my surprise, in Firefox it goes fast as hell, like, really really fast. So I made some digging with the performance tab of Google Chrome and I found that my code was using a lot of CPU and that's slow.
I also found this article where it says that Chrome uses some heuristics to determine if it uses a CPU or a GPU to draw the canvas.
So, my question is, how do I force the use of GPU on Chrome? Is there any flag I can set or something similar? Do you any other way to speed that the drawing process?
The problem is that apparently Chrome keeps SVG images in the CPU, and rasterizes it at every new call to drawImage().
Simply rasterizing it yourself will make Chrome's performances grow instantly.
To do that, use the createImageBitmap() method, which will create an ImageBitmap that the browser will be able to store directly in the GPU's memory.
Safari just did expose this method in the newest version of their browser, so you may still want to use a polyfill for it. While in this case, simply drawing on a canvas would be enough, I made such a polyfill which does include a few features that most browsers don't support yet.
(async () => {
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const select = document.querySelector("select");
const html_img = new Image();
const svg_str = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 25" width="18" height="25">
<circle cx="9" cy="9" r="6"/>
</svg>`;
const svg_blob = new Blob([svg_str], {
type: "image/svg+xml"
});
ctx.font = "20px sans-serif";
ctx.fillStyle = "red";
html_img.src = URL.createObjectURL(svg_blob);
const sources = {
html_img,
bitmap: await createImageBitmap(svg_blob)
};
const times = [];
await html_img.decode();
anim();
function anim() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let y = 0; y < canvas.width; y += 10) {
for (let x = 0; x < canvas.width; x += 5) {
ctx.drawImage(sources[select.value], x, y);
}
}
requestAnimationFrame(anim);
// ultra rough FPS counter
const now = performance.now();
while (times.length > 0 && times[0] <= now - 1000) {
times.shift();
}
times.push(now);
fps = times.length;
ctx.fillText(fps + "FPS", 30, 30);
}
})();
<!-- createImageBitmap polyfill for old browsers --> <script src="https://cdn.jsdelivr.net/gh/Kaiido/createImageBitmap/dist/createImageBitmap.js"></script>
source: <select>
<option value="bitmap">ImageBitmap</option>
<option value="html_img">HTMLImage</option>
</select><br>
<canvas width="300" height="300"></canvas>

Using getByteFrequencyData on iPad and Safari/Chrome

I am working on a (web-) animation that moves lips with played audio speech. I use Javascript getByteFrequencyData() to determine low and high frequencies and get a very realistic mouth movement.
The code runs perfectly on all Windows browsers, on android (chrome) but I face a huge problem on IOS with Safari and also with Chrome: getByteFrequencyData always returns a constant value. No errors are logged to the console so all objects are created, the audio plays (as started in a user interaction context).
UPDATE: I tested Safari, Firefox, Edge and Chrome, all with the same result.
UPDATE2: The whole app does not work on Safari OSX but on Chrome OSX. On Chrome OSX (Catalina) getByteFrequencyData also works well. It seems to be an iOS only issue.
This is how I init the audio system (once):
...
audio = document.getElementById("audio");
audio.addEventListener('ended', onAudioPlayed, false);
var AudioContext = window.AudioContext || window.webkitAudioContext;
ctx = new AudioContext();
audioSrc = ctx.createMediaElementSource(audio);
audioSrc.connect(ctx.destination);
analyser = ctx.createAnalyser();
frequencyData = new Uint8Array(analyser.frequencyBinCount);
...
And I use the requestAnimationFrame-Method to feed the animation:
...
audio.src = "audio/" + audioQueue.shift();
console.log("play: "+audio.src);
audio.type = "audio/mp3";
analyser.fftSize = 64;
analyser.smoothingTimeConstant = 0.4;
analyser.minDecibels = -80;
analyser.maxDecibels = -20;
audioSrc.connect(analyser);
analyser.getByteFrequencyData(frequencyData);
function renderFrame() {
if (!audio.paused) requestAnimationFrame(renderFrame);
analyser.getByteFrequencyData(frequencyData);
avgHigh = 0;
avgLow = 0;
count = analyser.frequencyBinCount;
//console.log("rendermund " + frequencyData[0]);
for (i = 0; i < count; i++) {
if (i < count/4)
avgLow += frequencyData[i]
else
avgHigh += frequencyData[i];
}
console.log("rendermund " + avgHigh + "/" + avgLow);
avgLow = avgLow/2000;
avgHigh = avgHigh/2000;
if (avgLow > 1) avgLow = 1;
if (avgHigh > 1) avgHigh = 1;
mund(avgHigh,avgLow); // Here I draw the mouth...
}
audio.load();
audio.play();
renderFrame();
...
Is there an alternative way to get any live information about the samples currently played?
The strange thing is that I do not get any error messages - getByteFrequencyData() returns the array of the expected size but with constant values (0) on iOS. I tested with the latest IO Version on Ipad Air, IPad Air 2 and IPads of the 5th and 6th generation all with the same result.

Why is this dynamic HTML5 video thumbnail generation failing in IE11?

I have a Javascript function setup to dynamically generate thumbnails from a given embedded HTML5 video once it loads. This works fine on other browsers. The problem arises with IE11. For some reason, it's just not outputting anything despite working perfectly on Firefox and Chrome.
I have a section of the site which has several HTML5 videos and they need to generate their respective thumbnails based on the first frame. I've done a good bit of researching around and I can't seem to find any IE11 compatibility issues with my code.
Here is the code I have now:
var vids = document.querySelectorAll('[id^=video-]');
for(var i = 0; i < vids.length; i++){
if(vids[i].tagName == 'VIDEO'){
$("#video-" + (i+1)).on("loadeddata", generate_handler(i+1));
}
...
}
generate_handler calls the shoot function (had to do it this way because of scoping issues in the loop),
function generate_handler(j) {
return function(event) {
shoot(j);
};
}
and the shoot function goes as follows:
function shoot(num){
var video = document.getElementById('video-' + num);
var output = document.getElementById('videothumbnail-' + num);
if(output.childNodes.length == 1){
var canvas = capture(video);
output.appendChild(canvas);
}
}
and finally, the capture function is as follows:
function capture(video) {
var canvas = document.createElement('canvas');
var w = 48;
var h = 40;
canvas.width = w;
canvas.height = h;
canvas.style.marginTop = "4px";
canvas.style.width = w + "px";
canvas.style.height = h + "px";
canvas.style.zIndex = "-999";
var ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, w, h);
return canvas;
}
Essentially, as a TL;DR: When the data of the video is loaded, it calls this function to shoot a screengrab and add it to the thumbnail div. In order to do that, it draws the video to a canvas. This is the order the functions are called:
on('loadeddata', generate_handler()) -> shoot() -> capture()
What's strange is that after some simple tests with console.log, it actually is reaching the inside of capture(), which inclines me to believe it's a compatibility issue with something in there, or with appendChild().
What this should do (and does do on Firefox/Chrome) is draw a 48x44px thumbnail of the HTML5 video being loaded on the page. Instead, on IE11 it displays nothing.

I cannot resize the Canvas in JavaScript

Ok, so, I'm working on a project in HTML5 and JavaScript. I'm trying to resize a Canvas, but it won't work. I don't think it's my browser, though, because I am using the latest version of FireFox. I've also researched this issue for a while, and I am confident I'm doing this correctly. So, I don't know why it won't work.
Here's my Code:
var level = 1;
var levelImg = undefined;
var width = 0;
var height = 0;
var cnvs = document.getElementById("cnvs").getContext("2d");
width = window.innerWidth
|| document.documentElement.clientWidth
|| document.body.clientWidth;
height = window.innerHeight
|| document.documentElement.cleintHeight
|| document.body.cleintHeight;
cnvs.width = width;
cnvs.height = height;
window.onload = function Init(){
levelImg = document.getElementById("level" + level);
setInterval("Draw()", 3);
}
function Draw(){
//Clear the Screen
cnvs.clearRect(0, 0, width, height);
//Draw stuff
DrawLevel();
}
function DrawLevel(){
cnvs.fillRect(0, 0, 10, 10);
}
Any help is appreciated. Thanks.
First correct all the typos in you code.
Use the browser console to detect errors.
Difference between canvas and the context
var cnvs = document.getElementById("cnvs").getContext("2d");
cnvs variable is not a canvas but the context for the canvas.
Canvas is the element and context is the object used to write in the canvas.
To access the canvas you need to do this:
var canvas = document.getElementById("cnvs");
var cnvs = canvas.getContext('2d'); //context
Now when you are trying to change the canvas with, you use canvas, not cnvs.
canvas.width = width;
canvas.height = height;
SetInterval expects a function and a number value that represents milliseconds.
"Draw()" is a string, not a function, and 3 is a really small number between each time the browser draws on canvas. It works, but it's very inefficient.
Other point about setInterval. Avoid it by using requestAnimationFrame() instead.
Take a look here: setTimeout or setInterval or requestAnimationFrame
Defining var levelImg = undefined has no utility. It can be replaced by var levelImg;

Categories