MediaRecorder switch video tracks - html

I am using MediaRecorder API to record videos in web applications. The application has the option to switch between the camera and screen. I am using Canvas to augment stream recording. The logic involves capturing stream from the camera and redirecting it to the video element. This video is then rendered on canvas and the stream from canvas is passed to MediaRecorder.
What I noticed is that switching from screen to video (and vice-versa) works fine as long as the user doesn't switch/minimize the chrome window. The canvas rendering uses requestAnimationFrame and it freezes after the tab loses its focus.
Is there any way to instruct chrome not to pause the execution of requestAnimationFrame? Is there any alternate way to switch streams without impacting MediaRecorder recording?
Update:
After reading through the documentation, tabs which play audio or having active websocket connection are not throttled. This is something which we are not doing at this moment. This might be a workaround, but hoping for any alternative solution from community. (setTimeout or setInterval are too throttled and hence not using that, plus it impacts rendering quality)
Update 2:
I could able to fix this problem using Worker. Instead of using Main UI Thread for requestAnimationFrame, the worker is invoking the API and the notification is sent to Main Thread via postMessage. Upon completion of rendering by UI Thread, a message is sent back to Worker. There is also a delta period calculation to throttle overwhelming messages from worker.

There is an ongoing proposal to add a .replaceTrack() method to the MediaRecorder API, but for the time being, the specs still read
If at any point, a track is added to or removed from streamโ€™s track set, the UA MUST immediately stop gathering data, discard any data that it has gathered [...]
And that's what is implemented.
So we still have to rely on hacks to make this by ourselves...
The best one is probably to create a local RTC connection, and to record the receiving end.
// creates a mixable stream
async function mixableStream( initial_track ) {
const source_stream = new MediaStream( [] );
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
pc1.onicecandidate = (evt) => pc2.addIceCandidate( evt.candidate );
pc2.onicecandidate = (evt) => pc1.addIceCandidate( evt.candidate );
const wait_for_stream = waitForEvent( pc2, 'track')
.then( evt => new MediaStream( [ evt.track ] ) );
pc1.addTrack( initial_track, source_stream );
await waitForEvent( pc1, 'negotiationneeded' );
try {
await pc1.setLocalDescription( await pc1.createOffer() );
await pc2.setRemoteDescription( pc1.localDescription );
await pc2.setLocalDescription( await pc2.createAnswer() );
await pc1.setRemoteDescription( pc2.localDescription );
} catch ( err ) {
console.error( err );
}
return {
stream: await wait_for_stream,
async replaceTrack( new_track ) {
const sender = pc1.getSenders().find( ( { track } ) => track.kind == new_track.kind );
return sender && sender.replaceTrack( new_track ) ||
Promise.reject( "no such track" );
}
}
}
{ // remap unstable FF version
const proto = HTMLMediaElement.prototype;
if( !proto.captureStream ) { proto.captureStream = proto.mozCaptureStream; }
}
waitForEvent( document.getElementById( 'starter' ), 'click' )
.then( (evt) => evt.target.parentNode.remove() )
.then( (async() => {
const urls = [
"2/22/Volcano_Lava_Sample.webm",
"/a/a4/BBH_gravitational_lensing_of_gw150914.webm"
].map( (suffix) => "https://upload.wikimedia.org/wikipedia/commons/" + suffix );
const switcher_btn = document.getElementById( 'switcher' );
const stop_btn = document.getElementById( 'stopper' );
const video_out = document.getElementById( 'out' );
let current = 0;
// see below for 'recordVid'
const video_tracks = await Promise.all( urls.map( (url, index) => getVideoTracks( url ) ) );
const mixable_stream = await mixableStream( video_tracks[ current ].track );
switcher_btn.onclick = async (evt) => {
current = +!current;
await mixable_stream.replaceTrack( video_tracks[ current ].track );
};
// final recording part below
// only for demo, so we can see what happens now
video_out.srcObject = mixable_stream.stream;
const rec = new MediaRecorder( mixable_stream.stream );
const chunks = [];
rec.ondataavailable = (evt) => chunks.push( evt.data );
rec.onerror = console.log;
rec.onstop = (evt) => {
const final_file = new Blob( chunks );
video_tracks.forEach( (track) => track.stop() );
// only for demo, since we did set its srcObject
video_out.srcObject = null;
video_out.src = URL.createObjectURL( final_file );
switcher_btn.remove();
stop_btn.remove();
const anchor = document.createElement( 'a' );
anchor.download = 'file.webm';
anchor.textContent = 'download';
anchor.href = video_out.src;
document.body.prepend( anchor );
};
stop_btn.onclick = (evt) => rec.stop();
rec.start();
}))
.catch( console.error )
// some helpers below
// returns a video loaded to given url
function makeVid( url ) {
const vid = document.createElement('video');
vid.crossOrigin = true;
vid.loop = true;
vid.muted = true;
vid.src = url;
return vid.play()
.then( (_) => vid );
}
/* Records videos from given url
** #method stop() ::pauses the linked <video>
** #property track ::the video track
*/
async function getVideoTracks( url ) {
const player = await makeVid( url );
const track = player.captureStream().getVideoTracks()[ 0 ];
return {
track,
stop() { player.pause(); }
};
}
// Promisifies EventTarget.addEventListener
function waitForEvent( target, type ) {
return new Promise( (res) => target.addEventListener( type, res, { once: true } ) );
}
video { max-height: 100vh; max-width: 100vw; vertical-align: top; }
.overlay {
background: #ded;
position: fixed;
z-index: 999;
height: 100vh;
width: 100vw;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
}
<div class="overlay">
<button id="starter">start demo</button>
</div>
<button id="switcher">switch source</button>
<button id="stopper">stop recording</button>
<video id="out" muted controls autoplay></video>
Otherwise you can still go the canvas way, with the Web Audio Timer I made for when the page is blurred, even though this will not work in Firefox since they do internally hook to rAF to push new frames in the recorder...

I had the same problem and trying to figure it out without too much complexity such as Canvas or SourceBuffer.
I used the PeerConnection for same page to make a connection. Once the connection is made you can use a rtpSender via peerconnection.addTrack And from there you can easily switch.
I just made a library and a demo that you can find:
https://github.com/meething/StreamSwitcher/

Related

Chrome resource loading behaviour

Today I ran into rather a strange behaviour of Chrome. I was playing with PerformanceObserver and found out that when you add two stylesheets with the same URL to the DOM very quickly then chrome fires only one request which obviously makes sense as it saves network load.
const testCase = async () => {
let numberOfRecords = 0
const observer = new PerformanceObserver((entryList) => {
const performanceEntries = entryList.getEntries()
numberOfRecords += performanceEntries.length
})
observer.observe({ entryTypes: ['resource'] })
// Test: Only one performance record is created because links are added at the same time
// and chrome detects duplicate request
const linkElement1 = document.createElement('link')
linkElement1.rel = 'stylesheet'
linkElement1.href = 'https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css'
document.head.appendChild(linkElement1)
const linkElement2 = document.createElement('link')
linkElement2.rel = 'stylesheet'
linkElement2.href = 'https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css'
document.head.appendChild(linkElement2)
// wait a little bit so performance observer callback is called
await new Promise((resolve) => {
setTimeout(() => resolve(), 1000)
})
console.assert(numberOfRecords === 1, 'Test')
console.log('Test finished')
}
testCase()
When sleep time is added between adding link nodes to DOM then chrome fires two requests (the second one is taken from cache)
const testCase = async () => {
let numberOfRecords = 0
const observer = new PerformanceObserver((entryList) => {
const performanceEntries = entryList.getEntries()
numberOfRecords += performanceEntries.length
})
observer.observe({ entryTypes: ['resource'] })
// Test: Only one performance record is created because links are added at the same time
// and chrome detects duplicate request
const linkElement1 = document.createElement('link')
linkElement1.rel = 'stylesheet'
linkElement1.href = 'https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css'
document.head.appendChild(linkElement1)
// wait here so chrome triggers two requests
await new Promise((resolve) => {
setTimeout(() => resolve(), 1000)
})
const linkElement2 = document.createElement('link')
linkElement2.rel = 'stylesheet'
linkElement2.href = 'https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css'
document.head.appendChild(linkElement2)
// wait a little bit so performance observer callback is called
await new Promise((resolve) => {
setTimeout(() => resolve(), 1000)
})
console.assert(numberOfRecords === 2, 'Test')
console.log('Test finished')
}
testCase()
However when I run this second code via automated test (webdriver.io) or I try it on cloud service like Browserstack/Lambdatest (the same browser version, OS version) it fails as it triggers only one request. So I wonder what's the difference?
To see it by yourself you can open some empty page (it's quite important that page is empty and doesn't contain any background requests) and copy the code examples to console.
I just wonder whether you disabled the cache for the automation tests. In Chrome, apparently if you didn't tick "Disable cache", then you second test should fail.

Event for every frame of HTML Video?

I'd like to build an event handler to deal with each new frame of an HTML 5 Video element. Unfortunately, there's no built in event that fires for each new video frame (the timeupdate event is the closest but fires for each time change rather than each video frame).
Has anyone else run into this same issue? Is there a good way around it?
There is an HTMLVideoElement.requestVideoFrameCallback() method that is still being drafted, and thus neither stable, nor widely implemented (it is only in Chromium based browsers), but which does what you want, along with giving many other details about that frame.
For your Firefox users, this browser has a non standard seekToNextFrame() method, which, depending on what you want to do you could use. This won't exactly work as an event though, it more of a way to, well... seek to the next frame. So this will greatly affect the playing of the video, since it won't respect the duration of each frames.
And for Safari users, the closest is indeed the timeupdate event, but as you know, this doesn't really match the displayed frame.
(async() => {
const log = document.querySelector("pre");
const vid = document.querySelector("video");
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
if( vid.requestVideoFrameCallback ) {
await vid.play();
canvas.width = vid.videoWidth;
canvas.height = vid.videoHeight;
ctx.filter = "invert(1)";
const drawingLoop = (timestamp, frame) => {
log.textContent = `timestamp: ${ timestamp }
frame: ${ JSON.stringify( frame, null, 4 ) }`;
ctx.drawImage( vid, 0, 0 );
vid.requestVideoFrameCallback( drawingLoop );
};
vid.requestVideoFrameCallback( drawingLoop );
}
else if( vid.seekToNextFrame ) {
const requestNextFrame = (callback) => {
vid.addEventListener( "seeked", () => callback( vid.currentTime ), { once: true } );
vid.seekToNextFrame();
};
await vid.play();
await vid.pause();
canvas.width = vid.videoWidth;
canvas.height = vid.videoHeight;
ctx.filter = "invert(1)";
const drawingLoop = (timestamp) => {
log.textContent = "timestamp: " + timestamp;
ctx.drawImage( vid, 0, 0 );
requestNextFrame( drawingLoop );
};
requestNextFrame( drawingLoop );
}
else {
console.error("Your browser doesn't support any of these methods, we should fallback to timeupdate");
}
})();
video, canvas {
width: 260px;
}
<pre></pre>
<video src="https://upload.wikimedia.org/wikipedia/commons/2/22/Volcano_Lava_Sample.webm" muted controls></video>
<canvas></canvas>
Note that the encoded frames and the displayed ones are not necessarily the same thing anyway and that browser may not respect the encoded frame rate at all. So based on what you are willing to do, maybe a simple requestAnimationFrame loop, which would fire at every update of the monitor might be better.

Implementing Three.js SSAOPass in AFrame

I was able to successfully integrate Threejs Effect composer in aframe as a component by exporting everything as THREE.Effectcomposer, THREE.SSAOPass etc. and adding the effect inside a aframe component and i tweaked the AFrame renderer to update the effects in the scene. OutlinePass from threejs worked fine in this code but SSAO is not working and i don't get any errors. Please someone help me figure out the problem. the code for SSAOPass looks like this
AFRAME.registerComponent('ssao', {
init: function () {
this.el.addEventListener('that', evt => this.onEnter());
this.el.addEventListener('mouseleave', evt => this.onLeave());
setTimeout(() => this.el.emit("that"), 2000);
},
onEnter: function () {
const scene = this.el.sceneEl.object3D;
const camera = this.el.sceneEl.camera;
const renderer = this.el.sceneEl.renderer;
const render = renderer.render;
const composer = new THREE.EffectComposer(renderer);
//let renderPass = new THREE.RenderPass(scene, camera);
//let outlinePass = new THREE.OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera);
const ssaoPass = new THREE.SSAOPass( scene, camera, window.innerWidth, window.innerHeight );
//composer.addPass(renderPass);
//composer.addPass(outlinePass);
ssaoPass.kernelRadius = 16;
composer.addPass( ssaoPass );
// let objects = [];
// this.el.object3D.traverse(node => {
// if (!node.isMesh) return;
// objects.push(node);
// });
// outlinePass.selectedObjects = objects;
// outlinePass.renderToScreen = true;
// outlinePass.edgeStrength = this.data.strength;
// outlinePass.edgeGlow = this.data.glow;
// outlinePass.visibleEdgeColor.set(this.data.color);
// HACK the AFRAME render method (a bit ugly)
const clock = new THREE.Clock();
this.originalRenderMethod = render;
let calledByComposer = false;
renderer.render = function () {
if (calledByComposer) {
render.apply(renderer, arguments);
} else {
calledByComposer = true;
composer.render(clock.getDelta());
calledByComposer = false;
}
};
},
onLeave: function () {
this.el.sceneEl.renderer.render = this.originalRenderMethod;
},
remove: function () {
this.onLeave();
}
});
I have also created a glitch project which i am sharing here. Please feel free to join and collaborate in my project
Edit link: https://glitch.com/edit/#!/accessible-torpid-partridge
Site link:https://accessible-torpid-partridge.glitch.me
Thanks in advance
The code is correct, all you need is to tweak the exposed SSAOShader uniforms: SSAOPass.kernelRadius, SSAOPass.minDistance, SSAOPass.maxDistance - like in the Three.js example.
Keep in mind - the scale in the example is huge, so the values will need to be different in a default aframe scene.
It's a good idea to be able to dynamically update a component (via setAttribute() if you properly handle updates), so you can see what's going on in realtime. Something like I did here - SSAO in a-frame (also based on Don McCurdys gist.
I've used some basic HTML elements, most threejs examples use dat.GUI - it is made for demo / debug tweaks.

Convert HTML5 Canvas Sequence to a Video File

I'd like to convert an animation in HTML5 canvas to a video file that could be uploaded to YouTube. Is there any sort of screen capture API or something that could allow me to do this programatically?
Back to 2020
Solved it by using MediaRecorder API. It builds exactly to do that, among other things.
Here is a solution that recorded X ms of canvas video
you can extend it with Buttons UI to start, pause, resume, stop, generate URL.
function record(canvas, time) {
var recordedChunks = [];
return new Promise(function (res, rej) {
var stream = canvas.captureStream(25 /*fps*/);
mediaRecorder = new MediaRecorder(stream, {
mimeType: "video/webm; codecs=vp9"
});
//ondataavailable will fire in interval of `time || 4000 ms`
mediaRecorder.start(time || 4000);
mediaRecorder.ondataavailable = function (event) {
recordedChunks.push(event.data);
// after stop `dataavilable` event run one more time
if (mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
}
mediaRecorder.onstop = function (event) {
var blob = new Blob(recordedChunks, {type: "video/webm" });
var url = URL.createObjectURL(blob);
res(url);
}
})
}
How to use:
const recording = record(canvas, 10000)
// play it on another video element
var video$ = document.createElement('video')
document.body.appendChild(video$)
recording.then(url => video$.setAttribute('src', url) )
// download it
var link$ = document.createElement('a')
link$.setAttribute('download','recordingVideo')
recording.then(url => {
link$.setAttribute('href', url)
link$.click()
})
Firefox has an experimental feature (disabled by default) that is called HTMLCanvasElement.captureStream()
Essentially it captures the canvas element as a video stream which can then be sent to another computer using RTCPeerConnection() or perhaps you can use the YouTube Live Streaming API to stream directly.
See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/captureStream
Also: https://developers.google.com/youtube/v3/live/getting-started
There exist the whammy library which claims to produce webm videos from stills using JavaScript:
http://antimatter15.com/wp/2012/08/whammy-a-real-time-javascript-webm-encoder/
Note that there are limitations (as to be expected). This encoder bases itself on the webp image format which is currently only supported in Chrome (perhaps the new Opera too but I haven't checked). This means you can't encode in other browsers unless you find a way to encode the image you want to use as a webp image first (see this link for possible solution for that).
Beyond that there is no way to create a video file from images using JavaScript and canvas using native browser APIs.
FileSaver.js + ffmpeg on the command line
With FilSaver.js we can download each canvas frame as PNG: Save to Local File from Blob
Then we just convert the PNGs to any video format with ffmpeg from the command line: How to create a video from images with FFmpeg?
Chromium 75 asks if you want to allow it to save multiple images. Then once you say yes, it downloads the images automatically one by one under your download folder, named as 0.png, 1.png, etc.
It also worked in Firefox 68, but less well, because the browser opens a bunch of "Do you want to save this file" windows. They do have a "do the same for similar downloads" popup, but you have to be quick to select it and hit enter, or else a new popup comes along!
To stop it, you have to close the tab, or add a stop button and some JavaScript logic.
var canvas = document.getElementById("my-canvas");
var ctx = canvas.getContext("2d");
var pixel_size = 1;
var t = 0;
/* We need this to fix t because toBlob calls are asynchronous. */
function createBlobFunc(t) {
return function(blob) {
saveAs(blob, t.toString() + '.png');
};
}
function draw() {
console.log("draw");
for (x = 0; x < canvas.width; x += pixel_size) {
for (y = 0; y < canvas.height; y += pixel_size) {
var b = ((1.0 + Math.sin(t * Math.PI / 16)) / 2.0);
ctx.fillStyle =
"rgba(" +
(x / canvas.width) * 255 + "," +
(y / canvas.height) * 255 + "," +
b * 255 +
",255)"
;
ctx.fillRect(x, y, pixel_size, pixel_size);
}
}
canvas.toBlob(createBlobFunc(t));
t++;
window.requestAnimationFrame(draw);
}
window.requestAnimationFrame(draw);
<canvas id="my-canvas" width="512" height="512" style="border:1px solid black;"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js"></script>
GitHub upstream.
Here's an image to GIF output using this instead: https://askubuntu.com/questions/648244/how-do-i-create-an-animated-gif-from-still-images-preferably-with-the-command-l
Frames get skipped if the FPS is too high
This can be observed by reducing the size of the canvas in the above demo to speed things up. At 32x32, my Chromium 77 download in chunks of about 10 files and skips about 50 files in between...
Unfortunately, there is no way to wait for the downloads to finish... close window after file save in FileSaver.js
So the only solution I can see if you have high framerate is framerate limiting... Controlling fps with requestAnimationFrame? Here is a live demo: https://cirosantilli.com/#html-canvas
Maybe one day someone will answer:
H.264 video encoder in javascript
Running ffmpeg in browser - options?
and then we will be able to download the video directly!
Here is an OpenGL version if you decide that the browser is not for you :-) How to use GLUT/OpenGL to render to a file?
Tested in Ubuntu 19.04.
This should help, it allows you to drop some images that get converted into HTML5 CANVAS and then converted into webm video: http://techslides.com/demos/image-video/create.html
Pure javascript, no other 3rd-package.
If you have a video and want to take some frames, you can try as below
class Video2Canvas {
/**
* #description Create a canvas and save the frame of the video that you are giving.
* #param {HTMLVideoElement} video
* #param {Number} fps
* #see https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_manipulation#video_manipulation
* */
constructor(video, fps) {
this.video = video
this.fps = fps
this.canvas = document.createElement("canvas");
[this.canvas.width, this.canvas.height] = [video.width, video.height]
document.querySelector("body").append(this.canvas)
this.ctx = this.canvas.getContext('2d')
this.initEventListener()
}
initEventListener() {
this.video.addEventListener("play", ()=>{
const timeout = Math.round(1000/this.fps)
const width = this.video.width
const height = this.video.height
const recordFunc = ()=> {
if (this.video.paused || this.video.ended) {
return
}
this.ctx.drawImage(this.video, 0, 0, width, height)
const frame = this.ctx.getImageData(0, 0, width, height)
// ... // you can make some modifications to change the frame. For example, create the grayscale frame: https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_manipulation#video_manipulation
// ๐Ÿ‘‡ Below is the options. That saves each frame as a link. If you wish, then you can click the link to download the picture.
const range = document.createRange()
const frag = range.createContextualFragment('<div><a></a></div>')
const tmpCanvas = document.createElement('canvas')
tmpCanvas.width = this.canvas.width
tmpCanvas.height = this.canvas.height
tmpCanvas.getContext('2d').putImageData(frame, 0, 0)
const a = frag.querySelector('a')
a.innerText = "my.png"
a.download = "my.png"
const quality = 1.0
a.href = tmpCanvas.toDataURL("image/png", quality)
a.append(tmpCanvas)
document.querySelector('body').append(frag)
setTimeout(recordFunc, timeout)
}
setTimeout(recordFunc, timeout)
})
}
}
const v2c = new Video2Canvas(document.querySelector("video"), 1)
<video id="my-video" controls="true" width="480" height="270" crossorigin="anonymous">
<source src="http://jplayer.org/video/webm/Big_Buck_Bunny_Trailer.webm" type="video/webm">
</video>
If you want to edit the video (for example, take 5~8sec+12~15sec and then create a new one) you can try
class CanvasRecord {
/**
* #param {HTMLCanvasElement} canvas
* #param {Number} fps
* #param {string} mediaType: video/webm, video/mp4(not support yet) ...
* */
constructor(canvas, fps, mediaType) {
this.canvas = canvas
const stream = canvas.captureStream(25) // fps // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/captureStream
this.mediaRecorder = new MediaRecorder(stream, { // https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/MediaRecorder
mimeType: mediaType
})
this.initControlBtn()
this.chunks = []
this.mediaRecorder.ondataavailable = (event) => {
this.chunks.push(event.data)
}
this.mediaRecorder.onstop = (event) => {
const blob = new Blob(this.chunks, {
type: mediaType
})
const url = URL.createObjectURL(blob)
// ๐Ÿ‘‡ Below is a test code for you to know you are successful. Also, you can download it if you wish.
const video = document.createElement('video')
video.src = url
video.onend = (e) => {
URL.revokeObjectURL(this.src);
}
document.querySelector("body").append(video)
video.controls = true
}
}
initControlBtn() {
const range = document.createRange()
const frag = range.createContextualFragment(`<div>
<button id="btn-start">Start</button>
<button id="btn-pause">Pause</button>
<button id="btn-resume">Resume</button>
<button id="btn-end">End</button>
</div>
`)
const btnStart = frag.querySelector(`button[id="btn-start"]`)
const btnPause = frag.querySelector(`button[id="btn-pause"]`)
const btnResume = frag.querySelector(`button[id="btn-resume"]`)
const btnEnd = frag.querySelector(`button[id="btn-end"]`)
document.querySelector('body').append(frag)
btnStart.onclick = (event) => {
this.chunks = [] // clear
this.mediaRecorder.start() // https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/start
console.log(this.mediaRecorder.state) // https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/state
}
btnPause.onclick = (event) => { // https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/pause
this.mediaRecorder.pause()
console.log(this.mediaRecorder.state)
}
btnResume.onclick = (event) => {
this.mediaRecorder.resume()
console.log(this.mediaRecorder.state)
}
btnEnd.onclick = (event) => {
this.mediaRecorder.requestData() // trigger ``ondataavailable`` // https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/requestData
this.mediaRecorder.stop()
console.log(this.mediaRecorder.state)
}
}
}
class Video2Canvas {
/**
* #description Create a canvas and save the frame of the video that you are giving.
* #param {HTMLVideoElement} video
* #param {Number} fps
* #see https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_manipulation#video_manipulation
* */
constructor(video, fps) {
this.video = video
this.fps = fps
this.canvas = document.createElement("canvas");
[this.canvas.width, this.canvas.height] = [video.width, video.height]
document.querySelector("body").append(this.canvas)
this.ctx = this.canvas.getContext('2d')
this.initEventListener()
}
initEventListener() {
this.video.addEventListener("play", ()=>{
const timeout = Math.round(1000/this.fps)
const width = this.video.width
const height = this.video.height
const recordFunc = ()=> {
if (this.video.paused || this.video.ended) {
return
}
this.ctx.drawImage(this.video, 0, 0, width, height)
/*
const frame = this.ctx.getImageData(0, 0, width, height)
// ... // you can make some modifications to change the frame. For example, create the grayscale frame: https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_manipulation#video_manipulation
// ๐Ÿ‘‡ Below is the options. That saves each frame as a link. If you wish, then you can click the link to download the picture.
const range = document.createRange()
const frag = range.createContextualFragment('<div><a></a></div>')
const tmpCanvas = document.createElement('canvas')
tmpCanvas.width = this.canvas.width
tmpCanvas.height = this.canvas.height
tmpCanvas.getContext('2d').putImageData(frame, 0, 0)
const a = frag.querySelector('a')
a.innerText = "my.png"
a.download = "my.png"
const quality = 1.0
a.href = tmpCanvas.toDataURL("image/png", quality)
a.append(tmpCanvas)
document.querySelector('body').append(frag)
*/
setTimeout(recordFunc, timeout)
}
setTimeout(recordFunc, timeout)
})
}
}
(()=>{
const v2c = new Video2Canvas(document.querySelector("video"), 60)
const canvasRecord = new CanvasRecord(v2c.canvas, 25, 'video/webm')
v2c.video.addEventListener("play", (event)=>{
if (canvasRecord.mediaRecorder.state === "inactive") {
return
}
document.getElementById("btn-resume").click()
})
v2c.video.addEventListener("pause", (event)=>{
if (canvasRecord.mediaRecorder.state === "inactive") {
return
}
document.getElementById("btn-pause").click()
})
})()
<video id="my-video" controls="true" width="480" height="270" crossorigin="anonymous">
<source src="http://jplayer.org/video/webm/Big_Buck_Bunny_Trailer.webm" type="video/webm">
</video>

Code to take snapshot of a html page? [duplicate]

Google's "Report a Bug" or "Feedback Tool" lets you select an area of your browser window to create a screenshot that is submitted with your feedback about a bug.
Screenshot by Jason Small, posted in a duplicate question.
How are they doing this? Google's JavaScript feedback API is loaded from here and their overview of the feedback module will demonstrate the screenshot capability.
JavaScript can read the DOM and render a fairly accurate representation of that using canvas. I have been working on a script which converts HTML into a canvas image. Decided today to make an implementation of it into sending feedbacks like you described.
The script allows you to create feedback forms which include a screenshot, created on the client's browser, along with the form. The screenshot is based on the DOM and as such may not be 100% accurate to the real representation as it does not make an actual screenshot, but builds the screenshot based on the information available on the page.
It does not require any rendering from the server, as the whole image is created on the client's browser. The HTML2Canvas script itself is still in a very experimental state, as it does not parse nearly as much of the CSS3 attributes I would want it to, nor does it have any support to load CORS images even if a proxy was available.
Still quite limited browser compatibility (not because more couldn't be supported, just haven't had time to make it more cross browser supported).
For more information, have a look at the examples here:
http://hertzen.com/experiments/jsfeedback/
edit
The html2canvas script is now available separately here and some examples here.
edit 2
Another confirmation that Google uses a very similar method (in fact, based on the documentation, the only major difference is their async method of traversing/drawing) can be found in this presentation by Elliott Sprehn from the Google Feedback team:
http://www.elliottsprehn.com/preso/fluentconf/
Your web app can now take a 'native' screenshot of the client's entire desktop using getUserMedia():
Have a look at this example:
https://www.webrtc-experiment.com/Pluginfree-Screen-Sharing/
The client will have to be using chrome (for now) and will need to enable screen capture support under chrome://flags.
PoC
As Niklas mentioned you can use the html2canvas library to take a screenshot using JS in the browser. I will extend his answer in this point by providing an example of taking a screenshot using this library ("Proof of Concept"):
function report() {
let region = document.querySelector("body"); // whole screen
html2canvas(region, {
onrendered: function(canvas) {
let pngUrl = canvas.toDataURL(); // png in dataURL format
let img = document.querySelector(".screen");
img.src = pngUrl;
// here you can allow user to set bug-region
// and send it with 'pngUrl' to server
},
});
}
.container {
margin-top: 10px;
border: solid 1px black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.min.js"></script>
<div>Screenshot tester</div>
<button onclick="report()">Take screenshot</button>
<div class="container">
<img width="75%" class="screen">
</div>
In report() function in onrendered after getting image as data URI you can show it to the user and allow him to draw "bug region" by mouse and then send a screenshot and region coordinates to the server.
In this example async/await version was made: with nice makeScreenshot() function.
UPDATE
Simple example which allows you to take screenshot, select region, describe bug and send POST request (here jsfiddle) (the main function is report()).
async function report() {
let screenshot = await makeScreenshot(); // png dataUrl
let img = q(".screen");
img.src = screenshot;
let c = q(".bug-container");
c.classList.remove('hide')
let box = await getBox();
c.classList.add('hide');
send(screenshot,box); // sed post request with bug image, region and description
alert('To see POST requset with image go to: chrome console > network tab');
}
// ----- Helper functions
let q = s => document.querySelector(s); // query selector helper
window.report = report; // bind report be visible in fiddle html
async function makeScreenshot(selector="body")
{
return new Promise((resolve, reject) => {
let node = document.querySelector(selector);
html2canvas(node, { onrendered: (canvas) => {
let pngUrl = canvas.toDataURL();
resolve(pngUrl);
}});
});
}
async function getBox(box) {
return new Promise((resolve, reject) => {
let b = q(".bug");
let r = q(".region");
let scr = q(".screen");
let send = q(".send");
let start=0;
let sx,sy,ex,ey=-1;
r.style.width=0;
r.style.height=0;
let drawBox= () => {
r.style.left = (ex > 0 ? sx : sx+ex ) +'px';
r.style.top = (ey > 0 ? sy : sy+ey) +'px';
r.style.width = Math.abs(ex) +'px';
r.style.height = Math.abs(ey) +'px';
}
//console.log({b,r, scr});
b.addEventListener("click", e=>{
if(start==0) {
sx=e.pageX;
sy=e.pageY;
ex=0;
ey=0;
drawBox();
}
start=(start+1)%3;
});
b.addEventListener("mousemove", e=>{
//console.log(e)
if(start==1) {
ex=e.pageX-sx;
ey=e.pageY-sy
drawBox();
}
});
send.addEventListener("click", e=>{
start=0;
let a=100/75 //zoom out img 75%
resolve({
x:Math.floor(((ex > 0 ? sx : sx+ex )-scr.offsetLeft)*a),
y:Math.floor(((ey > 0 ? sy : sy+ey )-b.offsetTop)*a),
width:Math.floor(Math.abs(ex)*a),
height:Math.floor(Math.abs(ex)*a),
desc: q('.bug-desc').value
});
});
});
}
function send(image,box) {
let formData = new FormData();
let req = new XMLHttpRequest();
formData.append("box", JSON.stringify(box));
formData.append("screenshot", image);
req.open("POST", '/upload/screenshot');
req.send(formData);
}
.bug-container { background: rgb(255,0,0,0.1); margin-top:20px; text-align: center; }
.send { border-radius:5px; padding:10px; background: green; cursor: pointer; }
.region { position: absolute; background: rgba(255,0,0,0.4); }
.example { height: 100px; background: yellow; }
.bug { margin-top: 10px; cursor: crosshair; }
.hide { display: none; }
.screen { pointer-events: none }
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.min.js"></script>
<body>
<div>Screenshot tester</div>
<button onclick="report()">Report bug</button>
<div class="example">Lorem ipsum</div>
<div class="bug-container hide">
<div>Select bug region: click once - move mouse - click again</div>
<div class="bug">
<img width="75%" class="screen" >
<div class="region"></div>
</div>
<div>
<textarea class="bug-desc">Describe bug here...</textarea>
</div>
<div class="send">SEND BUG</div>
</div>
</body>
Get screenshot as Canvas or Jpeg Blob / ArrayBuffer using getDisplayMedia API:
FIX 1: Use the getUserMedia with chromeMediaSource only for Electron.js
FIX 2: Throw error instead return null object
FIX 3: Fix demo to prevent the error: getDisplayMedia must be called from a user gesture handler
// docs: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
// see: https://www.webrtc-experiment.com/Pluginfree-Screen-Sharing/#20893521368186473
// see: https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Pluginfree-Screen-Sharing/conference.js
function getDisplayMedia(options) {
if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
return navigator.mediaDevices.getDisplayMedia(options)
}
if (navigator.getDisplayMedia) {
return navigator.getDisplayMedia(options)
}
if (navigator.webkitGetDisplayMedia) {
return navigator.webkitGetDisplayMedia(options)
}
if (navigator.mozGetDisplayMedia) {
return navigator.mozGetDisplayMedia(options)
}
throw new Error('getDisplayMedia is not defined')
}
function getUserMedia(options) {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
return navigator.mediaDevices.getUserMedia(options)
}
if (navigator.getUserMedia) {
return navigator.getUserMedia(options)
}
if (navigator.webkitGetUserMedia) {
return navigator.webkitGetUserMedia(options)
}
if (navigator.mozGetUserMedia) {
return navigator.mozGetUserMedia(options)
}
throw new Error('getUserMedia is not defined')
}
async function takeScreenshotStream() {
// see: https://developer.mozilla.org/en-US/docs/Web/API/Window/screen
const width = screen.width * (window.devicePixelRatio || 1)
const height = screen.height * (window.devicePixelRatio || 1)
const errors = []
let stream
try {
stream = await getDisplayMedia({
audio: false,
// see: https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints/video
video: {
width,
height,
frameRate: 1,
},
})
} catch (ex) {
errors.push(ex)
}
// for electron js
if (navigator.userAgent.indexOf('Electron') >= 0) {
try {
stream = await getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
// chromeMediaSourceId: source.id,
minWidth : width,
maxWidth : width,
minHeight : height,
maxHeight : height,
},
},
})
} catch (ex) {
errors.push(ex)
}
}
if (errors.length) {
console.debug(...errors)
if (!stream) {
throw errors[errors.length - 1]
}
}
return stream
}
async function takeScreenshotCanvas() {
const stream = await takeScreenshotStream()
// from: https://stackoverflow.com/a/57665309/5221762
const video = document.createElement('video')
const result = await new Promise((resolve, reject) => {
video.onloadedmetadata = () => {
video.play()
video.pause()
// from: https://github.com/kasprownik/electron-screencapture/blob/master/index.js
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const context = canvas.getContext('2d')
// see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement
context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
resolve(canvas)
}
video.srcObject = stream
})
stream.getTracks().forEach(function (track) {
track.stop()
})
if (result == null) {
throw new Error('Cannot take canvas screenshot')
}
return result
}
// from: https://stackoverflow.com/a/46182044/5221762
function getJpegBlob(canvas) {
return new Promise((resolve, reject) => {
// docs: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
canvas.toBlob(blob => resolve(blob), 'image/jpeg', 0.95)
})
}
async function getJpegBytes(canvas) {
const blob = await getJpegBlob(canvas)
return new Promise((resolve, reject) => {
const fileReader = new FileReader()
fileReader.addEventListener('loadend', function () {
if (this.error) {
reject(this.error)
return
}
resolve(this.result)
})
fileReader.readAsArrayBuffer(blob)
})
}
async function takeScreenshotJpegBlob() {
const canvas = await takeScreenshotCanvas()
return getJpegBlob(canvas)
}
async function takeScreenshotJpegBytes() {
const canvas = await takeScreenshotCanvas()
return getJpegBytes(canvas)
}
function blobToCanvas(blob, maxWidth, maxHeight) {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = function () {
const canvas = document.createElement('canvas')
const scale = Math.min(
1,
maxWidth ? maxWidth / img.width : 1,
maxHeight ? maxHeight / img.height : 1,
)
canvas.width = img.width * scale
canvas.height = img.height * scale
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height)
resolve(canvas)
}
img.onerror = () => {
reject(new Error('Error load blob to Image'))
}
img.src = URL.createObjectURL(blob)
})
}
DEMO:
document.body.onclick = async () => {
// take the screenshot
var screenshotJpegBlob = await takeScreenshotJpegBlob()
// show preview with max size 300 x 300 px
var previewCanvas = await blobToCanvas(screenshotJpegBlob, 300, 300)
previewCanvas.style.position = 'fixed'
document.body.appendChild(previewCanvas)
// send it to the server
var formdata = new FormData()
formdata.append("screenshot", screenshotJpegBlob)
await fetch('https://your-web-site.com/', {
method: 'POST',
body: formdata,
'Content-Type' : "multipart/form-data",
})
}
// and click on the page
Here is a complete screenshot example that works with chrome in 2021. The end result is a blob ready to be transmitted. Flow is: request media > grab frame > draw to canvas > transfer to blob. If you want to do it more memory efficient explore OffscreenCanvas or possibly ImageBitmapRenderingContext
https://jsfiddle.net/v24hyd3q/1/
// Request media
navigator.mediaDevices.getDisplayMedia().then(stream =>
{
// Grab frame from stream
let track = stream.getVideoTracks()[0];
let capture = new ImageCapture(track);
capture.grabFrame().then(bitmap =>
{
// Stop sharing
track.stop();
// Draw the bitmap to canvas
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.getContext('2d').drawImage(bitmap, 0, 0);
// Grab blob from canvas
canvas.toBlob(blob => {
// Do things with blob here
console.log('output blob:', blob);
});
});
})
.catch(e => console.log(e));
Heres an example using: getDisplayMedia
document.body.innerHTML = '<video style="width: 100%; height: 100%; border: 1px black solid;"/>';
navigator.mediaDevices.getDisplayMedia()
.then( mediaStream => {
const video = document.querySelector('video');
video.srcObject = mediaStream;
video.onloadedmetadata = e => {
video.play();
video.pause();
};
})
.catch( err => console.log(`${err.name}: ${err.message}`));
Also worth checking out is the Screen Capture API docs.
You can try my new JS library: screenshot.js.
It's enable to take real screenshot.
You load the script:
<script src="https://raw.githubusercontent.com/amiad/screenshot.js/master/screenshot.js"></script>
and take screenshot:
new Screenshot({success: img => {
// callback function
myimage = img;
}});
You can read more options in project page.