UNPKG

@vikasietum_tecknology/record-rtc

Version:

record-rtc is a library based on recordrtc library. In this forked version of the original library we have optimized the memory management. The video recording is stored in IndexDB in chunks.

495 lines (439 loc) 16.7 kB
// https://github.com/antimatter15/whammy/blob/master/LICENSE // _________ // Whammy.js // todo: Firefox now supports webp for webm containers! // their MediaRecorder implementation works well! // should we provide an option to record via Whammy.js or MediaRecorder API is a better solution? /** * Whammy is a standalone class used by {@link RecordRTC} to bring video recording in Chrome. It is written by {@link https://github.com/antimatter15|antimatter15} * @summary A real time javascript webm encoder based on a canvas hack. * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} * @author {@link https://MuazKhan.com|Muaz Khan} * @typedef Whammy * @class * @example * var recorder = new Whammy().Video(15); * recorder.add(context || canvas || dataURL); * var output = recorder.compile(); * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} */ var Whammy = (function() { // a more abstract-ish API function WhammyVideo(duration) { this.frames = []; this.duration = duration || 1; this.quality = 0.8; } /** * Pass Canvas or Context or image/webp(string) to {@link Whammy} encoder. * @method * @memberof Whammy * @example * recorder = new Whammy().Video(0.8, 100); * recorder.add(canvas || context || 'image/webp'); * @param {string} frame - Canvas || Context || image/webp * @param {number} duration - Stick a duration (in milliseconds) */ WhammyVideo.prototype.add = function(frame, duration) { if ("canvas" in frame) { //CanvasRenderingContext2D frame = frame.canvas; } if ("toDataURL" in frame) { frame = frame.toDataURL("image/webp", this.quality); } if (!/^data:image\/webp;base64,/gi.test(frame)) { throw "Input must be formatted properly as a base64 encoded DataURI of type image/webp"; } this.frames.push({ image: frame, duration: duration || this.duration, }); }; function processInWebWorker(_function) { var blob = URL.createObjectURL( new Blob( [ _function.toString(), "this.onmessage = function (eee) {" + _function.name + "(eee.data);}", ], { type: "application/javascript", } ) ); var worker = new Worker(blob); URL.revokeObjectURL(blob); return worker; } function whammyInWebWorker(frames) { function ArrayToWebM(frames) { var info = checkFrames(frames); if (!info) { return []; } var clusterMaxDuration = 30000; var EBML = [{ id: 0x1a45dfa3, // EBML data: [{ data: 1, id: 0x4286, // EBMLVersion }, { data: 1, id: 0x42f7, // EBMLReadVersion }, { data: 4, id: 0x42f2, // EBMLMaxIDLength }, { data: 8, id: 0x42f3, // EBMLMaxSizeLength }, { data: "webm", id: 0x4282, // DocType }, { data: 2, id: 0x4287, // DocTypeVersion }, { data: 2, id: 0x4285, // DocTypeReadVersion }, ], }, { id: 0x18538067, // Segment data: [{ id: 0x1549a966, // Info data: [{ data: 1e6, //do things in millisecs (num of nanosecs for duration scale) id: 0x2ad7b1, // TimecodeScale }, { data: "whammy", id: 0x4d80, // MuxingApp }, { data: "whammy", id: 0x5741, // WritingApp }, { data: doubleToString(info.duration), id: 0x4489, // Duration }, ], }, { id: 0x1654ae6b, // Tracks data: [{ id: 0xae, // TrackEntry data: [{ data: 1, id: 0xd7, // TrackNumber }, { data: 1, id: 0x73c5, // TrackUID }, { data: 0, id: 0x9c, // FlagLacing }, { data: "und", id: 0x22b59c, // Language }, { data: "V_VP8", id: 0x86, // CodecID }, { data: "VP8", id: 0x258688, // CodecName }, { data: 1, id: 0x83, // TrackType }, { id: 0xe0, // Video data: [{ data: info.width, id: 0xb0, // PixelWidth }, { data: info.height, id: 0xba, // PixelHeight }, ], }, ], }, ], }, ], }, ]; //Generate clusters (max duration) var frameNumber = 0; var clusterTimecode = 0; while (frameNumber < frames.length) { var clusterFrames = []; var clusterDuration = 0; do { clusterFrames.push(frames[frameNumber]); clusterDuration += frames[frameNumber].duration; frameNumber++; } while ( frameNumber < frames.length && clusterDuration < clusterMaxDuration ); var clusterCounter = 0; var cluster = { id: 0x1f43b675, // Cluster data: getClusterData(clusterTimecode, clusterCounter, clusterFrames), }; //Add cluster to segment EBML[1].data.push(cluster); clusterTimecode += clusterDuration; } return generateEBML(EBML); } function getClusterData(clusterTimecode, clusterCounter, clusterFrames) { return [{ data: clusterTimecode, id: 0xe7, // Timecode }, ].concat( clusterFrames.map(function(webp) { var block = makeSimpleBlock({ discardable: 0, frame: webp.data.slice(4), invisible: 0, keyframe: 1, lacing: 0, trackNum: 1, timecode: Math.round(clusterCounter), }); clusterCounter += webp.duration; return { data: block, id: 0xa3, }; }) ); } // sums the lengths of all the frames and gets the duration function checkFrames(frames) { if (!frames[0]) { postMessage({ error: "Something went wrong. Maybe WebP format is not supported in the current browser.", }); return; } var width = frames[0].width, height = frames[0].height, duration = frames[0].duration; for (var i = 1; i < frames.length; i++) { duration += frames[i].duration; } return { duration: duration, width: width, height: height, }; } function numToBuffer(num) { var parts = []; while (num > 0) { parts.push(num & 0xff); num = num >> 8; } return new Uint8Array(parts.reverse()); } function strToBuffer(str) { return new Uint8Array( str.split("").map(function(e) { return e.charCodeAt(0); }) ); } function bitsToBuffer(bits) { var data = []; var pad = bits.length % 8 ? new Array(1 + 8 - (bits.length % 8)).join("0") : ""; bits = pad + bits; for (var i = 0; i < bits.length; i += 8) { data.push(parseInt(bits.substr(i, 8), 2)); } return new Uint8Array(data); } function generateEBML(json) { var ebml = []; for (var i = 0; i < json.length; i++) { var data = json[i].data; if (typeof data === "object") { data = generateEBML(data); } if (typeof data === "number") { data = bitsToBuffer(data.toString(2)); } if (typeof data === "string") { data = strToBuffer(data); } var len = data.size || data.byteLength || data.length; var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8); var sizeToString = len.toString(2); var padded = new Array(zeroes * 7 + 7 + 1 - sizeToString.length).join("0") + sizeToString; var size = new Array(zeroes).join("0") + "1" + padded; ebml.push(numToBuffer(json[i].id)); ebml.push(bitsToBuffer(size)); ebml.push(data); } return new Blob(ebml, { type: "video/webm", }); } function toBinStrOld(bits) { var data = ""; var pad = bits.length % 8 ? new Array(1 + 8 - (bits.length % 8)).join("0") : ""; bits = pad + bits; for (var i = 0; i < bits.length; i += 8) { data += String.fromCharCode(parseInt(bits.substr(i, 8), 2)); } return data; } function makeSimpleBlock(data) { var flags = 0; if (data.keyframe) { flags |= 128; } if (data.invisible) { flags |= 8; } if (data.lacing) { flags |= data.lacing << 1; } if (data.discardable) { flags |= 1; } if (data.trackNum > 127) { throw "TrackNumber > 127 not supported"; } var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags] .map(function(e) { return String.fromCharCode(e); }) .join("") + data.frame; return out; } function parseWebP(riff) { var VP8 = riff.RIFF[0].WEBP[0]; var frameStart = VP8.indexOf("\x9d\x01\x2a"); // A VP8 keyframe starts with the 0x9d012a header for (var i = 0, c = []; i < 4; i++) { c[i] = VP8.charCodeAt(frameStart + 3 + i); } var width, height, tmp; //the code below is literally copied verbatim from the bitstream spec tmp = (c[1] << 8) | c[0]; width = tmp & 0x3fff; tmp = (c[3] << 8) | c[2]; height = tmp & 0x3fff; return { width: width, height: height, data: VP8, riff: riff, }; } function getStrLength(string, offset) { return parseInt( string .substr(offset + 4, 4) .split("") .map(function(i) { var unpadded = i.charCodeAt(0).toString(2); return new Array(8 - unpadded.length + 1).join("0") + unpadded; }) .join(""), 2 ); } function parseRIFF(string) { var offset = 0; var chunks = {}; while (offset < string.length) { var id = string.substr(offset, 4); var len = getStrLength(string, offset); var data = string.substr(offset + 4 + 4, len); offset += 4 + 4 + len; chunks[id] = chunks[id] || []; if (id === "RIFF" || id === "LIST") { chunks[id].push(parseRIFF(data)); } else { chunks[id].push(data); } } return chunks; } function doubleToString(num) { return [].slice .call(new Uint8Array(new Float64Array([num]).buffer), 0) .map(function(e) { return String.fromCharCode(e); }) .reverse() .join(""); } var webm = new ArrayToWebM( frames.map(function(frame) { var webp = parseWebP(parseRIFF(atob(frame.image.slice(23)))); webp.duration = frame.duration; return webp; }) ); postMessage(webm); } /** * Encodes frames in WebM container. It uses WebWorkinvoke to invoke 'ArrayToWebM' method. * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. * @method * @memberof Whammy * @example * recorder = new Whammy().Video(0.8, 100); * recorder.compile(function(blob) { * // blob.size - blob.type * }); */ WhammyVideo.prototype.compile = function(callback) { var webWorker = processInWebWorker(whammyInWebWorker); webWorker.onmessage = function(event) { if (event.data.error) { console.error(event.data.error); return; } callback(event.data); }; webWorker.postMessage(this.frames); }; return { /** * A more abstract-ish API. * @method * @memberof Whammy * @example * recorder = new Whammy().Video(0.8, 100); * @param {?number} speed - 0.8 * @param {?number} quality - 100 */ Video: WhammyVideo, }; })(); if (typeof RecordRTC !== "undefined") { RecordRTC.Whammy = Whammy; }