UNPKG

mp4box

Version:

JavaScript version of GPAC's MP4Box tool

411 lines (385 loc) 14.6 kB
/** * MultiBufferStream is a class that acts as a SimpleStream for parsing * It holds several, possibly non-contiguous ArrayBuffer objects, each with a fileStart property * containing the offset for the buffer data in an original/virtual file * * It inherits also from DataStream for all read/write/alloc operations */ /** * Constructor */ var MultiBufferStream = function(buffer) { /* List of ArrayBuffers, with a fileStart property, sorted in fileStart order and non overlapping */ this.buffers = []; this.bufferIndex = -1; if (buffer) { this.insertBuffer(buffer); this.bufferIndex = 0; } } MultiBufferStream.prototype = new DataStream(new ArrayBuffer(), 0, DataStream.BIG_ENDIAN); /************************************************************************************ Methods for the managnement of the buffers (insertion, removal, concatenation, ...) ***********************************************************************************/ MultiBufferStream.prototype.initialized = function() { var firstBuffer; if (this.bufferIndex > -1) { return true; } else if (this.buffers.length > 0) { firstBuffer = this.buffers[0]; if (firstBuffer.fileStart === 0) { this.buffer = firstBuffer; this.bufferIndex = 0; Log.debug("MultiBufferStream", "Stream ready for parsing"); return true; } else { Log.warn("MultiBufferStream", "The first buffer should have a fileStart of 0"); this.logBufferLevel(); return false; } } else { Log.warn("MultiBufferStream", "No buffer to start parsing from"); this.logBufferLevel(); return false; } } /** * helper functions to concatenate two ArrayBuffer objects * @param {ArrayBuffer} buffer1 * @param {ArrayBuffer} buffer2 * @return {ArrayBuffer} the concatenation of buffer1 and buffer2 in that order */ ArrayBuffer.concat = function(buffer1, buffer2) { Log.debug("ArrayBuffer", "Trying to create a new buffer of size: "+(buffer1.byteLength + buffer2.byteLength)); var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); tmp.set(new Uint8Array(buffer1), 0); tmp.set(new Uint8Array(buffer2), buffer1.byteLength); return tmp.buffer; }; /** * Reduces the size of a given buffer, but taking the part between offset and offset+newlength * @param {ArrayBuffer} buffer * @param {Number} offset the start of new buffer * @param {Number} newLength the length of the new buffer * @return {ArrayBuffer} the new buffer */ MultiBufferStream.prototype.reduceBuffer = function(buffer, offset, newLength) { var smallB; smallB = new Uint8Array(newLength); smallB.set(new Uint8Array(buffer, offset, newLength)); smallB.buffer.fileStart = buffer.fileStart+offset; smallB.buffer.usedBytes = 0; return smallB.buffer; } /** * Inserts the new buffer in the sorted list of buffers, * making sure, it is not overlapping with existing ones (possibly reducing its size). * if the new buffer overrides/replaces the 0-th buffer (for instance because it is bigger), * updates the DataStream buffer for parsing */ MultiBufferStream.prototype.insertBuffer = function(ab) { var to_add = true; /* TODO: improve insertion if many buffers */ for (var i = 0; i < this.buffers.length; i++) { var b = this.buffers[i]; if (ab.fileStart <= b.fileStart) { /* the insertion position is found */ if (ab.fileStart === b.fileStart) { /* The new buffer overlaps with an existing buffer */ if (ab.byteLength > b.byteLength) { /* the new buffer is bigger than the existing one remove the existing buffer and try again to insert the new buffer to check overlap with the next ones */ this.buffers.splice(i, 1); i--; continue; } else { /* the new buffer is smaller than the existing one, just drop it */ Log.warn("MultiBufferStream", "Buffer (fileStart: "+ab.fileStart+" - Length: "+ab.byteLength+") already appended, ignoring"); } } else { /* The beginning of the new buffer is not overlapping with an existing buffer let's check the end of it */ if (ab.fileStart + ab.byteLength <= b.fileStart) { /* no overlap, we can add it as is */ } else { /* There is some overlap, cut the new buffer short, and add it*/ ab = this.reduceBuffer(ab, 0, b.fileStart - ab.fileStart); } Log.debug("MultiBufferStream", "Appending new buffer (fileStart: "+ab.fileStart+" - Length: "+ab.byteLength+")"); this.buffers.splice(i, 0, ab); /* if this new buffer is inserted in the first place in the list of the buffer, and the DataStream is initialized, make it the buffer used for parsing */ if (i === 0) { this.buffer = ab; } } to_add = false; break; } else if (ab.fileStart < b.fileStart + b.byteLength) { /* the new buffer overlaps its beginning with the end of the current buffer */ var offset = b.fileStart + b.byteLength - ab.fileStart; var newLength = ab.byteLength - offset; if (newLength > 0) { /* the new buffer is bigger than the current overlap, drop the overlapping part and try again inserting the remaining buffer */ ab = this.reduceBuffer(ab, offset, newLength); } else { /* the content of the new buffer is entirely contained in the existing buffer, drop it entirely */ to_add = false; break; } } } /* if the buffer has not been added, we can add it at the end */ if (to_add) { Log.debug("MultiBufferStream", "Appending new buffer (fileStart: "+ab.fileStart+" - Length: "+ab.byteLength+")"); this.buffers.push(ab); /* if this new buffer is inserted in the first place in the list of the buffer, and the DataStream is initialized, make it the buffer used for parsing */ if (i === 0) { this.buffer = ab; } } } /** * Displays the status of the buffers (number and used bytes) * @param {Object} info callback method for display */ MultiBufferStream.prototype.logBufferLevel = function(info) { var i; var buffer; var used, total; var ranges = []; var range; var bufferedString = ""; used = 0; total = 0; for (i = 0; i < this.buffers.length; i++) { buffer = this.buffers[i]; if (i === 0) { range = {}; ranges.push(range); range.start = buffer.fileStart; range.end = buffer.fileStart+buffer.byteLength; bufferedString += "["+range.start+"-"; } else if (range.end === buffer.fileStart) { range.end = buffer.fileStart+buffer.byteLength; } else { range = {}; range.start = buffer.fileStart; bufferedString += (ranges[ranges.length-1].end-1)+"], ["+range.start+"-"; range.end = buffer.fileStart+buffer.byteLength; ranges.push(range); } used += buffer.usedBytes; total += buffer.byteLength; } if (ranges.length > 0) { bufferedString += (range.end-1)+"]"; } var log = (info ? Log.info : Log.debug) if (this.buffers.length === 0) { log("MultiBufferStream", "No more buffer in memory"); } else { log("MultiBufferStream", ""+this.buffers.length+" stored buffer(s) ("+used+"/"+total+" bytes), continuous ranges: "+bufferedString); } } MultiBufferStream.prototype.cleanBuffers = function () { var i; var buffer; for (i = 0; i < this.buffers.length; i++) { buffer = this.buffers[i]; if (buffer.usedBytes === buffer.byteLength) { Log.debug("MultiBufferStream", "Removing buffer #"+i); this.buffers.splice(i, 1); i--; } } } MultiBufferStream.prototype.mergeNextBuffer = function() { var next_buffer; if (this.bufferIndex+1 < this.buffers.length) { next_buffer = this.buffers[this.bufferIndex+1]; if (next_buffer.fileStart === this.buffer.fileStart + this.buffer.byteLength) { var oldLength = this.buffer.byteLength; var oldUsedBytes = this.buffer.usedBytes; var oldFileStart = this.buffer.fileStart; this.buffers[this.bufferIndex] = ArrayBuffer.concat(this.buffer, next_buffer); this.buffer = this.buffers[this.bufferIndex]; this.buffers.splice(this.bufferIndex+1, 1); this.buffer.usedBytes = oldUsedBytes; /* TODO: should it be += ? */ this.buffer.fileStart = oldFileStart; Log.debug("ISOFile", "Concatenating buffer for box parsing (length: "+oldLength+"->"+this.buffer.byteLength+")"); return true; } else { return false; } } else { return false; } } /************************************************************************* Seek-related functions *************************************************************************/ /** * Finds the buffer that holds the given file position * @param {Boolean} fromStart indicates if the search should start from the current buffer (false) * or from the first buffer (true) * @param {Number} filePosition position in the file to seek to * @param {Boolean} markAsUsed indicates if the bytes in between the current position and the seek position * should be marked as used for garbage collection * @return {Number} the index of the buffer holding the seeked file position, -1 if not found. */ MultiBufferStream.prototype.findPosition = function(fromStart, filePosition, markAsUsed) { var i; var abuffer = null; var index = -1; /* find the buffer with the largest position smaller than the given position */ if (fromStart === true) { /* the reposition can be in the past, we need to check from the beginning of the list of buffers */ i = 0; } else { i = this.bufferIndex; } while (i < this.buffers.length) { abuffer = this.buffers[i]; if (abuffer.fileStart <= filePosition) { index = i; if (markAsUsed) { if (abuffer.fileStart + abuffer.byteLength <= filePosition) { abuffer.usedBytes = abuffer.byteLength; } else { abuffer.usedBytes = filePosition - abuffer.fileStart; } this.logBufferLevel(); } } else { break; } i++; } if (index !== -1) { abuffer = this.buffers[index]; if (abuffer.fileStart + abuffer.byteLength >= filePosition) { Log.debug("MultiBufferStream", "Found position in existing buffer #"+index); return index; } else { return -1; } } else { return -1; } } /** * Finds the largest file position contained in a buffer or in the next buffers if they are contiguous (no gap) * starting from the given buffer index or from the current buffer if the index is not given * * @param {Number} inputindex Index of the buffer to start from * @return {Number} The largest file position found in the buffers */ MultiBufferStream.prototype.findEndContiguousBuf = function(inputindex) { var i; var currentBuf; var nextBuf; var index = (inputindex !== undefined ? inputindex : this.bufferIndex); currentBuf = this.buffers[index]; /* find the end of the contiguous range of data */ if (this.buffers.length > index+1) { for (i = index+1; i < this.buffers.length; i++) { nextBuf = this.buffers[i]; if (nextBuf.fileStart === currentBuf.fileStart + currentBuf.byteLength) { currentBuf = nextBuf; } else { break; } } } /* return the position of last byte in the file that we have */ return currentBuf.fileStart + currentBuf.byteLength; } /** * Returns the largest file position contained in the buffers, larger than the given position * @param {Number} pos the file position to start from * @return {Number} the largest position in the current buffer or in the buffer and the next contiguous * buffer that holds the given position */ MultiBufferStream.prototype.getEndFilePositionAfter = function(pos) { var index = this.findPosition(true, pos, false); if (index !== -1) { return this.findEndContiguousBuf(index); } else { return pos; } } /************************************************************************* Garbage collection related functions *************************************************************************/ /** * Marks a given number of bytes as used in the current buffer for garbage collection * @param {Number} nbBytes */ MultiBufferStream.prototype.addUsedBytes = function(nbBytes) { this.buffer.usedBytes += nbBytes; this.logBufferLevel(); } /** * Marks the entire current buffer as used, ready for garbage collection */ MultiBufferStream.prototype.setAllUsedBytes = function() { this.buffer.usedBytes = this.buffer.byteLength; this.logBufferLevel(); } /************************************************************************* Common API between MultiBufferStream and SimpleStream *************************************************************************/ /** * Tries to seek to a given file position * if possible, repositions the parsing from there and returns true * if not possible, does not change anything and returns false * @param {Number} filePosition position in the file to seek to * @param {Boolean} fromStart indicates if the search should start from the current buffer (false) * or from the first buffer (true) * @param {Boolean} markAsUsed indicates if the bytes in between the current position and the seek position * should be marked as used for garbage collection * @return {Boolean} true if the seek succeeded, false otherwise */ MultiBufferStream.prototype.seek = function(filePosition, fromStart, markAsUsed) { var index; index = this.findPosition(fromStart, filePosition, markAsUsed); if (index !== -1) { this.buffer = this.buffers[index]; this.bufferIndex = index; this.position = filePosition - this.buffer.fileStart; Log.debug("MultiBufferStream", "Repositioning parser at buffer position: "+this.position); return true; } else { Log.debug("MultiBufferStream", "Position "+filePosition+" not found in buffered data"); return false; } } /** * Returns the current position in the file * @return {Number} the position in the file */ MultiBufferStream.prototype.getPosition = function() { if (this.bufferIndex === -1 || this.buffers[this.bufferIndex] === null) { throw "Error accessing position in the MultiBufferStream"; } return this.buffers[this.bufferIndex].fileStart+this.position; } /** * Returns the length of the current buffer * @return {Number} the length of the current buffer */ MultiBufferStream.prototype.getLength = function() { return this.byteLength; } MultiBufferStream.prototype.getEndPosition = function() { if (this.bufferIndex === -1 || this.buffers[this.bufferIndex] === null) { throw "Error accessing position in the MultiBufferStream"; } return this.buffers[this.bufferIndex].fileStart+this.byteLength; } if (typeof exports !== 'undefined') { exports.MultiBufferStream = MultiBufferStream; }