Source: lib/util/id3_utils.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2022 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.util.Id3Utils');
  7. goog.require('shaka.log');
  8. goog.require('shaka.util.BufferUtils');
  9. goog.require('shaka.util.StringUtils');
  10. /**
  11. * @summary A set of Id3Utils utility functions.
  12. * @export
  13. */
  14. shaka.util.Id3Utils = class {
  15. /**
  16. * @param {Uint8Array} data
  17. * @param {number} offset
  18. * @return {boolean}
  19. * @private
  20. */
  21. static isHeader_(data, offset) {
  22. /*
  23. * http://id3.org/id3v2.3.0
  24. * [0] = 'I'
  25. * [1] = 'D'
  26. * [2] = '3'
  27. * [3,4] = {Version}
  28. * [5] = {Flags}
  29. * [6-9] = {ID3 Size}
  30. *
  31. * An ID3v2 tag can be detected with the following pattern:
  32. * $49 44 33 yy yy xx zz zz zz zz
  33. * Where yy is less than $FF, xx is the 'flags' byte and zz is less than $80
  34. */
  35. if (offset + 10 <= data.length) {
  36. // look for 'ID3' identifier
  37. if (data[offset] === 0x49 &&
  38. data[offset + 1] === 0x44 &&
  39. data[offset + 2] === 0x33) {
  40. // check version is within range
  41. if (data[offset + 3] < 0xff && data[offset + 4] < 0xff) {
  42. // check size is within range
  43. if (data[offset + 6] < 0x80 &&
  44. data[offset + 7] < 0x80 &&
  45. data[offset + 8] < 0x80 &&
  46. data[offset + 9] < 0x80) {
  47. return true;
  48. }
  49. }
  50. }
  51. }
  52. return false;
  53. }
  54. /**
  55. * @param {Uint8Array} data
  56. * @param {number} offset
  57. * @return {boolean}
  58. * @private
  59. */
  60. static isFooter_(data, offset) {
  61. /*
  62. * The footer is a copy of the header, but with a different identifier
  63. */
  64. if (offset + 10 <= data.length) {
  65. // look for '3DI' identifier
  66. if (data[offset] === 0x33 &&
  67. data[offset + 1] === 0x44 &&
  68. data[offset + 2] === 0x49) {
  69. // check version is within range
  70. if (data[offset + 3] < 0xff && data[offset + 4] < 0xff) {
  71. // check size is within range
  72. if (data[offset + 6] < 0x80 &&
  73. data[offset + 7] < 0x80 &&
  74. data[offset + 8] < 0x80 &&
  75. data[offset + 9] < 0x80) {
  76. return true;
  77. }
  78. }
  79. }
  80. }
  81. return false;
  82. }
  83. /**
  84. * @param {Uint8Array} data
  85. * @param {number} offset
  86. * @return {number}
  87. * @private
  88. */
  89. static readSize_(data, offset) {
  90. let size = 0;
  91. size = (data[offset] & 0x7f) << 21;
  92. size |= (data[offset + 1] & 0x7f) << 14;
  93. size |= (data[offset + 2] & 0x7f) << 7;
  94. size |= data[offset + 3] & 0x7f;
  95. return size;
  96. }
  97. /**
  98. * @param {Uint8Array} data
  99. * @return {shaka.extern.MetadataRawFrame}
  100. * @private
  101. */
  102. static getFrameData_(data) {
  103. /*
  104. * Frame ID $xx xx xx xx (four characters)
  105. * Size $xx xx xx xx
  106. * Flags $xx xx
  107. */
  108. const type = String.fromCharCode(data[0], data[1], data[2], data[3]);
  109. const size = shaka.util.Id3Utils.readSize_(data, 4);
  110. // skip frame id, size, and flags
  111. const offset = 10;
  112. return {
  113. type,
  114. size,
  115. data: data.subarray(offset, offset + size),
  116. };
  117. }
  118. /**
  119. * @param {shaka.extern.MetadataRawFrame} frame
  120. * @return {?shaka.extern.MetadataFrame}
  121. * @private
  122. */
  123. static decodeFrame_(frame) {
  124. const BufferUtils = shaka.util.BufferUtils;
  125. const StringUtils = shaka.util.StringUtils;
  126. const metadataFrame = {
  127. key: frame.type,
  128. description: '',
  129. data: '',
  130. mimeType: null,
  131. pictureType: null,
  132. };
  133. if (frame.type === 'APIC') {
  134. /*
  135. * Format:
  136. * [0] = {Text Encoding}
  137. * [1 - X] = {MIME Type}\0
  138. * [X+1] = {Picture Type}
  139. * [X+2 - Y] = {Description}\0
  140. * [Y - ?] = {Picture Data or Picture URL}
  141. */
  142. if (frame.size < 2) {
  143. return null;
  144. }
  145. if (frame.data[0] !== shaka.util.Id3Utils.UTF8_encoding) {
  146. shaka.log.warning('Ignore frame with unrecognized character ' +
  147. 'encoding');
  148. return null;
  149. }
  150. const mimeTypeEndIndex = frame.data.subarray(1).indexOf(0);
  151. if (mimeTypeEndIndex === -1) {
  152. return null;
  153. }
  154. const mimeType = StringUtils.fromUTF8(
  155. BufferUtils.toUint8(frame.data, 1, mimeTypeEndIndex));
  156. const pictureType = frame.data[2 + mimeTypeEndIndex];
  157. const descriptionEndIndex = frame.data.subarray(3 + mimeTypeEndIndex)
  158. .indexOf(0);
  159. if (descriptionEndIndex === -1) {
  160. return null;
  161. }
  162. const description = StringUtils.fromUTF8(
  163. BufferUtils.toUint8(frame.data, 3 + mimeTypeEndIndex,
  164. descriptionEndIndex));
  165. let data;
  166. if (mimeType === '-->') {
  167. data = StringUtils.fromUTF8(
  168. BufferUtils.toUint8(
  169. frame.data, 4 + mimeTypeEndIndex + descriptionEndIndex));
  170. } else {
  171. data = BufferUtils.toArrayBuffer(
  172. frame.data.subarray(4 + mimeTypeEndIndex + descriptionEndIndex));
  173. }
  174. metadataFrame.mimeType = mimeType;
  175. metadataFrame.pictureType = pictureType;
  176. metadataFrame.description = description;
  177. metadataFrame.data = data;
  178. return metadataFrame;
  179. } else if (frame.type === 'TXXX') {
  180. /*
  181. * Format:
  182. * [0] = {Text Encoding}
  183. * [1-?] = {Description}\0{Value}
  184. */
  185. if (frame.size < 2) {
  186. return null;
  187. }
  188. if (frame.data[0] !== shaka.util.Id3Utils.UTF8_encoding) {
  189. shaka.log.warning('Ignore frame with unrecognized character ' +
  190. 'encoding');
  191. return null;
  192. }
  193. const descriptionEndIndex = frame.data.subarray(1).indexOf(0);
  194. if (descriptionEndIndex === -1) {
  195. return null;
  196. }
  197. const description = StringUtils.fromUTF8(
  198. BufferUtils.toUint8(frame.data, 1, descriptionEndIndex));
  199. const data = StringUtils.fromUTF8(
  200. BufferUtils.toUint8(frame.data, 2 + descriptionEndIndex))
  201. .replace(/\0*$/, '');
  202. metadataFrame.description = description;
  203. metadataFrame.data = data;
  204. return metadataFrame;
  205. } else if (frame.type === 'WXXX') {
  206. /*
  207. * Format:
  208. * [0] = {Text Encoding}
  209. * [1-?] = {Description}\0{URL}
  210. */
  211. if (frame.size < 2) {
  212. return null;
  213. }
  214. if (frame.data[0] !== shaka.util.Id3Utils.UTF8_encoding) {
  215. shaka.log.warning('Ignore frame with unrecognized character ' +
  216. 'encoding');
  217. return null;
  218. }
  219. const descriptionEndIndex = frame.data.subarray(1).indexOf(0);
  220. if (descriptionEndIndex === -1) {
  221. return null;
  222. }
  223. const description = StringUtils.fromUTF8(
  224. BufferUtils.toUint8(frame.data, 1, descriptionEndIndex));
  225. const data = StringUtils.fromUTF8(
  226. BufferUtils.toUint8(frame.data, 2 + descriptionEndIndex))
  227. .replace(/\0*$/, '');
  228. metadataFrame.description = description;
  229. metadataFrame.data = data;
  230. return metadataFrame;
  231. } else if (frame.type === 'PRIV') {
  232. /*
  233. * Format: <text string>\0<binary data>
  234. */
  235. if (frame.size < 2) {
  236. return null;
  237. }
  238. const textEndIndex = frame.data.indexOf(0);
  239. if (textEndIndex === -1) {
  240. return null;
  241. }
  242. const text = StringUtils.fromUTF8(
  243. BufferUtils.toUint8(frame.data, 0, textEndIndex));
  244. metadataFrame.description = text;
  245. if (text == 'com.apple.streaming.transportStreamTimestamp') {
  246. const data = frame.data.subarray(text.length + 1);
  247. // timestamp is 33 bit expressed as a big-endian eight-octet number,
  248. // with the upper 31 bits set to zero.
  249. const pts33Bit = data[3] & 0x1;
  250. let timestamp =
  251. (data[4] << 23) + (data[5] << 15) + (data[6] << 7) + data[7];
  252. timestamp /= 45;
  253. if (pts33Bit) {
  254. timestamp += 47721858.84;
  255. } // 2^32 / 90
  256. metadataFrame.data = timestamp;
  257. } else {
  258. const data = BufferUtils.toArrayBuffer(
  259. frame.data.subarray(text.length + 1));
  260. metadataFrame.data = data;
  261. }
  262. return metadataFrame;
  263. } else if (frame.type[0] === 'T') {
  264. /*
  265. * Format:
  266. * [0] = {Text Encoding}
  267. * [1-?] = {Value}
  268. */
  269. if (frame.size < 2) {
  270. return null;
  271. }
  272. if (frame.data[0] !== shaka.util.Id3Utils.UTF8_encoding) {
  273. shaka.log.warning('Ignore frame with unrecognized character ' +
  274. 'encoding');
  275. return null;
  276. }
  277. const text = StringUtils.fromUTF8(frame.data.subarray(1))
  278. .replace(/\0*$/, '');
  279. metadataFrame.data = text;
  280. return metadataFrame;
  281. } else if (frame.type[0] === 'W') {
  282. /*
  283. * Format:
  284. * [0-?] = {URL}
  285. */
  286. const url = StringUtils.fromUTF8(frame.data)
  287. .replace(/\0*$/, '');
  288. metadataFrame.data = url;
  289. return metadataFrame;
  290. } else if (frame.data) {
  291. shaka.log.warning('Unrecognized ID3 frame type:', frame.type);
  292. metadataFrame.data = BufferUtils.toArrayBuffer(frame.data);
  293. return metadataFrame;
  294. }
  295. return null;
  296. }
  297. /**
  298. * Returns an array of ID3 frames found in all the ID3 tags in the id3Data
  299. * @param {Uint8Array} id3Data - The ID3 data containing one or more ID3 tags
  300. * @return {!Array<shaka.extern.MetadataFrame>}
  301. * @export
  302. */
  303. static getID3Frames(id3Data) {
  304. const Id3Utils = shaka.util.Id3Utils;
  305. let offset = 0;
  306. const frames = [];
  307. while (Id3Utils.isHeader_(id3Data, offset)) {
  308. const size = Id3Utils.readSize_(id3Data, offset + 6);
  309. if ((id3Data[offset + 5] >> 6) & 1) {
  310. // skip extended header
  311. offset += 10;
  312. }
  313. // skip past ID3 header
  314. offset += 10;
  315. const end = offset + size;
  316. // loop through frames in the ID3 tag
  317. while (offset + 10 < end) {
  318. const frameData = Id3Utils.getFrameData_(id3Data.subarray(offset));
  319. const frame = Id3Utils.decodeFrame_(frameData);
  320. if (frame) {
  321. frames.push(frame);
  322. }
  323. // skip frame header and frame data
  324. offset += frameData.size + 10;
  325. }
  326. if (Id3Utils.isFooter_(id3Data, offset)) {
  327. offset += 10;
  328. }
  329. }
  330. return frames;
  331. }
  332. /**
  333. * Returns any adjacent ID3 tags found in data starting at offset, as one
  334. * block of data
  335. * @param {Uint8Array} id3Data - The ID3 data containing one or more ID3 tags
  336. * @param {number=} offset - The offset at which to start searching
  337. * @return {!Uint8Array}
  338. * @export
  339. */
  340. static getID3Data(id3Data, offset = 0) {
  341. const Id3Utils = shaka.util.Id3Utils;
  342. const front = offset;
  343. let length = 0;
  344. while (Id3Utils.isHeader_(id3Data, offset)) {
  345. if ((id3Data[offset + 5] >> 6) & 1) {
  346. // skip extended header
  347. length += 10;
  348. }
  349. // skip past ID3 header
  350. length += 10;
  351. const size = Id3Utils.readSize_(id3Data, offset + 6);
  352. length += size;
  353. if (Id3Utils.isFooter_(id3Data, offset + 10)) {
  354. // ID3 footer is 10 bytes
  355. length += 10;
  356. }
  357. offset += length;
  358. }
  359. if (length > 0) {
  360. return id3Data.subarray(front, front + length);
  361. }
  362. return new Uint8Array([]);
  363. }
  364. };
  365. /**
  366. * UTF8 encoding byte
  367. * @const {number}
  368. */
  369. shaka.util.Id3Utils.UTF8_encoding = 0x03;