Capture group of HTML5 video screenshots - javascript

I am trying to generate a group of thumbnails in the browser out of a HTML5 video using canvas with this code:
var fps = video_model.getFps(); //frames per second, comes from another script
var start = shot.getStart(); //start time of capture, comes from another script
var end = shot.getEnd(); //end time of capture, comes from another script
for(var i = start; i <= end; i += 50){ //capture every 50 frames
video.get(0).currentTime = i / fps;
var capture = $(document.createElement("canvas"))
.attr({
id: video.get(0).currentTime + "sec",
width: video.get(0).videoWidth,
height: video.get(0).videoHeight
})
var ctx = capture.get(0).getContext("2d");
ctx.drawImage(video.get(0), 0, 0, video.get(0).videoWidth, video.get(0).videoHeight);
$("body").append(capture, " ");
}
The the amount of captures is correct, but the problem is that in Chrome all the canvases appear black and in Firefox they always show the same image.
Maybe the problem is that the loop is too fast to let the canvases be painted, but I read that .drawImage() is asynchronous, therefore, in theory, it should let the canvases be painted before jumping to the next line.
Any ideas on how to solve this issue?
Thanks.

After hours of fighting with this I finally came up with a solution based on the "seeked" event. For this to work, the video must be completely loaded:
The code goes like this:
var fps = video_model.getFps(); //screenshot data, comes from another script
var start = shot.getStart();
var end = shot.getEnd();
video.get(0).currentTime = start/fps; //make the video jump to the start
video.on("seeked", function(){ //when the time is seeked, capture screenshot
setTimeout( //the trick is in giving the canvas a little time to be created and painted, 500ms should be enough
function(){
if( video.get(0).currentTime <= end/fps ){
var capture = $(document.createElement("canvas")) //create canvas element on the fly
.attr({
id: video.get(0).currentTime + "sec",
width: video.get(0).videoWidth,
height: video.get(0).videoHeight
})
.appendTo("body");
var ctx = capture.get(0).getContext("2d"); //paint canvas
ctx.drawImage(video.get(0), 0, 0, video.get(0).videoWidth, video.get(0).videoHeight);
if(video.get(0).currentTime + 50/fps > end/fps){
video.off("seeked"); //if last screenshot was captured, unbind
}else{
video.get(0).currentTime += 50/fps; //capture every 50 frames
}
}
}
, 500); //timeout of 500ms
});
This has worked for me in Chrome and Firefox, I've read that the seeked event can be buggy in some version of particular browsers.
Hope this can be useful to anybody. If anyone comes up with a cleaner, better solution, it would be nice to see it.

Related

Can I "freeze" fixed elements to take a screenshot with getDisplayMedia?

I'm implementing a chrome extension with javascript to take a screenshot of a full page, so far I've managed to take the screenshot and make it into a canvas in a new tab, it shows the content of a tweet perfectly, but as you can see, the sidebars repeat all the time (Ignore the red button, that's part of my extension and I know how to delete it from the screenshots) screenshot of a tweet
This is the code I'm using to take the screenshot:
async function capture(){
navigator.mediaDevices.getDisplayMedia({preferCurrentTab:true}).then(mediaStream=>{
scrollTo(0,0);
var defaultOverflow = document.body.style.overflow;
//document.body.style.overflow = 'hidden';
var totalHeight = document.body.scrollHeight;
var actualHeight = document.documentElement.clientHeight;
var leftHeight = totalHeight-actualHeight;
var scroll = 200;
var blob = new Blob([document.documentElement.innerHTML],{ type: "text/plain;charset=utf-8" });
console.log('total Height:'+totalHeight+'client height:'+actualHeight+'Left Height:'+leftHeight);
var numScreenshots = Math.ceil(leftHeight/scroll);
var arrayImg = new Array();
var i = 0;
function myLoop() {
setTimeout(function() {
var track = mediaStream.getVideoTracks()[0];
let imgCapture = new ImageCapture(track);
imgCapture.grabFrame().then(bitmap=>{
arrayImg[i] = bitmap;
window.scrollBy(0,scroll);
console.log(i);
i++;
});
if (i <= numScreenshots) {
myLoop();
}else{
document.body.style.overflow = defaultOverflow;
saveAs(blob, "static.txt");
printBitMaps(arrayImg, numScreenshots, totalHeight);
}
}, 250)
}
myLoop();
})
}
async function printBitMaps(arrayImg, numScreenshots, totalHeight){
var win = window.open('about:blank', '_blank');
win.document.write('<canvas id="myCanvas" width="'+arrayImg[0].width+'px" height="'+totalHeight+'px" style="border:5px solid #000000;"></canvas>');
var e = numScreenshots+1;
function printToCanvas(){
setTimeout(function(){
var canvas = win.document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
ctx.drawImage(arrayImg[e], 0, 200*e);
e--;
if(e>=0){
printToCanvas();
}
},10);
}
printToCanvas();
}
Do you know any way by CSS or javascript that I can use to make the sidebars stay at the top of the page so they don't keep coming down with the scroll?
It's not really a case of "the sidebars ... coming down with the scroll" - the code you're using is taking a first screenshot, scrolling the page, taking another screenshot and then stitching it onto the last one and iterating to the bottom of the page. Thus it's inevitable you're seeing what you see on the screen at the point you take the subsequent screenshots.
To resolve your issue, after your first screenshot you would need to set the div element for the side bar to be display=None by use of CSS. You can find details of the side bar by using the browser Dev Tools, right clicking an using "Inspect" in Chrome.
From what I can see, Twitter seems to use fairly cryptic class names, so it might be easiest and more robust to identify the div for the side bar with some other attribute. It appears they set data-testid="sidebarColumn" on it so give using that a go (but YMMV).

Screensaver loads image in radom location after inactivity, restarts if user does anything

I have the following two pieces of code (awful but I have no idea what I'm doing):
var stage = new createjs.Stage("canvas");
createjs.Ticker.on("tick", tick);
// Simple loading for demo purposes.
var image = document.createElement("img");
image.src = "http://dossierindustries.co/wp-content/uploads/2017/07/DossierIndustries_Cactus-e1499205396119.png";
var _obstacle = new createjs.Bitmap(image);
setInterval(clone, 1000);
function clone() {
var bmp = _obstacle.clone();
bmp.x= Math.floor((Math.random() * 1920) + 1);
bmp.y = Math.floor((Math.random() * 1080) + 1);
stage.addChild(bmp);
}
function tick(event) {
stage.update(event);
}
<script>
$j=jQuery.noConflict();
jQuery(document).ready(function($){
var interval = 1;
setInterval(function(){
if(interval == 3){
$('canvas').show();
interval = 1;
}
interval = interval+1;
console.log(interval);
},1000);
$(document).bind('mousemove keypress', function() {
$('canvas').hide();
interval = 1;
});
});
<script src="https://code.createjs.com/easeljs-0.8.2.min.js"></script>
<canvas id="canvas" width="1920" height="1080"></canvas>
Basically what I'm hoping to achieve is that when a user is inactive for x amount of time the full page (no matter on size) slowly fills with the repeated image. When anything happens they all clear and it begins again after the set amount of inactivity.
The code above relies on an external resource which I'd like to avoid and needs to work on Wordpress.
Site is viewable at dossierindustries.co
Rather than interpret your code, I made a quick demo showing how I might approach this.
The big difference is that drawing new images over time is going to add up (they have to get rendered every frame), so this approach uses a cached container with one child, and each tick it just adds more to the cache (similar to the "updateCache" demo in GitHub.
Here is the fiddle.
http://jsfiddle.net/dcs5zebm/
Key pieces:
// Move the contents each tick, and update the cache
shape.x = Math.random() * stage.canvas.width;
shape.y = Math.random() * stage.canvas.height;
container.updateCache("source-over");
// Only do it when idle
function tick(event) {
if (idle) { addImage(); }
stage.update(event);
}
// Use a timeout to determine when idle. Clear it when the mouse moves.
var idle = false;
document.body.addEventListener("mousemove", resetIdle);
function resetIdle() {
clearTimeout(this.timeout);
container.visible = false;
idle = false;
this.timeout = setTimeout(goIdle, TIMEOUT);
}
resetIdle();
function goIdle() {
idle = true;
container.cache(0, 0, stage.canvas.width, stage.canvas.height);
container.visible = true;
}
Caching the container means this runs the same speed forever (no overhead), but you still have control over the rest of the stage (instead of just turning off auto-clear). If you have more complicated requirements, you can get fancier -- but this basically does what you want I think.

CreateJS FPS drop when calling stage.update

I'm making a game using CreateJS. On desktop my FPS is good but when I try to play this game on mobile (for example : iPhone 4) the FPS drops seriously.
I'm trying to figure out why but
Some code
My Canvas
<canvas id="gameCanvas"></canvas>
Setup
this.canvas = "gameCanvas";
this.stage = new createjs.Stage(this.canvas);
var context = this.stage.canvas.getContext("2d");
context.imageSmoothingEnabled = false;
createjs.Ticker.setFPS(30);
this.gameLoopBind = this.gameLoop.bind(this);
createjs.Ticker.addEventListener('tick', this.gameLoopBind);
GameLoop
// some extra code
this.stage.update();
When I comment the code 'this.stage.update()' my FPS on mobile/tablet is good...
I've no idea what I'm doing wrong...
EXTRA CODE
Play the game here => f.cowb.eu/mora/chick-ins
Gameloop Function
Game.prototype.gameLoop = function (e) {
if (this.running) {
this.timer++;
this.timer2++;
if (this.timer2 > 30) {
if (this.lastSnack + this.timeBewteen < this.stopwatch.seconds) {
var height = (this.topSnack) ? 150 : 300;
this.lastSnack = this.stopwatch.seconds;
new Snack(this, this.timer, height);
this.topSnack = this.topSnack ? false : true;
}
if (this.timer > (this.lastPostive + 300)) {
this.lastPostive = this.timer;
publisher.publish('showMessage',
this.positiveImages[Math.floor(Math.random() * this.positiveImages.length)],
common.lang,
'right');
}
this.timer2 = 0;
}
}
this.stage.update();
};
New Snack
You can find the code for creating a new snack here => http://jsfiddle.net/9ofpqq3z/
Here we create a new snack and animate it.
From looking at the architecture of the game, I would suggest that you draw the game elements on separate canvases as opposed to the way you have it right now where everything is on one canvas.
Place the TV parts that don't get redrawn often (or at all) on one canvas, and then the moving elements on a separate canvas.
That should help bring the frame rate up.
Also, you can take a look at the following set of slides on how to improve performance on mobile devices when using the canvas:
http://www.slideshare.net/DavidGoemans/html5-performance-optimization

pixijs swapping out an image without it jumping

I am working on a html 5 javascript game. It has a heavily textured background. I am looking at having one 3d background item and swapping it out on the fly. So in this instance we see a room with a closed door - then when a js event is fired - the image is swapped out to show an open door.
I am trying to create the function and although I can swap the image - I am unable to stop it from jumping.
so a new image path comes in - I null and remove the old backdrop and replace it with the new. I have read about adding it to the texture cache - not sure how to do that? Its my first time using pixijs
GroundPlane.prototype.resetBackdrop = function (imagePath) {
if(this.backdrop) {
this.backdrop.alpha = 0;
this.removeChild(this.backdrop);
this.backdrop = null;
this.backdrop = PIXI.Sprite.fromImage(imagePath);
this.backdrop.anchor.x = .5;
this.backdrop.anchor.y = .5;/*
this.backdrop.scale.x = 1.2;
this.backdrop.scale.y = 1.2;*/
this.addChildAt(this.backdrop, 0);
this.backdrop.alpha = 1;
}
};
The reason for the "jump" is that the image being swapped in takes some time to load before it can be displayed on the screen.
To prevent this, you can load the image into the TextureCache ahead of time, so when you swap images, there won't be any delay.
//set the initial backdrop image
this.backdrop = PIXI.Sprite.fromImage("Image1.png");
this.backdrop.anchor.x = 0.5;
this.backdrop.anchor.y = 0.5;
this.backdrop.scale.x = 1.2;
this.backdrop.scale.y = 1.2;
//this will store the second image into the texture cache
PIXI.Texture.fromImage("Image2.png");
//if you need to keep track of when the image has finished loading,
//use a new PIXI.ImageLoader() instead.
GroundPlane.prototype.resetBackdrop = function (imagePath)
{
//Update the image Texture
this.backdrop.setTexture(PIXI.Texture.fromFrame(imagePath));
};

Capture frames from video with HTML5 and JavaScript

I want to capture a frame from video every 5 seconds.
This is my JavaScript code:
video.addEventListener('loadeddata', function() {
var duration = video.duration;
var i = 0;
var interval = setInterval(function() {
video.currentTime = i;
generateThumbnail(i);
i = i+5;
if (i > duration) clearInterval(interval);
}, 300);
});
function generateThumbnail(i) {
//generate thumbnail URL data
var context = thecanvas.getContext('2d');
context.drawImage(video, 0, 0, 220, 150);
var dataURL = thecanvas.toDataURL();
//create img
var img = document.createElement('img');
img.setAttribute('src', dataURL);
//append img in container div
document.getElementById('thumbnailContainer').appendChild(img);
}
The problem I have is the 1st two images generated are the same and the duration-5 second image is not generated. I found out that the thumbnail is generated before the video frame of the specific time is displayed in < video> tag.
For example, when video.currentTime = 5, image of frame 0s is generated. Then the video frame jump to time 5s. So when video.currentTime = 10, image of frame 5s is generated.
Cause
The problem is that seeking video (by setting it's currentTime) is asynchronous.
You need to listen to the seeked event or else it will risk take the actual current frame which is likely your old value.
As it is asynchronous you must not use the setInterval() as it is asynchronous too and you will not be able to properly synchronize when the next frame is seeked to. There is no need to use setInterval() as we will utilize the seeked event instead which will keep everything is sync.
Solution
By re-writing the code a little you can use the seeked event to go through the video to capture the correct frame as this event ensures us that we are actually at the frame we requested by setting the currentTime property.
Example
// global or parent scope of handlers
var video = document.getElementById("video"); // added for clarity: this is needed
var i = 0;
video.addEventListener('loadeddata', function() {
this.currentTime = i;
});
Add this event handler to the party:
video.addEventListener('seeked', function() {
// now video has seeked and current frames will show
// at the time as we expect
generateThumbnail(i);
// when frame is captured, increase here by 5 seconds
i += 5;
// if we are not past end, seek to next interval
if (i <= this.duration) {
// this will trigger another seeked event
this.currentTime = i;
}
else {
// Done!, next action
}
});
If you'd like to extract all frames from a video, see this answer. The example below assumes that you want to extract a frame every 5 seconds, as OP requested.
This answer requires WebCodecs which is supported in Chrome and Edge as of writing.
<canvas id="canvasEl"></canvas>
<script type="module">
import getVideoFrames from "https://deno.land/x/get_video_frames#v0.0.8/mod.js"
let ctx = canvasEl.getContext("2d");
// `getVideoFrames` requires a video URL as input.
// If you have a file/blob instead of a videoUrl, turn it into a URL like this:
let videoUrl = URL.createObjectURL(fileOrBlob);
const saveFrameEverySeconds = 5;
let elapsedSinceLastSavedFrame = 0;
await getVideoFrames({
videoUrl,
onFrame(frame) { // `frame` is a VideoFrame object:
elapsedSinceLastSavedFrame += frame.duration / 1e6; // frame.duration is in microseconds, so we convert to seconds
if(elapsedSinceLastSavedFrame > saveFrameEverySeconds) {
ctx.drawImage(frame, 0, 0, canvasEl.width, canvasEl.height);
elapsedSinceLastSavedFrame = 0;
}
frame.close();
},
onConfig(config) {
canvasEl.width = config.codedWidth;
canvasEl.height = config.codedHeight;
},
});
URL.revokeObjectURL(fileOrBlob); // revoke URL to prevent memory leak
</script>
Demo: https://jsbin.com/qovapeziqi/edit?html,output
Github: https://github.com/josephrocca/getVideoFrames.js

Categories