metadata-stream.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. /**
  2. * Accepts program elementary stream (PES) data events and parses out
  3. * ID3 metadata from them, if present.
  4. * @see http://id3.org/id3v2.3.0
  5. */
  6. 'use strict';
  7. var
  8. Stream = require('../utils/stream'),
  9. StreamTypes = require('./stream-types'),
  10. // return a percent-encoded representation of the specified byte range
  11. // @see http://en.wikipedia.org/wiki/Percent-encoding
  12. percentEncode = function(bytes, start, end) {
  13. var i, result = '';
  14. for (i = start; i < end; i++) {
  15. result += '%' + ('00' + bytes[i].toString(16)).slice(-2);
  16. }
  17. return result;
  18. },
  19. // return the string representation of the specified byte range,
  20. // interpreted as UTf-8.
  21. parseUtf8 = function(bytes, start, end) {
  22. return decodeURIComponent(percentEncode(bytes, start, end));
  23. },
  24. // return the string representation of the specified byte range,
  25. // interpreted as ISO-8859-1.
  26. parseIso88591 = function(bytes, start, end) {
  27. return unescape(percentEncode(bytes, start, end)); // jshint ignore:line
  28. },
  29. parseSyncSafeInteger = function(data) {
  30. return (data[0] << 21) |
  31. (data[1] << 14) |
  32. (data[2] << 7) |
  33. (data[3]);
  34. },
  35. tagParsers = {
  36. TXXX: function(tag) {
  37. var i;
  38. if (tag.data[0] !== 3) {
  39. // ignore frames with unrecognized character encodings
  40. return;
  41. }
  42. for (i = 1; i < tag.data.length; i++) {
  43. if (tag.data[i] === 0) {
  44. // parse the text fields
  45. tag.description = parseUtf8(tag.data, 1, i);
  46. // do not include the null terminator in the tag value
  47. tag.value = parseUtf8(tag.data, i + 1, tag.data.length).replace(/\0*$/, '');
  48. break;
  49. }
  50. }
  51. tag.data = tag.value;
  52. },
  53. WXXX: function(tag) {
  54. var i;
  55. if (tag.data[0] !== 3) {
  56. // ignore frames with unrecognized character encodings
  57. return;
  58. }
  59. for (i = 1; i < tag.data.length; i++) {
  60. if (tag.data[i] === 0) {
  61. // parse the description and URL fields
  62. tag.description = parseUtf8(tag.data, 1, i);
  63. tag.url = parseUtf8(tag.data, i + 1, tag.data.length);
  64. break;
  65. }
  66. }
  67. },
  68. PRIV: function(tag) {
  69. var i;
  70. for (i = 0; i < tag.data.length; i++) {
  71. if (tag.data[i] === 0) {
  72. // parse the description and URL fields
  73. tag.owner = parseIso88591(tag.data, 0, i);
  74. break;
  75. }
  76. }
  77. tag.privateData = tag.data.subarray(i + 1);
  78. tag.data = tag.privateData;
  79. }
  80. },
  81. MetadataStream;
  82. MetadataStream = function(options) {
  83. var
  84. settings = {
  85. debug: !!(options && options.debug),
  86. // the bytes of the program-level descriptor field in MP2T
  87. // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and
  88. // program element descriptors"
  89. descriptor: options && options.descriptor
  90. },
  91. // the total size in bytes of the ID3 tag being parsed
  92. tagSize = 0,
  93. // tag data that is not complete enough to be parsed
  94. buffer = [],
  95. // the total number of bytes currently in the buffer
  96. bufferSize = 0,
  97. i;
  98. MetadataStream.prototype.init.call(this);
  99. // calculate the text track in-band metadata track dispatch type
  100. // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
  101. this.dispatchType = StreamTypes.METADATA_STREAM_TYPE.toString(16);
  102. if (settings.descriptor) {
  103. for (i = 0; i < settings.descriptor.length; i++) {
  104. this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2);
  105. }
  106. }
  107. this.push = function(chunk) {
  108. var tag, frameStart, frameSize, frame, i, frameHeader;
  109. if (chunk.type !== 'timed-metadata') {
  110. return;
  111. }
  112. // if data_alignment_indicator is set in the PES header,
  113. // we must have the start of a new ID3 tag. Assume anything
  114. // remaining in the buffer was malformed and throw it out
  115. if (chunk.dataAlignmentIndicator) {
  116. bufferSize = 0;
  117. buffer.length = 0;
  118. }
  119. // ignore events that don't look like ID3 data
  120. if (buffer.length === 0 &&
  121. (chunk.data.length < 10 ||
  122. chunk.data[0] !== 'I'.charCodeAt(0) ||
  123. chunk.data[1] !== 'D'.charCodeAt(0) ||
  124. chunk.data[2] !== '3'.charCodeAt(0))) {
  125. if (settings.debug) {
  126. // eslint-disable-next-line no-console
  127. console.log('Skipping unrecognized metadata packet');
  128. }
  129. return;
  130. }
  131. // add this chunk to the data we've collected so far
  132. buffer.push(chunk);
  133. bufferSize += chunk.data.byteLength;
  134. // grab the size of the entire frame from the ID3 header
  135. if (buffer.length === 1) {
  136. // the frame size is transmitted as a 28-bit integer in the
  137. // last four bytes of the ID3 header.
  138. // The most significant bit of each byte is dropped and the
  139. // results concatenated to recover the actual value.
  140. tagSize = parseSyncSafeInteger(chunk.data.subarray(6, 10));
  141. // ID3 reports the tag size excluding the header but it's more
  142. // convenient for our comparisons to include it
  143. tagSize += 10;
  144. }
  145. // if the entire frame has not arrived, wait for more data
  146. if (bufferSize < tagSize) {
  147. return;
  148. }
  149. // collect the entire frame so it can be parsed
  150. tag = {
  151. data: new Uint8Array(tagSize),
  152. frames: [],
  153. pts: buffer[0].pts,
  154. dts: buffer[0].dts
  155. };
  156. for (i = 0; i < tagSize;) {
  157. tag.data.set(buffer[0].data.subarray(0, tagSize - i), i);
  158. i += buffer[0].data.byteLength;
  159. bufferSize -= buffer[0].data.byteLength;
  160. buffer.shift();
  161. }
  162. // find the start of the first frame and the end of the tag
  163. frameStart = 10;
  164. if (tag.data[5] & 0x40) {
  165. // advance the frame start past the extended header
  166. frameStart += 4; // header size field
  167. frameStart += parseSyncSafeInteger(tag.data.subarray(10, 14));
  168. // clip any padding off the end
  169. tagSize -= parseSyncSafeInteger(tag.data.subarray(16, 20));
  170. }
  171. // parse one or more ID3 frames
  172. // http://id3.org/id3v2.3.0#ID3v2_frame_overview
  173. do {
  174. // determine the number of bytes in this frame
  175. frameSize = parseSyncSafeInteger(tag.data.subarray(frameStart + 4, frameStart + 8));
  176. if (frameSize < 1) {
  177. // eslint-disable-next-line no-console
  178. return console.log('Malformed ID3 frame encountered. Skipping metadata parsing.');
  179. }
  180. frameHeader = String.fromCharCode(tag.data[frameStart],
  181. tag.data[frameStart + 1],
  182. tag.data[frameStart + 2],
  183. tag.data[frameStart + 3]);
  184. frame = {
  185. id: frameHeader,
  186. data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10)
  187. };
  188. frame.key = frame.id;
  189. if (tagParsers[frame.id]) {
  190. tagParsers[frame.id](frame);
  191. // handle the special PRIV frame used to indicate the start
  192. // time for raw AAC data
  193. if (frame.owner === 'com.apple.streaming.transportStreamTimestamp') {
  194. var
  195. d = frame.data,
  196. size = ((d[3] & 0x01) << 30) |
  197. (d[4] << 22) |
  198. (d[5] << 14) |
  199. (d[6] << 6) |
  200. (d[7] >>> 2);
  201. size *= 4;
  202. size += d[7] & 0x03;
  203. frame.timeStamp = size;
  204. // in raw AAC, all subsequent data will be timestamped based
  205. // on the value of this frame
  206. // we couldn't have known the appropriate pts and dts before
  207. // parsing this ID3 tag so set those values now
  208. if (tag.pts === undefined && tag.dts === undefined) {
  209. tag.pts = frame.timeStamp;
  210. tag.dts = frame.timeStamp;
  211. }
  212. this.trigger('timestamp', frame);
  213. }
  214. }
  215. tag.frames.push(frame);
  216. frameStart += 10; // advance past the frame header
  217. frameStart += frameSize; // advance past the frame body
  218. } while (frameStart < tagSize);
  219. this.trigger('data', tag);
  220. };
  221. };
  222. MetadataStream.prototype = new Stream();
  223. module.exports = MetadataStream;