webm-duration-fix
Version:
based on ts-ebml and support large file(than 2GB) and optimize memory usage during repair
265 lines (243 loc) • 6.75 kB
text/typescript
import { Buffer } from "./tools";
export default class Tools {
/**
* read variable length integer per
* https://www.matroska.org/technical/specs/index.html#EBML_ex
* @static
* @param {Buffer} buffer containing input
* @param {Number} [start=0] position in buffer
* @returns {{length: Number, value: number}} value / length object
*/
static readVint(buffer, start = 0) {
const length = 8 - Math.floor(Math.log2(buffer[start]));
if (length > 8) {
const number = Tools.readHexString(buffer, start, start + length);
throw new Error(`Unrepresentable length: ${length} ${number}`);
}
if (start + length > buffer.length) {
return null;
}
let value = buffer[start] & ((1 << (8 - length)) - 1);
for (let i = 1; i < length; i += 1) {
if (i === 7) {
if (value >= 2 ** 8 && buffer[start + 7] > 0) {
return { length, value: -1 };
}
}
value *= 2 ** 8;
value += buffer[start + i];
}
return { length, value };
}
/**
* write variable length integer
* @static
* @param {Number} value to store into buffer
* @returns {Buffer} containing the value
*/
static writeVint(value) {
if (value < 0 || value > 2 ** 53) {
throw new Error(`Unrepresentable value: ${value}`);
}
let length = 1;
for (length = 1; length <= 8; length += 1) {
if (value < 2 ** (7 * length) - 1) {
break;
}
}
const buffer = Buffer.alloc(length);
let val = value;
for (let i = 1; i <= length; i += 1) {
const b = val & 0xff;
buffer[length - i] = b;
val -= b;
val /= 2 ** 8;
}
buffer[0] |= 1 << (8 - length);
return buffer;
}
/**
* *
* concatenate two arrays of bytes
* @static
* @param {Buffer} a1 First array
* @param {Buffer} a2 Second array
* @returns {Buffer} concatenated arrays
*/
static concatenate(a1, a2) {
// both null or undefined
if (!a1 && !a2) {
return Buffer.from([]);
}
if (!a1 || a1.byteLength === 0) {
return a2;
}
if (!a2 || a2.byteLength === 0) {
return a1;
}
return Buffer.from([...a1, ...a2]);
}
/**
* get a hex text string from Buff[start,end)
* @param {Buffer} buff from which to read the string
* @param {Number} [start=0] starting point (default 0)
* @param {Number} [end=buff.byteLength] ending point (default the whole buffer)
* @returns {string} the hex string
*/
static readHexString(buff, start = 0, end = buff.byteLength) {
return Array.from(buff.slice(start, end))
.map(q => Number(q).toString(16))
.reduce((acc, current) => `${acc}${current.padStart(2, '0')}`, '');
}
/**
* tries to read out a UTF-8 encoded string
* @param {Buffer} buff the buffer to attempt to read from
* @return {string|null} the decoded text, or null if unable to
*/
static readUtf8(buff) {
try {
return Buffer.from(buff).toString('utf8');
} catch (exception) {
return null;
}
}
/**
* get an unsigned number from a buffer
* @param {Buffer} buff from which to read variable-length unsigned number
* @returns {number|string} result (in hex for lengths > 6)
*/
static readUnsigned(buff) {
const b = new DataView(buff.buffer, buff.byteOffset, buff.byteLength);
switch (buff.byteLength) {
case 1:
return b.getUint8(0);
case 2:
return b.getUint16(0);
case 4:
return b.getUint32(0);
default:
break;
}
if (buff.byteLength <= 6) {
return buff.reduce((acc, current) => acc * 256 + current, 0);
}
return Tools.readHexString(buff, 0, buff.byteLength);
}
/**
* get an signed number from a buffer
* @static
* @param {Buffer} buff from which to read variable-length signed number
* @returns {number} result
*/
static readSigned(buff) {
const b = new DataView(buff.buffer, buff.byteOffset, buff.byteLength);
switch (buff.byteLength) {
case 1:
return b.getInt8(0);
case 2:
return b.getInt16(0);
case 4:
return b.getInt32(0);
default:
return NaN;
}
}
/**
* get an floating-point number from a buffer
* @static
* @param {Buffer} buff from which to read variable-length floating-point number
* @returns {number} result
*/
static readFloat(buff) {
const b = new DataView(buff.buffer, buff.byteOffset, buff.byteLength);
switch (buff.byteLength) {
case 4:
return b.getFloat32(0);
case 8:
return b.getFloat64(0);
default:
return NaN;
}
}
/**
* get a date from a buffer
* @static
* @param {Buffer} buff from which to read the date
* @return {Date} result
*/
static readDate(buff) {
const b = new DataView(buff.buffer, buff.byteOffset, buff.byteLength);
switch (buff.byteLength) {
case 1:
return new Date(b.getUint8(0));
case 2:
return new Date(b.getUint16(0));
case 4:
return new Date(b.getUint32(0));
case 8:
return new Date(Number.parseInt(Tools.readHexString(buff), 16));
default:
return new Date(0);
}
}
/**
* Reads the data from a tag
* @static
* @param {TagData} tagObj The tag object to be read
* @param {Buffer} data Data to be transformed
* @return {Tag} result
*/
static readDataFromTag(tagObj, data) {
const { type, name } = tagObj;
let { track } = tagObj;
let discardable = tagObj.discardable || false;
let keyframe = tagObj.keyframe || false;
let payload = null;
let value;
switch (type) {
case 'u':
value = Tools.readUnsigned(data);
break;
case 'f':
value = Tools.readFloat(data);
break;
case 'i':
value = Tools.readSigned(data);
break;
case 's':
value = String.fromCharCode(...data);
break;
case '8':
value = Tools.readUtf8(data);
break;
case 'd':
value = Tools.readDate(data);
break;
default:
break;
}
if (name === 'SimpleBlock' || name === 'Block') {
let p = 0;
const { length, value: trak } = Tools.readVint(data, p) as any;
p += length;
track = trak;
value = Tools.readSigned(data.subarray(p, p + 2));
p += 2;
if (name === 'SimpleBlock') {
keyframe = Boolean(data[length + 2] & 0x80);
discardable = Boolean(data[length + 2] & 0x01);
}
p += 1;
payload = data.subarray(p);
}
return {
...tagObj,
data,
discardable,
keyframe,
payload,
track,
value,
};
}
}