diff --git a/fixture/fixture-faac-adts.mp2 b/fixture/fixture-adts-mpeg2.aac similarity index 100% rename from fixture/fixture-faac-adts.mp2 rename to fixture/fixture-adts-mpeg2.aac diff --git a/fixture/fixture-adts-mpeg4-2.aac b/fixture/fixture-adts-mpeg4-2.aac new file mode 100644 index 00000000..06036ca0 Binary files /dev/null and b/fixture/fixture-adts-mpeg4-2.aac differ diff --git a/fixture/fixture-aac-adts.mp4 b/fixture/fixture-adts-mpeg4.aac similarity index 100% rename from fixture/fixture-aac-adts.mp4 rename to fixture/fixture-adts-mpeg4.aac diff --git a/index.d.ts b/index.d.ts index 1e2a8700..c22b0462 100644 --- a/index.d.ts +++ b/index.d.ts @@ -99,7 +99,8 @@ declare namespace fileType { | 'lnk' | 'alias' | 'voc' - | 'ac3'; + | 'ac3' + | 'aac'; interface FileTypeResult { /** diff --git a/index.js b/index.js index f438532a..86120637 100644 --- a/index.js +++ b/index.js @@ -38,6 +38,17 @@ const fileType = input => { const checkString = (header, options) => check(stringToBytes(header), options); + /** + * Specialized function to read ID3 payload length + * @param buf Buffer + * @param off offset in buffer + * @returns {number} ID3 payload length + */ + function readUINT32SYNCSAFE(buf, off) { + return (buf[off + 3] & 0x7F) | ((buf[off + 2]) << 7) | + ((buf[off + 1]) << 14) | ((buf[off]) << 21); + } + if (check([0xFF, 0xD8, 0xFF])) { return { ext: 'jpg', @@ -409,46 +420,71 @@ const fileType = input => { }; } - // Check for MPEG header at different starting offsets + let flagId3 = false; + for (let start = 0; start < 2 && start < (buffer.length - 16); start++) { - if ( - check([0x49, 0x44, 0x33], {offset: start}) || // ID3 header - check([0xFF, 0xE2], {offset: start, mask: [0xFF, 0xE6]}) // MPEG 1 or 2 Layer 3 header - ) { - return { - ext: 'mp3', - mime: 'audio/mpeg' - }; + // Check for ID3 header + if (buffer.length >= start + 10 && checkString('ID3', {offset: start})) { + const id3Len = readUINT32SYNCSAFE(buffer, start + 6); + start += (10 + id3Len - 1); // Skip ID3 header + flagId3 = true; + continue; } - if ( - check([0xFF, 0xE4], {offset: start, mask: [0xFF, 0xE6]}) // MPEG 1 or 2 Layer 2 header - ) { - return { - ext: 'mp2', - mime: 'audio/mpeg' - }; - } + // Check MPEG 1 or 2 Layer 3 header, or 'layer 0' for ADTS (MPEG sync-word 0xFFE) + if (buffer.length >= start + 2 && check([0xFF, 0xE0], {offset: start, mask: [0xFF, 0xE0]})) { + // Check for ADTS header (last bit of sync-word 0xFFF & layer=0) + if (check([0x10], {offset: start + 1, mask: [0x16]})) { + // Check for (ADTS) MPEG-2 + if (check([0x08], {offset: start + 1, mask: [0x08]})) { + return { + ext: 'aac', + mime: 'audio/aac' + }; + } - if ( - check([0xFF, 0xF8], {offset: start, mask: [0xFF, 0xFC]}) // MPEG 2 layer 0 using ADTS - ) { - return { - ext: 'mp2', - mime: 'audio/mpeg' - }; - } + // Must be (ADTS) MPEG-4 + return { + ext: 'aac', + mime: 'audio/aac' + }; + } - if ( - check([0xFF, 0xF0], {offset: start, mask: [0xFF, 0xFC]}) // MPEG 4 layer 0 using ADTS - ) { - return { - ext: 'mp4', - mime: 'audio/mpeg' - }; + // MPEG 1 or 2 Layer 3 header + // Check for MPEG layer 3 + if (check([0x02], {offset: start + 1, mask: [0x06]})) { + return { + ext: 'mp3', + mime: 'audio/mpeg' + }; + } + + // Check for MPEG layer 2 + if (check([0x04], {offset: start + 1, mask: [0x06]})) { + return { + ext: 'mp2', + mime: 'audio/mpeg' + }; + } + + // Check for MPEG layer 1 + if (check([0x06], {offset: start + 1, mask: [0x06]})) { + return { + ext: 'mp1', + mime: 'audio/mpeg' + }; + } } } + if (flagId3) { + // Guess it is MP3 if only an ID3 tag header is found + return { + ext: 'mp3', + mime: 'audio/mpeg' + }; + } + if ( check([0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x41], {offset: 4}) ) { diff --git a/readme.md b/readme.md index f6859d82..6b1e42d5 100644 --- a/readme.md +++ b/readme.md @@ -221,6 +221,7 @@ Type: [`stream.Readable`](https://nodejs.org/api/stream.html#stream_class_stream - [`alias`](https://en.wikipedia.org/wiki/Alias_%28Mac_OS%29) - macOS Alias file - [`voc`](https://wiki.multimedia.cx/index.php/Creative_Voice) - Creative Voice File - [`ac3`](https://www.atsc.org/standard/a522012-digital-audio-compression-ac-3-e-ac-3-standard-12172012/) - ATSC A/52 Audio File +- [`aac`](https://en.wikipedia.org/wiki/Advanced_Audio_Coding) - Advanced Audio Coding *SVG isn't included as it requires the whole file to be read, but you can get it [here](https://github.com/sindresorhus/is-svg).* diff --git a/test.js b/test.js index 0c5069b4..7280dc33 100644 --- a/test.js +++ b/test.js @@ -108,7 +108,9 @@ const types = [ 'lnk', 'alias', 'voc', - 'ac3' + 'ac3', + 'pcap', + 'aac' ]; // Define an entry here only if the fixture has a different @@ -137,8 +139,7 @@ const names = { ], mp2: [ 'fixture', - 'fixture-mpa', - 'fixture-faac-adts' + 'fixture-mpa' ], mp3: [ 'fixture', @@ -153,8 +154,7 @@ const names = { 'fixture-isomv2', 'fixture-mp4v2', 'fixture-m4v', - 'fixture-dash', - 'fixture-aac-adts' + 'fixture-dash' ], tif: [ 'fixture-big-endian', @@ -199,6 +199,11 @@ const names = { tar: [ 'fixture', 'fixture-v7' + ], + aac: [ + 'fixture-adts-mpeg4', + 'fixture-adts-mpeg4-2', + 'fixture-adts-mpeg2' ] }; @@ -257,13 +262,15 @@ for (const type of types) { } } -test('.stream() method - empty stream', async t => { - const emptyStream = fs.createReadStream('/dev/null'); - await t.throwsAsync( - fileType.stream(emptyStream), - /Expected the `input` argument to be of type `Uint8Array` / - ); -}); +if (process.platform !== 'win32') { + test('.stream() method - empty stream', async t => { + const emptyStream = fs.createReadStream('/dev/null'); + await t.throwsAsync( + fileType.stream(emptyStream), + /Expected the `input` argument to be of type `Uint8Array` / + ); + }); +} test('fileType.minimumBytes', t => { t.true(fileType.minimumBytes > 4000);