| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097 |
- /**
- * A tool for presenting an ArrayBuffer as a stream for writing some simple data types.
- *
- * By Nicholas Sherlock
- *
- * Released under the WTFPLv2 https://en.wikipedia.org/wiki/WTFPL
- */
- "use strict";
- (function(){
- /*
- * Create an ArrayBuffer of the given length and present it as a writable stream with methods
- * for writing data in different formats.
- */
- var ArrayBufferDataStream = function(length) {
- this.data = new Uint8Array(length);
- this.pos = 0;
- };
-
- ArrayBufferDataStream.prototype.seek = function(offset) {
- this.pos = offset;
- };
- ArrayBufferDataStream.prototype.writeBytes = function(arr) {
- for (var i = 0; i < arr.length; i++) {
- this.data[this.pos++] = arr[i];
- }
- };
- ArrayBufferDataStream.prototype.writeByte = function(b) {
- this.data[this.pos++] = b;
- };
-
- //Synonym:
- ArrayBufferDataStream.prototype.writeU8 = ArrayBufferDataStream.prototype.writeByte;
-
- ArrayBufferDataStream.prototype.writeU16BE = function(u) {
- this.data[this.pos++] = u >> 8;
- this.data[this.pos++] = u;
- };
- ArrayBufferDataStream.prototype.writeDoubleBE = function(d) {
- var
- bytes = new Uint8Array(new Float64Array([d]).buffer);
-
- for (var i = bytes.length - 1; i >= 0; i--) {
- this.writeByte(bytes[i]);
- }
- };
- ArrayBufferDataStream.prototype.writeFloatBE = function(d) {
- var
- bytes = new Uint8Array(new Float32Array([d]).buffer);
-
- for (var i = bytes.length - 1; i >= 0; i--) {
- this.writeByte(bytes[i]);
- }
- };
- /**
- * Write an ASCII string to the stream
- */
- ArrayBufferDataStream.prototype.writeString = function(s) {
- for (var i = 0; i < s.length; i++) {
- this.data[this.pos++] = s.charCodeAt(i);
- }
- };
- /**
- * Write the given 32-bit integer to the stream as an EBML variable-length integer using the given byte width
- * (use measureEBMLVarInt).
- *
- * No error checking is performed to ensure that the supplied width is correct for the integer.
- *
- * @param i Integer to be written
- * @param width Number of bytes to write to the stream
- */
- ArrayBufferDataStream.prototype.writeEBMLVarIntWidth = function(i, width) {
- switch (width) {
- case 1:
- this.writeU8((1 << 7) | i);
- break;
- case 2:
- this.writeU8((1 << 6) | (i >> 8));
- this.writeU8(i);
- break;
- case 3:
- this.writeU8((1 << 5) | (i >> 16));
- this.writeU8(i >> 8);
- this.writeU8(i);
- break;
- case 4:
- this.writeU8((1 << 4) | (i >> 24));
- this.writeU8(i >> 16);
- this.writeU8(i >> 8);
- this.writeU8(i);
- break;
- case 5:
- /*
- * JavaScript converts its doubles to 32-bit integers for bitwise operations, so we need to do a
- * division by 2^32 instead of a right-shift of 32 to retain those top 3 bits
- */
- this.writeU8((1 << 3) | ((i / 4294967296) & 0x7));
- this.writeU8(i >> 24);
- this.writeU8(i >> 16);
- this.writeU8(i >> 8);
- this.writeU8(i);
- break;
- default:
- throw new RuntimeException("Bad EBML VINT size " + width);
- }
- };
-
- /**
- * Return the number of bytes needed to encode the given integer as an EBML VINT.
- */
- ArrayBufferDataStream.prototype.measureEBMLVarInt = function(val) {
- if (val < (1 << 7) - 1) {
- /* Top bit is set, leaving 7 bits to hold the integer, but we can't store 127 because
- * "all bits set to one" is a reserved value. Same thing for the other cases below:
- */
- return 1;
- } else if (val < (1 << 14) - 1) {
- return 2;
- } else if (val < (1 << 21) - 1) {
- return 3;
- } else if (val < (1 << 28) - 1) {
- return 4;
- } else if (val < 34359738367) { // 2 ^ 35 - 1 (can address 32GB)
- return 5;
- } else {
- throw new RuntimeException("EBML VINT size not supported " + val);
- }
- };
-
- ArrayBufferDataStream.prototype.writeEBMLVarInt = function(i) {
- this.writeEBMLVarIntWidth(i, this.measureEBMLVarInt(i));
- };
-
- /**
- * Write the given unsigned 32-bit integer to the stream in big-endian order using the given byte width.
- * No error checking is performed to ensure that the supplied width is correct for the integer.
- *
- * Omit the width parameter to have it determined automatically for you.
- *
- * @param u Unsigned integer to be written
- * @param width Number of bytes to write to the stream
- */
- ArrayBufferDataStream.prototype.writeUnsignedIntBE = function(u, width) {
- if (width === undefined) {
- width = this.measureUnsignedInt(u);
- }
-
- // Each case falls through:
- switch (width) {
- case 5:
- this.writeU8(Math.floor(u / 4294967296)); // Need to use division to access >32 bits of floating point var
- case 4:
- this.writeU8(u >> 24);
- case 3:
- this.writeU8(u >> 16);
- case 2:
- this.writeU8(u >> 8);
- case 1:
- this.writeU8(u);
- break;
- default:
- throw new RuntimeException("Bad UINT size " + width);
- }
- };
-
- /**
- * Return the number of bytes needed to hold the non-zero bits of the given unsigned integer.
- */
- ArrayBufferDataStream.prototype.measureUnsignedInt = function(val) {
- // Force to 32-bit unsigned integer
- if (val < (1 << 8)) {
- return 1;
- } else if (val < (1 << 16)) {
- return 2;
- } else if (val < (1 << 24)) {
- return 3;
- } else if (val < 4294967296) {
- return 4;
- } else {
- return 5;
- }
- };
- /**
- * Return a view on the portion of the buffer from the beginning to the current seek position as a Uint8Array.
- */
- ArrayBufferDataStream.prototype.getAsDataArray = function() {
- if (this.pos < this.data.byteLength) {
- return this.data.subarray(0, this.pos);
- } else if (this.pos == this.data.byteLength) {
- return this.data;
- } else {
- throw "ArrayBufferDataStream's pos lies beyond end of buffer";
- }
- };
-
- if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
- module.exports = ArrayBufferDataStream;
- } else {
- window.ArrayBufferDataStream = ArrayBufferDataStream;
- }
- }());"use strict";
- /**
- * Allows a series of Blob-convertible objects (ArrayBuffer, Blob, String, etc) to be added to a buffer. Seeking and
- * overwriting of blobs is allowed.
- *
- * You can supply a FileWriter, in which case the BlobBuffer is just used as temporary storage before it writes it
- * through to the disk.
- *
- * By Nicholas Sherlock
- *
- * Released under the WTFPLv2 https://en.wikipedia.org/wiki/WTFPL
- */
- (function() {
- var BlobBuffer = function(fs) {
- return function(destination) {
- var
- buffer = [],
- writePromise = Promise.resolve(),
- fileWriter = null,
- fd = null;
-
- if (typeof FileWriter !== "undefined" && destination instanceof FileWriter) {
- fileWriter = destination;
- } else if (fs && destination) {
- fd = destination;
- }
-
- // Current seek offset
- this.pos = 0;
-
- // One more than the index of the highest byte ever written
- this.length = 0;
-
- // Returns a promise that converts the blob to an ArrayBuffer
- function readBlobAsBuffer(blob) {
- return new Promise(function (resolve, reject) {
- var
- reader = new FileReader();
-
- reader.addEventListener("loadend", function () {
- resolve(reader.result);
- });
-
- reader.readAsArrayBuffer(blob);
- });
- }
-
- function convertToUint8Array(thing) {
- return new Promise(function (resolve, reject) {
- if (thing instanceof Uint8Array) {
- resolve(thing);
- } else if (thing instanceof ArrayBuffer || ArrayBuffer.isView(thing)) {
- resolve(new Uint8Array(thing));
- } else if (thing instanceof Blob) {
- resolve(readBlobAsBuffer(thing).then(function (buffer) {
- return new Uint8Array(buffer);
- }));
- } else {
- //Assume that Blob will know how to read this thing
- resolve(readBlobAsBuffer(new Blob([thing])).then(function (buffer) {
- return new Uint8Array(buffer);
- }));
- }
- });
- }
-
- function measureData(data) {
- var
- result = data.byteLength || data.length || data.size;
-
- if (!Number.isInteger(result)) {
- throw "Failed to determine size of element";
- }
-
- return result;
- }
-
- /**
- * Seek to the given absolute offset.
- *
- * You may not seek beyond the end of the file (this would create a hole and/or allow blocks to be written in non-
- * sequential order, which isn't currently supported by the memory buffer backend).
- */
- this.seek = function (offset) {
- if (offset < 0) {
- throw "Offset may not be negative";
- }
-
- if (isNaN(offset)) {
- throw "Offset may not be NaN";
- }
-
- if (offset > this.length) {
- throw "Seeking beyond the end of file is not allowed";
- }
-
- this.pos = offset;
- };
-
- /**
- * Write the Blob-convertible data to the buffer at the current seek position.
- *
- * Note: If overwriting existing data, the write must not cross preexisting block boundaries (written data must
- * be fully contained by the extent of a previous write).
- */
- this.write = function (data) {
- var
- newEntry = {
- offset: this.pos,
- data: data,
- length: measureData(data)
- },
- isAppend = newEntry.offset >= this.length;
-
- this.pos += newEntry.length;
- this.length = Math.max(this.length, this.pos);
-
- // After previous writes complete, perform our write
- writePromise = writePromise.then(function () {
- if (fd) {
- return new Promise(function(resolve, reject) {
- convertToUint8Array(newEntry.data).then(function(dataArray) {
- var
- totalWritten = 0,
- buffer = Buffer.from(dataArray.buffer),
-
- handleWriteComplete = function(err, written, buffer) {
- totalWritten += written;
-
- if (totalWritten >= buffer.length) {
- resolve();
- } else {
- // We still have more to write...
- fs.write(fd, buffer, totalWritten, buffer.length - totalWritten, newEntry.offset + totalWritten, handleWriteComplete);
- }
- };
-
- fs.write(fd, buffer, 0, buffer.length, newEntry.offset, handleWriteComplete);
- });
- });
- } else if (fileWriter) {
- return new Promise(function (resolve, reject) {
- fileWriter.onwriteend = resolve;
-
- fileWriter.seek(newEntry.offset);
- fileWriter.write(new Blob([newEntry.data]));
- });
- } else if (!isAppend) {
- // We might be modifying a write that was already buffered in memory.
-
- // Slow linear search to find a block we might be overwriting
- for (var i = 0; i < buffer.length; i++) {
- var
- entry = buffer[i];
-
- // If our new entry overlaps the old one in any way...
- if (!(newEntry.offset + newEntry.length <= entry.offset || newEntry.offset >= entry.offset + entry.length)) {
- if (newEntry.offset < entry.offset || newEntry.offset + newEntry.length > entry.offset + entry.length) {
- throw new Error("Overwrite crosses blob boundaries");
- }
-
- if (newEntry.offset == entry.offset && newEntry.length == entry.length) {
- // We overwrote the entire block
- entry.data = newEntry.data;
-
- // We're done
- return;
- } else {
- return convertToUint8Array(entry.data)
- .then(function (entryArray) {
- entry.data = entryArray;
-
- return convertToUint8Array(newEntry.data);
- }).then(function (newEntryArray) {
- newEntry.data = newEntryArray;
-
- entry.data.set(newEntry.data, newEntry.offset - entry.offset);
- });
- }
- }
- }
- // Else fall through to do a simple append, as we didn't overwrite any pre-existing blocks
- }
-
- buffer.push(newEntry);
- });
- };
-
- /**
- * Finish all writes to the buffer, returning a promise that signals when that is complete.
- *
- * If a FileWriter was not provided, the promise is resolved with a Blob that represents the completed BlobBuffer
- * contents. You can optionally pass in a mimeType to be used for this blob.
- *
- * If a FileWriter was provided, the promise is resolved with null as the first argument.
- */
- this.complete = function (mimeType) {
- if (fd || fileWriter) {
- writePromise = writePromise.then(function () {
- return null;
- });
- } else {
- // After writes complete we need to merge the buffer to give to the caller
- writePromise = writePromise.then(function () {
- var
- result = [];
-
- for (var i = 0; i < buffer.length; i++) {
- result.push(buffer[i].data);
- }
-
- return new Blob(result, {mimeType: mimeType});
- });
- }
-
- return writePromise;
- };
- };
- };
-
- if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
- module.exports = BlobBuffer(require('fs'));
- } else {
- window.BlobBuffer = BlobBuffer(null);
- }
- })();/**
- * WebM video encoder for Google Chrome. This implementation is suitable for creating very large video files, because
- * it can stream Blobs directly to a FileWriter without buffering the entire video in memory.
- *
- * When FileWriter is not available or not desired, it can buffer the video in memory as a series of Blobs which are
- * eventually returned as one composite Blob.
- *
- * By Nicholas Sherlock.
- *
- * Based on the ideas from Whammy: https://github.com/antimatter15/whammy
- *
- * Released under the WTFPLv2 https://en.wikipedia.org/wiki/WTFPL
- */
- "use strict";
- (function() {
- var WebMWriter = function(ArrayBufferDataStream, BlobBuffer) {
- function extend(base, top) {
- var
- target = {};
-
- [base, top].forEach(function(obj) {
- for (var prop in obj) {
- if (Object.prototype.hasOwnProperty.call(obj, prop)) {
- target[prop] = obj[prop];
- }
- }
- });
-
- return target;
- }
-
- /**
- * Decode a Base64 data URL into a binary string.
- *
- * Returns the binary string, or false if the URL could not be decoded.
- */
- function decodeBase64WebPDataURL(url) {
- if (typeof url !== "string" || !url.match(/^data:image\/webp;base64,/i)) {
- return false;
- }
-
- return window.atob(url.substring("data:image\/webp;base64,".length));
- }
-
- /**
- * Convert a raw binary string (one character = one output byte) to an ArrayBuffer
- */
- function stringToArrayBuffer(string) {
- var
- buffer = new ArrayBuffer(string.length),
- int8Array = new Uint8Array(buffer);
-
- for (var i = 0; i < string.length; i++) {
- int8Array[i] = string.charCodeAt(i);
- }
-
- return buffer;
- }
-
- /**
- * Convert the given canvas to a WebP encoded image and return the image data as a string.
- */
- function renderAsWebP(canvas, quality) {
- var
- frame = canvas.toDataURL('image/webp', quality);
-
- return decodeBase64WebPDataURL(frame);
- }
-
- function extractKeyframeFromWebP(webP) {
- // Assume that Chrome will generate a Simple Lossy WebP which has this header:
- var
- keyframeStartIndex = webP.indexOf('VP8 ');
-
- if (keyframeStartIndex == -1) {
- throw "Failed to identify beginning of keyframe in WebP image";
- }
-
- // Skip the header and the 4 bytes that encode the length of the VP8 chunk
- keyframeStartIndex += 'VP8 '.length + 4;
-
- return webP.substring(keyframeStartIndex);
- }
-
- // Just a little utility so we can tag values as floats for the EBML encoder's benefit
- function EBMLFloat32(value) {
- this.value = value;
- }
-
- function EBMLFloat64(value) {
- this.value = value;
- }
-
- /**
- * Write the given EBML object to the provided ArrayBufferStream.
- *
- * The buffer's first byte is at bufferFileOffset inside the video file. This is used to complete offset and
- * dataOffset fields in each EBML structure, indicating the file offset of the first byte of the EBML element and
- * its data payload.
- */
- function writeEBML(buffer, bufferFileOffset, ebml) {
- // Is the ebml an array of sibling elements?
- if (Array.isArray(ebml)) {
- for (var i = 0; i < ebml.length; i++) {
- writeEBML(buffer, bufferFileOffset, ebml[i]);
- }
- // Is this some sort of raw data that we want to write directly?
- } else if (typeof ebml === "string") {
- buffer.writeString(ebml);
- } else if (ebml instanceof Uint8Array) {
- buffer.writeBytes(ebml);
- } else if (ebml.id){
- // We're writing an EBML element
- ebml.offset = buffer.pos + bufferFileOffset;
-
- buffer.writeUnsignedIntBE(ebml.id); // ID field
-
- // Now we need to write the size field, so we must know the payload size:
-
- if (Array.isArray(ebml.data)) {
- // Writing an array of child elements. We won't try to measure the size of the children up-front
-
- var
- sizePos, dataBegin, dataEnd;
-
- if (ebml.size === -1) {
- // Write the reserved all-one-bits marker to note that the size of this element is unknown/unbounded
- buffer.writeByte(0xFF);
- } else {
- sizePos = buffer.pos;
-
- /* Write a dummy size field to overwrite later. 4 bytes allows an element maximum size of 256MB,
- * which should be plenty (we don't want to have to buffer that much data in memory at one time
- * anyway!)
- */
- buffer.writeBytes([0, 0, 0, 0]);
- }
-
- dataBegin = buffer.pos;
-
- ebml.dataOffset = dataBegin + bufferFileOffset;
- writeEBML(buffer, bufferFileOffset, ebml.data);
-
- if (ebml.size !== -1) {
- dataEnd = buffer.pos;
-
- ebml.size = dataEnd - dataBegin;
-
- buffer.seek(sizePos);
- buffer.writeEBMLVarIntWidth(ebml.size, 4); // Size field
-
- buffer.seek(dataEnd);
- }
- } else if (typeof ebml.data === "string") {
- buffer.writeEBMLVarInt(ebml.data.length); // Size field
- ebml.dataOffset = buffer.pos + bufferFileOffset;
- buffer.writeString(ebml.data);
- } else if (typeof ebml.data === "number") {
- // Allow the caller to explicitly choose the size if they wish by supplying a size field
- if (!ebml.size) {
- ebml.size = buffer.measureUnsignedInt(ebml.data);
- }
-
- buffer.writeEBMLVarInt(ebml.size); // Size field
- ebml.dataOffset = buffer.pos + bufferFileOffset;
- buffer.writeUnsignedIntBE(ebml.data, ebml.size);
- } else if (ebml.data instanceof EBMLFloat64) {
- buffer.writeEBMLVarInt(8); // Size field
- ebml.dataOffset = buffer.pos + bufferFileOffset;
- buffer.writeDoubleBE(ebml.data.value);
- } else if (ebml.data instanceof EBMLFloat32) {
- buffer.writeEBMLVarInt(4); // Size field
- ebml.dataOffset = buffer.pos + bufferFileOffset;
- buffer.writeFloatBE(ebml.data.value);
- } else if (ebml.data instanceof Uint8Array) {
- buffer.writeEBMLVarInt(ebml.data.byteLength); // Size field
- ebml.dataOffset = buffer.pos + bufferFileOffset;
- buffer.writeBytes(ebml.data);
- } else {
- throw "Bad EBML datatype " + typeof ebml.data;
- }
- } else {
- throw "Bad EBML datatype " + typeof ebml.data;
- }
- }
-
- return function(options) {
- var
- MAX_CLUSTER_DURATION_MSEC = 5000,
- DEFAULT_TRACK_NUMBER = 1,
-
- writtenHeader = false,
- videoWidth, videoHeight,
-
- clusterFrameBuffer = [],
- clusterStartTime = 0,
- clusterDuration = 0,
-
- optionDefaults = {
- quality: 0.95, // WebM image quality from 0.0 (worst) to 1.0 (best)
- fileWriter: null, // Chrome FileWriter in order to stream to a file instead of buffering to memory (optional)
- fd: null, // Node.JS file descriptor to write to instead of buffering (optional)
-
- // You must supply one of:
- frameDuration: null, // Duration of frames in milliseconds
- frameRate: null, // Number of frames per second
- },
-
- seekPoints = {
- Cues: {id: new Uint8Array([0x1C, 0x53, 0xBB, 0x6B]), positionEBML: null},
- SegmentInfo: {id: new Uint8Array([0x15, 0x49, 0xA9, 0x66]), positionEBML: null},
- Tracks: {id: new Uint8Array([0x16, 0x54, 0xAE, 0x6B]), positionEBML: null},
- },
-
- ebmlSegment,
- segmentDuration = {
- "id": 0x4489, // Duration
- "data": new EBMLFloat64(0)
- },
-
- seekHead,
-
- cues = [],
-
- blobBuffer = new BlobBuffer(options.fileWriter || options.fd);
-
- function fileOffsetToSegmentRelative(fileOffset) {
- return fileOffset - ebmlSegment.dataOffset;
- }
-
- /**
- * Create a SeekHead element with descriptors for the points in the global seekPoints array.
- *
- * 5 bytes of position values are reserved for each node, which lie at the offset point.positionEBML.dataOffset,
- * to be overwritten later.
- */
- function createSeekHead() {
- var
- seekPositionEBMLTemplate = {
- "id": 0x53AC, // SeekPosition
- "size": 5, // Allows for 32GB video files
- "data": 0 // We'll overwrite this when the file is complete
- },
-
- result = {
- "id": 0x114D9B74, // SeekHead
- "data": []
- };
-
- for (var name in seekPoints) {
- var
- seekPoint = seekPoints[name];
-
- seekPoint.positionEBML = Object.create(seekPositionEBMLTemplate);
-
- result.data.push({
- "id": 0x4DBB, // Seek
- "data": [
- {
- "id": 0x53AB, // SeekID
- "data": seekPoint.id
- },
- seekPoint.positionEBML
- ]
- });
- }
-
- return result;
- }
-
- /**
- * Write the WebM file header to the stream.
- */
- function writeHeader() {
- seekHead = createSeekHead();
-
- var
- ebmlHeader = {
- "id": 0x1a45dfa3, // EBML
- "data": [
- {
- "id": 0x4286, // EBMLVersion
- "data": 1
- },
- {
- "id": 0x42f7, // EBMLReadVersion
- "data": 1
- },
- {
- "id": 0x42f2, // EBMLMaxIDLength
- "data": 4
- },
- {
- "id": 0x42f3, // EBMLMaxSizeLength
- "data": 8
- },
- {
- "id": 0x4282, // DocType
- "data": "webm"
- },
- {
- "id": 0x4287, // DocTypeVersion
- "data": 2
- },
- {
- "id": 0x4285, // DocTypeReadVersion
- "data": 2
- }
- ]
- },
-
- segmentInfo = {
- "id": 0x1549a966, // Info
- "data": [
- {
- "id": 0x2ad7b1, // TimecodeScale
- "data": 1e6 // Times will be in miliseconds (1e6 nanoseconds per step = 1ms)
- },
- {
- "id": 0x4d80, // MuxingApp
- "data": "webm-writer-js",
- },
- {
- "id": 0x5741, // WritingApp
- "data": "webm-writer-js"
- },
- segmentDuration // To be filled in later
- ]
- },
-
- tracks = {
- "id": 0x1654ae6b, // Tracks
- "data": [
- {
- "id": 0xae, // TrackEntry
- "data": [
- {
- "id": 0xd7, // TrackNumber
- "data": DEFAULT_TRACK_NUMBER
- },
- {
- "id": 0x73c5, // TrackUID
- "data": DEFAULT_TRACK_NUMBER
- },
- {
- "id": 0x9c, // FlagLacing
- "data": 0
- },
- {
- "id": 0x22b59c, // Language
- "data": "und"
- },
- {
- "id": 0x86, // CodecID
- "data": "V_VP8"
- },
- {
- "id": 0x258688, // CodecName
- "data": "VP8"
- },
- {
- "id": 0x83, // TrackType
- "data": 1
- },
- {
- "id": 0xe0, // Video
- "data": [
- {
- "id": 0xb0, // PixelWidth
- "data": videoWidth
- },
- {
- "id": 0xba, // PixelHeight
- "data": videoHeight
- }
- ]
- }
- ]
- }
- ]
- };
-
- ebmlSegment = {
- "id": 0x18538067, // Segment
- "size": -1, // Unbounded size
- "data": [
- seekHead,
- segmentInfo,
- tracks,
- ]
- };
-
- var
- bufferStream = new ArrayBufferDataStream(256);
-
- writeEBML(bufferStream, blobBuffer.pos, [ebmlHeader, ebmlSegment]);
- blobBuffer.write(bufferStream.getAsDataArray());
-
- // Now we know where these top-level elements lie in the file:
- seekPoints.SegmentInfo.positionEBML.data = fileOffsetToSegmentRelative(segmentInfo.offset);
- seekPoints.Tracks.positionEBML.data = fileOffsetToSegmentRelative(tracks.offset);
- };
-
- /**
- * Create a SimpleBlock keyframe header using these fields:
- * timecode - Time of this keyframe
- * trackNumber - Track number from 1 to 126 (inclusive)
- * frame - Raw frame data payload string
- *
- * Returns an EBML element.
- */
- function createKeyframeBlock(keyframe) {
- var
- bufferStream = new ArrayBufferDataStream(1 + 2 + 1);
-
- if (!(keyframe.trackNumber > 0 && keyframe.trackNumber < 127)) {
- throw "TrackNumber must be > 0 and < 127";
- }
-
- bufferStream.writeEBMLVarInt(keyframe.trackNumber); // Always 1 byte since we limit the range of trackNumber
- bufferStream.writeU16BE(keyframe.timecode);
-
- // Flags byte
- bufferStream.writeByte(
- 1 << 7 // Keyframe
- );
-
- return {
- "id": 0xA3, // SimpleBlock
- "data": [
- bufferStream.getAsDataArray(),
- keyframe.frame
- ]
- };
- }
-
- /**
- * Create a Cluster node using these fields:
- *
- * timecode - Start time for the cluster
- *
- * Returns an EBML element.
- */
- function createCluster(cluster) {
- return {
- "id": 0x1f43b675,
- "data": [
- {
- "id": 0xe7, // Timecode
- "data": Math.round(cluster.timecode)
- }
- ]
- };
- }
-
- function addCuePoint(trackIndex, clusterTime, clusterFileOffset) {
- cues.push({
- "id": 0xBB, // Cue
- "data": [
- {
- "id": 0xB3, // CueTime
- "data": clusterTime
- },
- {
- "id": 0xB7, // CueTrackPositions
- "data": [
- {
- "id": 0xF7, // CueTrack
- "data": trackIndex
- },
- {
- "id": 0xF1, // CueClusterPosition
- "data": fileOffsetToSegmentRelative(clusterFileOffset)
- }
- ]
- }
- ]
- });
- }
-
- /**
- * Write a Cues element to the blobStream using the global `cues` array of CuePoints (use addCuePoint()).
- * The seek entry for the Cues in the SeekHead is updated.
- */
- function writeCues() {
- var
- ebml = {
- "id": 0x1C53BB6B,
- "data": cues
- },
-
- cuesBuffer = new ArrayBufferDataStream(16 + cues.length * 32); // Pretty crude estimate of the buffer size we'll need
-
- writeEBML(cuesBuffer, blobBuffer.pos, ebml);
- blobBuffer.write(cuesBuffer.getAsDataArray());
-
- // Now we know where the Cues element has ended up, we can update the SeekHead
- seekPoints.Cues.positionEBML.data = fileOffsetToSegmentRelative(ebml.offset);
- }
-
- /**
- * Flush the frames in the current clusterFrameBuffer out to the stream as a Cluster.
- */
- function flushClusterFrameBuffer() {
- if (clusterFrameBuffer.length == 0) {
- return;
- }
-
- // First work out how large of a buffer we need to hold the cluster data
- var
- rawImageSize = 0;
-
- for (var i = 0; i < clusterFrameBuffer.length; i++) {
- rawImageSize += clusterFrameBuffer[i].frame.length;
- }
-
- var
- buffer = new ArrayBufferDataStream(rawImageSize + clusterFrameBuffer.length * 32), // Estimate 32 bytes per SimpleBlock header
-
- cluster = createCluster({
- timecode: Math.round(clusterStartTime),
- });
-
- for (var i = 0; i < clusterFrameBuffer.length; i++) {
- cluster.data.push(createKeyframeBlock(clusterFrameBuffer[i]));
- }
-
- writeEBML(buffer, blobBuffer.pos, cluster);
- blobBuffer.write(buffer.getAsDataArray());
-
- addCuePoint(DEFAULT_TRACK_NUMBER, Math.round(clusterStartTime), cluster.offset);
-
- clusterFrameBuffer = [];
- clusterStartTime += clusterDuration;
- clusterDuration = 0;
- }
-
- function validateOptions() {
- // Derive frameDuration setting if not already supplied
- if (!options.frameDuration) {
- if (options.frameRate) {
- options.frameDuration = 1000 / options.frameRate;
- } else {
- throw "Missing required frameDuration or frameRate setting";
- }
- }
- }
-
- function addFrameToCluster(frame) {
- frame.trackNumber = DEFAULT_TRACK_NUMBER;
-
- // Frame timecodes are relative to the start of their cluster:
- frame.timecode = Math.round(clusterDuration);
-
- clusterFrameBuffer.push(frame);
-
- clusterDuration += frame.duration;
-
- if (clusterDuration >= MAX_CLUSTER_DURATION_MSEC) {
- flushClusterFrameBuffer();
- }
- }
-
- /**
- * Rewrites the SeekHead element that was initially written to the stream with the offsets of top level elements.
- *
- * Call once writing is complete (so the offset of all top level elements is known).
- */
- function rewriteSeekHead() {
- var
- seekHeadBuffer = new ArrayBufferDataStream(seekHead.size),
- oldPos = blobBuffer.pos;
-
- // Write the rewritten SeekHead element's data payload to the stream (don't need to update the id or size)
- writeEBML(seekHeadBuffer, seekHead.dataOffset, seekHead.data);
-
- // And write that through to the file
- blobBuffer.seek(seekHead.dataOffset);
- blobBuffer.write(seekHeadBuffer.getAsDataArray());
-
- blobBuffer.seek(oldPos);
- }
-
- /**
- * Rewrite the Duration field of the Segment with the newly-discovered video duration.
- */
- function rewriteDuration() {
- var
- buffer = new ArrayBufferDataStream(8),
- oldPos = blobBuffer.pos;
-
- // Rewrite the data payload (don't need to update the id or size)
- buffer.writeDoubleBE(clusterStartTime);
-
- // And write that through to the file
- blobBuffer.seek(segmentDuration.dataOffset);
- blobBuffer.write(buffer.getAsDataArray());
-
- blobBuffer.seek(oldPos);
- }
-
- /**
- * Add a frame to the video. Currently the frame must be a Canvas element.
- */
- this.addFrame = function(canvas, duration) {
- //if (writtenHeader) {
- // if (canvas.width != videoWidth || canvas.height != videoHeight) {
- // throw "Frame size differs from previous frames";
- // }
- //} else {
- videoWidth = canvas.width;
- videoHeight = canvas.height;
-
- writeHeader();
- writtenHeader = true;
- //}
-
- var
- webP = renderAsWebP(canvas, options.quality);
-
- if (!webP) {
- throw "Couldn't decode WebP frame, does the browser support WebP?";
- }
-
- addFrameToCluster({
- frame: extractKeyframeFromWebP(webP),
- duration: ((typeof duration == 'number')?duration:options.frameDuration)
- });
- };
-
- /**
- * Finish writing the video and return a Promise to signal completion.
- *
- * If the destination device was memory (i.e. options.fileWriter was not supplied), the Promise is resolved with
- * a Blob with the contents of the entire video.
- */
- this.complete = function() {
- flushClusterFrameBuffer();
-
- writeCues();
- rewriteSeekHead();
- rewriteDuration();
-
- return blobBuffer.complete('video/webm');
- };
-
- this.getWrittenSize = function() {
- return blobBuffer.length;
- };
-
- options = extend(optionDefaults, options || {});
- validateOptions();
- };
- };
- if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
- module.exports = WebMWriter(require("./ArrayBufferDataStream"), require("./BlobBuffer"));
- } else {
- window.WebMWriter = WebMWriter(ArrayBufferDataStream, BlobBuffer);
- }
- })();
|