jsmediatags
Version:
Media Tags Reader (ID3, MP4)
222 lines (181 loc) • 7.86 kB
JavaScript
/**
* 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;