UNPKG

mp4-metadata

Version:

Fast mp4 moov metadata parsing via optimized file streaming

282 lines (233 loc) 9.73 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); /** * Converts a file or blob into an unsigned integers of 8 bytes array. * @param {(File | Blob)} file * @return {Uint8Array} */ var FileBlobToUint8Array = function FileBlobToUint8Array(file) { return new Promise(function (resolve, reject) { var reader = new FileReader(); reader.onload = function () { resolve(new Uint8Array(reader.result)); }; reader.onerror = function (e) { reject(e); }; reader.readAsArrayBuffer(file); }); }; /** * seconds in Mac HFS+ to be converted into a date time iso string * @param {Number} seconds - Mac HFS+ seconds * @return {String} date time iso string */ var macHFSPlusToISOString = function macHFSPlusToISOString(seconds) { // offset seconds and multipled by 1000 to use seconds in javascript date. return new Date((seconds - 2082844800) * 1000).toISOString(); }; /** * Convert an array of bytes into a hex string * @param {Uint8Array} byteArray * @param {String} hex version of the original byte array */ var toHexString = function toHexString(byteArray) { return Array.from(byteArray, function (_byte) { return ('0' + (_byte & 0xFF).toString(16)).slice(-2); }).join(''); }; /** * Converts a string of hex values into its decimal representation * @param {String} hexString * @param {Number} decimal version of the hex string */ var toDecimalFromHexString = function toDecimalFromHexString(hexString) { return parseInt(hexString, 16); }; /** * Convert an array of unsigned integers of 8 bytes into a string of chars. * This will convert each decimal value to a char and concat all the chars * together to form the string. * @param {Uint8Array} byteArray * @param {String} string representation of the original byte array */ var uint8ToCharCodeString = function uint8ToCharCodeString(byteArray) { return Array.from(byteArray, function (_byte2) { return String.fromCharCode(_byte2); }).join(''); }; /** * Reads the bytes after the mvhd str to parse the creation date only. * * 8+ bytes movie (presentation) header box * = long unsigned offset + long ASCII text string 'mvhd' * -> 1 byte version = 8-bit unsigned value * - if version is 1 then date and duration values are 8 bytes in length * -> 3 bytes flags = 24-bit hex flags (current = 0) * * -> 4 bytes created mac UTC date * = long unsigned value in seconds since beginning 1904 to 2040 * -> 4 bytes modified mac UTC date * = long unsigned value in seconds since beginning 1904 to 2040 * OR * -> 8 bytes created mac UTC date * = 64-bit unsigned value in seconds since beginning 1904 * -> 8 bytes modified mac UTC date * = 64-bit unsigned value in seconds since beginning 1904 * @param {Uint8Array} uInt8Chunk * @return {(String | null)} ISO Datetime string or null if no creation date * was found */ var parseBytesAfterMvhd = function parseBytesAfterMvhd(uInt8Chunk) { var version = toDecimalFromHexString(toHexString([uInt8Chunk[0]])); var seconds = null; if (version === 0) { // read the 4 byte creation time var start = 4; // 1 byte for version, 3 bytes of flags var end = start + 4; // offset from the start for the 4 bytes of creation time var createdBytes = uInt8Chunk.slice(start, end); seconds = toDecimalFromHexString(toHexString(createdBytes)); } else if (version === 1) { // read the 8 creation time var _start = 4; // 1 byte for version, 3 bytes of flags var _end = _start + 8; // offset from the start for the 8 bytes of creation time var _createdBytes = uInt8Chunk.slice(_start, _end); seconds = toDecimalFromHexString(toHexString(_createdBytes)); } return seconds === null ? null : macHFSPlusToISOString(seconds); }; /** * Reads a file backwards to find the index of the str you are searching for * i.g. you can search for moov, mvhd, etc... * @param {(File | Blob)} file * @param {String} str - String contents you are trying to search in the file * @param {Number} defaultChunkSize chunk size to read in bytes * @param {Number} maxBytesRead number of bytes to read before killing * @return {Number} globalFileIndex the global file index were the str begins in * the original file */ var readFileByChunksBackwardsForStringIndex = /*#__PURE__*/function () { var _ref = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(file, str, defaultChunkSize, maxBytesRead) { var chunkSize, iteration, start, end, indexOfStr, globalFileIndex, strLengthEdgeCase, chunk, uint8Chunk, charCodeString; return _regenerator["default"].wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: chunkSize = file.size < defaultChunkSize ? file.size : defaultChunkSize; iteration = 0; start = null; end = file.size - chunkSize * iteration; indexOfStr = null; globalFileIndex = null; strLengthEdgeCase = str.length - 1; // read backwards case 7: if (!(end > 0 && iteration * chunkSize < maxBytesRead)) { _context.next = 23; break; } start = file.size - chunkSize * (iteration + 1); start = start < 0 ? 0 : start; end = file.size - chunkSize * iteration; // since we are reading by a random chunk size we have a few edge conditions to worry about // chunk bounds chunk = file.slice(start, end + strLengthEdgeCase); _context.next = 14; return FileBlobToUint8Array(chunk); case 14: uint8Chunk = _context.sent; charCodeString = uint8ToCharCodeString(uint8Chunk); indexOfStr = charCodeString.indexOf(str); if (!(indexOfStr !== -1)) { _context.next = 20; break; } // Since we are reading backwards from the end of the file read, compute // the global file index of where the str begins globalFileIndex = file.size - (iteration * chunkSize + (chunkSize - 1 - indexOfStr)) - 1; return _context.abrupt("break", 23); case 20: iteration++; _context.next = 7; break; case 23: return _context.abrupt("return", globalFileIndex); case 24: case "end": return _context.stop(); } } }, _callee); })); return function readFileByChunksBackwardsForStringIndex(_x, _x2, _x3, _x4) { return _ref.apply(this, arguments); }; }(); /** * Retrieves the creation time of the mp4 video * @param {(File | Blob)} file * @param options configuration options for the parser * @param options.defaultChunkSize the default chunk size to parse per iteration in bytes * @param options.maxBytesRead the amount of bytes the parser will read before stopping. Make this the file size to read the entire file. * @return {(String | null)} a date time iso string of the creation time or null */ var getCreationTime = /*#__PURE__*/function () { var _ref2 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(file) { var options, defaultChunkSize, maxBytesRead, globalFileIndex, parsingIndex, fileChunk, uInt8Chunk, creationTime, _args2 = arguments; return _regenerator["default"].wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: options = _args2.length > 1 && _args2[1] !== undefined ? _args2[1] : {}; defaultChunkSize = options.defaultChunkSize ? options.defaultChunkSize : 100000; maxBytesRead = options.maxBytesRead ? options.maxBytesRead : 100000000; _context2.next = 5; return readFileByChunksBackwardsForStringIndex(file, 'mvhd', defaultChunkSize, maxBytesRead); case 5: globalFileIndex = _context2.sent; if (globalFileIndex) { _context2.next = 8; break; } return _context2.abrupt("return", null); case 8: // global file index points the start of m in mvhd, lets move it 4 bytes past this str name // now it will point at the version parsingIndex = globalFileIndex + 4; // move it 12 bytes ahead to parse either version of the creation time // 4 for version + flags // 8 for the creation time // 12 in total fileChunk = file.slice(parsingIndex, globalFileIndex + 12); _context2.next = 12; return FileBlobToUint8Array(fileChunk); case 12: uInt8Chunk = _context2.sent; creationTime = parseBytesAfterMvhd(uInt8Chunk); return _context2.abrupt("return", creationTime); case 15: case "end": return _context2.stop(); } } }, _callee2); })); return function getCreationTime(_x5) { return _ref2.apply(this, arguments); }; }(); var _default = { getCreationTime: getCreationTime }; exports["default"] = _default;