Home Reference Source

src/demux/id3.js

  1. import { getSelfScope } from '../utils/get-self-scope';
  2.  
  3. /**
  4. * ID3 parser
  5. */
  6. class ID3 {
  7. /**
  8. * Returns true if an ID3 header can be found at offset in data
  9. * @param {Uint8Array} data - The data to search in
  10. * @param {number} offset - The offset at which to start searching
  11. * @return {boolean} - True if an ID3 header is found
  12. */
  13. static isHeader (data, offset) {
  14. /*
  15. * http://id3.org/id3v2.3.0
  16. * [0] = 'I'
  17. * [1] = 'D'
  18. * [2] = '3'
  19. * [3,4] = {Version}
  20. * [5] = {Flags}
  21. * [6-9] = {ID3 Size}
  22. *
  23. * An ID3v2 tag can be detected with the following pattern:
  24. * $49 44 33 yy yy xx zz zz zz zz
  25. * Where yy is less than $FF, xx is the 'flags' byte and zz is less than $80
  26. */
  27. if (offset + 10 <= data.length) {
  28. // look for 'ID3' identifier
  29. if (data[offset] === 0x49 && data[offset + 1] === 0x44 && data[offset + 2] === 0x33) {
  30. // check version is within range
  31. if (data[offset + 3] < 0xFF && data[offset + 4] < 0xFF) {
  32. // check size is within range
  33. if (data[offset + 6] < 0x80 && data[offset + 7] < 0x80 && data[offset + 8] < 0x80 && data[offset + 9] < 0x80) {
  34. return true;
  35. }
  36. }
  37. }
  38. }
  39.  
  40. return false;
  41. }
  42.  
  43. /**
  44. * Returns true if an ID3 footer can be found at offset in data
  45. * @param {Uint8Array} data - The data to search in
  46. * @param {number} offset - The offset at which to start searching
  47. * @return {boolean} - True if an ID3 footer is found
  48. */
  49. static isFooter (data, offset) {
  50. /*
  51. * The footer is a copy of the header, but with a different identifier
  52. */
  53. if (offset + 10 <= data.length) {
  54. // look for '3DI' identifier
  55. if (data[offset] === 0x33 && data[offset + 1] === 0x44 && data[offset + 2] === 0x49) {
  56. // check version is within range
  57. if (data[offset + 3] < 0xFF && data[offset + 4] < 0xFF) {
  58. // check size is within range
  59. if (data[offset + 6] < 0x80 && data[offset + 7] < 0x80 && data[offset + 8] < 0x80 && data[offset + 9] < 0x80) {
  60. return true;
  61. }
  62. }
  63. }
  64. }
  65.  
  66. return false;
  67. }
  68.  
  69. /**
  70. * Returns any adjacent ID3 tags found in data starting at offset, as one block of data
  71. * @param {Uint8Array} data - The data to search in
  72. * @param {number} offset - The offset at which to start searching
  73. * @return {Uint8Array} - The block of data containing any ID3 tags found
  74. */
  75. static getID3Data (data, offset) {
  76. const front = offset;
  77. let length = 0;
  78.  
  79. while (ID3.isHeader(data, offset)) {
  80. // ID3 header is 10 bytes
  81. length += 10;
  82.  
  83. const size = ID3._readSize(data, offset + 6);
  84. length += size;
  85.  
  86. if (ID3.isFooter(data, offset + 10)) {
  87. // ID3 footer is 10 bytes
  88. length += 10;
  89. }
  90.  
  91. offset += length;
  92. }
  93.  
  94. if (length > 0) {
  95. return data.subarray(front, front + length);
  96. }
  97.  
  98. return undefined;
  99. }
  100.  
  101. static _readSize (data, offset) {
  102. let size = 0;
  103. size = ((data[offset] & 0x7f) << 21);
  104. size |= ((data[offset + 1] & 0x7f) << 14);
  105. size |= ((data[offset + 2] & 0x7f) << 7);
  106. size |= (data[offset + 3] & 0x7f);
  107. return size;
  108. }
  109.  
  110. /**
  111. * Searches for the Elementary Stream timestamp found in the ID3 data chunk
  112. * @param {Uint8Array} data - Block of data containing one or more ID3 tags
  113. * @return {number} - The timestamp
  114. */
  115. static getTimeStamp (data) {
  116. const frames = ID3.getID3Frames(data);
  117. for (let i = 0; i < frames.length; i++) {
  118. const frame = frames[i];
  119. if (ID3.isTimeStampFrame(frame)) {
  120. return ID3._readTimeStamp(frame);
  121. }
  122. }
  123.  
  124. return undefined;
  125. }
  126.  
  127. /**
  128. * Returns true if the ID3 frame is an Elementary Stream timestamp frame
  129. * @param {ID3 frame} frame
  130. */
  131. static isTimeStampFrame (frame) {
  132. return (frame && frame.key === 'PRIV' && frame.info === 'com.apple.streaming.transportStreamTimestamp');
  133. }
  134.  
  135. static _getFrameData (data) {
  136. /*
  137. Frame ID $xx xx xx xx (four characters)
  138. Size $xx xx xx xx
  139. Flags $xx xx
  140. */
  141. const type = String.fromCharCode(data[0], data[1], data[2], data[3]);
  142. const size = ID3._readSize(data, 4);
  143.  
  144. // skip frame id, size, and flags
  145. let offset = 10;
  146.  
  147. return { type, size, data: data.subarray(offset, offset + size) };
  148. }
  149.  
  150. /**
  151. * Returns an array of ID3 frames found in all the ID3 tags in the id3Data
  152. * @param {Uint8Array} id3Data - The ID3 data containing one or more ID3 tags
  153. * @return {ID3 frame[]} - Array of ID3 frame objects
  154. */
  155. static getID3Frames (id3Data) {
  156. let offset = 0;
  157. const frames = [];
  158.  
  159. while (ID3.isHeader(id3Data, offset)) {
  160. const size = ID3._readSize(id3Data, offset + 6);
  161. // skip past ID3 header
  162. offset += 10;
  163. const end = offset + size;
  164. // loop through frames in the ID3 tag
  165. while (offset + 8 < end) {
  166. const frameData = ID3._getFrameData(id3Data.subarray(offset));
  167. const frame = ID3._decodeFrame(frameData);
  168. if (frame) {
  169. frames.push(frame);
  170. }
  171.  
  172. // skip frame header and frame data
  173. offset += frameData.size + 10;
  174. }
  175.  
  176. if (ID3.isFooter(id3Data, offset)) {
  177. offset += 10;
  178. }
  179. }
  180.  
  181. return frames;
  182. }
  183.  
  184. static _decodeFrame (frame) {
  185. if (frame.type === 'PRIV') {
  186. return ID3._decodePrivFrame(frame);
  187. } else if (frame.type[0] === 'T') {
  188. return ID3._decodeTextFrame(frame);
  189. } else if (frame.type[0] === 'W') {
  190. return ID3._decodeURLFrame(frame);
  191. }
  192.  
  193. return undefined;
  194. }
  195.  
  196. static _readTimeStamp (timeStampFrame) {
  197. if (timeStampFrame.data.byteLength === 8) {
  198. const data = new Uint8Array(timeStampFrame.data);
  199. // timestamp is 33 bit expressed as a big-endian eight-octet number,
  200. // with the upper 31 bits set to zero.
  201. const pts33Bit = data[3] & 0x1;
  202. let timestamp = (data[4] << 23) +
  203. (data[5] << 15) +
  204. (data[6] << 7) +
  205. data[7];
  206. timestamp /= 45;
  207.  
  208. if (pts33Bit) {
  209. timestamp += 47721858.84;
  210. } // 2^32 / 90
  211.  
  212. return Math.round(timestamp);
  213. }
  214.  
  215. return undefined;
  216. }
  217.  
  218. static _decodePrivFrame (frame) {
  219. /*
  220. Format: <text string>\0<binary data>
  221. */
  222. if (frame.size < 2) {
  223. return undefined;
  224. }
  225.  
  226. const owner = ID3._utf8ArrayToStr(frame.data, true);
  227. const privateData = new Uint8Array(frame.data.subarray(owner.length + 1));
  228.  
  229. return { key: frame.type, info: owner, data: privateData.buffer };
  230. }
  231.  
  232. static _decodeTextFrame (frame) {
  233. if (frame.size < 2) {
  234. return undefined;
  235. }
  236.  
  237. if (frame.type === 'TXXX') {
  238. /*
  239. Format:
  240. [0] = {Text Encoding}
  241. [1-?] = {Description}\0{Value}
  242. */
  243. let index = 1;
  244. const description = ID3._utf8ArrayToStr(frame.data.subarray(index), true);
  245.  
  246. index += description.length + 1;
  247. const value = ID3._utf8ArrayToStr(frame.data.subarray(index));
  248.  
  249. return { key: frame.type, info: description, data: value };
  250. } else {
  251. /*
  252. Format:
  253. [0] = {Text Encoding}
  254. [1-?] = {Value}
  255. */
  256. const text = ID3._utf8ArrayToStr(frame.data.subarray(1));
  257. return { key: frame.type, data: text };
  258. }
  259. }
  260.  
  261. static _decodeURLFrame (frame) {
  262. if (frame.type === 'WXXX') {
  263. /*
  264. Format:
  265. [0] = {Text Encoding}
  266. [1-?] = {Description}\0{URL}
  267. */
  268. if (frame.size < 2) {
  269. return undefined;
  270. }
  271.  
  272. let index = 1;
  273. const description = ID3._utf8ArrayToStr(frame.data.subarray(index), true);
  274.  
  275. index += description.length + 1;
  276. const value = ID3._utf8ArrayToStr(frame.data.subarray(index));
  277.  
  278. return { key: frame.type, info: description, data: value };
  279. } else {
  280. /*
  281. Format:
  282. [0-?] = {URL}
  283. */
  284. const url = ID3._utf8ArrayToStr(frame.data);
  285. return { key: frame.type, data: url };
  286. }
  287. }
  288.  
  289. // http://stackoverflow.com/questions/8936984/uint8array-to-string-in-javascript/22373197
  290. // http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt
  291. /* utf.js - UTF-8 <=> UTF-16 convertion
  292. *
  293. * Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp>
  294. * Version: 1.0
  295. * LastModified: Dec 25 1999
  296. * This library is free. You can redistribute it and/or modify it.
  297. */
  298. static _utf8ArrayToStr (array, exitOnNull = false) {
  299. const decoder = getTextDecoder();
  300. if (decoder) {
  301. const decoded = decoder.decode(array);
  302.  
  303. if (exitOnNull) {
  304. // grab up to the first null
  305. const idx = decoded.indexOf('\0');
  306. return idx !== -1 ? decoded.substring(0, idx) : decoded;
  307. }
  308.  
  309. // remove any null characters
  310. return decoded.replace(/\0/g, '');
  311. }
  312.  
  313. const len = array.length;
  314. let c;
  315. let char2;
  316. let char3;
  317. let out = '';
  318. let i = 0;
  319. while (i < len) {
  320. c = array[i++];
  321. if (c === 0x00 && exitOnNull) {
  322. return out;
  323. } else if (c === 0x00 || c === 0x03) {
  324. // If the character is 3 (END_OF_TEXT) or 0 (NULL) then skip it
  325. continue;
  326. }
  327. switch (c >> 4) {
  328. case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
  329. // 0xxxxxxx
  330. out += String.fromCharCode(c);
  331. break;
  332. case 12: case 13:
  333. // 110x xxxx 10xx xxxx
  334. char2 = array[i++];
  335. out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
  336. break;
  337. case 14:
  338. // 1110 xxxx 10xx xxxx 10xx xxxx
  339. char2 = array[i++];
  340. char3 = array[i++];
  341. out += String.fromCharCode(((c & 0x0F) << 12) |
  342. ((char2 & 0x3F) << 6) |
  343. ((char3 & 0x3F) << 0));
  344. break;
  345. default:
  346. }
  347. }
  348. return out;
  349. }
  350. }
  351.  
  352. let decoder;
  353.  
  354. function getTextDecoder () {
  355. const global = getSelfScope(); // safeguard for code that might run both on worker and main thread
  356. if (!decoder && typeof global.TextDecoder !== 'undefined') {
  357. decoder = new global.TextDecoder('utf-8');
  358. }
  359.  
  360. return decoder;
  361. }
  362.  
  363. const utf8ArrayToStr = ID3._utf8ArrayToStr;
  364.  
  365. export default ID3;
  366.  
  367. export { utf8ArrayToStr };