I am developing VR html5 page in javascript (no jQuery or other frameworks) that uses WebGL to render a sphere and has texture to which streamed video is rendered into.
All that works fine on iPhone 6, 6+, however on Android I've hit the wall - simply, video is not being transferred into texture (gl.texSubImage2D). Texture remains black. No WebGL errors thrown.
So I have created a test, without WebGL, that just tries to play a video and draw it's frames into canvas2D, so I can at least verify that frames are indeed extracted from streamed video.
Video is played on user interaction (touch) and when canplay event is triggered, I start frame loop (window.requestAnimationFrame) to draw video into canvas (canvas.drawImage( video, 0,0 ))
Both video and canvas are on the document body visible, next to each other.
Result: On desktop it works as expected, two screens, left is video with native controls, right is canvas. When I click play, video starts, and canvas gets refreshed at the same time. On Android Chrome 48.0.2564.106 no pixels drawn - canvas is totally empty.
I have installed Android Chrome Beta (50.0.2661.57) and it works there, but on Android Chrome 48.0.2564.106 it does not.
Codes to setup video and canvas:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="format-detection" content="telephone-no" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Video Canvas Test</title>
<script>
var video;
var canvas;
var ctx;
var info;
var pass = 0;
window.onload = function()
{
video = document.createElement("video");
video.oncanplay = function(){ initializeCanvas(); }
video.onerror = function(e){ console.error("video problem"); }
video.loop = true;
video.controls = "true";
video.src = "video/big-buck-bunny_trailer.webm";
video.style.width = "400px";
video.style.height = "300px";
video.style.position = "absolute";
video.style.left = "20px";
video.style.top = "20px";
video.style.backgroundColor = "#8080FF";
canvas = document.createElement("canvas");
canvas.style.backgroundColor = "#8080FF";
canvas.style.width = "400px";
canvas.style.height = "300px";
canvas.style.position = "absolute";
canvas.style.left = "420px";
canvas.style.top = "20px";
ctx = canvas.getContext("2d");
info = document.createElement("p");
info.innerHTML = window.navigator.userAgent;
info.style.position = "absolute";
info.style.width = "200px";
info.style.height = "auto";
info.style.position = "absolute";
info.style.left = "20px";
info.style.top = "320px";
document.body.appendChild(video);
document.body.appendChild(canvas);
document.body.appendChild(info);
}
function initializeCanvas()
{
console.log("Video ready to play. Init canvas.");
ctx.canvas.width = 640; // I am 100% sure this is correct video size.
ctx.canvas.height = 360;
console.log("Video size: " + video.videoWidth + "x" + video.videoHeight);
ctx.font = "30px Arial";
updateCanvas();
}
function updateCanvas()
{
pass ++;
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
ctx.drawImage(video,0,0);
ctx.fillText("Pass: " + pass,30,120);
window.requestAnimationFrame( updateCanvas );
}
</script>
</head>
<body>
</body>
</html>
Can anyone confirm if canvas.drawImage on Chrome 48 and older cannot accept video as drawing source? This looks impossible to me considering WebGL has been in Chrome's support for ages and with plenty of experiments with video textures out there.
I have tried other samples that copy video into WebGL texture or Canvas2D and they do not work as well.
Is there anything I am missing here?
After thorough research it appears that:
Canvas2D.drawImage( video, 0,0 ) was broken since July 2015 (at least as I found bug reports that date from then) in Android Chrome and was kinda fixed-not-fixed-fixed-again-and-again-not-fixed - but I can confirm that it is fixed in Android Chrome 49 and in Android Chrome 50 (Beta). Same issue with decoding pixels from video to be drawn into Canvas2D has affected drawing video into WebGL texture.
The only workaround is to have either: a) custom-made video streaming server using Websocket + JPG sequence streamer in JavaScript (images received through opened websocket connection) or b) JPG sequence streamer in JavaScript (downloads images one by one and takes care of loading new - deleting old).
I went with option 2.b and with CDN kicked in it really works great for 1024x512 video size, which was good enough solution.
I would first preload mp3 file using http request, then load it into Audio object. Once that's done, JPG streamer would kick in and would then commence playback using sound currentTime value. It was 100% synchronised with sound!
However, issue is fixed in Chrome 49, and Chrome 50 (beta), so this likely will be obsolete in 2-3 months.
Related
My application works fine in Chrome, but in IE/EDGE the canvas doesnt show the video.
This started to happen when i use as source a encrypted video, when i used open source video the canvas showed the video.
I cant find a solution mostly because IE/EDGE doesnt show errors in developer tools console.
IE/EDGE has some policy that doesnt allow to draw a encrypted video?
In future i will remove video element from html, create only in javascript and write some text in canvas as a watermark.
<canvas runat="server" id="canvas1"></canvas>
<video
id="video1"
runat="server"
class="azuremediaplayer amp-default-skin amp-big-play-centered"
controls
poster="">
</video>
<script>
var videoElement = document.getElementById('<%=video1.ClientID%>');
videoElement.setAttribute('webkit-playsinline', 'true');
videoElement.width = '1280';
videoElement.height = '720';
var x, y, min, tempo = 0;
var nroRender = 201;
var myPlayer = amp(videoElement);
myPlayer.src([{
src: '<URL VIDEO>',
protectionInfo: [
{
type: 'Widevine',
authenticationToken: 'Bearer=<TOKEN>'
}, {
type: 'PlayReady',
authenticationToken: 'Bearer=<TOKEN>'
}]
}]);
var canvasElement = document.getElementById('<%=canvas1.ClientID%>');
canvasElement.width = '1280';
canvasElement.height = '720';
var ctx = canvasElement.getContext('2d');
function desenha() {
ctx.clearRect(0, 0, canvasElement.width, canvasElement.height);
ctx.drawImage($('#video1 video')[0], 0, 0, canvasElement.width, canvasElement.height);
}
function loop() {
desenha();
setTimeout(loop, 1000 / 60);
}
loop();
</script>
If you have some problem to understand what is the problem, run in Chrome and then in IE. In Chrome canvas appears like video, in IE canvas appears black.
Full code in https://github.com/tobiasrighi/video-canvas/blob/master/WebForm1.aspx
Because the video is protected with DRM, by design IE/Edge block the ability to capture frames - its actually not an error and this is built down lower in the media pipeline. It seems Chrome's current implementation with Widevine does not block frames, although this may happen in the near future depending on Google's future design considerations.
I have run into a rather strange situation while trying to build a HTML5 Video player using the new Adobe Flash HTML5 Canvas projects.
I have added the video to the canvas in flash, however the video only plays if there is an uncaught reference error. If there is no error the video will not play in the canvas.
I am wondering if anyone has any insight into why this may be happening or if you have a better way to add video to this flash Canvas project.
I have included all of my code below.
this.stop();
var mainStage = this;
var movieWidth = 640;//Choose Scaled Width of Video
var movieHeight = 360;//Choose Scaled Height of Video
var autoplayVideo = true;//Make it Autoplay
var vidya = document.createElement('video');//Creates the Video Element that is referenced later
var canvasEle = document.getElementById('canvas');//Identify the Canvas element
ctx = canvasEle.getContext('2d');//get canvas's context
canvasEle.parentNode.insertBefore(vidya, canvasEle);//insert video element before canvas element
vidya.setAttribute("src", "testing.mp4");//Place URL of the Video here (Local in this Example)
vidya.setAttribute("type","video/mp4");//What type of Video it is, Should be MP4
vidya.setAttribute("width", movieWidth);//scales the video to the width you had set above
vidya.setAttribute("controls","");//Turns on the default generic video controls
vidya.setAttribute("id","VIDEO");//gives the element an id for reference(Not Used yet)
if (autoplayVideo == true){ vidya.setAttribute("autoplay","");};
createjs.Ticker.addEventListener("tick", handleTick);
function handleTick(event){
ctx.drawImage(vidya, 30, 70, movieWidth, movieHeight);
console.log(ctx + " "+v);//here is where the uncaught reference is the "v"
}
What I'd like to do is draw my graphics on a buffer and then be able to copy it as is to the canvas so I can do animation and avoid flickering. But I couldn't find this option. Anyone know how I can go about this?
A very simple method is to have two canvas-elements at the same screen location and set visibility for the buffer that you need to show. Draw on the hidden and flip when you are done.
Some code:
CSS:
canvas { border: 2px solid #000; position:absolute; top:0;left:0;
visibility: hidden; }
Flipping in JS:
Buffers[1-DrawingBuffer].style.visibility='hidden';
Buffers[DrawingBuffer].style.visibility='visible';
DrawingBuffer=1-DrawingBuffer;
In this code the array 'Buffers[]' holds both canvas-objects. So when you want to start drawing you still need to get the context:
var context = Buffers[DrawingBuffer].getContext('2d');
The following helpful link, in addition to showing examples and advantages of using double buffering, shows several other performance tips for using the html5 canvas element. It includes links to jsPerf tests, which aggregate test results across browsers into a Browserscope database. This ensures that the performance tips are verified.
https://web.dev/canvas-performance/
For your convenience, I have included a minimal example of effective double buffering as described in the article.
// canvas element in DOM
var canvas1 = document.getElementById('canvas1');
var context1 = canvas1.getContext('2d');
// buffer canvas
var canvas2 = document.createElement('canvas');
canvas2.width = 150;
canvas2.height = 150;
var context2 = canvas2.getContext('2d');
// create something on the canvas
context2.beginPath();
context2.moveTo(10,10);
context2.lineTo(10,30);
context2.stroke();
//render the buffered canvas onto the original canvas element
context1.drawImage(canvas2, 0, 0);
Browsers I've tested all handle this buffering for you by not repainting the canvas until the code that draws your frame has completed. See also the WHATWG mailing list: http://www.mail-archive.com/whatwg#lists.whatwg.org/msg19969.html
You could always do
var canvas2 = document.createElement("canvas");
and not append it to the DOM at all.
Just saying since you guys seem so obsessed with display:none;
it just seems cleaner to me and mimicks the idea of double buffering way more accurately than just having an awkwardly invisible canvas.
More than two years later:
There is no need for 'manually' implement double buffering. Mr. Geary wrote about this in his book "HTML5 Canvas".
To effectively reduce flicker use requestAnimationFrame()!
For the unbelievers, here's some flickering code. Note that I'm explicitly clearing to erase the previous circle.
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
function draw_ball(ball) {
ctx.clearRect(0, 0, 400, 400);
ctx.fillStyle = "#FF0000";
ctx.beginPath();
ctx.arc(ball.x, ball.y, 30, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
}
var deltat = 1;
var ball = {};
ball.y = 0;
ball.x = 200;
ball.vy = 0;
ball.vx = 10;
ball.ay = 9.8;
ball.ax = 0.1;
function compute_position() {
if (ball.y > 370 && ball.vy > 0) {
ball.vy = -ball.vy * 84 / 86;
}
if (ball.x < 30) {
ball.vx = -ball.vx;
ball.ax = -ball.ax;
} else if (ball.x > 370) {
ball.vx = -ball.vx;
ball.ax = -ball.ax;
}
ball.ax = ball.ax / 2;
ball.vx = ball.vx * 185 / 186;
ball.y = ball.y + ball.vy * deltat + ball.ay * deltat * deltat / 2
ball.x = ball.x + ball.vx * deltat + ball.ax * deltat * deltat / 2
ball.vy = ball.vy + ball.ay * deltat
ball.vx = ball.vx + ball.ax * deltat
draw_ball(ball);
}
setInterval(compute_position, 40);
<!DOCTYPE html>
<html>
<head><title>Basketball</title></head>
<body>
<canvas id="myCanvas" width="400" height="400" style="border:1px solid #c3c3c3;">
Your browser does not support the canvas element.
</canvas>
</body></html>
Josh asked (a while back) about how the browser knows "when the drawing process ends" so as to avoid flicker. I would have commented directly to his post but my rep isn't high enough. Also this is just my opinion. I don't have facts to back it up, but I feel fairly confident about it and it may be helpful to others reading this in the future.
I'm guessing the browser doesn't "know" when you're done drawing. But just like most javascript, as long as your code runs without relinquishing control to the browser, the browser is essentially locked up and won't/can't update/respond to its UI. I'm guessing that if you clear the canvas and draw your entire frame without relinquishing control to the browser, it won't actually draw your canvas until you're done.
If you set up a situation where your rendering spans multiple setTimeout/setInterval/requestAnimationFrame calls, where you clear the canvas in one call and draw elements on your canvas in the next several calls, repeating the cycle (for example) every 5 calls, I'd be willing to bet you'd see flicker since the canvas would be updated after each call.
That said, I'm not sure I'd trust that. We're already at the point that javascript is compiled down to native machine code before execution (at least that's what Chrome's V8 engine does from what I understand). I wouldn't be surprised if it wasn't too long before browsers started running their javascript in a separate thread from the UI and synchronizing any access to UI elements allowing the UI to update/respond during javascript execution that wasn't accessing UI. When/if that happens (and I understand there are many hurdles that would have to be overcome, such as event handlers kicking off while you're still running other code), we'll probably see flicker on canvas animation that aren't using some kind of double-buffering.
Personally, I love the idea of two canvas elements positioned over top of each other and alternating which is shown/drawn on each frame. Fairly unintrusive and probably pretty easily added to an existing application with a few lines of code.
There is no flickering in web browsers! They already use dbl buffering for their rendering. Js engine will make all your rendering before showing it. Also, context save and restore only stack transformational matrix data and such, not the canvas content itself.
So, you do not need or want dbl buffering!
Rather than rolling your own, you're probably going to get the best mileage by using an existing library for creating clean and flicker-free JavaScript animation:
Here's a popular one: http://processingjs.org
you need 2 canvas: (notice the css z-index and position:absolute)
<canvas id="layer1" width="760" height="600" style=" position:absolute; top:0;left:0;
visibility: visible; z-index: 0; solid #c3c3c3;">
Your browser does not support the canvas element.
</canvas>
<canvas id="layer2" width="760" height="600" style="position:absolute; top:0;left:0;
visibility: visible; z-index: 1; solid #c3c3c3;">
Your browser does not support the canvas element.
</canvas>
you can notice that the first canvas is visible and the second it's hidden the idea it's to draw on the hidden after that we will hide the visible and make the hidden canvas visible. when it's hidden 'clear hidden canvas
<script type="text/javascript">
var buff=new Array(2);
buff[0]=document.getElementById("layer1");
buff[1]=document.getElementById("layer2");
ctx[0]=buff[0].getContext("2d");
ctx[1]=buff[1].getContext("2d");
var current=0;
// draw the canvas (ctx[ current ]);
buff[1- current ].style.visibility='hidden';
buff[ current ].style.visibility='visible';
ctx[1-current].clearRect(0,0,760,600);
current =1-current;
In most situations, you don't need to do this, the browser implements this for you. But not always useful!
You still have to implement this when your drawing is very complicated.
Most of the screen update rate is about 60Hz, it means the screen updates per 16ms. The browser's update rate may near this number. If your shape need 100ms to be completed, you'll see a uncompleted shape. So you can implement double buffering in this situation.
I have made a test: Clear a rect, wait for some time, then fill with some color. If I set the time to 10ms, I won't see flickering. But if I set it to 20ms, the flickering happens.
Opera 9.10 is very slow and shows the drawing process. If you want to see a browser not use double buffering, try Opera 9.10 out.
Some people suggested that browsers are somehow determining when the drawing process ends but can you explain how that can work? I haven't noticed any obvious flicker in Firefox, Chrome, or IE9 even when the drawing is slow so it seems like that is what they are doing but how that is accomplished is a mystery to me. How would the browser ever know that it is refreshing the display just before more drawing instructions are to be executed? Do you think they just time it so if an interval of more than 5ms or so goes by without executing a canvas drawing instruction, it assumes it can safely swap buffers?
This is an example of double buffering with image data.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML5 Canvas Demo of PixelBuffer</title>
<style>
html,
body {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<canvas id="buffer1"></canvas>
<script>
const canvas = document.getElementById('buffer1');
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(100, 100);
const pixelBuffer = new Uint32Array(imageData.data.buffer);
for (let i = 0; i < pixelBuffer.length; i++) {
pixelBuffer[i] = 0xFF0000FF;
}
ctx.putImageData(imageData, 0, 0);
</script>
</body>
</html>
I currently have a canvas html5 element setup to receive an uploaded image, resize it, and pass its data uri to a form value which is then submitted to the server. This works well on computers but has run into a problem when I tested it on my iPhone through Safari.
With the new IOS 6.0.1 update when uploading an image I can choose 'Take Photo or Video', however two issues arise when doing so from an iPhone (i do not have an iPad handy to test :( )...
1) The image is either appears squished or "unfinished" as in the top 1/3 of the image is all that remains
I removed my context.scale() function and these issues did not
appear. Is there some processing limitation for scaling on iphone
devices? A simple context.scale(0.5, 0.5) does not seem to work.
2) The image is rotated (this problem persists when context.scale() is removed).
I've read that image may preserve some of its orientation data, but how would one remove that?
These issues seem isolated to only when I 'Take Photo' or use an image that my iPhone took. When using images downloaded from my computer onto my iPhone there seems to be no problem for either issues 1) or 2). Also when using pictures taken by my iPhone, sent to my computer, and then uploaded from the computer there seems to be no problems as well.
For issue 1) since the same picture is used by my computer (no problems) and iPhone (problem) I venture to guess it is a iPhone limitation for context.scale(), unless when sending an image by email my iPhone compresses it somehow and removes the orientation data which is also not a problem when used by my computer. As you can see I'm quite confused! Thanks for any help!
my code for canvas resizing...
reader.onload = function(e) {
preview.html('<img id="scream" src="' + e.target.result + '" ' + (preview.css('max-height') != 'none' ? 'style="max-height: ' + preview.css('max-height') + ';"' : '') + ' />')
element.addClass('fileupload-exists').removeClass('fileupload-new')
var canvas = document.createElement("canvas"),
context = canvas.getContext("2d"),
image = new Image();
image.onload = function () {
var img = this,
width = img.width,
height = img.height;
canvas.width = 300;
canvas.height = 300;
context.scale(0.25, 0.25);
context.drawImage(img, 0, 0);
var base64 = canvas.toDataURL();
//console.log(base64);
//$('form_6').val(base64);
document.body.appendChild(canvas);
};
image.src = e.target.result;
}
I'm drawing a video on a canvas, this works fine with Safari / Chrome / Firefox / Opera, but on the iPad, even though the video plays, (correct codec, etc) it is never rendered on the canvas,
Basically I just call :
canvas.getContext("2d").drawImage(video, 0, 0);
when the video is playing, and stop doing this when the video is paused or ended.
Is there anything else I should consider? Like clearing the canvas?
For now safari on iPad is not supporting this feature. There are some limits on the attributes and events of canvas tag and video tag of HTML5 particularly on iPad. The attributes and events of canvas and video tags which work fine on desktop browsers wont work on iPad. This is my personal experience too.
See Putting Video on Canvas
You basically can’t pass a video object to the canvas drawImage method. Apple suggests having the video positioned behind the canvas but this won’t help if you want to manipulate the video somehow.
Have you tried wrapping it inside the requestAnimationFrame() function.
<video src="YourSrcFile.webm" autolay muted></video>
// Fallback to mp4 if not supported.
<canvas></canvas>
const video = document.querySelector("video"); // Offscreen Canvas.
const canvas = document.querySelector("canvas"); // Onscreen Canvas.
canvas.style.zIndex = '50';
const ctx = canvas.getContext("2d");
video.addEventListener('play',()=>{
function step() {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
requestAnimationFrame(step)
}
requestAnimationFrame(step);
})
Make sure to Match both the onscreen & offscreen canvas to the original videos aspect ratio otherwise the extra calculations make it laggy & poor performance..
You can use Transform Scale inside your css to resize it aslong as its proportionately. Which doesn't seem to make it glitchy, but I'd suggest converting the video from mp4, avi or other file type to webm..
just used this for a vjloop and its running smooth.
Try these and see if it makes any difference..
<script>
document.addEventListener('DOMContentLoaded', function(){
var v = document.getElementById('v');
var canvas = document.getElementById('c');
var context = canvas.getContext('2d');
var cw = Math.floor(canvas.clientWidth / 100);
var ch = Math.floor(canvas.clientHeight / 100);
canvas.width = cw;
canvas.height = ch;
v.addEventListener('play', function(){
draw(this,context,cw,ch);
},false);
},false);
function draw(v,c,w,h) {
if(v.paused || v.ended) return false;
c.drawImage(v,0,0,w,h);
setTimeout(draw,20,v,c,w,h);
}
</script>