metadata-stream.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. /**
  2. * mux.js
  3. *
  4. * Copyright (c) Brightcove
  5. * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
  6. *
  7. * Accepts program elementary stream (PES) data events and parses out
  8. * ID3 metadata from them, if present.
  9. * @see http://id3.org/id3v2.3.0
  10. */
  11. 'use strict';
  12. var
  13. Stream = require('../utils/stream'),
  14. StreamTypes = require('./stream-types'),
  15. id3 = require('../tools/parse-id3'),
  16. MetadataStream;
  17. MetadataStream = function(options) {
  18. var
  19. settings = {
  20. // the bytes of the program-level descriptor field in MP2T
  21. // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and
  22. // program element descriptors"
  23. descriptor: options && options.descriptor
  24. },
  25. // the total size in bytes of the ID3 tag being parsed
  26. tagSize = 0,
  27. // tag data that is not complete enough to be parsed
  28. buffer = [],
  29. // the total number of bytes currently in the buffer
  30. bufferSize = 0,
  31. i;
  32. MetadataStream.prototype.init.call(this);
  33. // calculate the text track in-band metadata track dispatch type
  34. // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
  35. this.dispatchType = StreamTypes.METADATA_STREAM_TYPE.toString(16);
  36. if (settings.descriptor) {
  37. for (i = 0; i < settings.descriptor.length; i++) {
  38. this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2);
  39. }
  40. }
  41. this.push = function(chunk) {
  42. var tag, frameStart, frameSize, frame, i, frameHeader;
  43. if (chunk.type !== 'timed-metadata') {
  44. return;
  45. }
  46. // if data_alignment_indicator is set in the PES header,
  47. // we must have the start of a new ID3 tag. Assume anything
  48. // remaining in the buffer was malformed and throw it out
  49. if (chunk.dataAlignmentIndicator) {
  50. bufferSize = 0;
  51. buffer.length = 0;
  52. }
  53. // ignore events that don't look like ID3 data
  54. if (buffer.length === 0 &&
  55. (chunk.data.length < 10 ||
  56. chunk.data[0] !== 'I'.charCodeAt(0) ||
  57. chunk.data[1] !== 'D'.charCodeAt(0) ||
  58. chunk.data[2] !== '3'.charCodeAt(0))) {
  59. this.trigger('log', {
  60. level: 'warn',
  61. message: 'Skipping unrecognized metadata packet'
  62. });
  63. return;
  64. }
  65. // add this chunk to the data we've collected so far
  66. buffer.push(chunk);
  67. bufferSize += chunk.data.byteLength;
  68. // grab the size of the entire frame from the ID3 header
  69. if (buffer.length === 1) {
  70. // the frame size is transmitted as a 28-bit integer in the
  71. // last four bytes of the ID3 header.
  72. // The most significant bit of each byte is dropped and the
  73. // results concatenated to recover the actual value.
  74. tagSize = id3.parseSyncSafeInteger(chunk.data.subarray(6, 10));
  75. // ID3 reports the tag size excluding the header but it's more
  76. // convenient for our comparisons to include it
  77. tagSize += 10;
  78. }
  79. // if the entire frame has not arrived, wait for more data
  80. if (bufferSize < tagSize) {
  81. return;
  82. }
  83. // collect the entire frame so it can be parsed
  84. tag = {
  85. data: new Uint8Array(tagSize),
  86. frames: [],
  87. pts: buffer[0].pts,
  88. dts: buffer[0].dts
  89. };
  90. for (i = 0; i < tagSize;) {
  91. tag.data.set(buffer[0].data.subarray(0, tagSize - i), i);
  92. i += buffer[0].data.byteLength;
  93. bufferSize -= buffer[0].data.byteLength;
  94. buffer.shift();
  95. }
  96. // find the start of the first frame and the end of the tag
  97. frameStart = 10;
  98. if (tag.data[5] & 0x40) {
  99. // advance the frame start past the extended header
  100. frameStart += 4; // header size field
  101. frameStart += id3.parseSyncSafeInteger(tag.data.subarray(10, 14));
  102. // clip any padding off the end
  103. tagSize -= id3.parseSyncSafeInteger(tag.data.subarray(16, 20));
  104. }
  105. // parse one or more ID3 frames
  106. // http://id3.org/id3v2.3.0#ID3v2_frame_overview
  107. do {
  108. // determine the number of bytes in this frame
  109. frameSize = id3.parseSyncSafeInteger(tag.data.subarray(frameStart + 4, frameStart + 8));
  110. if (frameSize < 1) {
  111. this.trigger('log', {
  112. level: 'warn',
  113. message: 'Malformed ID3 frame encountered. Skipping remaining metadata parsing.'
  114. });
  115. // If the frame is malformed, don't parse any further frames but allow previous valid parsed frames
  116. // to be sent along.
  117. break;
  118. }
  119. frameHeader = String.fromCharCode(tag.data[frameStart],
  120. tag.data[frameStart + 1],
  121. tag.data[frameStart + 2],
  122. tag.data[frameStart + 3]);
  123. frame = {
  124. id: frameHeader,
  125. data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10)
  126. };
  127. frame.key = frame.id;
  128. // parse frame values
  129. if (id3.frameParsers[frame.id]) {
  130. // use frame specific parser
  131. id3.frameParsers[frame.id](frame);
  132. } else if (frame.id[0] === 'T') {
  133. // use text frame generic parser
  134. id3.frameParsers['T*'](frame);
  135. } else if (frame.id[0] === 'W') {
  136. // use URL link frame generic parser
  137. id3.frameParsers['W*'](frame);
  138. }
  139. // handle the special PRIV frame used to indicate the start
  140. // time for raw AAC data
  141. if (frame.owner === 'com.apple.streaming.transportStreamTimestamp') {
  142. var
  143. d = frame.data,
  144. size = ((d[3] & 0x01) << 30) |
  145. (d[4] << 22) |
  146. (d[5] << 14) |
  147. (d[6] << 6) |
  148. (d[7] >>> 2);
  149. size *= 4;
  150. size += d[7] & 0x03;
  151. frame.timeStamp = size;
  152. // in raw AAC, all subsequent data will be timestamped based
  153. // on the value of this frame
  154. // we couldn't have known the appropriate pts and dts before
  155. // parsing this ID3 tag so set those values now
  156. if (tag.pts === undefined && tag.dts === undefined) {
  157. tag.pts = frame.timeStamp;
  158. tag.dts = frame.timeStamp;
  159. }
  160. this.trigger('timestamp', frame);
  161. }
  162. tag.frames.push(frame);
  163. frameStart += 10; // advance past the frame header
  164. frameStart += frameSize; // advance past the frame body
  165. } while (frameStart < tagSize);
  166. this.trigger('data', tag);
  167. };
  168. };
  169. MetadataStream.prototype = new Stream();
  170. module.exports = MetadataStream;