EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
webm-writer.js
Go to the documentation of this file.
1/**
2 * A tool for presenting an ArrayBuffer as a stream for writing some simple data types.
3 *
4 * By Nicholas Sherlock
5 *
6 * Released under the WTFPLv2 https://en.wikipedia.org/wiki/WTFPL
7 */
8
9"use strict";
10
11(function(){
12 /*
13 * Create an ArrayBuffer of the given length and present it as a writable stream with methods
14 * for writing data in different formats.
15 */
16 var ArrayBufferDataStream = function(length) {
17 this.data = new Uint8Array(length);
18 this.pos = 0;
19 };
20
21 ArrayBufferDataStream.prototype.seek = function(offset) {
22 this.pos = offset;
23 };
24
25 ArrayBufferDataStream.prototype.writeBytes = function(arr) {
26 for (var i = 0; i < arr.length; i++) {
27 this.data[this.pos++] = arr[i];
28 }
29 };
30
31 ArrayBufferDataStream.prototype.writeByte = function(b) {
32 this.data[this.pos++] = b;
33 };
34
35 //Synonym:
36 ArrayBufferDataStream.prototype.writeU8 = ArrayBufferDataStream.prototype.writeByte;
37
38 ArrayBufferDataStream.prototype.writeU16BE = function(u) {
39 this.data[this.pos++] = u >> 8;
40 this.data[this.pos++] = u;
41 };
42
43 ArrayBufferDataStream.prototype.writeDoubleBE = function(d) {
44 var
45 bytes = new Uint8Array(new Float64Array([d]).buffer);
46
47 for (var i = bytes.length - 1; i >= 0; i--) {
48 this.writeByte(bytes[i]);
49 }
50 };
51
52 ArrayBufferDataStream.prototype.writeFloatBE = function(d) {
53 var
54 bytes = new Uint8Array(new Float32Array([d]).buffer);
55
56 for (var i = bytes.length - 1; i >= 0; i--) {
57 this.writeByte(bytes[i]);
58 }
59 };
60
61 /**
62 * Write an ASCII string to the stream
63 */
64 ArrayBufferDataStream.prototype.writeString = function(s) {
65 for (var i = 0; i < s.length; i++) {
66 this.data[this.pos++] = s.charCodeAt(i);
67 }
68 };
69
70 /**
71 * Write the given 32-bit integer to the stream as an EBML variable-length integer using the given byte width
72 * (use measureEBMLVarInt).
73 *
74 * No error checking is performed to ensure that the supplied width is correct for the integer.
75 *
76 * @param i Integer to be written
77 * @param width Number of bytes to write to the stream
78 */
79 ArrayBufferDataStream.prototype.writeEBMLVarIntWidth = function(i, width) {
80 switch (width) {
81 case 1:
82 this.writeU8((1 << 7) | i);
83 break;
84 case 2:
85 this.writeU8((1 << 6) | (i >> 8));
86 this.writeU8(i);
87 break;
88 case 3:
89 this.writeU8((1 << 5) | (i >> 16));
90 this.writeU8(i >> 8);
91 this.writeU8(i);
92 break;
93 case 4:
94 this.writeU8((1 << 4) | (i >> 24));
95 this.writeU8(i >> 16);
96 this.writeU8(i >> 8);
97 this.writeU8(i);
98 break;
99 case 5:
100 /*
101 * JavaScript converts its doubles to 32-bit integers for bitwise operations, so we need to do a
102 * division by 2^32 instead of a right-shift of 32 to retain those top 3 bits
103 */
104 this.writeU8((1 << 3) | ((i / 4294967296) & 0x7));
105 this.writeU8(i >> 24);
106 this.writeU8(i >> 16);
107 this.writeU8(i >> 8);
108 this.writeU8(i);
109 break;
110 default:
111 throw new RuntimeException("Bad EBML VINT size " + width);
112 }
113 };
114
115 /**
116 * Return the number of bytes needed to encode the given integer as an EBML VINT.
117 */
118 ArrayBufferDataStream.prototype.measureEBMLVarInt = function(val) {
119 if (val < (1 << 7) - 1) {
120 /* Top bit is set, leaving 7 bits to hold the integer, but we can't store 127 because
121 * "all bits set to one" is a reserved value. Same thing for the other cases below:
122 */
123 return 1;
124 } else if (val < (1 << 14) - 1) {
125 return 2;
126 } else if (val < (1 << 21) - 1) {
127 return 3;
128 } else if (val < (1 << 28) - 1) {
129 return 4;
130 } else if (val < 34359738367) { // 2 ^ 35 - 1 (can address 32GB)
131 return 5;
132 } else {
133 throw new RuntimeException("EBML VINT size not supported " + val);
134 }
135 };
136
137 ArrayBufferDataStream.prototype.writeEBMLVarInt = function(i) {
138 this.writeEBMLVarIntWidth(i, this.measureEBMLVarInt(i));
139 };
140
141 /**
142 * Write the given unsigned 32-bit integer to the stream in big-endian order using the given byte width.
143 * No error checking is performed to ensure that the supplied width is correct for the integer.
144 *
145 * Omit the width parameter to have it determined automatically for you.
146 *
147 * @param u Unsigned integer to be written
148 * @param width Number of bytes to write to the stream
149 */
150 ArrayBufferDataStream.prototype.writeUnsignedIntBE = function(u, width) {
151 if (width === undefined) {
152 width = this.measureUnsignedInt(u);
153 }
154
155 // Each case falls through:
156 switch (width) {
157 case 5:
158 this.writeU8(Math.floor(u / 4294967296)); // Need to use division to access >32 bits of floating point var
159 case 4:
160 this.writeU8(u >> 24);
161 case 3:
162 this.writeU8(u >> 16);
163 case 2:
164 this.writeU8(u >> 8);
165 case 1:
166 this.writeU8(u);
167 break;
168 default:
169 throw new RuntimeException("Bad UINT size " + width);
170 }
171 };
172
173 /**
174 * Return the number of bytes needed to hold the non-zero bits of the given unsigned integer.
175 */
176 ArrayBufferDataStream.prototype.measureUnsignedInt = function(val) {
177 // Force to 32-bit unsigned integer
178 if (val < (1 << 8)) {
179 return 1;
180 } else if (val < (1 << 16)) {
181 return 2;
182 } else if (val < (1 << 24)) {
183 return 3;
184 } else if (val < 4294967296) {
185 return 4;
186 } else {
187 return 5;
188 }
189 };
190
191 /**
192 * Return a view on the portion of the buffer from the beginning to the current seek position as a Uint8Array.
193 */
194 ArrayBufferDataStream.prototype.getAsDataArray = function() {
195 if (this.pos < this.data.byteLength) {
196 return this.data.subarray(0, this.pos);
197 } else if (this.pos == this.data.byteLength) {
198 return this.data;
199 } else {
200 throw "ArrayBufferDataStream's pos lies beyond end of buffer";
201 }
202 };
203
204 if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
205 module.exports = ArrayBufferDataStream;
206 } else {
207 window.ArrayBufferDataStream = ArrayBufferDataStream;
208 }
209}());"use strict";
210
211/**
212 * Allows a series of Blob-convertible objects (ArrayBuffer, Blob, String, etc) to be added to a buffer. Seeking and
213 * overwriting of blobs is allowed.
214 *
215 * You can supply a FileWriter, in which case the BlobBuffer is just used as temporary storage before it writes it
216 * through to the disk.
217 *
218 * By Nicholas Sherlock
219 *
220 * Released under the WTFPLv2 https://en.wikipedia.org/wiki/WTFPL
221 */
222(function() {
223 var BlobBuffer = function(fs) {
224 return function(destination) {
225 var
226 buffer = [],
227 writePromise = Promise.resolve(),
228 fileWriter = null,
229 fd = null;
230
231 if (typeof FileWriter !== "undefined" && destination instanceof FileWriter) {
232 fileWriter = destination;
233 } else if (fs && destination) {
234 fd = destination;
235 }
236
237 // Current seek offset
238 this.pos = 0;
239
240 // One more than the index of the highest byte ever written
241 this.length = 0;
242
243 // Returns a promise that converts the blob to an ArrayBuffer
244 function readBlobAsBuffer(blob) {
245 return new Promise(function (resolve, reject) {
246 var
247 reader = new FileReader();
248
249 reader.addEventListener("loadend", function () {
250 resolve(reader.result);
251 });
252
253 reader.readAsArrayBuffer(blob);
254 });
255 }
256
257 function convertToUint8Array(thing) {
258 return new Promise(function (resolve, reject) {
259 if (thing instanceof Uint8Array) {
260 resolve(thing);
261 } else if (thing instanceof ArrayBuffer || ArrayBuffer.isView(thing)) {
262 resolve(new Uint8Array(thing));
263 } else if (thing instanceof Blob) {
264 resolve(readBlobAsBuffer(thing).then(function (buffer) {
265 return new Uint8Array(buffer);
266 }));
267 } else {
268 //Assume that Blob will know how to read this thing
269 resolve(readBlobAsBuffer(new Blob([thing])).then(function (buffer) {
270 return new Uint8Array(buffer);
271 }));
272 }
273 });
274 }
275
276 function measureData(data) {
277 var
278 result = data.byteLength || data.length || data.size;
279
280 if (!Number.isInteger(result)) {
281 throw "Failed to determine size of element";
282 }
283
284 return result;
285 }
286
287 /**
288 * Seek to the given absolute offset.
289 *
290 * You may not seek beyond the end of the file (this would create a hole and/or allow blocks to be written in non-
291 * sequential order, which isn't currently supported by the memory buffer backend).
292 */
293 this.seek = function (offset) {
294 if (offset < 0) {
295 throw "Offset may not be negative";
296 }
297
298 if (isNaN(offset)) {
299 throw "Offset may not be NaN";
300 }
301
302 if (offset > this.length) {
303 throw "Seeking beyond the end of file is not allowed";
304 }
305
306 this.pos = offset;
307 };
308
309 /**
310 * Write the Blob-convertible data to the buffer at the current seek position.
311 *
312 * Note: If overwriting existing data, the write must not cross preexisting block boundaries (written data must
313 * be fully contained by the extent of a previous write).
314 */
315 this.write = function (data) {
316 var
317 newEntry = {
318 offset: this.pos,
319 data: data,
320 length: measureData(data)
321 },
322 isAppend = newEntry.offset >= this.length;
323
324 this.pos += newEntry.length;
325 this.length = Math.max(this.length, this.pos);
326
327 // After previous writes complete, perform our write
328 writePromise = writePromise.then(function () {
329 if (fd) {
330 return new Promise(function(resolve, reject) {
331 convertToUint8Array(newEntry.data).then(function(dataArray) {
332 var
333 totalWritten = 0,
334 buffer = Buffer.from(dataArray.buffer),
335
336 handleWriteComplete = function(err, written, buffer) {
337 totalWritten += written;
338
339 if (totalWritten >= buffer.length) {
340 resolve();
341 } else {
342 // We still have more to write...
343 fs.write(fd, buffer, totalWritten, buffer.length - totalWritten, newEntry.offset + totalWritten, handleWriteComplete);
344 }
345 };
346
347 fs.write(fd, buffer, 0, buffer.length, newEntry.offset, handleWriteComplete);
348 });
349 });
350 } else if (fileWriter) {
351 return new Promise(function (resolve, reject) {
352 fileWriter.onwriteend = resolve;
353
354 fileWriter.seek(newEntry.offset);
355 fileWriter.write(new Blob([newEntry.data]));
356 });
357 } else if (!isAppend) {
358 // We might be modifying a write that was already buffered in memory.
359
360 // Slow linear search to find a block we might be overwriting
361 for (var i = 0; i < buffer.length; i++) {
362 var
363 entry = buffer[i];
364
365 // If our new entry overlaps the old one in any way...
366 if (!(newEntry.offset + newEntry.length <= entry.offset || newEntry.offset >= entry.offset + entry.length)) {
367 if (newEntry.offset < entry.offset || newEntry.offset + newEntry.length > entry.offset + entry.length) {
368 throw new Error("Overwrite crosses blob boundaries");
369 }
370
371 if (newEntry.offset == entry.offset && newEntry.length == entry.length) {
372 // We overwrote the entire block
373 entry.data = newEntry.data;
374
375 // We're done
376 return;
377 } else {
378 return convertToUint8Array(entry.data)
379 .then(function (entryArray) {
380 entry.data = entryArray;
381
382 return convertToUint8Array(newEntry.data);
383 }).then(function (newEntryArray) {
384 newEntry.data = newEntryArray;
385
386 entry.data.set(newEntry.data, newEntry.offset - entry.offset);
387 });
388 }
389 }
390 }
391 // Else fall through to do a simple append, as we didn't overwrite any pre-existing blocks
392 }
393
394 buffer.push(newEntry);
395 });
396 };
397
398 /**
399 * Finish all writes to the buffer, returning a promise that signals when that is complete.
400 *
401 * If a FileWriter was not provided, the promise is resolved with a Blob that represents the completed BlobBuffer
402 * contents. You can optionally pass in a mimeType to be used for this blob.
403 *
404 * If a FileWriter was provided, the promise is resolved with null as the first argument.
405 */
406 this.complete = function (mimeType) {
407 if (fd || fileWriter) {
408 writePromise = writePromise.then(function () {
409 return null;
410 });
411 } else {
412 // After writes complete we need to merge the buffer to give to the caller
413 writePromise = writePromise.then(function () {
414 var
415 result = [];
416
417 for (var i = 0; i < buffer.length; i++) {
418 result.push(buffer[i].data);
419 }
420
421 return new Blob(result, {mimeType: mimeType});
422 });
423 }
424
425 return writePromise;
426 };
427 };
428 };
429
430 if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
431 module.exports = BlobBuffer(require('fs'));
432 } else {
433 window.BlobBuffer = BlobBuffer(null);
434 }
435})();/**
436 * WebM video encoder for Google Chrome. This implementation is suitable for creating very large video files, because
437 * it can stream Blobs directly to a FileWriter without buffering the entire video in memory.
438 *
439 * When FileWriter is not available or not desired, it can buffer the video in memory as a series of Blobs which are
440 * eventually returned as one composite Blob.
441 *
442 * By Nicholas Sherlock.
443 *
444 * Based on the ideas from Whammy: https://github.com/antimatter15/whammy
445 *
446 * Released under the WTFPLv2 https://en.wikipedia.org/wiki/WTFPL
447 */
448
449"use strict";
450
451(function() {
452 var WebMWriter = function(ArrayBufferDataStream, BlobBuffer) {
453 function extend(base, top) {
454 var
455 target = {};
456
457 [base, top].forEach(function(obj) {
458 for (var prop in obj) {
459 if (Object.prototype.hasOwnProperty.call(obj, prop)) {
460 target[prop] = obj[prop];
461 }
462 }
463 });
464
465 return target;
466 }
467
468 /**
469 * Decode a Base64 data URL into a binary string.
470 *
471 * Returns the binary string, or false if the URL could not be decoded.
472 */
473 function decodeBase64WebPDataURL(url) {
474 if (typeof url !== "string" || !url.match(/^data:image\/webp;base64,/i)) {
475 return false;
476 }
477
478 return window.atob(url.substring("data:image\/webp;base64,".length));
479 }
480
481 /**
482 * Convert a raw binary string (one character = one output byte) to an ArrayBuffer
483 */
484 function stringToArrayBuffer(string) {
485 var
486 buffer = new ArrayBuffer(string.length),
487 int8Array = new Uint8Array(buffer);
488
489 for (var i = 0; i < string.length; i++) {
490 int8Array[i] = string.charCodeAt(i);
491 }
492
493 return buffer;
494 }
495
496 /**
497 * Convert the given canvas to a WebP encoded image and return the image data as a string.
498 */
499 function renderAsWebP(canvas, quality) {
500 var
501 frame = canvas.toDataURL('image/webp', quality);
502
503 return decodeBase64WebPDataURL(frame);
504 }
505
506 function extractKeyframeFromWebP(webP) {
507 // Assume that Chrome will generate a Simple Lossy WebP which has this header:
508 var
509 keyframeStartIndex = webP.indexOf('VP8 ');
510
511 if (keyframeStartIndex == -1) {
512 throw "Failed to identify beginning of keyframe in WebP image";
513 }
514
515 // Skip the header and the 4 bytes that encode the length of the VP8 chunk
516 keyframeStartIndex += 'VP8 '.length + 4;
517
518 return webP.substring(keyframeStartIndex);
519 }
520
521 // Just a little utility so we can tag values as floats for the EBML encoder's benefit
522 function EBMLFloat32(value) {
523 this.value = value;
524 }
525
526 function EBMLFloat64(value) {
527 this.value = value;
528 }
529
530 /**
531 * Write the given EBML object to the provided ArrayBufferStream.
532 *
533 * The buffer's first byte is at bufferFileOffset inside the video file. This is used to complete offset and
534 * dataOffset fields in each EBML structure, indicating the file offset of the first byte of the EBML element and
535 * its data payload.
536 */
537 function writeEBML(buffer, bufferFileOffset, ebml) {
538 // Is the ebml an array of sibling elements?
539 if (Array.isArray(ebml)) {
540 for (var i = 0; i < ebml.length; i++) {
541 writeEBML(buffer, bufferFileOffset, ebml[i]);
542 }
543 // Is this some sort of raw data that we want to write directly?
544 } else if (typeof ebml === "string") {
545 buffer.writeString(ebml);
546 } else if (ebml instanceof Uint8Array) {
547 buffer.writeBytes(ebml);
548 } else if (ebml.id){
549 // We're writing an EBML element
550 ebml.offset = buffer.pos + bufferFileOffset;
551
552 buffer.writeUnsignedIntBE(ebml.id); // ID field
553
554 // Now we need to write the size field, so we must know the payload size:
555
556 if (Array.isArray(ebml.data)) {
557 // Writing an array of child elements. We won't try to measure the size of the children up-front
558
559 var
560 sizePos, dataBegin, dataEnd;
561
562 if (ebml.size === -1) {
563 // Write the reserved all-one-bits marker to note that the size of this element is unknown/unbounded
564 buffer.writeByte(0xFF);
565 } else {
566 sizePos = buffer.pos;
567
568 /* Write a dummy size field to overwrite later. 4 bytes allows an element maximum size of 256MB,
569 * which should be plenty (we don't want to have to buffer that much data in memory at one time
570 * anyway!)
571 */
572 buffer.writeBytes([0, 0, 0, 0]);
573 }
574
575 dataBegin = buffer.pos;
576
577 ebml.dataOffset = dataBegin + bufferFileOffset;
578 writeEBML(buffer, bufferFileOffset, ebml.data);
579
580 if (ebml.size !== -1) {
581 dataEnd = buffer.pos;
582
583 ebml.size = dataEnd - dataBegin;
584
585 buffer.seek(sizePos);
586 buffer.writeEBMLVarIntWidth(ebml.size, 4); // Size field
587
588 buffer.seek(dataEnd);
589 }
590 } else if (typeof ebml.data === "string") {
591 buffer.writeEBMLVarInt(ebml.data.length); // Size field
592 ebml.dataOffset = buffer.pos + bufferFileOffset;
593 buffer.writeString(ebml.data);
594 } else if (typeof ebml.data === "number") {
595 // Allow the caller to explicitly choose the size if they wish by supplying a size field
596 if (!ebml.size) {
597 ebml.size = buffer.measureUnsignedInt(ebml.data);
598 }
599
600 buffer.writeEBMLVarInt(ebml.size); // Size field
601 ebml.dataOffset = buffer.pos + bufferFileOffset;
602 buffer.writeUnsignedIntBE(ebml.data, ebml.size);
603 } else if (ebml.data instanceof EBMLFloat64) {
604 buffer.writeEBMLVarInt(8); // Size field
605 ebml.dataOffset = buffer.pos + bufferFileOffset;
606 buffer.writeDoubleBE(ebml.data.value);
607 } else if (ebml.data instanceof EBMLFloat32) {
608 buffer.writeEBMLVarInt(4); // Size field
609 ebml.dataOffset = buffer.pos + bufferFileOffset;
610 buffer.writeFloatBE(ebml.data.value);
611 } else if (ebml.data instanceof Uint8Array) {
612 buffer.writeEBMLVarInt(ebml.data.byteLength); // Size field
613 ebml.dataOffset = buffer.pos + bufferFileOffset;
614 buffer.writeBytes(ebml.data);
615 } else {
616 throw "Bad EBML datatype " + typeof ebml.data;
617 }
618 } else {
619 throw "Bad EBML datatype " + typeof ebml.data;
620 }
621 }
622
623 return function(options) {
624 var
625 MAX_CLUSTER_DURATION_MSEC = 5000,
626 DEFAULT_TRACK_NUMBER = 1,
627
628 writtenHeader = false,
629 videoWidth, videoHeight,
630
631 clusterFrameBuffer = [],
632 clusterStartTime = 0,
633 clusterDuration = 0,
634
635 optionDefaults = {
636 quality: 0.95, // WebM image quality from 0.0 (worst) to 1.0 (best)
637 fileWriter: null, // Chrome FileWriter in order to stream to a file instead of buffering to memory (optional)
638 fd: null, // Node.JS file descriptor to write to instead of buffering (optional)
639
640 // You must supply one of:
641 frameDuration: null, // Duration of frames in milliseconds
642 frameRate: null, // Number of frames per second
643 },
644
645 seekPoints = {
646 Cues: {id: new Uint8Array([0x1C, 0x53, 0xBB, 0x6B]), positionEBML: null},
647 SegmentInfo: {id: new Uint8Array([0x15, 0x49, 0xA9, 0x66]), positionEBML: null},
648 Tracks: {id: new Uint8Array([0x16, 0x54, 0xAE, 0x6B]), positionEBML: null},
649 },
650
651 ebmlSegment,
652 segmentDuration = {
653 "id": 0x4489, // Duration
654 "data": new EBMLFloat64(0)
655 },
656
657 seekHead,
658
659 cues = [],
660
661 blobBuffer = new BlobBuffer(options.fileWriter || options.fd);
662
663 function fileOffsetToSegmentRelative(fileOffset) {
664 return fileOffset - ebmlSegment.dataOffset;
665 }
666
667 /**
668 * Create a SeekHead element with descriptors for the points in the global seekPoints array.
669 *
670 * 5 bytes of position values are reserved for each node, which lie at the offset point.positionEBML.dataOffset,
671 * to be overwritten later.
672 */
673 function createSeekHead() {
674 var
675 seekPositionEBMLTemplate = {
676 "id": 0x53AC, // SeekPosition
677 "size": 5, // Allows for 32GB video files
678 "data": 0 // We'll overwrite this when the file is complete
679 },
680
681 result = {
682 "id": 0x114D9B74, // SeekHead
683 "data": []
684 };
685
686 for (var name in seekPoints) {
687 var
688 seekPoint = seekPoints[name];
689
690 seekPoint.positionEBML = Object.create(seekPositionEBMLTemplate);
691
692 result.data.push({
693 "id": 0x4DBB, // Seek
694 "data": [
695 {
696 "id": 0x53AB, // SeekID
697 "data": seekPoint.id
698 },
699 seekPoint.positionEBML
700 ]
701 });
702 }
703
704 return result;
705 }
706
707 /**
708 * Write the WebM file header to the stream.
709 */
710 function writeHeader() {
711 seekHead = createSeekHead();
712
713 var
714 ebmlHeader = {
715 "id": 0x1a45dfa3, // EBML
716 "data": [
717 {
718 "id": 0x4286, // EBMLVersion
719 "data": 1
720 },
721 {
722 "id": 0x42f7, // EBMLReadVersion
723 "data": 1
724 },
725 {
726 "id": 0x42f2, // EBMLMaxIDLength
727 "data": 4
728 },
729 {
730 "id": 0x42f3, // EBMLMaxSizeLength
731 "data": 8
732 },
733 {
734 "id": 0x4282, // DocType
735 "data": "webm"
736 },
737 {
738 "id": 0x4287, // DocTypeVersion
739 "data": 2
740 },
741 {
742 "id": 0x4285, // DocTypeReadVersion
743 "data": 2
744 }
745 ]
746 },
747
748 segmentInfo = {
749 "id": 0x1549a966, // Info
750 "data": [
751 {
752 "id": 0x2ad7b1, // TimecodeScale
753 "data": 1e6 // Times will be in miliseconds (1e6 nanoseconds per step = 1ms)
754 },
755 {
756 "id": 0x4d80, // MuxingApp
757 "data": "webm-writer-js",
758 },
759 {
760 "id": 0x5741, // WritingApp
761 "data": "webm-writer-js"
762 },
763 segmentDuration // To be filled in later
764 ]
765 },
766
767 tracks = {
768 "id": 0x1654ae6b, // Tracks
769 "data": [
770 {
771 "id": 0xae, // TrackEntry
772 "data": [
773 {
774 "id": 0xd7, // TrackNumber
775 "data": DEFAULT_TRACK_NUMBER
776 },
777 {
778 "id": 0x73c5, // TrackUID
779 "data": DEFAULT_TRACK_NUMBER
780 },
781 {
782 "id": 0x9c, // FlagLacing
783 "data": 0
784 },
785 {
786 "id": 0x22b59c, // Language
787 "data": "und"
788 },
789 {
790 "id": 0x86, // CodecID
791 "data": "V_VP8"
792 },
793 {
794 "id": 0x258688, // CodecName
795 "data": "VP8"
796 },
797 {
798 "id": 0x83, // TrackType
799 "data": 1
800 },
801 {
802 "id": 0xe0, // Video
803 "data": [
804 {
805 "id": 0xb0, // PixelWidth
806 "data": videoWidth
807 },
808 {
809 "id": 0xba, // PixelHeight
810 "data": videoHeight
811 }
812 ]
813 }
814 ]
815 }
816 ]
817 };
818
819 ebmlSegment = {
820 "id": 0x18538067, // Segment
821 "size": -1, // Unbounded size
822 "data": [
823 seekHead,
824 segmentInfo,
825 tracks,
826 ]
827 };
828
829 var
830 bufferStream = new ArrayBufferDataStream(256);
831
832 writeEBML(bufferStream, blobBuffer.pos, [ebmlHeader, ebmlSegment]);
833 blobBuffer.write(bufferStream.getAsDataArray());
834
835 // Now we know where these top-level elements lie in the file:
836 seekPoints.SegmentInfo.positionEBML.data = fileOffsetToSegmentRelative(segmentInfo.offset);
837 seekPoints.Tracks.positionEBML.data = fileOffsetToSegmentRelative(tracks.offset);
838 };
839
840 /**
841 * Create a SimpleBlock keyframe header using these fields:
842 * timecode - Time of this keyframe
843 * trackNumber - Track number from 1 to 126 (inclusive)
844 * frame - Raw frame data payload string
845 *
846 * Returns an EBML element.
847 */
848 function createKeyframeBlock(keyframe) {
849 var
850 bufferStream = new ArrayBufferDataStream(1 + 2 + 1);
851
852 if (!(keyframe.trackNumber > 0 && keyframe.trackNumber < 127)) {
853 throw "TrackNumber must be > 0 and < 127";
854 }
855
856 bufferStream.writeEBMLVarInt(keyframe.trackNumber); // Always 1 byte since we limit the range of trackNumber
857 bufferStream.writeU16BE(keyframe.timecode);
858
859 // Flags byte
860 bufferStream.writeByte(
861 1 << 7 // Keyframe
862 );
863
864 return {
865 "id": 0xA3, // SimpleBlock
866 "data": [
867 bufferStream.getAsDataArray(),
868 keyframe.frame
869 ]
870 };
871 }
872
873 /**
874 * Create a Cluster node using these fields:
875 *
876 * timecode - Start time for the cluster
877 *
878 * Returns an EBML element.
879 */
880 function createCluster(cluster) {
881 return {
882 "id": 0x1f43b675,
883 "data": [
884 {
885 "id": 0xe7, // Timecode
886 "data": Math.round(cluster.timecode)
887 }
888 ]
889 };
890 }
891
892 function addCuePoint(trackIndex, clusterTime, clusterFileOffset) {
893 cues.push({
894 "id": 0xBB, // Cue
895 "data": [
896 {
897 "id": 0xB3, // CueTime
898 "data": clusterTime
899 },
900 {
901 "id": 0xB7, // CueTrackPositions
902 "data": [
903 {
904 "id": 0xF7, // CueTrack
905 "data": trackIndex
906 },
907 {
908 "id": 0xF1, // CueClusterPosition
909 "data": fileOffsetToSegmentRelative(clusterFileOffset)
910 }
911 ]
912 }
913 ]
914 });
915 }
916
917 /**
918 * Write a Cues element to the blobStream using the global `cues` array of CuePoints (use addCuePoint()).
919 * The seek entry for the Cues in the SeekHead is updated.
920 */
921 function writeCues() {
922 var
923 ebml = {
924 "id": 0x1C53BB6B,
925 "data": cues
926 },
927
928 cuesBuffer = new ArrayBufferDataStream(16 + cues.length * 32); // Pretty crude estimate of the buffer size we'll need
929
930 writeEBML(cuesBuffer, blobBuffer.pos, ebml);
931 blobBuffer.write(cuesBuffer.getAsDataArray());
932
933 // Now we know where the Cues element has ended up, we can update the SeekHead
934 seekPoints.Cues.positionEBML.data = fileOffsetToSegmentRelative(ebml.offset);
935 }
936
937 /**
938 * Flush the frames in the current clusterFrameBuffer out to the stream as a Cluster.
939 */
940 function flushClusterFrameBuffer() {
941 if (clusterFrameBuffer.length == 0) {
942 return;
943 }
944
945 // First work out how large of a buffer we need to hold the cluster data
946 var
947 rawImageSize = 0;
948
949 for (var i = 0; i < clusterFrameBuffer.length; i++) {
950 rawImageSize += clusterFrameBuffer[i].frame.length;
951 }
952
953 var
954 buffer = new ArrayBufferDataStream(rawImageSize + clusterFrameBuffer.length * 32), // Estimate 32 bytes per SimpleBlock header
955
956 cluster = createCluster({
957 timecode: Math.round(clusterStartTime),
958 });
959
960 for (var i = 0; i < clusterFrameBuffer.length; i++) {
961 cluster.data.push(createKeyframeBlock(clusterFrameBuffer[i]));
962 }
963
964 writeEBML(buffer, blobBuffer.pos, cluster);
965 blobBuffer.write(buffer.getAsDataArray());
966
967 addCuePoint(DEFAULT_TRACK_NUMBER, Math.round(clusterStartTime), cluster.offset);
968
969 clusterFrameBuffer = [];
970 clusterStartTime += clusterDuration;
971 clusterDuration = 0;
972 }
973
974 function validateOptions() {
975 // Derive frameDuration setting if not already supplied
976 if (!options.frameDuration) {
977 if (options.frameRate) {
978 options.frameDuration = 1000 / options.frameRate;
979 } else {
980 throw "Missing required frameDuration or frameRate setting";
981 }
982 }
983 }
984
985 function addFrameToCluster(frame) {
986 frame.trackNumber = DEFAULT_TRACK_NUMBER;
987
988 // Frame timecodes are relative to the start of their cluster:
989 frame.timecode = Math.round(clusterDuration);
990
991 clusterFrameBuffer.push(frame);
992
993 clusterDuration += frame.duration;
994
995 if (clusterDuration >= MAX_CLUSTER_DURATION_MSEC) {
996 flushClusterFrameBuffer();
997 }
998 }
999
1000 /**
1001 * Rewrites the SeekHead element that was initially written to the stream with the offsets of top level elements.
1002 *
1003 * Call once writing is complete (so the offset of all top level elements is known).
1004 */
1005 function rewriteSeekHead() {
1006 var
1007 seekHeadBuffer = new ArrayBufferDataStream(seekHead.size),
1008 oldPos = blobBuffer.pos;
1009
1010 // Write the rewritten SeekHead element's data payload to the stream (don't need to update the id or size)
1011 writeEBML(seekHeadBuffer, seekHead.dataOffset, seekHead.data);
1012
1013 // And write that through to the file
1014 blobBuffer.seek(seekHead.dataOffset);
1015 blobBuffer.write(seekHeadBuffer.getAsDataArray());
1016
1017 blobBuffer.seek(oldPos);
1018 }
1019
1020 /**
1021 * Rewrite the Duration field of the Segment with the newly-discovered video duration.
1022 */
1023 function rewriteDuration() {
1024 var
1025 buffer = new ArrayBufferDataStream(8),
1026 oldPos = blobBuffer.pos;
1027
1028 // Rewrite the data payload (don't need to update the id or size)
1029 buffer.writeDoubleBE(clusterStartTime);
1030
1031 // And write that through to the file
1032 blobBuffer.seek(segmentDuration.dataOffset);
1033 blobBuffer.write(buffer.getAsDataArray());
1034
1035 blobBuffer.seek(oldPos);
1036 }
1037
1038 /**
1039 * Add a frame to the video. Currently the frame must be a Canvas element.
1040 */
1041 this.addFrame = function(canvas, duration) {
1042 //if (writtenHeader) {
1043 // if (canvas.width != videoWidth || canvas.height != videoHeight) {
1044 // throw "Frame size differs from previous frames";
1045 // }
1046 //} else {
1047 videoWidth = canvas.width;
1048 videoHeight = canvas.height;
1049
1050 writeHeader();
1051 writtenHeader = true;
1052 //}
1053
1054 var
1055 webP = renderAsWebP(canvas, options.quality);
1056
1057 if (!webP) {
1058 throw "Couldn't decode WebP frame, does the browser support WebP?";
1059 }
1060
1061 addFrameToCluster({
1062 frame: extractKeyframeFromWebP(webP),
1063 duration: ((typeof duration == 'number')?duration:options.frameDuration)
1064 });
1065 };
1066
1067 /**
1068 * Finish writing the video and return a Promise to signal completion.
1069 *
1070 * If the destination device was memory (i.e. options.fileWriter was not supplied), the Promise is resolved with
1071 * a Blob with the contents of the entire video.
1072 */
1073 this.complete = function() {
1074 flushClusterFrameBuffer();
1075
1076 writeCues();
1077 rewriteSeekHead();
1078 rewriteDuration();
1079
1080 return blobBuffer.complete('video/webm');
1081 };
1082
1083 this.getWrittenSize = function() {
1084 return blobBuffer.length;
1085 };
1086
1087 options = extend(optionDefaults, options || {});
1088 validateOptions();
1089 };
1090 };
1091
1092 if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
1093 module.exports = WebMWriter(require("./ArrayBufferDataStream"), require("./BlobBuffer"));
1094 } else {
1095 window.WebMWriter = WebMWriter(ArrayBufferDataStream, BlobBuffer);
1096 }
1097})();