Looping fix for Depthy.me

Inject patched version of Whammy to make videos generated by depthy.me loop in Firefox.

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