Capturing video stills with a bookmarklet

Cough… My blog is back!

Last week a friend shared the video trailer for the upcoming Indiana Jones flick, watching it I noticed how obviously superimposed Harrison Ford’s head looked on the horse rider. Not to knock the effects artists! Like the rest of us, I’m sure they were doing the best with what they had on hand.

This inane observation only fits my story because I went on to attempt to screenshot the offending image. Frustratingly, while the trailer was paused there was a bunch of user interface rubbish over the video image, including a dark overlay (maybe this was in Slack, I forget).

This little annoyance primed me this weekend when I was browsing my RSS feed and opened a post from Dave Rupert How to capture single frame from an HTML video.

I’m not sure how Dave keeps his console scripts handy, but my approach is to create a bookmarklet.

Making a bookmarklet

Dave’s script in nice and simple:

const v = document.querySelector('video')
let c = document.createElement('canvas')
c.height = v.videoHeight || parseInt(v.style.height)
c.width = v.videoWidth || parseInt(v.style.width)
const ctx = c.getContext('2d')
ctx.drawImage(v, 0, 0)
const wnd = window.open('')
wnd.document.write(`<img src="${c.toDataURL()}"/>`)

My first attempt to make it into bookmarklet was overly crude. I pasted the Dave’s JavaScript into a BBEdit document and ran John Gruber’s JavaScript Bookmarklet Builder.

It worked, but only the first time I clicked it without reloading the page.

The reason is not complicated, running the script again throws an error when the first const variable is encountered.

Preventing variables from being accidentally redefined is one of the reasons const exists. Running the script twice does exactly that. Side note: using this script in Chrome’s console dodges this problem since Google fixed it specially for the console: Support for const redeclarations in the Console.

Wrapping the script in an Immediately Invoked Function Expression (IFFE) fixes this by containing the defined const within the scope of a function.

(function(){
/* code with const here */
})()

Progress! Subsequently though, I discovered that it was useful to reuse some of these variables after-all. In particular the reference to the opened window. Reusing the same window will collate multiple created images from a particular video in one place. Much more convenient than the alternative.

Dave’s first posted script presumed the size of the video in the page matched the size of the video. This resulting in images sat within a larger transparent box, depending on the scaling of the image. Looking into this I learnt the video element provides videoHeight and videoWidth properties that match the pixel dimensions of the image grabbed by getContext. Meanwhile, with no help from me, Dave has updated his script to use these properties.

To shorten this story before it gets too dreary, this is what I ended up with:

(function(w){
	const capture = {
		v: document.querySelector('video'),
		c: document.createElement('canvas')
	};

	const cap = w.capture || capture;

	cap.c.height = parseInt(cap.v.videoHeight);
	cap.c.width = parseInt(cap.v.videoWidth);

	cap.ctx = cap.c.getContext('2d');
	cap.ctx.drawImage(cap.v, 0, 0);

	cap.tab = cap.tab && cap.tab.opener ? cap.tab : w.open('capture');
	cap.tab.document.write(`<img src="${cap.c.toDataURL()}"/>`);

	w.capture = cap;
})(window);

The bookmarket

I’ve published the appropriately encoded (compiled?) bookmarket here: Video still bookmarklet.

Blogging the dream

Yay me! Yay blogging! In summary:

  1. Read a useful tip for bloggers in my RSS reader.
  2. Built something to make a tool based on the tip.
  3. Encountered some problems.
  4. Learnt some things researching the relevant web APIs.
  5. Fixed the problems and published a new thing to my blog.