Skip to content

Instantly share code, notes, and snippets.

@josephrocca
Last active July 27, 2023 04:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save josephrocca/0d6ea26370b897cb71ef9cb3b947fcd6 to your computer and use it in GitHub Desktop.
Save josephrocca/0d6ea26370b897cb71ef9cb3b947fcd6 to your computer and use it in GitHub Desktop.
// This is a version of this file: https://w3c.github.io/webcodecs/samples/capture-to-file/webm-writer2.js
// With these fixed applied: https://github.com/w3c/webcodecs/issues/332#issuecomment-1077442192
/**
* 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) {
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,
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.
*/
let firstCueWritten = false;
function writeCues() {
if(firstCueWritten) return;
firstCueWritten = true;
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') {
let frameData = new Uint8Array(frame.byteLength);
frame.copyTo(frameData);
addFrameToCluster({
frame: frameData,
intime: frame.timestamp,
type: frame.type,
});
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 {
self.WebMWriter =
WebMWriter(self.ArrayBufferDataStream, self.BlobBuffer);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment