ebml.js 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/testing.yml +28 -0
- package/.husky/pre-commit +4 -0
- package/.prettierrc.json +4 -0
- package/.vscode/settings.json +19 -0
- package/LICENSE +17 -0
- package/README.md +154 -0
- package/dist/ebml.js +10452 -0
- package/dist/ebml.js.map +1 -0
- package/dist/ebml.min.js +12 -0
- package/dist/ebml.min.js.map +1 -0
- package/dist/ebml.min.node.js +2 -0
- package/dist/ebml.min.node.js.map +1 -0
- package/dist/ebml.node.js +4036 -0
- package/dist/ebml.node.js.map +1 -0
- package/example.js +14 -0
- package/media/test.webm +0 -0
- package/package.json +56 -0
- package/src/debug-log.js +12 -0
- package/src/decoder.js +301 -0
- package/src/decoder.test.js +158 -0
- package/src/encoder.js +246 -0
- package/src/encoder.test.js +206 -0
- package/src/index.js +11 -0
- package/src/schema.js +3023 -0
- package/src/tools.js +355 -0
- package/src/tools.test.js +738 -0
- package/src/types/schema.types.js +30 -0
- package/src/types/tag.types.js +25 -0
- package/streamExample.js +17 -0
package/src/tools.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
const schema = require("./schema");
|
|
2
|
+
|
|
3
|
+
class Tools {
|
|
4
|
+
/**
|
|
5
|
+
* read variable length integer per
|
|
6
|
+
* https://www.matroska.org/technical/specs/index.html#EBML_ex
|
|
7
|
+
* @static
|
|
8
|
+
* @param {Buffer} buffer containing input
|
|
9
|
+
* @param {Number} [start=0] position in buffer
|
|
10
|
+
* @returns {{length: Number, value: number}} value / length object
|
|
11
|
+
*/
|
|
12
|
+
static readVint(buffer, start = 0) {
|
|
13
|
+
// Calculate length based on leading zeros in the first byte
|
|
14
|
+
const firstByte = buffer[start];
|
|
15
|
+
|
|
16
|
+
// Find the first 1 bit to determine length (VINT_WIDTH)
|
|
17
|
+
let length = 1;
|
|
18
|
+
let mask = 0x80; // 10000000 binary
|
|
19
|
+
while ((firstByte & mask) === 0 && length <= 8) {
|
|
20
|
+
length++;
|
|
21
|
+
mask >>= 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (length > 8) {
|
|
25
|
+
const number = Tools.readHexString(buffer, start, start + length);
|
|
26
|
+
throw new Error(`Unrepresentable length: ${length} ${number}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (start + length > buffer.length) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if all data bits are 1 (indicating unknown/infinite size)
|
|
34
|
+
const dataMask = (1 << (8 - length)) - 1;
|
|
35
|
+
let value = firstByte & dataMask;
|
|
36
|
+
|
|
37
|
+
// Check for "all bits set" pattern (unknown size)
|
|
38
|
+
let allBitsSet = (firstByte & dataMask) === dataMask;
|
|
39
|
+
for (let i = 1; i < length && allBitsSet; i++) {
|
|
40
|
+
if (buffer[start + i] !== 0xFF) {
|
|
41
|
+
allBitsSet = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// If all data bits are 1, it represents unknown/infinite size
|
|
46
|
+
if (allBitsSet) {
|
|
47
|
+
return { length, value: -1 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Parse the remaining bytes
|
|
51
|
+
for (let i = 1; i < length; i++) {
|
|
52
|
+
value = (value << 8) | buffer[start + i];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { length, value };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* write variable length integer
|
|
60
|
+
* @static
|
|
61
|
+
* @param {Number} value to store into buffer
|
|
62
|
+
* @returns {Buffer} containing the value
|
|
63
|
+
*/
|
|
64
|
+
static writeVint(value, minLength = 1) {
|
|
65
|
+
if (value < 0 || value > 2 ** 53) {
|
|
66
|
+
throw new Error(`Unrepresentable value: ${value}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
minLength = typeof minLength === 'number' && !isNaN(minLength) ? Math.max(1, minLength) : 1;
|
|
70
|
+
minLength = Math.min(8, minLength);
|
|
71
|
+
let length = 1;
|
|
72
|
+
for (length = minLength; length <= 8; length += 1) {
|
|
73
|
+
if (value < 2 ** (7 * length) - 1) {
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const buffer = Buffer.alloc(length);
|
|
79
|
+
let val = value;
|
|
80
|
+
for (let i = 1; i <= length; i += 1) {
|
|
81
|
+
const b = val & 0xff;
|
|
82
|
+
buffer[length - i] = b;
|
|
83
|
+
val -= b;
|
|
84
|
+
val /= 2 ** 8;
|
|
85
|
+
}
|
|
86
|
+
buffer[0] |= 1 << (8 - length);
|
|
87
|
+
|
|
88
|
+
return buffer;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* *
|
|
93
|
+
* concatenate two arrays of bytes
|
|
94
|
+
* @static
|
|
95
|
+
* @param {Buffer} a1 First array
|
|
96
|
+
* @param {Buffer} a2 Second array
|
|
97
|
+
* @returns {Buffer} concatenated arrays
|
|
98
|
+
*/
|
|
99
|
+
static concatenate(a1, a2) {
|
|
100
|
+
// both null or undefined
|
|
101
|
+
if (!a1 && !a2) {
|
|
102
|
+
return Buffer.from([]);
|
|
103
|
+
}
|
|
104
|
+
if (!a1 || a1.byteLength === 0) {
|
|
105
|
+
return a2;
|
|
106
|
+
}
|
|
107
|
+
if (!a2 || a2.byteLength === 0) {
|
|
108
|
+
return a1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return Buffer.concat([a1, a2]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* get a hex text string from Buff[start,end)
|
|
116
|
+
* @param {Buffer} buff from which to read the string
|
|
117
|
+
* @param {Number} [start=0] starting point (default 0)
|
|
118
|
+
* @param {Number} [end=buff.byteLength] ending point (default the whole buffer)
|
|
119
|
+
* @returns {string} the hex string
|
|
120
|
+
*/
|
|
121
|
+
static readHexString(buff, start = 0, end = buff.byteLength) {
|
|
122
|
+
return Array.from(buff.slice(start, end))
|
|
123
|
+
.map(q => Number(q).toString(16))
|
|
124
|
+
.reduce((acc, current) => `${acc}${current.padStart(2, '0')}`, '');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* tries to read out a UTF-8 encoded string
|
|
129
|
+
* @param {Buffer} buff the buffer to attempt to read from
|
|
130
|
+
* @return {string|null} the decoded text, or null if unable to
|
|
131
|
+
*/
|
|
132
|
+
static readUtf8(buff) {
|
|
133
|
+
try {
|
|
134
|
+
return Buffer.from(buff).toString('utf8');
|
|
135
|
+
} catch (exception) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* get an unsigned number from a buffer
|
|
142
|
+
* @param {Buffer} buff from which to read variable-length unsigned number
|
|
143
|
+
* @returns {number|string} result (in hex for lengths > 6)
|
|
144
|
+
*/
|
|
145
|
+
static readUnsigned(buff) {
|
|
146
|
+
const b = new DataView(buff.buffer, buff.byteOffset, buff.byteLength);
|
|
147
|
+
switch (buff.byteLength) {
|
|
148
|
+
case 1:
|
|
149
|
+
return b.getUint8(0);
|
|
150
|
+
case 2:
|
|
151
|
+
return b.getUint16(0);
|
|
152
|
+
case 4:
|
|
153
|
+
return b.getUint32(0);
|
|
154
|
+
default:
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
if (buff.byteLength <= 6) {
|
|
158
|
+
return buff.reduce((acc, current) => acc * 256 + current, 0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return Tools.readHexString(buff, 0, buff.byteLength);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* get an signed number from a buffer
|
|
166
|
+
* @static
|
|
167
|
+
* @param {Buffer} buff from which to read variable-length signed number
|
|
168
|
+
* @returns {number} result
|
|
169
|
+
*/
|
|
170
|
+
static readSigned(buff) {
|
|
171
|
+
const b = new DataView(buff.buffer, buff.byteOffset, buff.byteLength);
|
|
172
|
+
switch (buff.byteLength) {
|
|
173
|
+
case 1:
|
|
174
|
+
return b.getInt8(0);
|
|
175
|
+
case 2:
|
|
176
|
+
return b.getInt16(0);
|
|
177
|
+
case 4:
|
|
178
|
+
return b.getInt32(0);
|
|
179
|
+
default:
|
|
180
|
+
return NaN;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* get an floating-point number from a buffer
|
|
186
|
+
* @static
|
|
187
|
+
* @param {Buffer} buff from which to read variable-length floating-point number
|
|
188
|
+
* @returns {number} result
|
|
189
|
+
*/
|
|
190
|
+
static readFloat(buff) {
|
|
191
|
+
const b = new DataView(buff.buffer, buff.byteOffset, buff.byteLength);
|
|
192
|
+
switch (buff.byteLength) {
|
|
193
|
+
case 4:
|
|
194
|
+
return b.getFloat32(0);
|
|
195
|
+
case 8:
|
|
196
|
+
return b.getFloat64(0);
|
|
197
|
+
default:
|
|
198
|
+
return NaN;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* get a date from a buffer
|
|
204
|
+
* @static
|
|
205
|
+
* @param {Buffer} buff from which to read the date
|
|
206
|
+
* @return {Date} result
|
|
207
|
+
*/
|
|
208
|
+
static readDate(buff) {
|
|
209
|
+
const b = new DataView(buff.buffer, buff.byteOffset, buff.byteLength);
|
|
210
|
+
switch (buff.byteLength) {
|
|
211
|
+
case 1:
|
|
212
|
+
return new Date(b.getUint8(0));
|
|
213
|
+
case 2:
|
|
214
|
+
return new Date(b.getUint16(0));
|
|
215
|
+
case 4:
|
|
216
|
+
return new Date(b.getUint32(0));
|
|
217
|
+
case 8:
|
|
218
|
+
return new Date(Number.parseInt(Tools.readHexString(buff), 16));
|
|
219
|
+
default:
|
|
220
|
+
return new Date(0);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Reads the data from a tag
|
|
226
|
+
* @static
|
|
227
|
+
* @param {TagData} tagObj The tag object to be read
|
|
228
|
+
* @param {Buffer} data Data to be transformed
|
|
229
|
+
* @return {Tag} result
|
|
230
|
+
*/
|
|
231
|
+
static readDataFromTag(tagObj, data) {
|
|
232
|
+
const { type, name } = tagObj;
|
|
233
|
+
let { track } = tagObj;
|
|
234
|
+
let discardable = tagObj.discardable || false;
|
|
235
|
+
let keyframe = tagObj.keyframe || false;
|
|
236
|
+
let payload = null;
|
|
237
|
+
let value;
|
|
238
|
+
|
|
239
|
+
switch (type) {
|
|
240
|
+
case 'u':
|
|
241
|
+
value = Tools.readUnsigned(data);
|
|
242
|
+
break;
|
|
243
|
+
case 'f':
|
|
244
|
+
value = Tools.readFloat(data);
|
|
245
|
+
break;
|
|
246
|
+
case 'i':
|
|
247
|
+
value = Tools.readSigned(data);
|
|
248
|
+
break;
|
|
249
|
+
case 's':
|
|
250
|
+
value = String.fromCharCode(...data);
|
|
251
|
+
break;
|
|
252
|
+
case '8':
|
|
253
|
+
value = Tools.readUtf8(data);
|
|
254
|
+
break;
|
|
255
|
+
case 'd':
|
|
256
|
+
value = Tools.readDate(data);
|
|
257
|
+
break;
|
|
258
|
+
default:
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (name === 'SimpleBlock' || name === 'Block') {
|
|
263
|
+
let p = 0;
|
|
264
|
+
const { length, value: trak } = Tools.readVint(data, p);
|
|
265
|
+
p += length;
|
|
266
|
+
track = trak;
|
|
267
|
+
value = Tools.readSigned(data.subarray(p, p + 2));
|
|
268
|
+
p += 2;
|
|
269
|
+
if (name === 'SimpleBlock') {
|
|
270
|
+
keyframe = Boolean(data[length + 2] & 0x80);
|
|
271
|
+
discardable = Boolean(data[length + 2] & 0x01);
|
|
272
|
+
}
|
|
273
|
+
p += 1;
|
|
274
|
+
payload = data.subarray(p);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
...tagObj,
|
|
279
|
+
data,
|
|
280
|
+
discardable,
|
|
281
|
+
keyframe,
|
|
282
|
+
payload,
|
|
283
|
+
track,
|
|
284
|
+
value,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @param {Array<[number, import("./types/schema.types").EBMLSchema]>} items
|
|
290
|
+
*/
|
|
291
|
+
static addSchema(items) {
|
|
292
|
+
if (!Array.isArray(items)) {
|
|
293
|
+
return schema
|
|
294
|
+
}
|
|
295
|
+
for (const item of items) {
|
|
296
|
+
schema.set(item[0], item[1])
|
|
297
|
+
}
|
|
298
|
+
return schema
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @param {number} value
|
|
303
|
+
* @param {number} size
|
|
304
|
+
*/
|
|
305
|
+
static toUint8(value, size) {
|
|
306
|
+
var buf = new Uint8Array(size)
|
|
307
|
+
for (let i = 0, k = size - 1; i < size; ++i, --k) {
|
|
308
|
+
buf[i] = (value >> (8 * k)) & 0xFF
|
|
309
|
+
}
|
|
310
|
+
return buf
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** @param {number} value */
|
|
314
|
+
static measureUnsignedInt(value) {
|
|
315
|
+
// Force to 32-bit unsigned integer
|
|
316
|
+
if (value < (1 << 8)) {
|
|
317
|
+
return 1;
|
|
318
|
+
} else if (value < (1 << 16)) {
|
|
319
|
+
return 2;
|
|
320
|
+
} else if (value < (1 << 24)) {
|
|
321
|
+
return 3;
|
|
322
|
+
} else if (value < 2 ** 32) {
|
|
323
|
+
return 4;
|
|
324
|
+
} else if (value < 2 ** 40) {
|
|
325
|
+
return 5;
|
|
326
|
+
} else {
|
|
327
|
+
return 6;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/** @param {number} value */
|
|
332
|
+
static measureEBMLVarInt(value) {
|
|
333
|
+
if (value < (1 << 7) - 1) {
|
|
334
|
+
/** Top bit is set, leaving 7 bits to hold the integer, but we can't store
|
|
335
|
+
* 127 because "all bits set to one" is a reserved value. Same thing for the
|
|
336
|
+
* other cases below:
|
|
337
|
+
*/
|
|
338
|
+
return 1;
|
|
339
|
+
} else if (value < (1 << 14) - 1) {
|
|
340
|
+
return 2;
|
|
341
|
+
} else if (value < (1 << 21) - 1) {
|
|
342
|
+
return 3;
|
|
343
|
+
} else if (value < (1 << 28) - 1) {
|
|
344
|
+
return 4;
|
|
345
|
+
} else if (value < 2 ** 35 - 1) {
|
|
346
|
+
return 5;
|
|
347
|
+
} else if (value < 2 ** 42 - 1) {
|
|
348
|
+
return 6;
|
|
349
|
+
} else {
|
|
350
|
+
throw new Error('EBML VINT size not supported ' + value);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
module.exports = Tools
|