webm-writer.js 40 KB

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