diff --git a/samples/capture-to-file/capture-to-file.html b/samples/capture-to-file/capture-to-file.html
new file mode 100755
index 00000000..2aa82b56
--- /dev/null
+++ b/samples/capture-to-file/capture-to-file.html
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+ WebCodecs API demo: Encoding and Decoding
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/capture-to-file/webm-writer2.js b/samples/capture-to-file/webm-writer2.js
new file mode 100755
index 00000000..ccf64a0b
--- /dev/null
+++ b/samples/capture-to-file/webm-writer2.js
@@ -0,0 +1,1162 @@
+/**
+ * A tool for presenting an ArrayBuffer as a stream for writing some simple data
+ * types.
+ *
+ * By Nicholas Sherlock, with updates from jimbankoski
+ *
+ * - make it work off frames with timestamps from webcodecs
+ * - make it write via Native File IO apis instead of FileWriter
+ * - remove alpha and transparency
+ * -
+ *
+ * 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.
+ */
+let ArrayBufferDataStream = function(length) {
+ this.data = new Uint8Array(length);
+ this.pos = 0;
+};
+
+ArrayBufferDataStream.prototype.seek = function(toOffset) {
+ this.pos = toOffset;
+};
+
+ArrayBufferDataStream.prototype.writeBytes = function(arr) {
+ for (let 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) {
+ let bytes = new Uint8Array(new Float64Array([d]).buffer);
+
+ for (let i = bytes.length - 1; i >= 0; i--) {
+ this.writeByte(bytes[i]);
+ }
+};
+
+ArrayBufferDataStream.prototype.writeFloatBE = function(d) {
+ let bytes = new Uint8Array(new Float32Array([d]).buffer);
+
+ for (let i = bytes.length - 1; i >= 0; i--) {
+ this.writeByte(bytes[i]);
+ }
+};
+
+/**
+ * Write an ASCII string to the stream
+ */
+ArrayBufferDataStream.prototype.writeString = function(s) {
+ for (let 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 Error('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 Error('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 Error('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 new Error('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() {
+let BlobBuffer = function(fs) {
+ return function(destination) {
+ let buffer = [], writePromise = Promise.resolve(), fileWriter = null,
+ fd = null;
+
+ if (destination &&
+ destination.constructor.name === 'FileSystemWritableFileStream') {
+ 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) {
+ let 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) {
+ let result = data.byteLength || data.length || data.size;
+
+ if (!Number.isInteger(result)) {
+ throw new Error('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 new Error('Offset may not be negative');
+ }
+
+ if (isNaN(offset)) {
+ throw new Error('Offset may not be NaN');
+ }
+
+ if (offset > this.length) {
+ throw new Error('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) {
+ let 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(async function() {
+ if (fd) {
+ return new Promise(function(resolve, reject) {
+ convertToUint8Array(newEntry.data).then(function(dataArray) {
+ let 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.seek(newEntry.offset)
+ .then(() => {fileWriter.write(new Blob([newEntry.data]))})
+ .then(() => {resolve();
+ })
+ });
+ } 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 (let i = 0; i < buffer.length; i++) {
+ let 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() {
+ let result = [];
+
+ for (let i = 0; i < buffer.length; i++) {
+ result.push(buffer[i].data);
+ }
+
+ return new Blob(result, {type: 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() {
+function extend(base, top) {
+ let target = {};
+
+ [base, top].forEach(function(obj) {
+ for (let prop in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, prop)) {
+ target[prop] = obj[prop];
+ }
+ }
+ });
+
+ return target;
+}
+
+/**
+ * @param {String} string
+ * @returns {number}
+ */
+function byteStringToUint32LE(string) {
+ let a = string.charCodeAt(0), b = string.charCodeAt(1),
+ c = string.charCodeAt(2), d = string.charCodeAt(3);
+
+ return (a | (b << 8) | (c << 16) | (d << 24)) >>> 0;
+}
+
+
+// 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.
+ *
+ * @param buffer
+ * @param {Number} bufferFileOffset - The buffer's first byte is at this
+ * position 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.
+ * @param {*} ebml
+ */
+function writeEBML(buffer, bufferFileOffset, ebml) {
+ // Is the ebml an array of sibling elements?
+ if (Array.isArray(ebml)) {
+ for (let 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
+
+ let 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 new Error('Bad EBML datatype ' + typeof ebml.data);
+ }
+ } else {
+ throw new Error('Bad EBML datatype ' + typeof ebml.data);
+ }
+}
+
+/**
+ * @typedef {Object} Frame
+ * @property {string} frame - Raw VP8 frame data
+ * @property {Number} trackNumber - From 1 to 126 (inclusive)
+ * @property {Number} timecode
+ */
+
+/**
+ * @typedef {Object} Cluster
+ * @property {Number} timecode - Start time for the cluster
+ */
+
+/**
+ * @param ArrayBufferDataStream - Imported library
+ * @param BlobBuffer - Imported library
+ *
+ * @returns WebMWriter
+ *
+ * @constructor
+ */
+let WebMWriter = function(ArrayBufferDataStream, BlobBuffer) {
+ return function(options) {
+ let MAX_CLUSTER_DURATION_MSEC = 5000000, DEFAULT_TRACK_NUMBER = 1,
+ writtenHeader = false, videoWidth = 0, videoHeight = 0,
+ firstTimestampEver = true, earliestTimestamp = 0,
+
+
+ /**
+ *
+ * @type {Frame[]}
+ */
+ clusterFrameBuffer = [], clusterStartTime = 0, clusterDuration = 0,
+ lastTimeCode = 0,
+
+ optionDefaults = {
+ 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)
+ codec: 'VP8', // Codec to write to webm file
+
+ },
+
+ 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, // Root element of the EBML document
+
+ 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() {
+ let 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 (let name in seekPoints) {
+ let 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();
+
+ let 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 microseconds (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
+ ]
+ },
+
+ videoProperties = [
+ {
+ 'id': 0xb0, // PixelWidth
+ 'data': videoWidth
+ },
+ {
+ 'id': 0xba, // PixelHeight
+ 'data': videoHeight
+ }
+ ];
+
+ let tracks = {
+ 'id': 0x1654ae6b, // Tracks
+ 'data': [{
+ 'id': 0xae, // TrackEntry
+ 'data': [
+ {
+ 'id': 0xd7, // TrackNumber
+ 'data': DEFAULT_TRACK_NUMBER
+ },
+ {
+ 'id': 0x73c5, // TrackUID
+ 'data': DEFAULT_TRACK_NUMBER
+ },
+ {
+ 'id': 0x83, // TrackType
+ 'data': 1
+ },
+ {
+ 'id': 0xe0, // Video
+ 'data': videoProperties
+ },
+ {
+ 'id': 0x9c, // FlagLacing
+ 'data': 0
+ },
+ {
+ 'id': 0x22b59c, // Language
+ 'data': 'und'
+ },
+ {
+ 'id': 0xb9, // FlagEnabled
+ 'data': 1
+ },
+ {
+ 'id': 0x88, // FlagDefault
+ 'data': 1
+ },
+ {
+ 'id': 0x55aa, // FlagForced
+ 'data': 0
+ },
+
+ {
+ 'id': 0x86, // CodecID
+ 'data': 'V_' + options.codec
+ }, /*
+ (options.codec == 'VP8' ?
+ {
+ 'id': 0x63A2, // Codec private data
+ 'data': []
+ } :
+ {
+ 'id': 0x63A2, // Codec private data for vp9
+ 'data': [
+ {
+ 'id': 1, // vp9 Profile
+ 'size': 1,
+ 'data': 0
+ },
+ {
+ 'id': 2, // Feature level
+ 'size': 1,
+ 'data': 10
+ },
+ {
+ 'id': 3, // bitdepth level
+ 'size': 1,
+ 'data': 8
+ },
+ {
+ 'id': 4, // color sampling
+ 'size': 1,
+ 'data': 0
+ }
+ ]
+ }),
+ {
+ 'id': 0x258688, // CodecName
+ 'data': options.codec
+ },*/
+ ]
+ }]
+ };
+
+ ebmlSegment = {
+ 'id': 0x18538067, // Segment
+ 'size': -1, // Unbounded size
+ 'data': [
+ seekHead,
+ segmentInfo,
+ tracks,
+ ]
+ };
+
+ let 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);
+
+ writtenHeader = true;
+ }
+
+ /**
+ * Create a SimpleBlock element to hold the given frame.
+ *
+ * @param {Frame} frame
+ *
+ * @return A SimpleBlock EBML element.
+ */
+ function createSimpleBlockForframe(frame) {
+ let bufferStream = new ArrayBufferDataStream(1 + 2 + 1);
+
+ if (!(frame.trackNumber > 0 && frame.trackNumber < 127)) {
+ throw new Error('TrackNumber must be > 0 and < 127');
+ }
+
+ bufferStream.writeEBMLVarInt(
+ frame.trackNumber); // Always 1 byte since we limit the range of
+ // trackNumber
+ bufferStream.writeU16BE(frame.timecode);
+
+ // Flags byte
+ bufferStream.writeByte(
+ (frame.type == "key" ? 1 : 0) << 7 // frame
+ );
+
+ return {
+ 'id': 0xA3, // SimpleBlock
+ 'data': [bufferStream.getAsDataArray(), frame.frame]
+ };
+ }
+
+ /**
+ * Create a Cluster EBML node.
+ *
+ * @param {Cluster} 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() {
+ let 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
+ let rawImageSize = 0;
+
+ for (let i = 0; i < clusterFrameBuffer.length; i++) {
+ rawImageSize += clusterFrameBuffer[i].frame.byteLength;
+ }
+
+ let buffer = new ArrayBufferDataStream(
+ rawImageSize +
+ clusterFrameBuffer.length *
+ 64), // Estimate 64 bytes per block header
+
+ cluster = createCluster({
+ timecode: Math.round(clusterStartTime),
+ });
+
+ for (let i = 0; i < clusterFrameBuffer.length; i++) {
+ cluster.data.push(createSimpleBlockForframe(clusterFrameBuffer[i]));
+ }
+
+ writeEBML(buffer, blobBuffer.pos, cluster);
+ blobBuffer.write(buffer.getAsDataArray());
+
+ addCuePoint(
+ DEFAULT_TRACK_NUMBER, Math.round(clusterStartTime), cluster.offset);
+
+ clusterFrameBuffer = [];
+ clusterDuration = 0;
+ }
+
+ function validateOptions() {
+ }
+
+ /**
+ *
+ * @param {Frame} frame
+ */
+ function addFrameToCluster(frame) {
+ frame.trackNumber = DEFAULT_TRACK_NUMBER;
+ var time = frame.intime / 1000;
+ if (firstTimestampEver) {
+ earliestTimestamp = time;
+ time = 0;
+ firstTimestampEver = false;
+ } else {
+ time = time - earliestTimestamp;
+ }
+ lastTimeCode = time;
+ if (clusterDuration == 0) clusterStartTime = time;
+
+ // Frame timecodes are relative to the start of their cluster:
+ // frame.timecode = Math.round(clusterDuration);
+ frame.timecode = Math.round(time - clusterStartTime);
+
+ clusterFrameBuffer.push(frame);
+ clusterDuration = frame.timecode + 1;
+
+ 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() {
+ let 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() {
+ let buffer = new ArrayBufferDataStream(8), oldPos = blobBuffer.pos;
+
+ // Rewrite the data payload (don't need to update the id or size)
+ buffer.writeDoubleBE(lastTimeCode);
+
+ // And write that through to the file
+ blobBuffer.seek(segmentDuration.dataOffset);
+ blobBuffer.write(buffer.getAsDataArray());
+
+ blobBuffer.seek(oldPos);
+ }
+
+ /**
+ * Add a frame to the video.
+ *
+ * @param {HTMLCanvasElement|String} frame - A Canvas element that
+ * contains the frame, or a WebP string you obtained by calling
+ * toDataUrl() on an image yourself.
+ *
+ */
+ this.addFrame = function(frame) {
+ if (!writtenHeader) {
+ videoWidth = options.width;
+ videoHeight = options.height;
+ writeHeader();
+ }
+ if (frame.constructor.name == 'EncodedVideoChunk') {
+ addFrameToCluster({
+ frame: new Uint8Array(frame.data),
+ intime: frame.timestamp,
+ });
+ return;
+ }
+ };
+
+ /**
+ * 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() {
+ if (!writtenHeader) {
+ writeHeader();
+ }
+ firstTimestampEver = true;
+
+ 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(window.ArrayBufferDataStream, window.BlobBuffer);
+}
+})();
diff --git a/samples/index.html b/samples/index.html
index 181960b8..72789f35 100644
--- a/samples/index.html
+++ b/samples/index.html
@@ -23,9 +23,9 @@