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/decoder.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
const { Transform } = require('stream');
|
|
2
|
+
const tools = require('./tools');
|
|
3
|
+
const schema = require('./schema');
|
|
4
|
+
const { debugLog } = require('./debug-log');
|
|
5
|
+
|
|
6
|
+
const debug = debugLog('ebml:decoder');
|
|
7
|
+
|
|
8
|
+
const STATE_TAG = 1;
|
|
9
|
+
const STATE_SIZE = 2;
|
|
10
|
+
const STATE_CONTENT = 3;
|
|
11
|
+
|
|
12
|
+
class EbmlDecoder extends Transform {
|
|
13
|
+
/**
|
|
14
|
+
* @property
|
|
15
|
+
* @private
|
|
16
|
+
* @type {Buffer}
|
|
17
|
+
*/
|
|
18
|
+
mBuffer = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @private
|
|
22
|
+
* @property
|
|
23
|
+
* @readonly
|
|
24
|
+
*/
|
|
25
|
+
mTagStack = [];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @property
|
|
29
|
+
* @private
|
|
30
|
+
* @type {Number}
|
|
31
|
+
*/
|
|
32
|
+
mState = STATE_TAG;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @property
|
|
36
|
+
* @private
|
|
37
|
+
* @type {Number}
|
|
38
|
+
*/
|
|
39
|
+
mCursor = 0;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @property
|
|
43
|
+
* @private
|
|
44
|
+
* @type {Number}
|
|
45
|
+
*/
|
|
46
|
+
mTotal = 0;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @constructor
|
|
50
|
+
* @param {Object} options The options to be passed along to the super class
|
|
51
|
+
*/
|
|
52
|
+
constructor(options = {}) {
|
|
53
|
+
super({ ...options, readableObjectMode: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get buffer() {
|
|
57
|
+
return this.mBuffer;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get cursor() {
|
|
61
|
+
return this.mCursor;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get state() {
|
|
65
|
+
return this.mState;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get tagStack() {
|
|
69
|
+
return this.mTagStack;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get total() {
|
|
73
|
+
return this.mTotal;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
set buffer(buffer) {
|
|
77
|
+
this.mBuffer = buffer;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {number} cursor
|
|
82
|
+
*/
|
|
83
|
+
set cursor(cursor) {
|
|
84
|
+
this.mCursor = cursor;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
set state(state) {
|
|
88
|
+
this.mState = state;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
set total(total) {
|
|
92
|
+
this.mTotal = total;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_transform(chunk, enc, done) {
|
|
96
|
+
if (!this.buffer) {
|
|
97
|
+
this.buffer = Buffer.from(chunk);
|
|
98
|
+
} else {
|
|
99
|
+
this.buffer = tools.concatenate(this.buffer, Buffer.from(chunk));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
while (this.cursor < this.buffer.length) {
|
|
103
|
+
if (this.state === STATE_TAG && !this.readTag()) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
if (this.state === STATE_SIZE && !this.readSize()) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
if (this.state === STATE_CONTENT && !this.readContent()) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
done();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static getSchemaInfo(tag) {
|
|
118
|
+
if (Number.isInteger(tag) && schema.has(tag)) {
|
|
119
|
+
return schema.get(tag);
|
|
120
|
+
}
|
|
121
|
+
const tagStr = `0x${tag.toString(16).toUpperCase()}`
|
|
122
|
+
const unknown = {
|
|
123
|
+
type: null,
|
|
124
|
+
name: `unknown-${tagStr}`,
|
|
125
|
+
description: `${tagStr}`,
|
|
126
|
+
level: -1,
|
|
127
|
+
minver: -1,
|
|
128
|
+
multiple: false,
|
|
129
|
+
webm: false,
|
|
130
|
+
};
|
|
131
|
+
schema.set(tag, unknown)
|
|
132
|
+
console.warn('[SCHEMA]', 'unknown tag:', tagStr)
|
|
133
|
+
return unknown
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
readTag() {
|
|
137
|
+
/* istanbul ignore if */
|
|
138
|
+
if (debug.enabled) {
|
|
139
|
+
debug('parsing tag');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (this.cursor >= this.buffer.length) {
|
|
143
|
+
/* istanbul ignore if */
|
|
144
|
+
if (debug.enabled) {
|
|
145
|
+
debug('waiting for more data');
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const start = this.total;
|
|
151
|
+
const tag = tools.readVint(this.buffer, this.cursor);
|
|
152
|
+
|
|
153
|
+
if (tag == null) {
|
|
154
|
+
/* istanbul ignore if */
|
|
155
|
+
if (debug.enabled) {
|
|
156
|
+
debug('waiting for more data');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const tagStr = tools.readHexString(
|
|
163
|
+
this.buffer,
|
|
164
|
+
this.cursor,
|
|
165
|
+
this.cursor + tag.length,
|
|
166
|
+
);
|
|
167
|
+
const tagNum = Number.parseInt(tagStr, 16);
|
|
168
|
+
this.cursor += tag.length;
|
|
169
|
+
this.total += tag.length;
|
|
170
|
+
this.state = STATE_SIZE;
|
|
171
|
+
|
|
172
|
+
const tagObj = {
|
|
173
|
+
tag: tag.value,
|
|
174
|
+
tagStr,
|
|
175
|
+
type: EbmlDecoder.getSchemaInfo(tagNum).type,
|
|
176
|
+
name: EbmlDecoder.getSchemaInfo(tagNum).name,
|
|
177
|
+
start,
|
|
178
|
+
end: start + tag.length,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
this.tagStack.push(tagObj);
|
|
182
|
+
/* istanbul ignore if */
|
|
183
|
+
if (debug.enabled) {
|
|
184
|
+
debug(`read tag: ${tagStr}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
readSize() {
|
|
191
|
+
const tagObj = this.tagStack[this.tagStack.length - 1];
|
|
192
|
+
|
|
193
|
+
/* istanbul ignore if */
|
|
194
|
+
if (debug.enabled) {
|
|
195
|
+
debug(`parsing size for tag: ${tagObj.tagStr}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (this.cursor >= this.buffer.length) {
|
|
199
|
+
/* istanbul ignore if */
|
|
200
|
+
if (debug.enabled) {
|
|
201
|
+
debug('waiting for more data');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const size = tools.readVint(this.buffer, this.cursor);
|
|
208
|
+
|
|
209
|
+
if (size == null) {
|
|
210
|
+
/* istanbul ignore if */
|
|
211
|
+
if (debug.enabled) {
|
|
212
|
+
debug('waiting for more data');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.cursor += size.length;
|
|
219
|
+
this.total += size.length;
|
|
220
|
+
this.state = STATE_CONTENT;
|
|
221
|
+
tagObj.dataSize = size.value;
|
|
222
|
+
|
|
223
|
+
// unknown size
|
|
224
|
+
if (size.value === -1) {
|
|
225
|
+
tagObj.end = -1;
|
|
226
|
+
} else {
|
|
227
|
+
tagObj.end += size.value + size.length;
|
|
228
|
+
}
|
|
229
|
+
/* istanbul ignore if */
|
|
230
|
+
if (debug.enabled) {
|
|
231
|
+
debug(`read size: ${size.value}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
readContent() {
|
|
238
|
+
const { tagStr, type, dataSize, ...rest } = this.tagStack[this.tagStack.length - 1];
|
|
239
|
+
|
|
240
|
+
/* istanbul ignore if */
|
|
241
|
+
if (debug.enabled) {
|
|
242
|
+
debug(`parsing content for tag: ${tagStr}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (type === 'm') {
|
|
246
|
+
/* istanbul ignore if */
|
|
247
|
+
if (debug.enabled) {
|
|
248
|
+
debug('content should be tags');
|
|
249
|
+
}
|
|
250
|
+
this.push(['start', { tagStr, type, dataSize, ...rest }]);
|
|
251
|
+
this.state = STATE_TAG;
|
|
252
|
+
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (this.buffer.length < this.cursor + dataSize) {
|
|
257
|
+
/* istanbul ignore if */
|
|
258
|
+
if (debug.enabled) {
|
|
259
|
+
debug(`got: ${this.buffer.length}`);
|
|
260
|
+
debug(`need: ${this.cursor + dataSize}`);
|
|
261
|
+
debug('waiting for more data');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const data = this.buffer.subarray(this.cursor, this.cursor + dataSize);
|
|
268
|
+
this.total += dataSize;
|
|
269
|
+
this.state = STATE_TAG;
|
|
270
|
+
this.buffer = this.buffer.subarray(this.cursor + dataSize);
|
|
271
|
+
this.cursor = 0;
|
|
272
|
+
|
|
273
|
+
this.tagStack.pop(); // remove the object from the stack
|
|
274
|
+
|
|
275
|
+
this.push([
|
|
276
|
+
'tag',
|
|
277
|
+
tools.readDataFromTag(
|
|
278
|
+
{ tagStr, type, dataSize, ...rest },
|
|
279
|
+
Buffer.from(data),
|
|
280
|
+
),
|
|
281
|
+
]);
|
|
282
|
+
|
|
283
|
+
while (this.tagStack.length > 0) {
|
|
284
|
+
const topEle = this.tagStack[this.tagStack.length - 1];
|
|
285
|
+
if (this.total < topEle.end) {
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
this.push(['end', topEle]);
|
|
289
|
+
this.tagStack.pop();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* istanbul ignore if */
|
|
293
|
+
if (debug.enabled) {
|
|
294
|
+
debug(`read data: ${data.toString('hex')}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = EbmlDecoder
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const unexpected = require('unexpected');
|
|
2
|
+
const unexpectedDate = require('unexpected-date');
|
|
3
|
+
|
|
4
|
+
const Decoder = require('./decoder');
|
|
5
|
+
|
|
6
|
+
const expect = unexpected.clone().use(unexpectedDate);
|
|
7
|
+
|
|
8
|
+
const STATE_TAG = 1;
|
|
9
|
+
const STATE_SIZE = 2;
|
|
10
|
+
const STATE_CONTENT = 3;
|
|
11
|
+
|
|
12
|
+
describe('EBML', () => {
|
|
13
|
+
describe('Decoder', () => {
|
|
14
|
+
it('should wait for more data if a tag is longer than the buffer', () => {
|
|
15
|
+
const decoder = new Decoder();
|
|
16
|
+
decoder.write(Buffer.from([0x1a, 0x45]));
|
|
17
|
+
|
|
18
|
+
expect(decoder.state, 'to be', STATE_TAG);
|
|
19
|
+
expect(decoder.buffer.length, 'to be', 2);
|
|
20
|
+
expect(decoder.cursor, 'to be', 0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should clear the buffer after a full tag is written in one chunk', () => {
|
|
24
|
+
const decoder = new Decoder();
|
|
25
|
+
decoder.write(Buffer.from([0x42, 0x86, 0x81, 0x01]));
|
|
26
|
+
|
|
27
|
+
expect(decoder.state, 'to be', STATE_TAG);
|
|
28
|
+
expect(decoder.buffer.length, 'to be', 0);
|
|
29
|
+
expect(decoder.cursor, 'to be', 0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should clear the buffer after a full tag is written in multiple chunks', () => {
|
|
33
|
+
const decoder = new Decoder();
|
|
34
|
+
|
|
35
|
+
decoder.write(Buffer.from([0x42, 0x86]));
|
|
36
|
+
decoder.write(Buffer.from([0x81, 0x01]));
|
|
37
|
+
|
|
38
|
+
expect(decoder.state, 'to be', STATE_TAG);
|
|
39
|
+
expect(decoder.buffer.length, 'to be', 0);
|
|
40
|
+
expect(decoder.cursor, 'to be', 0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should increment the cursor on each step', () => {
|
|
44
|
+
const decoder = new Decoder();
|
|
45
|
+
|
|
46
|
+
decoder.write(Buffer.from([0x42])); // 4
|
|
47
|
+
|
|
48
|
+
expect(decoder.state, 'to be', STATE_TAG);
|
|
49
|
+
expect(decoder.buffer.length, 'to be', 1);
|
|
50
|
+
expect(decoder.cursor, 'to be', 0);
|
|
51
|
+
|
|
52
|
+
decoder.write(Buffer.from([0x86])); // 5
|
|
53
|
+
|
|
54
|
+
expect(decoder.state, 'to be', STATE_SIZE);
|
|
55
|
+
expect(decoder.buffer.length, 'to be', 2);
|
|
56
|
+
expect(decoder.cursor, 'to be', 2);
|
|
57
|
+
|
|
58
|
+
decoder.write(Buffer.from([0x81])); // 6 & 7
|
|
59
|
+
|
|
60
|
+
expect(decoder.state, 'to be', STATE_CONTENT);
|
|
61
|
+
expect(decoder.buffer.length, 'to be', 3);
|
|
62
|
+
expect(decoder.cursor, 'to be', 3);
|
|
63
|
+
|
|
64
|
+
decoder.write(Buffer.from([0x01])); // 6 & 7
|
|
65
|
+
|
|
66
|
+
expect(decoder.state, 'to be', STATE_TAG);
|
|
67
|
+
expect(decoder.buffer.length, 'to be', 0);
|
|
68
|
+
expect(decoder.cursor, 'to be', 0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should emit correct tag events for simple data', done => {
|
|
72
|
+
const decoder = new Decoder();
|
|
73
|
+
decoder.on('data', ([state, { dataSize, tag, type, tagStr, data }]) => {
|
|
74
|
+
expect(state, 'to be', 'tag');
|
|
75
|
+
expect(tag, 'to be', 0x286);
|
|
76
|
+
expect(tagStr, 'to be', '4286');
|
|
77
|
+
expect(dataSize, 'to be', 0x01);
|
|
78
|
+
expect(type, 'to be', 'u');
|
|
79
|
+
expect(data, 'to equal', Buffer.from([0x01]));
|
|
80
|
+
done();
|
|
81
|
+
decoder.on('finish', done);
|
|
82
|
+
});
|
|
83
|
+
decoder.on('finish', done);
|
|
84
|
+
decoder.write(Buffer.from([0x42, 0x86, 0x81, 0x01]));
|
|
85
|
+
decoder.end();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should emit correct EBML tag events for master tags', done => {
|
|
89
|
+
const decoder = new Decoder();
|
|
90
|
+
|
|
91
|
+
decoder.on('data', ([state, { dataSize, tag, type, tagStr, data }]) => {
|
|
92
|
+
expect(state, 'to be', 'start');
|
|
93
|
+
expect(tag, 'to be', 0x0a45dfa3);
|
|
94
|
+
expect(tagStr, 'to be', '1a45dfa3');
|
|
95
|
+
expect(dataSize, 'to be', 0);
|
|
96
|
+
expect(type, 'to be', 'm');
|
|
97
|
+
expect(data, 'to be undefined');
|
|
98
|
+
done();
|
|
99
|
+
decoder.on('finish', done);
|
|
100
|
+
});
|
|
101
|
+
decoder.on('finish', done);
|
|
102
|
+
|
|
103
|
+
decoder.write(Buffer.from([0x1a, 0x45, 0xdf, 0xa3, 0x80]));
|
|
104
|
+
decoder.end();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should emit correct EBML:end events for master tags', done => {
|
|
108
|
+
const decoder = new Decoder();
|
|
109
|
+
let tags = 0;
|
|
110
|
+
decoder.on('data', d => {
|
|
111
|
+
const [state, data] = d;
|
|
112
|
+
if (state === 'end') {
|
|
113
|
+
expect(tags, 'to be', 2); // two tags
|
|
114
|
+
expect(data.tag, 'to be', 0x0a45dfa3);
|
|
115
|
+
expect(data.tagStr, 'to be', '1a45dfa3');
|
|
116
|
+
expect(data.dataSize, 'to be', 4);
|
|
117
|
+
expect(data.type, 'to be', 'm');
|
|
118
|
+
expect(data.data, 'to be undefined');
|
|
119
|
+
done();
|
|
120
|
+
decoder.on('finish', done);
|
|
121
|
+
} else {
|
|
122
|
+
tags += 1;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
decoder.on('finish', done);
|
|
126
|
+
|
|
127
|
+
decoder.write(Buffer.from([0x1a, 0x45, 0xdf, 0xa3]));
|
|
128
|
+
decoder.write(Buffer.from([0x84, 0x42, 0x86, 0x81, 0x00]));
|
|
129
|
+
decoder.end();
|
|
130
|
+
});
|
|
131
|
+
describe('::getSchemaInfo', () => {
|
|
132
|
+
it('returns a correct tag if possible', () => {
|
|
133
|
+
expect(Decoder.getSchemaInfo(0x4286), 'to satisfy', {
|
|
134
|
+
name: 'EBMLVersion',
|
|
135
|
+
level: 1,
|
|
136
|
+
type: 'u',
|
|
137
|
+
mandatory: true,
|
|
138
|
+
default: 1,
|
|
139
|
+
minver: 1,
|
|
140
|
+
description: 'The version of EBML parser used to create the file.',
|
|
141
|
+
multiple: false,
|
|
142
|
+
webm: false,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
it('returns a default object if not found', () => {
|
|
146
|
+
expect(Decoder.getSchemaInfo(0x404), 'to satisfy', {
|
|
147
|
+
type: expect.it('to be null'),
|
|
148
|
+
name: expect.it('to be a string').and('to be', 'unknown'),
|
|
149
|
+
description: expect.it('to be a string').and('to be empty'),
|
|
150
|
+
level: expect.it('to be a number').and('not to be positive'),
|
|
151
|
+
minver: expect.it('to be a number').and('not to be positive'),
|
|
152
|
+
multiple: expect.it('to be a boolean'),
|
|
153
|
+
webm: expect.it('to be a boolean'),
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
package/src/encoder.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
const { Transform } = require('stream');
|
|
2
|
+
const schema = require('./schema');
|
|
3
|
+
const tools = require('./tools');
|
|
4
|
+
const { debugLog } = require('./debug-log');
|
|
5
|
+
|
|
6
|
+
const debug = debugLog('ebml:encoder');
|
|
7
|
+
|
|
8
|
+
/** @typedef {import('./types/tag.types').Tag} Tag */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {number} tagId
|
|
12
|
+
* @param {Buffer} tagData
|
|
13
|
+
* @param {number} [end]
|
|
14
|
+
*/
|
|
15
|
+
function encodeTag(tagId, tagData, end) {
|
|
16
|
+
const data = [Buffer.from(tagId.toString(16), 'hex')];
|
|
17
|
+
if (end === -1) {
|
|
18
|
+
data.push(Buffer.from([0xFF]))
|
|
19
|
+
} else {
|
|
20
|
+
data.push(tools.writeVint(tagData.length))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// cast ArrayBuffer to Buffer
|
|
24
|
+
if (!Buffer.isBuffer(tagData)) {
|
|
25
|
+
tagData = Buffer.from(tagData); // eslint-disable-line no-param-reassign
|
|
26
|
+
}
|
|
27
|
+
data.push(tagData);
|
|
28
|
+
return Buffer.concat(data);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Encodes a raw EBML stream
|
|
33
|
+
* @class EbmlEncoder
|
|
34
|
+
* @extends Transform
|
|
35
|
+
*/
|
|
36
|
+
class EbmlEncoder extends Transform {
|
|
37
|
+
/**
|
|
38
|
+
* @type {Buffer}
|
|
39
|
+
* @property
|
|
40
|
+
* @private
|
|
41
|
+
*/
|
|
42
|
+
mBuffer = null;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @private
|
|
46
|
+
* @property
|
|
47
|
+
* @type {Boolean}
|
|
48
|
+
*/
|
|
49
|
+
mCorked = false;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @private
|
|
53
|
+
* @property
|
|
54
|
+
* @type {Array<Tag>}
|
|
55
|
+
*/
|
|
56
|
+
mStack = [];
|
|
57
|
+
|
|
58
|
+
constructor(options = {}) {
|
|
59
|
+
super({ ...options, writableObjectMode: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get buffer() {
|
|
63
|
+
return this.mBuffer;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get corked() {
|
|
67
|
+
return this.mCorked;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get stack() {
|
|
71
|
+
return this.mStack;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
set buffer(buffer) {
|
|
75
|
+
this.mBuffer = buffer;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
set corked(corked) {
|
|
79
|
+
this.mCorked = corked;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
set stack(stak) {
|
|
83
|
+
this.mStack = stak;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
*
|
|
88
|
+
* @param {[string, Tag]} chunk array of chunk data, starting with the tag
|
|
89
|
+
* @param {string} enc the encoding type (not used)
|
|
90
|
+
* @param {Function} done a callback method to call after the transformation
|
|
91
|
+
*/
|
|
92
|
+
_transform(chunk, enc, done) {
|
|
93
|
+
const [tag, { data, name, ...rest }] = chunk;
|
|
94
|
+
/* istanbul ignore if */
|
|
95
|
+
if (debug.enabled) {
|
|
96
|
+
debug(`encode ${tag} ${name}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
switch (tag) {
|
|
100
|
+
case 'start':
|
|
101
|
+
this.startTag(name, { ...rest });
|
|
102
|
+
break;
|
|
103
|
+
case 'tag':
|
|
104
|
+
this.writeTag(name, data);
|
|
105
|
+
break;
|
|
106
|
+
case 'end':
|
|
107
|
+
this.endTag();
|
|
108
|
+
break;
|
|
109
|
+
default:
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return done();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @private
|
|
118
|
+
* @param {Function} done callback function
|
|
119
|
+
*/
|
|
120
|
+
flush(done = () => {}) {
|
|
121
|
+
if (!this.buffer || this.corked) {
|
|
122
|
+
/* istanbul ignore if */
|
|
123
|
+
if (debug.enabled) {
|
|
124
|
+
debug('no buffer/nothing pending');
|
|
125
|
+
}
|
|
126
|
+
return done();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.buffer.byteLength === 0) {
|
|
130
|
+
/* istanbul ignore if */
|
|
131
|
+
if (debug.enabled) {
|
|
132
|
+
debug('empty buffer');
|
|
133
|
+
}
|
|
134
|
+
return done();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* istanbul ignore if */
|
|
138
|
+
if (debug.enabled) {
|
|
139
|
+
debug(`writing ${this.buffer.length} bytes`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const chunk = Buffer.from(this.buffer);
|
|
143
|
+
this.buffer = null;
|
|
144
|
+
this.push(chunk);
|
|
145
|
+
return done();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @private
|
|
150
|
+
* @param {Buffer | Buffer[]} buffer
|
|
151
|
+
*/
|
|
152
|
+
bufferAndFlush(buffer) {
|
|
153
|
+
this.buffer = tools.concatenate(this.buffer, buffer);
|
|
154
|
+
this.flush();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_flush(done = () => {}) {
|
|
158
|
+
this.flush(done);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_bufferAndFlush(buffer) {
|
|
162
|
+
this.bufferAndFlush(buffer);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* gets the ID of the type of tagName
|
|
167
|
+
* @static
|
|
168
|
+
* @param {string} tagName to be looked up
|
|
169
|
+
* @return {number} A buffer containing the schema information
|
|
170
|
+
*/
|
|
171
|
+
static getSchemaInfo(tagName) {
|
|
172
|
+
const tagId = Array.from(schema.keys()).find(
|
|
173
|
+
str => schema.get(str).name === tagName,
|
|
174
|
+
);
|
|
175
|
+
if (tagId) {
|
|
176
|
+
return tagId;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
cork() {
|
|
183
|
+
this.corked = true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
uncork() {
|
|
187
|
+
this.corked = false;
|
|
188
|
+
this.flush();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** @private */
|
|
192
|
+
writeTag(tagName, tagData) {
|
|
193
|
+
const tagId = EbmlEncoder.getSchemaInfo(tagName);
|
|
194
|
+
if (!tagId) {
|
|
195
|
+
throw new Error(`No schema entry found for ${tagName}`);
|
|
196
|
+
}
|
|
197
|
+
if (tagData) {
|
|
198
|
+
const data = encodeTag(tagId, tagData);
|
|
199
|
+
if (this.stack.length > 0) {
|
|
200
|
+
this.stack[this.stack.length - 1].children.push({ data });
|
|
201
|
+
} else {
|
|
202
|
+
this.bufferAndFlush(data);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @private
|
|
209
|
+
* @param {String} tagName The name of the tag to start
|
|
210
|
+
* @param {{end: Number}} info an information object with a `end` parameter
|
|
211
|
+
*/
|
|
212
|
+
startTag(tagName, { end }) {
|
|
213
|
+
const tagId = EbmlEncoder.getSchemaInfo(tagName);
|
|
214
|
+
if (!tagId) {
|
|
215
|
+
throw new Error(`No schema entry found for ${tagName}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const tag = {
|
|
219
|
+
data: null,
|
|
220
|
+
id: tagId,
|
|
221
|
+
name: tagName,
|
|
222
|
+
end,
|
|
223
|
+
children: [],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
if (this.stack.length > 0) {
|
|
227
|
+
this.stack[this.stack.length - 1].children.push(tag);
|
|
228
|
+
}
|
|
229
|
+
this.stack.push(tag);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** @private */
|
|
233
|
+
endTag() {
|
|
234
|
+
const tag = this.stack.pop() || {
|
|
235
|
+
children: [],
|
|
236
|
+
data: { buffer: Buffer.from([]) },
|
|
237
|
+
};
|
|
238
|
+
const childTagDataBuffers = tag.children.map(child => child.data);
|
|
239
|
+
tag.data = encodeTag(tag.id, Buffer.concat(childTagDataBuffers), tag.end);
|
|
240
|
+
if (this.stack.length < 1) {
|
|
241
|
+
this.bufferAndFlush(tag.data);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = EbmlEncoder
|