Your YouTube screenshot is black because you screenshotted the wrong layer
The Problem
You hit PrintScreen on a video, paste, and get a black rectangle where the picture was. The window frame is there. The controls are there. The video is a hole. The video was never in the pixels you copied.
What actually happened: A hardware-accelerated video does not get painted into the same surface as the rest of the window. The decoder hands frames to the GPU, which composites them into an overlay plane at the very last step, after the window contents have been assembled. A screen capture that reads the window surface reads what is underneath that plane, which is the punch-out colour: black.
This is why the same PrintScreen works fine on some machines and fails on others, why it starts failing after a GPU driver update, and why "disable hardware acceleration" is the advice that keeps getting handed out. Turning off acceleration does fix it, by making the video render into the normal paint path, along with the battery cost.
The Fix
The better move is to stop capturing the window and ask the <video> element for the frame instead. drawImage accepts a <video> and reads the frame it is currently showing:
const v = document.querySelector("video");
const c = document.createElement("canvas");
c.width = v.videoWidth; // NOT v.clientWidth
c.height = v.videoHeight;
c.getContext("2d").drawImage(v, 0, 0);
c.toBlob(b => open(URL.createObjectURL(b)), "image/png");
Paste that into the console on a YouTube tab and you get the exact frame, no black.
The One Line Worth Staring At
v.videoWidth is the resolution of the decoded stream, which is what you want. clientWidth is the size of the CSS box, so using it silently downsamples a 4K frame to whatever your player happens to be sized at in that window. This is the difference between a usable still and a soft 640-pixel-wide one, and it is easy to get wrong because both produce an image.
Set the player to 2160p first if you want 2160p out. videoWidth reports the stream YouTube is currently feeding you, not the best one available.
Straight to the Clipboard
Skipping the file is often what you want, since the frame is usually headed for a slide or a note:
const png = await new Promise(r => c.toBlob(r, "image/png"));
await navigator.clipboard.write([new ClipboardItem({ "image/png": png })]);
Two constraints that will bite you:
- The clipboard only takes PNG here, so if you produced a JPEG blob you have to re-encode before writing.
clipboard.writeneeds a user gesture and a focused document, so it works from a click handler and fails from asetTimeout.
When It Still Comes Out Black
drawImage on a DRM-protected video throws instead of returning pixels. Widevine-protected content - a rented film, most paid streaming - is out of reach by design, and no extension or console snippet changes that. YouTube's ordinary videos are not protected, so they are fine.
Wrap it, because an unhandled throw here looks identical to a broken tool:
try { ctx.drawImage(v, 0, 0); } catch { /* protected stream */ }
Naming the File
If you are saving rather than copying, put the timestamp in the name while you still have it:
const t = Math.floor(v.currentTime);
const stamp = `${String(t / 3600 | 0).padStart(2, "0")}-${String(t / 60 % 60 | 0).padStart(2, "0")}-${String(t % 60).padStart(2, "0")}`;
Frames without a timestamp in the filename are unfindable a week later, which anyone who has taken lecture notes this way has learned the hard way.
The Extension
I packaged this as Screenshot for YouTube - Video Frame Capture. Disclosure: mine, free, no account, no network requests. It puts a camera button in the player controls, binds Alt+S, saves PNG or JPEG or copies to the clipboard, and names files with the video title plus the timestamp above. There is a popup capture button as a fallback for Shorts and embeds, where the standard control bar is absent.
Getting the button to survive YouTube's SPA navigation needed a MutationObserver plus the yt-navigate-finish event, which is the least interesting and most fiddly part of the whole thing.
If you only need one frame today, the console snippet above is the entire mechanism. The extension exists because the twentieth time you do it by hand, videoWidth is not the thing you want to be remembering.
Comments
No comments yet. Start the discussion.