123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- /**
- * Accepts program elementary stream (PES) data events and parses out
- * ID3 metadata from them, if present.
- * @see http://id3.org/id3v2.3.0
- */
- 'use strict';
- var
- Stream = require('../utils/stream'),
- StreamTypes = require('./stream-types'),
- // return a percent-encoded representation of the specified byte range
- // @see http://en.wikipedia.org/wiki/Percent-encoding
- percentEncode = function(bytes, start, end) {
- var i, result = '';
- for (i = start; i < end; i++) {
- result += '%' + ('00' + bytes[i].toString(16)).slice(-2);
- }
- return result;
- },
- // return the string representation of the specified byte range,
- // interpreted as UTf-8.
- parseUtf8 = function(bytes, start, end) {
- return decodeURIComponent(percentEncode(bytes, start, end));
- },
- // return the string representation of the specified byte range,
- // interpreted as ISO-8859-1.
- parseIso88591 = function(bytes, start, end) {
- return unescape(percentEncode(bytes, start, end)); // jshint ignore:line
- },
- parseSyncSafeInteger = function(data) {
- return (data[0] << 21) |
- (data[1] << 14) |
- (data[2] << 7) |
- (data[3]);
- },
- tagParsers = {
- TXXX: function(tag) {
- var i;
- if (tag.data[0] !== 3) {
- // ignore frames with unrecognized character encodings
- return;
- }
- for (i = 1; i < tag.data.length; i++) {
- if (tag.data[i] === 0) {
- // parse the text fields
- tag.description = parseUtf8(tag.data, 1, i);
- // do not include the null terminator in the tag value
- tag.value = parseUtf8(tag.data, i + 1, tag.data.length).replace(/\0*$/, '');
- break;
- }
- }
- tag.data = tag.value;
- },
- WXXX: function(tag) {
- var i;
- if (tag.data[0] !== 3) {
- // ignore frames with unrecognized character encodings
- return;
- }
- for (i = 1; i < tag.data.length; i++) {
- if (tag.data[i] === 0) {
- // parse the description and URL fields
- tag.description = parseUtf8(tag.data, 1, i);
- tag.url = parseUtf8(tag.data, i + 1, tag.data.length);
- break;
- }
- }
- },
- PRIV: function(tag) {
- var i;
- for (i = 0; i < tag.data.length; i++) {
- if (tag.data[i] === 0) {
- // parse the description and URL fields
- tag.owner = parseIso88591(tag.data, 0, i);
- break;
- }
- }
- tag.privateData = tag.data.subarray(i + 1);
- tag.data = tag.privateData;
- }
- },
- MetadataStream;
- MetadataStream = function(options) {
- var
- settings = {
- debug: !!(options && options.debug),
- // the bytes of the program-level descriptor field in MP2T
- // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and
- // program element descriptors"
- descriptor: options && options.descriptor
- },
- // the total size in bytes of the ID3 tag being parsed
- tagSize = 0,
- // tag data that is not complete enough to be parsed
- buffer = [],
- // the total number of bytes currently in the buffer
- bufferSize = 0,
- i;
- MetadataStream.prototype.init.call(this);
- // calculate the text track in-band metadata track dispatch type
- // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
- this.dispatchType = StreamTypes.METADATA_STREAM_TYPE.toString(16);
- if (settings.descriptor) {
- for (i = 0; i < settings.descriptor.length; i++) {
- this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2);
- }
- }
- this.push = function(chunk) {
- var tag, frameStart, frameSize, frame, i, frameHeader;
- if (chunk.type !== 'timed-metadata') {
- return;
- }
- // if data_alignment_indicator is set in the PES header,
- // we must have the start of a new ID3 tag. Assume anything
- // remaining in the buffer was malformed and throw it out
- if (chunk.dataAlignmentIndicator) {
- bufferSize = 0;
- buffer.length = 0;
- }
- // ignore events that don't look like ID3 data
- if (buffer.length === 0 &&
- (chunk.data.length < 10 ||
- chunk.data[0] !== 'I'.charCodeAt(0) ||
- chunk.data[1] !== 'D'.charCodeAt(0) ||
- chunk.data[2] !== '3'.charCodeAt(0))) {
- if (settings.debug) {
- // eslint-disable-next-line no-console
- console.log('Skipping unrecognized metadata packet');
- }
- return;
- }
- // add this chunk to the data we've collected so far
- buffer.push(chunk);
- bufferSize += chunk.data.byteLength;
- // grab the size of the entire frame from the ID3 header
- if (buffer.length === 1) {
- // the frame size is transmitted as a 28-bit integer in the
- // last four bytes of the ID3 header.
- // The most significant bit of each byte is dropped and the
- // results concatenated to recover the actual value.
- tagSize = parseSyncSafeInteger(chunk.data.subarray(6, 10));
- // ID3 reports the tag size excluding the header but it's more
- // convenient for our comparisons to include it
- tagSize += 10;
- }
- // if the entire frame has not arrived, wait for more data
- if (bufferSize < tagSize) {
- return;
- }
- // collect the entire frame so it can be parsed
- tag = {
- data: new Uint8Array(tagSize),
- frames: [],
- pts: buffer[0].pts,
- dts: buffer[0].dts
- };
- for (i = 0; i < tagSize;) {
- tag.data.set(buffer[0].data.subarray(0, tagSize - i), i);
- i += buffer[0].data.byteLength;
- bufferSize -= buffer[0].data.byteLength;
- buffer.shift();
- }
- // find the start of the first frame and the end of the tag
- frameStart = 10;
- if (tag.data[5] & 0x40) {
- // advance the frame start past the extended header
- frameStart += 4; // header size field
- frameStart += parseSyncSafeInteger(tag.data.subarray(10, 14));
- // clip any padding off the end
- tagSize -= parseSyncSafeInteger(tag.data.subarray(16, 20));
- }
- // parse one or more ID3 frames
- // http://id3.org/id3v2.3.0#ID3v2_frame_overview
- do {
- // determine the number of bytes in this frame
- frameSize = parseSyncSafeInteger(tag.data.subarray(frameStart + 4, frameStart + 8));
- if (frameSize < 1) {
- // eslint-disable-next-line no-console
- return console.log('Malformed ID3 frame encountered. Skipping metadata parsing.');
- }
- frameHeader = String.fromCharCode(tag.data[frameStart],
- tag.data[frameStart + 1],
- tag.data[frameStart + 2],
- tag.data[frameStart + 3]);
- frame = {
- id: frameHeader,
- data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10)
- };
- frame.key = frame.id;
- if (tagParsers[frame.id]) {
- tagParsers[frame.id](frame);
- // handle the special PRIV frame used to indicate the start
- // time for raw AAC data
- if (frame.owner === 'com.apple.streaming.transportStreamTimestamp') {
- var
- d = frame.data,
- size = ((d[3] & 0x01) << 30) |
- (d[4] << 22) |
- (d[5] << 14) |
- (d[6] << 6) |
- (d[7] >>> 2);
- size *= 4;
- size += d[7] & 0x03;
- frame.timeStamp = size;
- // in raw AAC, all subsequent data will be timestamped based
- // on the value of this frame
- // we couldn't have known the appropriate pts and dts before
- // parsing this ID3 tag so set those values now
- if (tag.pts === undefined && tag.dts === undefined) {
- tag.pts = frame.timeStamp;
- tag.dts = frame.timeStamp;
- }
- this.trigger('timestamp', frame);
- }
- }
- tag.frames.push(frame);
- frameStart += 10; // advance past the frame header
- frameStart += frameSize; // advance past the frame body
- } while (frameStart < tagSize);
- this.trigger('data', tag);
- };
- };
- MetadataStream.prototype = new Stream();
- module.exports = MetadataStream;
|