'use strict'; var aacStream, AacStream = require('../lib/aac'), QUnit = require('qunit'), utils = require('./utils'), createId3Header, createId3FrameHeader, createAdtsHeader; createId3Header = function(tagSize) { var header = []; header[0] = 'I'.charCodeAt(0); header[1] = 'D'.charCodeAt(0); header[2] = '3'.charCodeAt(0); // 2 version bytes, ID3v2.4.0 (major 4, revision 0) header[3] = 4; header[4] = 0; // unsynchronization, extended header, experimental indicator, footer present flags header[5] = 0; // "The ID3v2 tag size is the sum of the byte length of the extended // header, the padding and the frames after unsynchronisation. If a // footer is present this equals to ('total size' - 20) bytes, otherwise // ('total size' - 10) bytes." // http://id3.org/id3v2.4.0-structure header[6] = 0; header[7] = 0; header[8] = 0; header[9] = tagSize; return header; }; createId3FrameHeader = function() { var header = []; // four byte frame ID, XYZ are experimental header[0] = 'X'.charCodeAt(0); header[1] = 'Y'.charCodeAt(0); header[2] = 'Z'.charCodeAt(0); header[3] = '0'.charCodeAt(0); // four byte sync safe integer size (excluding frame header) header[4] = 0; header[5] = 0; header[6] = 0; header[7] = 10; // two bytes for flags header[8] = 0; header[9] = 0; return header; }; createAdtsHeader = function(frameLength) { // Header consists of 7 or 9 bytes (without or with CRC). // see: https://wiki.multimedia.cx/index.php/ADTS return utils.binaryStringToArrayOfBytes(''.concat( // 12 bits for syncword (0xFFF) '111111111111', // 1 bit MPEG version '0', // 2 bit layer (always 0) '00', // 1 bit protection absent (1 for no CRC) '1', // 2 bit profile '10', // 4 bit sampling frequency index '0110', // 1 bit private bit '0', // 3 bit channel config '100', // 2 bit (ignore) '00', // 2 bit (copright bits) '00', // 13 bit frame length (includes header length) utils.leftPad(frameLength.toString(2), 13), // 11 bit buffer fullness '11111111111', // 2 bit number of AAC frames minus 1 '00' // 16 bit CRC (if present) )); }; QUnit.module('AAC Stream', { beforeEach: function() { aacStream = new AacStream(); } }); QUnit.test('parses ID3 tag', function(assert) { var id3Count = 0, adtsCount = 0, frameHeader = createId3FrameHeader(), id3Tag = createId3Header(frameHeader.length).concat(frameHeader); aacStream.on('data', function(frame) { if (frame.type === 'timed-metadata') { id3Count += 1; } else if (frame.type === 'audio') { adtsCount += 1; } }); aacStream.push(new Uint8Array(id3Tag)); assert.equal(adtsCount, 0, 'no adts frames'); assert.equal(id3Count, 1, 'one id3 chunk'); }); QUnit.test('parses two ID3 tags in sequence', function(assert) { var id3Count = 0, adtsCount = 0, frameHeader = createId3FrameHeader(), id3Tag = createId3Header(frameHeader.length).concat(frameHeader); aacStream.on('data', function(frame) { if (frame.type === 'timed-metadata') { id3Count += 1; } else if (frame.type === 'audio') { adtsCount += 1; } }); aacStream.push(new Uint8Array(id3Tag.concat(id3Tag))); assert.equal(adtsCount, 0, 'no adts frames'); assert.equal(id3Count, 2, 'two id3 chunks'); }); QUnit.test('does not parse second ID3 tag if it\'s incomplete', function(assert) { var id3Count = 0, adtsCount = 0, frameHeader = createId3FrameHeader(), id3Tag = createId3Header(frameHeader.length).concat(frameHeader); aacStream.on('data', function(frame) { if (frame.type === 'timed-metadata') { id3Count += 1; } else if (frame.type === 'audio') { adtsCount += 1; } }); aacStream.push(new Uint8Array(id3Tag.concat(id3Tag.slice(0, id3Tag.length - 1)))); assert.equal(adtsCount, 0, 'no adts frames'); assert.equal(id3Count, 1, 'one id3 chunk'); }); QUnit.test('handles misaligned adts header', function(assert) { var id3Count = 0, adtsCount = 0, // fake adts frame adtsFrame = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], packetStream = createAdtsHeader(adtsFrame.length).concat(adtsFrame); aacStream.on('data', function(frame) { if (frame.type === 'timed-metadata') { id3Count += 1; } else if (frame.type === 'audio') { adtsCount += 1; } }); // misalign by two bytes specific to a bug related to detecting sync bytes // (where we were only properly checking the second byte) aacStream.push(new Uint8Array([0x01, 0xf0].concat(packetStream))); assert.equal(adtsCount, 1, 'one adts frames'); assert.equal(id3Count, 0, 'no id3 chunk'); }); QUnit.test('handles incomplete adts frame after id3 frame', function(assert) { var id3Count = 0, adtsCount = 0, id3FrameHeader = createId3FrameHeader(), id3Tag = createId3Header(id3FrameHeader.length).concat(id3FrameHeader), // in this case: // id3 tag = 20 bytes // adts header = 7 bytes // total = 27 bytes // report the ADTS frame size as 20 bytes adtsHeader = createAdtsHeader(20), // no adts frame, stream was cut off packetStream = id3Tag.concat(adtsHeader); aacStream.on('data', function(frame) { if (frame.type === 'timed-metadata') { id3Count += 1; } else if (frame.type === 'audio') { adtsCount += 1; } }); aacStream.push(new Uint8Array(packetStream)); assert.equal(adtsCount, 0, 'no adts frame'); assert.equal(id3Count, 1, 'one id3 chunk'); }); QUnit.test('emits data after receiving push', function(assert) { var array = new Uint8Array(109), count = 0; array[0] = 255; array[1] = 241; array[2] = 92; array[3] = 128; array[4] = 13; array[5] = 191; array[6] = 252; array[7] = 33; array[8] = 32; array[9] = 3; array[10] = 64; array[11] = 104; array[12] = 27; array[13] = 212; aacStream.setTimestamp(90); aacStream.on('data', function(frame) { if (frame.pts === 90 && frame.dts === 90) { count += 1; } }); aacStream.push(array); assert.equal(count, 1); }); QUnit.test('continues parsing after corrupted stream', function(assert) { var array = new Uint8Array(10000), adtsCount = 0, id3Count = 0; // an ID3 frame array[0] = 73; array[1] = 68; array[2] = 51; array[3] = 4; array[4] = 0; array[5] = 0; array[6] = 0; array[7] = 0; array[8] = 0; array[9] = 63; array[10] = 80; array[11] = 82; array[12] = 73; array[13] = 86; // an atds frame array[1020] = 255; array[1021] = 241; array[1022] = 92; array[1023] = 128; array[1024] = 13; array[1025] = 191; array[1026] = 252; array[1027] = 33; array[1028] = 32; array[1029] = 3; array[1030] = 64; array[1031] = 104; array[1032] = 27; array[1033] = 212; aacStream.on('data', function(frame) { if (frame.type === 'timed-metadata') { id3Count += 1; } else if (frame.type === 'audio') { adtsCount += 1; } }); aacStream.push(array); assert.equal(adtsCount, 1); assert.equal(id3Count, 1); });