/**
 * 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 {
  self.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 {
  self.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 ||
        (ArrayBuffer.isView(ebml.data) &&
         ebml.data.BYTES_PER_ELEMENT === 1)) {
      // isView as well as instanceof: a byte array that crossed a realm
      // boundary (worker, vm context) fails the instanceof check
      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 = 5000, DEFAULT_TRACK_NUMBER = 1,
        AUDIO_TRACK_NUMBER = 2,
        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

      // Optional second track holding Opus audio. Supply:
      //   {sampleRate, channels, codecPrivate: Uint8Array (OpusHead)}
      audio: null,
    },

    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 trackEntries = [{
          '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
             },*/
          ]
        }];

      // Optional Opus audio track.
      // CodecPrivate must be an OpusHead block, and Opus needs the pre-roll
      // hints or players will start it with audible artefacts.
      if (options.audio) {
        trackEntries.push({
          'id': 0xae,  // TrackEntry
          'data': [
            {
              'id': 0xd7,  // TrackNumber
              'data': AUDIO_TRACK_NUMBER
            },
            {
              'id': 0x73c5,  // TrackUID
              'data': AUDIO_TRACK_NUMBER
            },
            {
              'id': 0x83,  // TrackType (2 = audio)
              'data': 2
            },
            {
              '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': 'A_OPUS'
            },
            {
              'id': 0x63A2,  // CodecPrivate
              'data': options.audio.codecPrivate
            },
            {
              'id': 0x56AA,  // CodecDelay (ns)
              'data': 6500000
            },
            {
              'id': 0x56BB,  // SeekPreRoll (ns)
              'data': 80000000
            },
            {
              'id': 0xe1,  // Audio
              'data': [
                {
                  'id': 0xb5,  // SamplingFrequency
                  'data': new EBMLFloat64(options.audio.sampleRate)
                },
                {
                  'id': 0x9f,  // Channels
                  'data': options.audio.channels
                },
                {
                  'id': 0x6264,  // BitDepth
                  'data': 32
                }
              ]
            }
          ]
        });
      }

      let tracks = {
        'id': 0x1654ae6b,  // Tracks
        'data': trackEntries
      };

      ebmlSegment = {
        'id': 0x18538067,  // Segment
        'size': -1,        // Unbounded size
        'data': [
          seekHead,
          segmentInfo,
          tracks,
        ]
      };

      // Has to fit the header, SeekHead, SegmentInfo and every TrackEntry.
      // 256 was only ever enough for a single video track.
      let bufferStream = new ArrayBufferDataStream(1024);

      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 = frame.trackNumber || DEFAULT_TRACK_NUMBER;
        var time = frame.intime / 1000;
        if (firstTimestampEver) {
          earliestTimestamp = time;
          time = 0;
          firstTimestampEver = false;
        } else {
          time = time - earliestTimestamp;
        }
        if (time > lastTimeCode) {
          lastTimeCode = time;
        }

        // Start a new cluster on a video keyframe once the current one has
        // grown past the limit. SimpleBlock timecodes are a signed 16 bit
        // offset from the cluster, so clusters cannot span more than ~32s.
        const isVideoKeyframe =
            frame.trackNumber == DEFAULT_TRACK_NUMBER && frame.type == 'key';
        if (
          clusterFrameBuffer.length > 0 && isVideoKeyframe &&
          time - clusterStartTime >= MAX_CLUSTER_DURATION_MSEC) {
          flushClusterFrameBuffer();
        }

        if (clusterFrameBuffer.length === 0) {
          clusterStartTime = time;
        }

        // Frame timecodes are relative to the start of their cluster:
        frame.timecode = Math.round(time - clusterStartTime);

        clusterFrameBuffer.push(frame);
        clusterDuration = frame.timecode + 1;
      }

      /**
       * 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, trackNumber) {
        if (!writtenHeader) {
          videoWidth = options.width;
          videoHeight = options.height;
          writeHeader();
        }
        const name = frame.constructor.name;
        if (name == 'EncodedVideoChunk' || name == 'EncodedAudioChunk') {
          let frameData = new Uint8Array(frame.byteLength);
          frame.copyTo(frameData);
          addFrameToCluster({
            frame: frameData,
            intime: frame.timestamp,
            type: frame.type,
            trackNumber: trackNumber ||
                (name == 'EncodedAudioChunk' ? AUDIO_TRACK_NUMBER :
                                              DEFAULT_TRACK_NUMBER),
          });
          return;
        }
      };

      // Encoded audio for the optional second track
      this.addAudioChunk = function(chunk) {
        this.addFrame(chunk, AUDIO_TRACK_NUMBER);
      };

      /**
       * 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 {
  self.WebMWriter =
      WebMWriter(self.ArrayBufferDataStream, self.BlobBuffer);
}
})();
