'HTML5 Canvas drawImage from video not showing on first draw
I have a video element with a canvas overlay. Then I have a drawing tool setup to draw over the video and a save button that does a drawImage from both the video and then the canvas to save a comped frame. However, the first time I press Save I only get the result from the canvas drawImage, the video does not show. On subsequent Saves I receive both images properly layered. I thought this might be an issue with loading of the video image, but the video is fully loaded before I hit save and can even advance frames and have it work properly on the 2nd Save.
Here is the code...
<div style="width:960px; height:540px; display:inline-block;">
<video id="video" src="media/_tmp/AA_017_COMP_v37.mov" width="960" height="540" ></video>
</div>
<canvas id="canvas" width="960" height="540" style="position:absolute; top:40px; left:9px; z-index:100;"></canvas>
<input type="button" value="save" id="btn" size="30" onclick="save()" style="float:left; padding:4px; margin-right:4px;" >
<div id="saved" style="border:1px solid #000; position:absolute; top:626px; left:10px; bottom:40px; width:958px; overflow:auto;">SAVED:</div>
function save() {
//COMP CANVAS OVER VIDEOFRAME
var video = document.getElementById("video");
var currentFrame = Math.floor((<?php echo $mov_frames ?> / video.duration) * video.currentTime);
var compCanvas = document.createElement('canvas');
compCanvas.width = video.width;
compCanvas.height = video.height;
compContext = compCanvas.getContext('2d');
compContext.drawImage(video, 0, 0);
compContext.drawImage(canvas, 0, 0);
var dataURL = compCanvas.toDataURL();
$("#saved").append('<div style="width:954px; border-bottom:1px solid #000; padding:2px 2px 0 2px;"><img id="compFrame_'+currentFrame+'" width="180" height="90" src="'+dataURL+'" />Frame: '+currentFrame+'</div>');
}
Solution 1:[1]
As this is on OSX with Safari there is some bad news:
Note: Video as a source for the canvas drawImage() method is not currently supported on iOS.
If this is outdated information I don't know but in either case the current implementation has issues. As established (in comments) the problem is not likely related to codecs as this occur with other formats as well. The fiddle eliminates the toDataURL()
method as source so what is left is the drawImage()
method.
As this works on other platform with the provided code, and from the look of it, this is neither the source of the problem so you are here looking at a very possible issue (bug) with the Safari browser on the OSX platform.
The official documentation quoted above also states this is not supported so this is probably a very early implementation that still has some issues.
There is not much you can do in that regard but to wait and also report it as an issue (I looked for a reported issue but couldn't find any so I recommend you do this).
Solution 2:[2]
I recently ran into the same issue myself, and worked around it by drawing the video onto a "dummy" canvas before doing "real" work with it. This works well as all of the methods in my code that attempt to parse the video wait until the "canplaythrough" event has been triggered on the video. I wrap the "canplaythrough" event handler in a Promise, and only resolve the promise when the "canplaythrough" event has been received AND an attempt has been made to draw the video onto a dummy canvas. This appears to be an effective workaround for the Safari bug.
For example:
var readyPromise = new Promise(function(resolve) {
video.addEventListener("canplaythrough", function() {
var canvas = document.createElement("canvas"),
context = canvas.getContext("2d");
context.drawImage(video, 0, 0);
resolve(video);
});
});
readyPromise.then(function() {
// NOW manipulate the video, draw it onto a canvas, etc
});
Solution 3:[3]
I met the same issue and tried out the solution in the last comment. It didn't work for me quite yet.
I figured later and found a workaround:
in my code I have some other drawing along with drawImage from video like
context.rect(0, 0, 50, 50);
context.fillStyle = 'black';
context.fill();
and with these, the second draw still won't work even when redraw it after 'canplaythrough' event.
So I removed the above three lines, and just did a second draw after event 'canplaythrough'. Then it worked.
Simply, the code that worked for me is like this:
context.drawImage(video, 0, 0);
video.addEventListener('canplaythrough', function () {
context.drawImage(video, 0, 0);
});
Solution 4:[4]
None of these worked for me. It seems like chrome (atm it's version 97) is doing some nasty optimization in background.
canplaythrough
seems to be lazy, and doesn't guaranty that 1st frame is loaded- if
video
has styledisplay: none
it won't be loaded as well, thus drawing won't work either. - video element should be attached to DOM
- canvas element should not have any css related to its width or size.
The only thing worked is setTimeout, it's a nasty hack, but it is like it is.
fileinput.addEventListener('change', function (e) {
var ctx = canvas.getContext("2d");
video.addEventListener('loadedmetadata', function () {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
setTimeout(() => {
ctx.drawImage(video, 0, 0);
}, 100)
});
video.querySelector("source").setAttribute('src', URL.createObjectURL(fileinput.files[0]));
video.load();
});
video {
opacity: 0;
width: 0;
height: 0;
}
<input type="file" id="fileinput" name="file">
<video id="video" controls>
<source type="video/mp4">
</video>
<canvas id="canvas"></canvas>
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | |
Solution 3 | paige |
Solution 4 |