video-helpers.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. /*
  2. https://github.com/antimatter15/whammy
  3. The MIT License (MIT)
  4. Copyright (c) 2015 Kevin Kwok
  5. Permission is hereby granted, free of charge, to any person obtaining a copy
  6. of this software and associated documentation files (the "Software"), to deal
  7. in the Software without restriction, including without limitation the rights
  8. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. copies of the Software, and to permit persons to whom the Software is
  10. furnished to do so, subject to the following conditions:
  11. The above copyright notice and this permission notice shall be included in all
  12. copies or substantial portions of the Software.
  13. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  19. SOFTWARE.
  20. */
  21. function atob (str) {
  22. return Buffer.from(str, 'base64').toString('binary');
  23. }
  24. // in this case, frames has a very specific meaning, which will be
  25. // detailed once i finish writing the code
  26. function ToWebM (frames) {
  27. const info = checkFrames(frames);
  28. // max duration by cluster in milliseconds
  29. const CLUSTER_MAX_DURATION = 30000;
  30. const EBML = [
  31. {
  32. id: 0x1a45dfa3, // EBML
  33. data: [
  34. {
  35. data: 1,
  36. id: 0x4286 // EBMLVersion
  37. },
  38. {
  39. data: 1,
  40. id: 0x42f7 // EBMLReadVersion
  41. },
  42. {
  43. data: 4,
  44. id: 0x42f2 // EBMLMaxIDLength
  45. },
  46. {
  47. data: 8,
  48. id: 0x42f3 // EBMLMaxSizeLength
  49. },
  50. {
  51. data: 'webm',
  52. id: 0x4282 // DocType
  53. },
  54. {
  55. data: 2,
  56. id: 0x4287 // DocTypeVersion
  57. },
  58. {
  59. data: 2,
  60. id: 0x4285 // DocTypeReadVersion
  61. }
  62. ]
  63. },
  64. {
  65. id: 0x18538067, // Segment
  66. data: [
  67. {
  68. id: 0x1549a966, // Info
  69. data: [
  70. {
  71. data: 1e6, // do things in millisecs (num of nanosecs for duration scale)
  72. id: 0x2ad7b1 // TimecodeScale
  73. },
  74. {
  75. data: 'whammy',
  76. id: 0x4d80 // MuxingApp
  77. },
  78. {
  79. data: 'whammy',
  80. id: 0x5741 // WritingApp
  81. },
  82. {
  83. data: doubleToString(info.duration),
  84. id: 0x4489 // Duration
  85. }
  86. ]
  87. },
  88. {
  89. id: 0x1654ae6b, // Tracks
  90. data: [
  91. {
  92. id: 0xae, // TrackEntry
  93. data: [
  94. {
  95. data: 1,
  96. id: 0xd7 // TrackNumber
  97. },
  98. {
  99. data: 1,
  100. id: 0x73c5 // TrackUID
  101. },
  102. {
  103. data: 0,
  104. id: 0x9c // FlagLacing
  105. },
  106. {
  107. data: 'und',
  108. id: 0x22b59c // Language
  109. },
  110. {
  111. data: 'V_VP8',
  112. id: 0x86 // CodecID
  113. },
  114. {
  115. data: 'VP8',
  116. id: 0x258688 // CodecName
  117. },
  118. {
  119. data: 1,
  120. id: 0x83 // TrackType
  121. },
  122. {
  123. id: 0xe0, // Video
  124. data: [
  125. {
  126. data: info.width,
  127. id: 0xb0 // PixelWidth
  128. },
  129. {
  130. data: info.height,
  131. id: 0xba // PixelHeight
  132. }
  133. ]
  134. }
  135. ]
  136. }
  137. ]
  138. },
  139. {
  140. id: 0x1c53bb6b, // Cues
  141. data: [
  142. // cue insertion point
  143. ]
  144. }
  145. // cluster insertion point
  146. ]
  147. }
  148. ];
  149. const segment = EBML[1];
  150. const cues = segment.data[2];
  151. // Generate clusters (max duration)
  152. let frameNumber = 0;
  153. let clusterTimecode = 0;
  154. while (frameNumber < frames.length) {
  155. const cuePoint = {
  156. id: 0xbb, // CuePoint
  157. data: [
  158. {
  159. data: Math.round(clusterTimecode),
  160. id: 0xb3 // CueTime
  161. },
  162. {
  163. id: 0xb7, // CueTrackPositions
  164. data: [
  165. {
  166. data: 1,
  167. id: 0xf7 // CueTrack
  168. },
  169. {
  170. data: 0, // to be filled in when we know it
  171. size: 8,
  172. id: 0xf1 // CueClusterPosition
  173. }
  174. ]
  175. }
  176. ]
  177. };
  178. cues.data.push(cuePoint);
  179. const clusterFrames = [];
  180. let clusterDuration = 0;
  181. do {
  182. clusterFrames.push(frames[frameNumber]);
  183. clusterDuration += frames[frameNumber].duration;
  184. frameNumber++;
  185. } while (frameNumber < frames.length && clusterDuration < CLUSTER_MAX_DURATION);
  186. let clusterCounter = 0;
  187. const cluster = {
  188. id: 0x1f43b675, // Cluster
  189. data: [
  190. {
  191. data: Math.round(clusterTimecode),
  192. id: 0xe7 // Timecode
  193. }
  194. ].concat(clusterFrames.map(function (webp) {
  195. const block = makeSimpleBlock({
  196. discardable: 0,
  197. frame: webp.data.slice(4),
  198. invisible: 0,
  199. keyframe: 1,
  200. lacing: 0,
  201. trackNum: 1,
  202. timecode: Math.round(clusterCounter)
  203. });
  204. clusterCounter += webp.duration;
  205. return {
  206. data: block,
  207. id: 0xa3
  208. };
  209. }))
  210. };
  211. // Add cluster to segment
  212. segment.data.push(cluster);
  213. clusterTimecode += clusterDuration;
  214. }
  215. // First pass to compute cluster positions
  216. let position = 0;
  217. for (let i = 0; i < segment.data.length; i++) {
  218. if (i >= 3) {
  219. cues.data[i - 3].data[1].data[1].data = position;
  220. }
  221. const data = generateEBML([segment.data[i]]);
  222. position += data.size || data.byteLength || data.length;
  223. if (i !== 2) { // not cues
  224. // Save results to avoid having to encode everything twice
  225. segment.data[i] = data;
  226. }
  227. }
  228. return generateEBML(EBML);
  229. }
  230. // sums the lengths of all the frames and gets the duration, woo
  231. function checkFrames (frames) {
  232. const width = frames[0].width;
  233. const height = frames[0].height;
  234. let duration = frames[0].duration;
  235. for (let i = 1; i < frames.length; i++) {
  236. if (frames[i].width !== width) throw new Error('Frame ' + (i + 1) + ' has a different width');
  237. if (frames[i].height !== height) throw new Error('Frame ' + (i + 1) + ' has a different height');
  238. if (frames[i].duration < 0 || frames[i].duration > 0x7fff) throw new Error('Frame ' + (i + 1) + ' has a weird duration (must be between 0 and 32767)');
  239. duration += frames[i].duration;
  240. }
  241. return {
  242. duration: duration,
  243. width: width,
  244. height: height
  245. };
  246. }
  247. function numToBuffer (num) {
  248. const parts = [];
  249. while (num > 0) {
  250. parts.push(num & 0xff);
  251. num = num >> 8;
  252. }
  253. return new Uint8Array(parts.reverse());
  254. }
  255. function numToFixedBuffer (num, size) {
  256. const parts = new Uint8Array(size);
  257. for (let i = size - 1; i >= 0; i--) {
  258. parts[i] = num & 0xff;
  259. num = num >> 8;
  260. }
  261. return parts;
  262. }
  263. function strToBuffer (str) {
  264. // return new Blob([str]);
  265. const arr = new Uint8Array(str.length);
  266. for (let i = 0; i < str.length; i++) {
  267. arr[i] = str.charCodeAt(i);
  268. }
  269. return arr;
  270. // this is slower
  271. // return new Uint8Array(str.split('').map(function(e){
  272. // return e.charCodeAt(0)
  273. // }))
  274. }
  275. // sorry this is ugly, and sort of hard to understand exactly why this was done
  276. // at all really, but the reason is that there's some code below that i dont really
  277. // feel like understanding, and this is easier than using my brain.
  278. function bitsToBuffer (bits) {
  279. const data = [];
  280. const pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
  281. bits = pad + bits;
  282. for (let i = 0; i < bits.length; i += 8) {
  283. data.push(parseInt(bits.substr(i, 8), 2));
  284. }
  285. return new Uint8Array(data);
  286. }
  287. function generateEBML (json) {
  288. const ebml = [];
  289. for (const item of json) {
  290. if (!('id' in item)) {
  291. // already encoded blob or byteArray
  292. ebml.push(item);
  293. continue;
  294. }
  295. let data = item.data;
  296. if (typeof data === 'object') data = generateEBML(data);
  297. if (typeof data === 'number') data = ('size' in item) ? numToFixedBuffer(data, item.size) : bitsToBuffer(data.toString(2));
  298. if (typeof data === 'string') data = strToBuffer(data);
  299. const len = data.size || data.byteLength || data.length;
  300. const zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8);
  301. const sizeStr = len.toString(2);
  302. const padded = (new Array((zeroes * 7 + 7 + 1) - sizeStr.length)).join('0') + sizeStr;
  303. const size = (new Array(zeroes)).join('0') + '1' + padded;
  304. // i actually dont quite understand what went on up there, so I'm not really
  305. // going to fix this, i'm probably just going to write some hacky thing which
  306. // converts that string into a buffer-esque thing
  307. ebml.push(numToBuffer(item.id));
  308. ebml.push(bitsToBuffer(size));
  309. ebml.push(data);
  310. }
  311. // convert ebml to an array
  312. const buffer = toFlatArray(ebml);
  313. return new Uint8Array(buffer);
  314. }
  315. function toFlatArray (arr, outBuffer) {
  316. if (outBuffer == null) {
  317. outBuffer = [];
  318. }
  319. for (const item of arr) {
  320. if (typeof item === 'object') {
  321. // an array
  322. toFlatArray(item, outBuffer);
  323. } else {
  324. // a simple element
  325. outBuffer.push(item);
  326. }
  327. }
  328. return outBuffer;
  329. }
  330. function makeSimpleBlock (data) {
  331. let flags = 0;
  332. if (data.keyframe) flags |= 128;
  333. if (data.invisible) flags |= 8;
  334. if (data.lacing) flags |= (data.lacing << 1);
  335. if (data.discardable) flags |= 1;
  336. if (data.trackNum > 127) {
  337. throw new Error('TrackNumber > 127 not supported');
  338. }
  339. const out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function (e) {
  340. return String.fromCharCode(e);
  341. }).join('') + data.frame;
  342. return out;
  343. }
  344. // here's something else taken verbatim from weppy, awesome rite?
  345. function parseWebP (riff) {
  346. const VP8 = riff.RIFF[0].WEBP[0];
  347. const frameStart = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header
  348. const c = [];
  349. for (let i = 0; i < 4; i++) c[i] = VP8.charCodeAt(frameStart + 3 + i);
  350. // the code below is literally copied verbatim from the bitstream spec
  351. let tmp = (c[1] << 8) | c[0];
  352. const width = tmp & 0x3FFF;
  353. const horizontalScale = tmp >> 14;
  354. tmp = (c[3] << 8) | c[2];
  355. const height = tmp & 0x3FFF;
  356. const verticalScale = tmp >> 14;
  357. return {
  358. width,
  359. height,
  360. horizontalScale,
  361. verticalScale,
  362. data: VP8,
  363. riff: riff
  364. };
  365. }
  366. // i think i'm going off on a riff by pretending this is some known
  367. // idiom which i'm making a casual and brilliant pun about, but since
  368. // i can't find anything on google which conforms to this idiomatic
  369. // usage, I'm assuming this is just a consequence of some psychotic
  370. // break which makes me make up puns. well, enough riff-raff (aha a
  371. // rescue of sorts), this function was ripped wholesale from weppy
  372. function parseRIFF (string) {
  373. let offset = 0;
  374. const chunks = {};
  375. while (offset < string.length) {
  376. const id = string.substr(offset, 4);
  377. chunks[id] = chunks[id] || [];
  378. if (id === 'RIFF' || id === 'LIST') {
  379. const len = parseInt(string.substr(offset + 4, 4).split('').map(function (i) {
  380. const unpadded = i.charCodeAt(0).toString(2);
  381. return (new Array(8 - unpadded.length + 1)).join('0') + unpadded;
  382. }).join(''), 2);
  383. const data = string.substr(offset + 4 + 4, len);
  384. offset += 4 + 4 + len;
  385. chunks[id].push(parseRIFF(data));
  386. } else if (id === 'WEBP') {
  387. // Use (offset + 8) to skip past "VP8 "/"VP8L"/"VP8X" field after "WEBP"
  388. chunks[id].push(string.substr(offset + 8));
  389. offset = string.length;
  390. } else {
  391. // Unknown chunk type; push entire payload
  392. chunks[id].push(string.substr(offset + 4));
  393. offset = string.length;
  394. }
  395. }
  396. return chunks;
  397. }
  398. // here's a little utility function that acts as a utility for other functions
  399. // basically, the only purpose is for encoding "Duration", which is encoded as
  400. // a double (considerably more difficult to encode than an integer)
  401. function doubleToString (num) {
  402. return Array.prototype.slice.call(
  403. new Uint8Array(
  404. (
  405. new Float64Array([num]) // create a float64 array
  406. ).buffer) // extract the array buffer
  407. , 0) // convert the Uint8Array into a regular array
  408. .map(function (e) { // since it's a regular array, we can now use map
  409. return String.fromCharCode(e); // encode all the bytes individually
  410. })
  411. .reverse() // correct the byte endianness (assume it's little endian for now)
  412. .join(''); // join the bytes in holy matrimony as a string
  413. }
  414. function WhammyVideo (speed, quality = 0.8) { // a more abstract-ish API
  415. this.frames = [];
  416. this.duration = 1000 / speed;
  417. this.quality = quality;
  418. }
  419. /**
  420. *
  421. * @param {string} frame
  422. * @param {number} [duration]
  423. */
  424. WhammyVideo.prototype.add = function (frame, duration) {
  425. if (typeof duration !== 'undefined' && this.duration) throw new Error("you can't pass a duration if the fps is set");
  426. if (typeof duration === 'undefined' && !this.duration) throw new Error("if you don't have the fps set, you need to have durations here.");
  427. if (frame.canvas) { // CanvasRenderingContext2D
  428. frame = frame.canvas;
  429. }
  430. if (frame.toDataURL) {
  431. // frame = frame.toDataURL('image/webp', this.quality);
  432. // quickly store image data so we don't block cpu. encode in compile method.
  433. frame = frame.getContext('2d').getImageData(0, 0, frame.width, frame.height);
  434. } else if (typeof frame !== 'string') {
  435. throw new TypeError('frame must be a a HTMLCanvasElement, a CanvasRenderingContext2D or a DataURI formatted string');
  436. }
  437. if (typeof frame === 'string' && !(/^data:image\/webp;base64,/ig).test(frame)) {
  438. throw new Error('Input must be formatted properly as a base64 encoded DataURI of type image/webp');
  439. }
  440. this.frames.push({
  441. image: frame,
  442. duration: duration || this.duration
  443. });
  444. };
  445. WhammyVideo.prototype.compile = function (callback) {
  446. const webm = new ToWebM(this.frames.map(function (frame) {
  447. const webp = parseWebP(parseRIFF(atob(frame.image.slice(23))));
  448. webp.duration = frame.duration;
  449. return webp;
  450. }));
  451. callback(webm);
  452. };
  453. export const WebmGenerator = WhammyVideo;