@creenv/capture
Version:
Creenv Capture allows you to export your work as a video file, in the easiest possible way
324 lines (291 loc) • 9.63 kB
JavaScript
/**
* @license MIT
* @author Baptiste Crespy <baptiste.crespy@gmail.com>
*
* This exporter uses Chrome's FileSystem API to store data on the disk, therefore allowing greater ammounts of data
* stored for a capture. However, if such an API is not available for known or unknown reasons, it fallsbacks to
* saving the frames in the memory.
*
* Once the capture is done, the frames are compressed within an archive.
*
* Useful ressources:
*
* - Exploring the FileSystem APIs
* @author Eric Bidelman
* <https://www.html5rocks.com/en/tutorials/file/filesystem/>
*
* - MDN FileSystem API page
* <https://developer.mozilla.org/en-US/docs/Web/API/FileSystem>
*/
import Exporter from "./exporter";
import BlobDownload from "../blob-download";
import JSZip from "jszip";
import ProgressModal from "../utils/modal-progress";
const DEFAULT_OPTIONS = {
}
// the sandbox working directory
const WD = "/capture-sequence-jpeg";
/**
* better than importing moment-js
*
* @return {string} date, format : H:i_d-m-Y
*/
const convertDate = () => {
function pad(s) { return (s < 10) ? '0' + s : s; }
var d = new Date();
return pad(d.getHours())+":"+pad(d.getMinutes())+"_"+[pad(d.getDate()), pad(d.getMonth()+1), d.getFullYear()].join('-');
}
/**
* @param {string} prefix the string prepended to the filename
* @param {number} index current captured frame, appended to the index
*
* @return {string} the filename that will be written
*/
const getFilename = (prefix, index) => {
let suffix = "0000"+index;
return prefix+suffix.substr(suffix.length-4, 4)+".jpeg";
}
class JPEGSequenceExporter extends Exporter {
/**
* @param {HTMLCanvasElement} canvas the canvas within the render takes place
* @param {Object} options an object of options for the .webm export
*/
constructor (canvas, options) {
super(canvas, {...DEFAULT_OPTIONS, ...options});
/**
* weither the filesystem api is used during this export or not
* @type {false|FileSystem}
* @public
*/
this.filesystem = false;
/**
* the current working directory
* @type {FileSystemDirectoryEntry}
*/
this.directory = null;
/**
* the prefix appended to all files stored within the sandbox
* @type {string}
*/
this.prefix = ""; // mm:ss_DD-MM-YYYY_
/**
* the number of frames saved to the sandbox, is set to 0 at start
* @type {number}
*/
this.framesSaved = 0;
/**
* the saved files, by filename
* @type {Array.<string>}
*/
this.filesSaved = [];
/**
* the zip api used to save the zip
* @type {JSZip}
*/
this.jzip = null;
this.handleError = this.handleError.bind(this);
}
/**
* calls reqFs() to ask for disk allocation, if none is available fallbacks to storing t
*/
init () {
return new Promise((resolve, reject) => {
this.reqFs().then(resolve).catch(() => {
console.error("currently JPEG sequence export is only available within Chrome. please upgrade your brownser or switch to chrome for the record");
});
});
}
/**
* tries to allocate space using the Chrome FileSystem API, unfortunatly only available within Chrome. Throw an
* exception if such an allocation is not possible so that the exporter can fallback to a method using cache
* a very good article presents this technology <https://www.html5rocks.com/en/tutorials/file/filesystem/>
*
* @return {Promise} resolve if requestFileSystem is available and successful
*/
reqFs () {
return new Promise((resolve, reject) => {
if (!window.webkitRequestFileSystem) {
console.error("capturing a jpeg sequence is only available within chrome. update your version if you're already using chrome");
reject();
} else {
// we request for 500Mb
window.webkitRequestFileSystem(window.TEMPORARY, 500*1024*1024, (fs) => {
console.info("capture is ready");
this.filesystem = fs;
resolve();
}, (error) => {
reject(error);
});
}
});
}
/**
* clear the files of the last export in case they are still alive
*/
start () {
return new Promise((resolve, reject) => {
this.cleanDirectory().then(() => {
this.createDirectory().then(dir => {
this.framesSaved = 0;
this.prefix = "image-";
this.directory = dir;
this.filesSaved = [];
this.jzip = JSZip();
resolve();
})
});
});
}
/**
* saved the canvas frame to a png file, in the sandbox WD directory
*/
frameRendered () {
return new Promise((resolve, reject) => {
this.canvas.toBlob(blob => {
let filename = getFilename(this.prefix, this.framesSaved);
this.directory.getFile(filename, {create: true}, fileEntry => {
fileEntry.createWriter(fileWriter => {
fileWriter.onwriteend = e => {
this.filesSaved.push(filename);
this.framesSaved++;
resolve();
}
fileWriter.onerror = this.handleError;
fileWriter.write(blob);
}, this.handleError);
}, this.handleError);
}, "image/jpeg");
})
}
/**
* when all desired frames are captured, puts all the images into a zip, compress it and then generates a download to the
* resulting archive
*/
stop () {
return new Promise((resolve, reject) => {
let modal = new ProgressModal("Grouping files...");
this.filesToZip(modal).then(() => {
modal.setTitle("Compressing files...");
let finished = false,
added = 0;
this.jzip.generateAsync({type: "blob"}, (event) => {
let progress = 0.2 + 0.8*(event.percent/100);
if (event.percent == 100) {
if (!finished) {
finished = true;
modal.setTitle("Generating zip...");
}
progress = 0.9 + added/this.toZip * 0.1;
added++;
}
modal.updateProgress(Math.ceil(progress*100));
}).then(content => {
new BlobDownload(content, this.options.filename);
modal.kill();
this.cleanDirectory().then(resolve).catch(reject);
}).catch(reject);
});
});
}
/**
* parse the working directory within the sandbox as long as there are files left. the files are then added to the zip which
* which will be ready to be compressed and then downloaded
*
* @param {ProgressModal} modal the information box that display the progress
*/
filesToZip (modal) {
return new Promise((resolve, reject) => {
let remaning = this.framesSaved;
this.toZip = remaning;
let reader = this.directory.createReader();
if (this.audio) {
this.audio.lastModifiedDate = new Date();
this.audio.name = "audio.wav";
this.jzip.file("audio.wav", this.audio, { binary: true });
}
// called recursively as long as there are files within the working directory
const readEntries = () => {
reader.readEntries(results => {
results.forEach(result => {
this.readFile(result).then(content => {
remaning--;
modal.updateProgress(Math.ceil((1-remaning/this.toZip)*20));
this.fileContentToZip(result.name, content);
if (remaning <= 0) {
resolve();
}
}).catch(error => {
console.error(error);
reject();
});
});
if (results.length) {
readEntries();
}
});
};
readEntries();
});
}
/**
* adds the binary content to a file named @param name within the archive
*
* @param {string} name the name of the file to be added to the archive
* @param {string} content the binary content of the image to be added to the archive
*/
fileContentToZip (name, content) {
this.jzip.file(name, content, { binary: true });
}
/**
* reads the file @param entry and resolve its content, if such a thing is impossible, reject
*
* @param {FileEntry} entry to be read
*/
readFile (entry) {
return new Promise((resolve, reject) => {
entry.file(file => {
let reader = new FileReader();
reader.onload = e => {
resolve(e.currentTarget.result);
}
reader.onerror = reject;
reader.readAsBinaryString(file);
});
});
}
/**
* handles the possible errors returned by the file system api
*
* @param {DOMError} error the error returned by the filesystem api
*/
handleError (error) {
console.error(`an error has occured while writing files to your disk. CODE: ${error.code} MESSAGE: ${error.message}`);
}
/**
* cleans the working directory and returns a promise when such a task is performed
*
* @return {Promise}
*/
cleanDirectory () {
return new Promise((resolve) => {
this.filesystem.root.getDirectory(WD, {}, (directory) => {
directory.removeRecursively(() => {
resolve();
});
console.log("removed");
}, (error) => {
resolve();
});
});
}
/**
* creates the working directory and resolve it
*/
createDirectory () {
return new Promise((resolve, reject) => {
this.filesystem.root.getDirectory(WD, {create: true}, resolve, this.handleError);
});
}
}
JPEGSequenceExporter.ID = "jpeg-sequence";
export default JPEGSequenceExporter;