UNPKG

jsmediatags

Version:
222 lines (181 loc) 7.86 kB
/** * This class represents a file that might not have all its data loaded yet. * It is used when loading the entire file is not an option because it's too * expensive. Instead, parts of the file are loaded and added only when needed. * From a reading point of view is as if the entire file is loaded. The * exception is when the data is not available yet, an error will be thrown. * This class does not load the data, it just manages it. It provides operations * to add and read data from the file. * * */ 'use strict'; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } var NOT_FOUND = -1; var ChunkedFileData = /*#__PURE__*/function () { function ChunkedFileData() { _classCallCheck(this, ChunkedFileData); _defineProperty(this, "_fileData", void 0); this._fileData = []; } /** * Adds data to the file storage at a specific offset. */ _createClass(ChunkedFileData, [{ key: "addData", value: function addData(offset, data) { var offsetEnd = offset + data.length - 1; var chunkRange = this._getChunkRange(offset, offsetEnd); if (chunkRange.startIx === NOT_FOUND) { this._fileData.splice(chunkRange.insertIx || 0, 0, { offset: offset, data: data }); } else { // If the data to add collides with existing chunks we prepend and // append data from the half colliding chunks to make the collision at // 100%. The new data can then replace all the colliding chunkes. var firstChunk = this._fileData[chunkRange.startIx]; var lastChunk = this._fileData[chunkRange.endIx]; var needsPrepend = offset > firstChunk.offset; var needsAppend = offsetEnd < lastChunk.offset + lastChunk.data.length - 1; var chunk = { offset: Math.min(offset, firstChunk.offset), data: data }; if (needsPrepend) { var slicedData = this._sliceData(firstChunk.data, 0, offset - firstChunk.offset); chunk.data = this._concatData(slicedData, data); } if (needsAppend) { // Use the lastChunk because the slice logic is easier to handle. var slicedData = this._sliceData(chunk.data, 0, lastChunk.offset - chunk.offset); chunk.data = this._concatData(slicedData, lastChunk.data); } this._fileData.splice(chunkRange.startIx, chunkRange.endIx - chunkRange.startIx + 1, chunk); } } }, { key: "_concatData", value: function _concatData(dataA, dataB) { // TypedArrays don't support concat. if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView && ArrayBuffer.isView(dataA)) { // $FlowIssue - flow thinks dataAandB is a string but it's not var dataAandB = new dataA.constructor(dataA.length + dataB.length); // $FlowIssue - flow thinks dataAandB is a string but it's not dataAandB.set(dataA, 0); // $FlowIssue - flow thinks dataAandB is a string but it's not dataAandB.set(dataB, dataA.length); return dataAandB; } else { // $FlowIssue - flow thinks dataAandB is a TypedArray but it's not return dataA.concat(dataB); } } }, { key: "_sliceData", value: function _sliceData(data, begin, end) { // Some TypeArray implementations do not support slice yet. if (data.slice) { return data.slice(begin, end); } else { // $FlowIssue - flow thinks data is a string but it's not return data.subarray(begin, end); } } /** * Finds the chunk range that overlaps the [offsetStart-1,offsetEnd+1] range. * When a chunk is adjacent to the offset we still consider it part of the * range (this is the situation of offsetStart-1 or offsetEnd+1). * When no chunks are found `insertIx` denotes the index where the data * should be inserted in the data list (startIx == NOT_FOUND and endIX == * NOT_FOUND). */ }, { key: "_getChunkRange", value: function _getChunkRange(offsetStart, offsetEnd) { var startChunkIx = NOT_FOUND; var endChunkIx = NOT_FOUND; var insertIx = 0; // Could use binary search but not expecting that many blocks to exist. for (var i = 0; i < this._fileData.length; i++, insertIx = i) { var chunkOffsetStart = this._fileData[i].offset; var chunkOffsetEnd = chunkOffsetStart + this._fileData[i].data.length; if (offsetEnd < chunkOffsetStart - 1) { // This offset range doesn't overlap with any chunks. break; } // If it is adjacent we still consider it part of the range because // we're going end up with a single block with all contiguous data. if (offsetStart <= chunkOffsetEnd + 1 && offsetEnd >= chunkOffsetStart - 1) { startChunkIx = i; break; } } // No starting chunk was found, meaning that the offset is either before // or after the current stored chunks. if (startChunkIx === NOT_FOUND) { return { startIx: NOT_FOUND, endIx: NOT_FOUND, insertIx: insertIx }; } // Find the ending chunk. for (var i = startChunkIx; i < this._fileData.length; i++) { var chunkOffsetStart = this._fileData[i].offset; var chunkOffsetEnd = chunkOffsetStart + this._fileData[i].data.length; if (offsetEnd >= chunkOffsetStart - 1) { // Candidate for the end chunk, it doesn't mean it is yet. endChunkIx = i; } if (offsetEnd <= chunkOffsetEnd + 1) { break; } } if (endChunkIx === NOT_FOUND) { endChunkIx = startChunkIx; } return { startIx: startChunkIx, endIx: endChunkIx }; } }, { key: "hasDataRange", value: function hasDataRange(offsetStart, offsetEnd) { for (var i = 0; i < this._fileData.length; i++) { var chunk = this._fileData[i]; if (offsetEnd < chunk.offset) { return false; } if (offsetStart >= chunk.offset && offsetEnd < chunk.offset + chunk.data.length) { return true; } } return false; } }, { key: "getByteAt", value: function getByteAt(offset) { var dataChunk; for (var i = 0; i < this._fileData.length; i++) { var dataChunkStart = this._fileData[i].offset; var dataChunkEnd = dataChunkStart + this._fileData[i].data.length - 1; if (offset >= dataChunkStart && offset <= dataChunkEnd) { dataChunk = this._fileData[i]; break; } } if (dataChunk) { return dataChunk.data[offset - dataChunk.offset]; } throw new Error("Offset " + offset + " hasn't been loaded yet."); } }], [{ key: "NOT_FOUND", get: // $FlowIssue - get/set properties not yet supported function get() { return NOT_FOUND; } }]); return ChunkedFileData; }(); module.exports = ChunkedFileData;