UNPKG

@creenv/capture

Version:

Creenv Capture allows you to export your work as a video file, in the easiest possible way

362 lines (318 loc) 10.4 kB
/** * @license MIT * @author Baptiste Crespy <baptiste.crespy@gmail.com> * * This class was created to ease the capturing process within the browser and the Creative Environment. As it * is right now, I think it can be improved, so if you have any ideas feel free to contribute to the project :) * * * Resources: * <https://zhirzh.github.io/2017/09/02/mediarecorder/> * <https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder> * <https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/captureStream> * * * The behavior: * To have a perfect render, we are going to emulate how the render method should behave under circumstances * where framerate provided as an options is the framerate at which it's rendered. To do so, we will emulate the * rendering conditions using a fake delta time sent to the rendering method, the .inbetweenFrames variable. * * This is how the rendering cycle will work through Capture: * - render method is called, with a custom delta time / elapsed time that matches the capture timing * - render has all the time in the world to process - Capture will wait for the render to be finished before * calling for the next frame * - render is done -> frame is sent to the writer, that will add it to the pile * - new frame render is called, with the same custom delta time * - repeat. **/ import Creenv from "@creenv/core"; import Canvas from "@creenv/canvas"; import WebmWriter from "webm-writer"; import CaptureControls from "./controls"; import FramesManager from "./frames"; import Exporter from "./exporters/exporter"; import ExporterFactory from "./exporters/factory"; import Screenshot from "./screenshot"; const OPTIONS = { // the framerate of the capture framerate: 60, export: { type: "png-sequence", options: { quality: 0.95, framerate: 60, filename: "sequence.zip" } }, canvas: null, infobox: true, buttonInfos: true, audio: null /** audio: { manager: null, } */ }; class Capture { /** * The capture object allows you to render your project without a fastidious settup. Instead of calling * the bootstrap method to start the rendering process, you will instead pass your project class as a parameter * of Capture and let him do the job. If this is the first time you are rendering your project, it is strongly * advised that you take a look at the tutorial provided below. [will hopefully be added soon]. * * @param {Creenv} project the main class of the project * @param {Object} options the rendering options */ constructor (project, options) { // checks for compatibility this.compatibility(); // bindings this.onFrameRendered = this.onFrameRendered.bind(this); this.onNext = this.onNext.bind(this); this.onPrev = this.onPrev.bind(this); this.onPause = this.onPause.bind(this); this.onPlay = this.onPlay.bind(this); this.onRecord = this.onRecord.bind(this); this.onStop = this.onStop.bind(this); this.onCapture = this.onCapture.bind(this); /** * the project that will be renderer. its update method will be called at a correct framerate * @type {Creenv} * @private */ this.project = project; /** * the options, used to configure the export * @type {Object} * @private */ this.options = { ...OPTIONS, ...options }; /** * @type {CaptureControls} */ this.controls = new CaptureControls(this.onPlay, this.onPause, this.onRecord, this.onStop, this.onPrev, this.onNext, this.onCapture, this.options.framerate, this.options.infobox, this.options.buttonInfos); /** * weither the record is active or not. if a keyStart is specified, the record won't start as long as the key * isn't pressed. */ this.active = false; /** * if a canvas source is not specified, Capture will look for a canvas element within the DOM and assign it * as the source for rendering. * @type {HTMLCanvasElement} * @private */ this.canvas = null; /** * the frames manager handles the render of the frames from the project * @type {FramesManager} * @private */ this.frames = new FramesManager(this.project, this.options.framerate, this.onFrameRendered); /** * the timestamp at which the capture has started * @type {number} * @private */ this.startTimer = 0; /** * the media recorder will be plugged to the canvas stream to generate exportable data * @type {MediaRecorder} * @private */ this.recorder = null; /** * will be set to true once the record is done * @type {boolean} * @public */ this.done = false; /** * this data will be populated by the capture * @type {Object} * @private */ this.recordData = { started: 0, ended: 0, duration: 0 }; /** * the exporter * @type {Exporter} * @private */ this.exporter = null; // those variables are used to fasten later computations this.inbetweenFrames = 1 / this.options.framerate; // setting up let initialization = this.project.init(); if (initialization instanceof Promise) { initialization.then(() => { this.init(); }); } else { this.init(); } } /** * Initialize the different modules in the right order */ init () { this.findActiveCanvas(); this.exporter = new (ExporterFactory(this.options.export.type))(this.canvas, this.options.export.options); this.exporter.init().then(() => { this.setupControls(); }); } /** * sets up the events (cf: controls) to control the record */ setupControls () { document.addEventListener("keydown", (e) => { if (e.key === this.options.keyStop) { this.stopRecord(); } else if (this.options.keyStart && this.options.keyStart.toLowerCase() === e.key) { this.startRecord(); } }); } /** * this method will be called when a frame has been rendered */ onFrameRendered (renderCallback) { if (this.active) { this.exporter.frameRendered().then(() => { this.controls.updateFramesRecorded(); if (this.frames.playing) { // timeout is required if we don't want to see the browser ending up in an ending loop setTimeout(() => { renderCallback(); }, 10); } }); } } /** * called by the controls by clicking on the "play" button */ onPlay () { this.frames.play(); } /** * called by the controls by clicking on the "pause" button */ onPause () { this.frames.stop(); } /** * called by the controls by clicking on the "previous frame" button */ onPrev () { if (this.frames.playing) { console.warn("it is not advised to navigate between frames while render is in play mode"); } this.frames.prevFrame(); } /** * called by the controls by clicking on the "next frame" button */ onNext () { if (this.frames.playing) { console.warn("it is not advised to navigate between frames while render is in play mode"); } this.frames.nextFrame(); } /** * called by the controls by clicking on the "record" button */ onRecord () { this.startRecord(); this.controls.updateFramesRecorded(0); } /** * called by the controls by clicking on the "stop" button */ onStop () { this.stopRecord(); } onCapture () { this.frames.stop(); this.controls.pause(); new Screenshot(this.canvas); } /** * this function will set the required variables as they are supposed to be so that the render loop * records the */ startRecord () { this.exporter.start().then(() => { this.active = true; this.recordData.started = this.project.elapsedTime; this.frames.recording = true; }); } /** * stops the record and set the required to the required state when capture is done */ stopRecord () { this.active = false; this.recordData.ended = this.project.elapsedTime; this.frames.recording = false; this.frames.stop(); this.controls.updateFramesRecorded(0); this.done = true; // we send the audio to the exporter if (this.options.audio) { let blob = this.options.audio.manager.getAudioCopy(this.recordData.started, this.recordData.ended-this.recordData.started); this.exporter.addAudioTrack(blob); } this.exporter.stop().then(() => {}); } /** * can be called to start the capture, after everything was set up */ start () { this.startTimer = performance.now(); this.frames.play(); } /** * checks for browser compatiblity, if an error is noticed, it is only logged to the console, but script execution * continues. this behavior could be changed */ compatibility () { if (typeof performance === "undefined" || typeof MediaStream === "undefined" || typeof MediaRecorder === "undefined") { console.error("your browser is not modern enough to handle the capture. please upgrade it or switch to chromium/chrome"); } } /** * first look into the options to see if a canvas property was provided, if not tries to look within the DOM if * such a <canvas> exists */ findActiveCanvas () { if (this.options.canvas) { if (this.options.canvas instanceof Canvas) { this.canvas = this.options.canvas.canvas; } else { if (!(this.options.canvas instanceof HTMLCanvasElement)) { console.warn("the canvas property you provided to the Capture is not an html canvas element. Capture will still proceed"); } this.canvas = this.options.canvas; } } else { // canvas was not provided as an option, we'll look for it wihtin the dom let captureClass = document.querySelector("canvas.creenv"); if (captureClass) { this.canvas = captureClass; } else { let canvas = document.querySelector("canvas"); if (!canvas) { console.error("Capture was not able find a canvas within the dom. You can specify one by adding a canvas property to the options"); } this.canvas = canvas; } } } } export default Capture;