diff --git a/fixture/fixture.3g2 b/fixture/fixture.3g2 new file mode 100644 index 00000000..f5d4fb57 Binary files /dev/null and b/fixture/fixture.3g2 differ diff --git a/fixture/fixture.f4a b/fixture/fixture.f4a new file mode 100644 index 00000000..34e31040 Binary files /dev/null and b/fixture/fixture.f4a differ diff --git a/fixture/fixture.f4b b/fixture/fixture.f4b new file mode 100644 index 00000000..6a504a53 Binary files /dev/null and b/fixture/fixture.f4b differ diff --git a/fixture/fixture.f4p b/fixture/fixture.f4p new file mode 100644 index 00000000..3a705a23 Binary files /dev/null and b/fixture/fixture.f4p differ diff --git a/fixture/fixture.f4v b/fixture/fixture.f4v new file mode 100644 index 00000000..33ce175c Binary files /dev/null and b/fixture/fixture.f4v differ diff --git a/fixture/fixture.m4b b/fixture/fixture.m4b new file mode 100644 index 00000000..1825895c Binary files /dev/null and b/fixture/fixture.m4b differ diff --git a/fixture/fixture.m4p b/fixture/fixture.m4p new file mode 100644 index 00000000..7a9eef75 Binary files /dev/null and b/fixture/fixture.m4p differ diff --git a/fixture/fixture-m4v.mp4 b/fixture/fixture.m4v similarity index 100% rename from fixture/fixture-m4v.mp4 rename to fixture/fixture.m4v diff --git a/index.d.ts b/index.d.ts index 1e2a8700..c71abb97 100644 --- a/index.d.ts +++ b/index.d.ts @@ -21,7 +21,6 @@ declare namespace fileType { | '7z' | 'dmg' | 'mp4' - | 'm4v' | 'mid' | 'mkv' | 'webm' @@ -73,6 +72,7 @@ declare namespace fileType { | 'pptx' | 'xlsx' | '3gp' + | '3g2' | 'jp2' | 'jpm' | 'jpx' @@ -99,7 +99,15 @@ declare namespace fileType { | 'lnk' | 'alias' | 'voc' - | 'ac3'; + | 'ac3' + | 'm4a' + | 'm4b' + | 'm4p' + | 'm4v' + | 'f4a' + | 'f4b' + | 'f4p' + | 'f4v'; interface FileTypeResult { /** diff --git a/index.js b/index.js index f438532a..f31a1cc1 100644 --- a/index.js +++ b/index.js @@ -266,25 +266,70 @@ const fileType = input => { }; } - if (check([0x33, 0x67, 0x70, 0x35]) || // 3gp5 - ( - check([0x0, 0x0, 0x0]) && check([0x66, 0x74, 0x79, 0x70], {offset: 4}) && - ( - check([0x6D, 0x70, 0x34, 0x31], {offset: 8}) || // MP41 - check([0x6D, 0x70, 0x34, 0x32], {offset: 8}) || // MP42 - check([0x69, 0x73, 0x6F, 0x6D], {offset: 8}) || // ISOM - check([0x69, 0x73, 0x6F, 0x32], {offset: 8}) || // ISO2 - check([0x6D, 0x6D, 0x70, 0x34], {offset: 8}) || // MMP4 - check([0x4D, 0x34, 0x56], {offset: 8}) || // M4V - check([0x64, 0x61, 0x73, 0x68], {offset: 8}) // DASH - ) - )) { + // `mov` format variants + if ( + check([0x66, 0x72, 0x65, 0x65], {offset: 4}) || // `free` + check([0x6D, 0x64, 0x61, 0x74], {offset: 4}) || // `mdat` MJPEG + check([0x6D, 0x6F, 0x6F, 0x76], {offset: 4}) || // `moov` + check([0x77, 0x69, 0x64, 0x65], {offset: 4}) // `wide` + ) { return { - ext: 'mp4', - mime: 'video/mp4' + ext: 'mov', + mime: 'video/quicktime' }; } + // File Type Box (https://en.wikipedia.org/wiki/ISO_base_media_file_format) + // It's not required to be first, but it's recommended to be. Almost all ISO base media files start with `ftyp` box. + // `ftyp` box must contain a brand major identifier, which must consist of ISO 8859-1 printable characters. + // Here we check for 8859-1 printable characters (for simplicity, it's a mask which also catches one non-printable character). + if ( + check([0x66, 0x74, 0x79, 0x70], {offset: 4}) && // `ftyp` + (buffer[8] & 0x60) !== 0x00 && (buffer[9] & 0x60) !== 0x00 && (buffer[10] & 0x60) !== 0x00 && (buffer[11] & 0x60) !== 0x00 // Brand major + ) { + // They all can have MIME `video/mp4` except `application/mp4` special-case which is hard to detect. + // For some cases, we're specific, everything else falls to `video/mp4` with `mp4` extension. + const brandMajor = buffer.toString('utf8', 8, 12); + switch (brandMajor) { + case 'mif1': + return {ext: 'heic', mime: 'image/heif'}; + case 'msf1': + return {ext: 'heic', mime: 'image/heif-sequence'}; + case 'heic': case 'heix': + return {ext: 'heic', mime: 'image/heic'}; + case 'hevc': case 'hevx': + return {ext: 'heic', mime: 'image/heic-sequence'}; + case 'qt ': + return {ext: 'mov', mime: 'video/quicktime'}; + case 'M4V ': case 'M4VH': case 'M4VP': + return {ext: 'm4v', mime: 'video/x-m4v'}; + case 'M4P ': + return {ext: 'm4p', mime: 'video/mp4'}; + case 'M4B ': + return {ext: 'm4b', mime: 'audio/mp4'}; + case 'M4A ': + return {ext: 'm4a', mime: 'audio/x-m4a'}; + case 'F4V ': + return {ext: 'f4v', mime: 'video/mp4'}; + case 'F4P ': + return {ext: 'f4p', mime: 'video/mp4'}; + case 'F4A ': + return {ext: 'f4a', mime: 'audio/mp4'}; + case 'F4B ': + return {ext: 'f4b', mime: 'audio/mp4'}; + default: + if (brandMajor.startsWith('3g')) { + if (brandMajor.startsWith('3g2')) { + return {ext: '3g2', mime: 'video/3gpp2'}; + } + + return {ext: '3gp', mime: 'video/3gpp'}; + } + + return {ext: 'mp4', mime: 'video/mp4'}; + } + } + if (check([0x4D, 0x54, 0x68, 0x64])) { return { ext: 'mid', @@ -317,18 +362,6 @@ const fileType = input => { } } - if (check([0x0, 0x0, 0x0, 0x14, 0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20]) || - check([0x66, 0x72, 0x65, 0x65], {offset: 4}) || // Type: `free` - check([0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20], {offset: 4}) || - check([0x6D, 0x64, 0x61, 0x74], {offset: 4}) || // MJPEG - check([0x6D, 0x6F, 0x6F, 0x76], {offset: 4}) || // Type: `moov` - check([0x77, 0x69, 0x64, 0x65], {offset: 4})) { - return { - ext: 'mov', - mime: 'video/quicktime' - }; - } - // RIFF file format which might be AVI, WAV, QCP, etc if (check([0x52, 0x49, 0x46, 0x46])) { if (check([0x41, 0x56, 0x49], {offset: 8})) { @@ -402,13 +435,6 @@ const fileType = input => { }; } - if (check([0x66, 0x74, 0x79, 0x70, 0x33, 0x67], {offset: 4})) { - return { - ext: '3gp', - mime: 'video/3gpp' - }; - } - // Check for MPEG header at different starting offsets for (let start = 0; start < 2 && start < (buffer.length - 16); start++) { if ( @@ -449,15 +475,6 @@ const fileType = input => { } } - if ( - check([0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x41], {offset: 4}) - ) { - return { // MPEG-4 layer 3 (audio) - ext: 'm4a', - mime: 'audio/mp4' // RFC 4337 - }; - } - // Needs to be before `ogg` check if (check([0x4F, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64], {offset: 28})) { return { @@ -830,37 +847,6 @@ const fileType = input => { }; } - // File Type Box (https://en.wikipedia.org/wiki/ISO_base_media_file_format) - if (check([0x66, 0x74, 0x79, 0x70], {offset: 4})) { - if (check([0x6D, 0x69, 0x66, 0x31], {offset: 8})) { - return { - ext: 'heic', - mime: 'image/heif' - }; - } - - if (check([0x6D, 0x73, 0x66, 0x31], {offset: 8})) { - return { - ext: 'heic', - mime: 'image/heif-sequence' - }; - } - - if (check([0x68, 0x65, 0x69, 0x63], {offset: 8}) || check([0x68, 0x65, 0x69, 0x78], {offset: 8})) { - return { - ext: 'heic', - mime: 'image/heic' - }; - } - - if (check([0x68, 0x65, 0x76, 0x63], {offset: 8}) || check([0x68, 0x65, 0x76, 0x78], {offset: 8})) { - return { - ext: 'heic', - mime: 'image/heic-sequence' - }; - } - } - if (check([0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A])) { return { ext: 'ktx', diff --git a/package.json b/package.json index ebfd6e60..01c98800 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "7z", "dmg", "mp4", - "m4v", "mid", "mkv", "webm", @@ -124,7 +123,16 @@ "lnk", "alias", "voc", - "ac3" + "ac3", + "3g2", + "m4a", + "m4b", + "m4p", + "m4v", + "f4a", + "f4b", + "f4p", + "f4v" ], "devDependencies": { "@types/node": "^11.12.2", diff --git a/readme.md b/readme.md index f6859d82..45bb94fa 100644 --- a/readme.md +++ b/readme.md @@ -142,7 +142,6 @@ Type: [`stream.Readable`](https://nodejs.org/api/stream.html#stream_class_stream - [`7z`](https://en.wikipedia.org/wiki/7z) - [`dmg`](https://en.wikipedia.org/wiki/Apple_Disk_Image) - [`mp4`](https://en.wikipedia.org/wiki/MPEG-4_Part_14#Filename_extensions) -- [`m4v`](https://en.wikipedia.org/wiki/M4V) - [`mid`](https://en.wikipedia.org/wiki/MIDI) - [`mkv`](https://en.wikipedia.org/wiki/Matroska) - [`webm`](https://en.wikipedia.org/wiki/WebM) @@ -193,7 +192,6 @@ Type: [`stream.Readable`](https://nodejs.org/api/stream.html#stream_class_stream - [`docx`](https://en.wikipedia.org/wiki/Office_Open_XML) - [`pptx`](https://en.wikipedia.org/wiki/Office_Open_XML) - [`xlsx`](https://en.wikipedia.org/wiki/Office_Open_XML) -- [`3gp`](https://en.wikipedia.org/wiki/3GP_and_3G2) - [`jp2`](https://en.wikipedia.org/wiki/JPEG_2000) - JPEG 2000 - [`jpm`](https://en.wikipedia.org/wiki/JPEG_2000) - JPEG 2000 - [`jpx`](https://en.wikipedia.org/wiki/JPEG_2000) - JPEG 2000 @@ -221,6 +219,16 @@ 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 +- [`3gp`](https://en.wikipedia.org/wiki/3GP_and_3G2#3GP) - Multimedia container format defined by the Third Generation Partnership Project (3GPP) for 3G UMTS multimedia services +- [`3g2`](https://en.wikipedia.org/wiki/3GP_and_3G2#3G2) - Multimedia container format defined by the 3GPP2 for 3G CDMA2000 multimedia services +- [`m4v`](https://en.wikipedia.org/wiki/M4V) - MPEG-4 Visual bitstreams +- [`m4p`](https://en.wikipedia.org/wiki/MPEG-4_Part_14#Filename_extensions) - MPEG-4 files with audio streams encrypted by FairPlay Digital Rights Management as were sold through the iTunes Store +- [`m4a`](https://en.wikipedia.org/wiki/M4A) - Audio-only MPEG-4 files +- [`m4b`](https://en.wikipedia.org/wiki/M4B) - Audiobook and podcast MPEG-4 files, which also contain metadata including chapter markers, images, and hyperlinks +- [`f4v`](https://en.wikipedia.org/wiki/Flash_Video) - ISO base media file format used by Adobe Flash Player +- [`f4p`](https://en.wikipedia.org/wiki/Flash_Video) - ISO base media file format protected by Adobe Access DRM used by Adobe Flash Player +- [`f4a`](https://en.wikipedia.org/wiki/Flash_Video) - Audio-only ISO base media file format used by Adobe Flash Player +- [`f4b`](https://en.wikipedia.org/wiki/Flash_Video) - Audiobook and podcast ISO base media file format used by Adobe Flash Player *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..43cadbd8 100644 --- a/test.js +++ b/test.js @@ -82,6 +82,7 @@ const types = [ 'pptx', 'xlsx', '3gp', + '3g2', 'jp2', 'jpm', 'jpx', @@ -108,7 +109,14 @@ const types = [ 'lnk', 'alias', 'voc', - 'ac3' + 'ac3', + 'm4v', + 'm4p', + 'm4b', + 'f4v', + 'f4p', + 'f4b', + 'f4a' ]; // Define an entry here only if the fixture has a different @@ -152,7 +160,6 @@ const names = { 'fixture-isom', 'fixture-isomv2', 'fixture-mp4v2', - 'fixture-m4v', 'fixture-dash', 'fixture-aac-adts' ],