mp4-metadata
Version:
Fast mp4 moov metadata parsing via optimized file streaming
282 lines (233 loc) • 9.73 kB
JavaScript
;
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;