feat: first step
This commit is contained in:
135
tests/decoder.spec.ts
Normal file
135
tests/decoder.spec.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { assert, describe, it } from 'vitest';
|
||||
import {
|
||||
EbmlTagPosition,
|
||||
EbmlElementType,
|
||||
EbmlStreamDecoder as Decoder,
|
||||
EbmlDataTag,
|
||||
type EbmlTagTrait,
|
||||
} from 'konoebml';
|
||||
|
||||
const bufFrom = (data: Uint8Array | readonly number[]): ArrayBuffer =>
|
||||
new Uint8Array(data).buffer;
|
||||
|
||||
const getDecoderWithNullSink = () => {
|
||||
const decoder = new Decoder();
|
||||
decoder.readable.pipeTo(new WritableStream({}));
|
||||
return decoder;
|
||||
};
|
||||
|
||||
async function collectTags(decoder: Decoder): Promise<EbmlTagTrait[]> {
|
||||
const tags: EbmlTagTrait[] = [];
|
||||
await decoder.readable.pipeTo(
|
||||
new WritableStream({
|
||||
write: (tag) => {
|
||||
tags.push(tag);
|
||||
},
|
||||
})
|
||||
);
|
||||
return tags;
|
||||
}
|
||||
|
||||
describe('EbmlStreamDecoder', () => {
|
||||
it('should wait for more data if a tag is longer than the buffer', async () => {
|
||||
const decoder = getDecoderWithNullSink();
|
||||
const writer = decoder.writable.getWriter();
|
||||
await writer.write(bufFrom([0x1a, 0x45]));
|
||||
|
||||
assert.strictEqual(decoder.transformer.getBuffer().byteLength, 2);
|
||||
});
|
||||
|
||||
it('should clear the buffer after a full tag is written in one chunk', async () => {
|
||||
const decoder = getDecoderWithNullSink();
|
||||
const writer = decoder.writable.getWriter();
|
||||
await writer.write(bufFrom([0x42, 0x86, 0x81, 0x01]));
|
||||
|
||||
assert.strictEqual(decoder.transformer.getBuffer().byteLength, 0);
|
||||
});
|
||||
|
||||
it('should clear the buffer after a full tag is written in multiple chunks', async () => {
|
||||
const decoder = getDecoderWithNullSink();
|
||||
const writer = decoder.writable.getWriter();
|
||||
|
||||
await writer.write(bufFrom([0x42, 0x86]));
|
||||
await writer.write(bufFrom([0x81, 0x01]));
|
||||
|
||||
assert.strictEqual(decoder.transformer.getBuffer().byteLength, 0);
|
||||
});
|
||||
|
||||
it('should increment the cursor on each step', async () => {
|
||||
const decoder = getDecoderWithNullSink();
|
||||
const writer = decoder.writable.getWriter();
|
||||
|
||||
await writer.write(bufFrom([0x42])); // 4
|
||||
|
||||
assert.strictEqual(decoder.transformer.getBuffer().byteLength, 1);
|
||||
|
||||
await writer.write(bufFrom([0x86])); // 5
|
||||
|
||||
assert.strictEqual(decoder.transformer.getBuffer().byteLength, 2);
|
||||
|
||||
await writer.write(bufFrom([0x81])); // 6 & 7
|
||||
|
||||
assert.strictEqual(decoder.transformer.getBuffer().byteLength, 0);
|
||||
|
||||
await writer.write(bufFrom([0x01])); // 6 & 7
|
||||
|
||||
assert.strictEqual(decoder.transformer.getBuffer().byteLength, 0);
|
||||
});
|
||||
|
||||
it('should emit correct tag events for simple data', async () => {
|
||||
const decoder = new Decoder();
|
||||
const writer = decoder.writable.getWriter();
|
||||
|
||||
const tags = collectTags(decoder);
|
||||
|
||||
await writer.write(bufFrom([0x42, 0x86, 0x81, 0x01]));
|
||||
await writer.close();
|
||||
|
||||
const [tag] = await tags;
|
||||
|
||||
assert.strictEqual(tag.position, EbmlTagPosition.Content);
|
||||
assert.strictEqual(tag.id.toString(16), '4286');
|
||||
assert.strictEqual(tag.contentLength, 0x01);
|
||||
assert.strictEqual(tag.type, EbmlElementType.UnsignedInt);
|
||||
assert.ok(tag instanceof EbmlDataTag);
|
||||
assert.deepStrictEqual(tag.data, 1);
|
||||
});
|
||||
|
||||
it('should emit correct EBML tag events for master tags', async () => {
|
||||
const decoder = new Decoder();
|
||||
const writer = decoder.writable.getWriter();
|
||||
|
||||
writer.write(bufFrom([0x1a, 0x45, 0xdf, 0xa3, 0x80]));
|
||||
writer.close();
|
||||
|
||||
const [tag] = await collectTags(decoder);
|
||||
|
||||
assert.strictEqual(tag.position, EbmlTagPosition.Start);
|
||||
assert.strictEqual(tag.id.toString(16), '1a45dfa3');
|
||||
assert.strictEqual(tag.contentLength, 0);
|
||||
assert.strictEqual(tag.type, EbmlElementType.Master);
|
||||
assert.ok(!(tag instanceof EbmlDataTag));
|
||||
assert.ok(!('data' in tag));
|
||||
});
|
||||
|
||||
it('should emit correct EBML:end events for master tags', async () => {
|
||||
const decoder = new Decoder();
|
||||
const writer = decoder.writable.getWriter();
|
||||
|
||||
writer.write(bufFrom([0x1a, 0x45, 0xdf, 0xa3]));
|
||||
writer.write(bufFrom([0x84, 0x42, 0x86, 0x81, 0x00]));
|
||||
writer.close();
|
||||
|
||||
const tags = await collectTags(decoder);
|
||||
|
||||
assert.strictEqual(tags.length, 3);
|
||||
|
||||
const firstEndTag = tags.find((t) => t.position === EbmlTagPosition.End)!;
|
||||
|
||||
assert.strictEqual(firstEndTag.id.toString(16), '1a45dfa3');
|
||||
assert.strictEqual(firstEndTag.contentLength, 4);
|
||||
assert.strictEqual(firstEndTag.type, EbmlElementType.Master);
|
||||
assert.ok(!(firstEndTag instanceof EbmlDataTag));
|
||||
assert.ok(!('data' in firstEndTag));
|
||||
});
|
||||
});
|
||||
121
tests/encoder.spec.ts
Normal file
121
tests/encoder.spec.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { assert, expect, describe, it } from 'vitest';
|
||||
import {
|
||||
EbmlTagPosition,
|
||||
type EbmlTagTrait,
|
||||
EbmlTagIdEnum,
|
||||
createEbmlTagForManuallyBuild,
|
||||
EbmlStreamEncoder,
|
||||
} from 'konoebml';
|
||||
|
||||
const invalidTag: EbmlTagTrait = <EbmlTagTrait>(<any>{
|
||||
id: undefined,
|
||||
type: <any>'404NotFound',
|
||||
position: undefined,
|
||||
size: -1,
|
||||
data: null,
|
||||
});
|
||||
|
||||
const incompleteTag: EbmlTagTrait = undefined!;
|
||||
|
||||
const ebmlStartTag = createEbmlTagForManuallyBuild(EbmlTagIdEnum.EBML, {
|
||||
position: EbmlTagPosition.Start,
|
||||
});
|
||||
|
||||
const ebmlEndTag: EbmlTagTrait = createEbmlTagForManuallyBuild(
|
||||
EbmlTagIdEnum.EBML,
|
||||
{
|
||||
contentLength: 10,
|
||||
position: EbmlTagPosition.End,
|
||||
}
|
||||
);
|
||||
|
||||
const ebmlVersion1Tag = Object.assign(
|
||||
createEbmlTagForManuallyBuild(EbmlTagIdEnum.EBMLVersion, {
|
||||
position: EbmlTagPosition.Content,
|
||||
}),
|
||||
{
|
||||
data: 1,
|
||||
}
|
||||
);
|
||||
|
||||
const ebmlVersion0Tag: EbmlTagTrait = Object.assign(
|
||||
createEbmlTagForManuallyBuild(EbmlTagIdEnum.EBMLVersion, {
|
||||
position: EbmlTagPosition.Content,
|
||||
}),
|
||||
{
|
||||
data: 0,
|
||||
}
|
||||
);
|
||||
|
||||
const makeEncoderTest = async (tags: EbmlTagTrait[]) => {
|
||||
const source = new ReadableStream({
|
||||
pull(controller) {
|
||||
for (const tag of tags) {
|
||||
controller.enqueue(tag);
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const encoder = new EbmlStreamEncoder();
|
||||
const chunks: ArrayBuffer[] = [];
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
source
|
||||
.pipeThrough(encoder)
|
||||
.pipeTo(
|
||||
new WritableStream({
|
||||
write(chunk) {
|
||||
chunks.push(chunk);
|
||||
},
|
||||
close() {
|
||||
resolve();
|
||||
},
|
||||
})
|
||||
)
|
||||
.catch(reject);
|
||||
});
|
||||
return {
|
||||
encoder,
|
||||
chunks,
|
||||
};
|
||||
};
|
||||
|
||||
describe('EBML Encoder', () => {
|
||||
it('should write a single tag', async () => {
|
||||
const { chunks } = await makeEncoderTest([ebmlVersion1Tag]);
|
||||
|
||||
assert.deepEqual(chunks, [
|
||||
new Uint8Array([0x42, 0x86]),
|
||||
new Uint8Array([0x81]),
|
||||
new Uint8Array([0x01]),
|
||||
]);
|
||||
});
|
||||
it('should write a tag with a single child', async () => {
|
||||
const { chunks } = await makeEncoderTest([
|
||||
ebmlStartTag,
|
||||
ebmlVersion0Tag,
|
||||
ebmlEndTag,
|
||||
]);
|
||||
|
||||
assert.deepEqual(chunks, [
|
||||
new Uint8Array([0x1a, 0x45, 0xdf, 0xa3]),
|
||||
new Uint8Array([0x83]),
|
||||
new Uint8Array([0x42, 0x86]),
|
||||
new Uint8Array([0x80]),
|
||||
new Uint8Array([]),
|
||||
]);
|
||||
});
|
||||
|
||||
describe('#writeTag', () => {
|
||||
it('throws with an incomplete tag data', async () => {
|
||||
await expect(() => makeEncoderTest([incompleteTag])).rejects.toThrow(
|
||||
/should only accept embl tag but not/
|
||||
);
|
||||
});
|
||||
it('throws with an invalid tag id', async () => {
|
||||
await expect(() => makeEncoderTest([invalidTag])).rejects.toThrow(
|
||||
/should only accept embl tag but not/
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
0
tests/init-test.ts
Normal file
0
tests/init-test.ts
Normal file
99
tests/pipeline.spec.ts
Normal file
99
tests/pipeline.spec.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { assert, describe, it, expect } from 'vitest';
|
||||
import {
|
||||
EbmlStreamDecoder,
|
||||
EbmlStreamEncoder,
|
||||
type EbmlTagTrait,
|
||||
EbmlTagIdEnum,
|
||||
type EbmlBlockTag,
|
||||
createEbmlTagForManuallyBuild,
|
||||
} from 'konoebml';
|
||||
import { concatArrayBuffers } from 'konoebml/tools';
|
||||
|
||||
describe('EBML Pipeline', () => {
|
||||
async function assertPipelineOutputEquals(
|
||||
input: number[],
|
||||
expected: number[]
|
||||
) {
|
||||
const buffer = new Uint8Array(input);
|
||||
|
||||
const source = new ReadableStream<ArrayBuffer>({
|
||||
pull(controller) {
|
||||
controller.enqueue(buffer.buffer);
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const chunks: ArrayBuffer[] = [];
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const sink = new WritableStream<ArrayBuffer>({
|
||||
write(chunk) {
|
||||
chunks.push(chunk);
|
||||
},
|
||||
close() {
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
|
||||
source
|
||||
.pipeThrough(new EbmlStreamDecoder())
|
||||
.pipeThrough(new EbmlStreamEncoder())
|
||||
.pipeTo(sink)
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
expect(concatArrayBuffers(...chunks)).toEqual(new Uint8Array(expected));
|
||||
}
|
||||
|
||||
it('should not immediately output with not unknown sized and not paired master tag', async () => {
|
||||
await assertPipelineOutputEquals(
|
||||
[0x1a, 0x45, 0xdf, 0xa3, 0x83, 0x42, 0x86, 0x81],
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
it('should immediately output with unknown sized master tag', async () => {
|
||||
await assertPipelineOutputEquals(
|
||||
[0x1a, 0x45, 0xdf, 0xa3, 0xff, 0x42, 0x86, 0x81],
|
||||
[0x1a, 0x45, 0xdf, 0xa3, 0xff]
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode and decode Blocks correctly', async () => {
|
||||
const block = createEbmlTagForManuallyBuild(EbmlTagIdEnum.Block, {});
|
||||
block.track = 5;
|
||||
block.invisible = true;
|
||||
const payload = new Uint8Array(50);
|
||||
for (let i = 0; i < payload.byteLength; i++) {
|
||||
payload[i] = Math.floor(Math.random() * 255);
|
||||
}
|
||||
block.payload = payload;
|
||||
const encoder = new EbmlStreamEncoder();
|
||||
const decoder = new EbmlStreamDecoder();
|
||||
const tags: EbmlTagTrait[] = [];
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const source = new ReadableStream<EbmlTagTrait>({
|
||||
pull(controller) {
|
||||
controller.enqueue(block);
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
source
|
||||
.pipeThrough(encoder)
|
||||
.pipeThrough(decoder)
|
||||
.pipeTo(
|
||||
new WritableStream<EbmlTagTrait>({
|
||||
write(tag) {
|
||||
tags.push(tag);
|
||||
},
|
||||
close: () => resolve(),
|
||||
})
|
||||
)
|
||||
.catch(reject);
|
||||
});
|
||||
const tag = tags[0] as EbmlBlockTag;
|
||||
assert.strictEqual(tag.id, EbmlTagIdEnum.Block);
|
||||
assert.strictEqual(tag.track, block.track);
|
||||
assert.strictEqual(tag.invisible, block.invisible);
|
||||
assert.deepEqual(tag.payload, block.payload);
|
||||
});
|
||||
});
|
||||
340
tests/tools.spec.ts
Normal file
340
tests/tools.spec.ts
Normal file
@@ -0,0 +1,340 @@
|
||||
import { assert, describe, it } from 'vitest';
|
||||
import {
|
||||
readAscii,
|
||||
readElementIdVint,
|
||||
readFloat,
|
||||
readSigned,
|
||||
readUnsigned,
|
||||
readUtf8,
|
||||
readVint,
|
||||
writeVint,
|
||||
} from 'konoebml/tools';
|
||||
|
||||
function bufFrom(data: Uint8Array | readonly number[]): Uint8Array {
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
|
||||
function viewFrom(
|
||||
data: Uint8Array | readonly number[],
|
||||
start?: number,
|
||||
length?: number
|
||||
): DataView {
|
||||
const buf = bufFrom(data);
|
||||
return new DataView(buf.buffer, start, length);
|
||||
}
|
||||
|
||||
describe('EBML Tools', () => {
|
||||
describe('#readVint()', () => {
|
||||
function assertReadVint(
|
||||
data: Uint8Array | readonly number[],
|
||||
expect: number | bigint | [number | bigint, number | undefined],
|
||||
start?: number,
|
||||
length?: number
|
||||
) {
|
||||
const view = viewFrom(data, start, length);
|
||||
const vint = readVint(view)!;
|
||||
|
||||
const expectValue = Array.isArray(expect) ? expect[0] : expect;
|
||||
const expectLength =
|
||||
(Array.isArray(expect) ? expect[1] : undefined) ??
|
||||
view.byteLength - view.byteOffset;
|
||||
assert.strictEqual(vint.value, expectValue);
|
||||
assert.strictEqual(vint.length, expectLength);
|
||||
}
|
||||
|
||||
function assertReadElementIdVint(
|
||||
data: Uint8Array | readonly number[],
|
||||
expect: number | bigint | [number | bigint, number | undefined],
|
||||
start?: number,
|
||||
length?: number
|
||||
) {
|
||||
const view = viewFrom(data, start, length);
|
||||
const vint = readElementIdVint(view)!;
|
||||
|
||||
const expectValue = Array.isArray(expect) ? expect[0] : expect;
|
||||
const expectLength =
|
||||
(Array.isArray(expect) ? expect[1] : undefined) ??
|
||||
view.byteLength - view.byteOffset;
|
||||
assert.strictEqual(vint.value, expectValue);
|
||||
assert.strictEqual(vint.length, expectLength);
|
||||
}
|
||||
|
||||
it('should read the correct value for all 1 byte ints', () => {
|
||||
assertReadVint([0x80], 0);
|
||||
assert.throws(() => {
|
||||
readElementIdVint(viewFrom([0x80]));
|
||||
}, /Element ID VINT_DATA can not be all zeros/);
|
||||
|
||||
for (let i = 1; i < 0x80 - 1; i += 1) {
|
||||
assertReadElementIdVint([i | 0x80], i);
|
||||
assertReadVint([i | 0x80], i);
|
||||
}
|
||||
|
||||
assertReadVint([0xff], 127);
|
||||
assert.throws(() => {
|
||||
readElementIdVint(viewFrom([0xff]));
|
||||
}, /Element ID VINT_DATA can not be all ones/);
|
||||
});
|
||||
|
||||
it('should read the correct value for 1 byte int with non-zero start', () => {
|
||||
assertReadVint([0x00, 0x81], [1, 1], 1);
|
||||
});
|
||||
|
||||
it('should read the correct value for all 2 byte ints', () => {
|
||||
for (let i = 0; i < 0x40; i += 1) {
|
||||
for (let j = 0; j < 0xff; j += 1) {
|
||||
assertReadVint([i | 0x40, j], (i << 8) + j);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should read the correct value for all 3 byte ints', () => {
|
||||
for (let i = 0; i < 0x20; i += 1) {
|
||||
for (let j = 0; j < 0xff; j += 2) {
|
||||
for (let k = 0; k < 0xff; k += 3) {
|
||||
assertReadVint([i | 0x20, j, k], (i << 16) + (j << 8) + k);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// not brute forcing any more bytes, takes sooo long
|
||||
|
||||
it('should read the correct value for 4 byte int min/max values', () => {
|
||||
assertReadVint([0x10, 0x20, 0x00, 0x00], 2 ** 21);
|
||||
assertReadVint([0x1f, 0xff, 0xff, 0xfe], 2 ** 28 - 2);
|
||||
});
|
||||
|
||||
it('should read the correct value for 5 byte int min/max values', () => {
|
||||
assertReadVint([0x08, 0x10, 0x00, 0x00, 0x00], 2 ** 28);
|
||||
assertReadVint([0x0f, 0xff, 0xff, 0xff, 0xfe], 2 ** 35 - 2);
|
||||
});
|
||||
|
||||
it('should read the correct value for 6 byte int min/max values', () => {
|
||||
assertReadVint([0x04, 0x08, 0x00, 0x00, 0x00, 0x00], 2 ** 35);
|
||||
assertReadVint([0x07, 0xff, 0xff, 0xff, 0xff, 0xfe], 2 ** 42 - 2);
|
||||
});
|
||||
|
||||
it('should read the correct value for 7 byte int min/max values', () => {
|
||||
assertReadVint([0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00], 2 ** 42);
|
||||
assertReadVint([0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe], 2 ** 49 - 2);
|
||||
});
|
||||
|
||||
it('should read the correct value for 8 byte int min value', () => {
|
||||
assertReadVint([0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], 2 ** 49);
|
||||
});
|
||||
|
||||
it('should read the correct value for the max representable JS number (2^53 - 1)', () => {
|
||||
assertReadVint(
|
||||
[0x01, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
||||
Number.MAX_SAFE_INTEGER
|
||||
);
|
||||
});
|
||||
|
||||
it('should return bigint for more than max representable JS number (2^53)', () => {
|
||||
assertReadVint(
|
||||
[0x01, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
BigInt(Number.MAX_SAFE_INTEGER) + 1n
|
||||
);
|
||||
});
|
||||
|
||||
it('should return bigint for more than max representable JS number (8 byte int max value)', () => {
|
||||
assertReadVint(
|
||||
[0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
||||
2n ** 56n - 1n
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for 9+ byte int values', () => {
|
||||
assert.throws(() => {
|
||||
readVint(
|
||||
viewFrom([0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff])
|
||||
);
|
||||
}, /Vint length out of range/);
|
||||
});
|
||||
|
||||
it('should throw for not shortest element id', () => {
|
||||
assert.throws(() => {
|
||||
readElementIdVint(viewFrom([0x40, 0x3f]));
|
||||
}, /Element ID VINT_DATA should be shortest/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#writeVint()', () => {
|
||||
function assertWriteVint(
|
||||
value: number | bigint,
|
||||
expected: Uint8Array | readonly number[]
|
||||
): void {
|
||||
const actual = writeVint(value);
|
||||
assert.strictEqual(
|
||||
Buffer.from(expected).toString('hex'),
|
||||
Buffer.from(actual).toString('hex')
|
||||
);
|
||||
}
|
||||
|
||||
it('should throw when writing -1', () => {
|
||||
assert.throws(() => {
|
||||
writeVint(-1);
|
||||
}, /VINT_DATA out of range/);
|
||||
});
|
||||
|
||||
it('should write all 1 byte ints', () => {
|
||||
for (let i = 0; i < 0x80 - 1; i += 1) {
|
||||
assertWriteVint(i, [i | 0x80]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should write 2 byte int min/max values', () => {
|
||||
assertWriteVint(2 ** 7 - 1, [0x40, 0x7f]);
|
||||
assertWriteVint(2 ** 14 - 2, [0x7f, 0xfe]);
|
||||
});
|
||||
|
||||
it('should write 3 byte int min/max values', () => {
|
||||
assertWriteVint(2 ** 14 - 1, [0x20, 0x3f, 0xff]);
|
||||
assertWriteVint(2 ** 21 - 2, [0x3f, 0xff, 0xfe]);
|
||||
});
|
||||
|
||||
it('should write 4 byte int min/max values', () => {
|
||||
assertWriteVint(2 ** 21 - 1, [0x10, 0x1f, 0xff, 0xff]);
|
||||
assertWriteVint(2 ** 28 - 2, [0x1f, 0xff, 0xff, 0xfe]);
|
||||
});
|
||||
|
||||
it('should write 5 byte int min/max value', () => {
|
||||
assertWriteVint(2 ** 28 - 1, [0x08, 0x0f, 0xff, 0xff, 0xff]);
|
||||
assertWriteVint(2 ** 35 - 2, [0x0f, 0xff, 0xff, 0xff, 0xfe]);
|
||||
});
|
||||
|
||||
it('should write 6 byte int min/max value', () => {
|
||||
assertWriteVint(2 ** 35 - 1, [0x04, 0x07, 0xff, 0xff, 0xff, 0xff]);
|
||||
assertWriteVint(2 ** 42 - 2, [0x07, 0xff, 0xff, 0xff, 0xff, 0xfe]);
|
||||
});
|
||||
|
||||
it('should write 7 byte int min/max value', () => {
|
||||
assertWriteVint(2 ** 42 - 1, [0x02, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff]);
|
||||
assertWriteVint(2 ** 49 - 2, [0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe]);
|
||||
});
|
||||
|
||||
it('should write 8 byte int min/max value', () => {
|
||||
assertWriteVint(
|
||||
2 ** 49 - 1,
|
||||
[0x01, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]
|
||||
);
|
||||
assertWriteVint(
|
||||
2n ** 56n - 2n,
|
||||
[0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe]
|
||||
);
|
||||
});
|
||||
|
||||
it('should write the correct value for the max representable JS number (2^53 - 1)', () => {
|
||||
assertWriteVint(
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
[0x01, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for more than max representable JS number (8 byte int max value)', () => {
|
||||
assert.throws(() => {
|
||||
writeVint(2n ** 56n + 1n);
|
||||
}, /VINT_DATA out of range/);
|
||||
});
|
||||
|
||||
it('should throw for 9+ byte int values', () => {
|
||||
assert.throws(() => {
|
||||
writeVint(2n ** 56n + 1n);
|
||||
}, /VINT_DATA out of range/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#readUnsigned', () => {
|
||||
it('handles 8-bit ints', () => {
|
||||
assert.strictEqual(readUnsigned(viewFrom([0x07])), 7);
|
||||
});
|
||||
it('handles 16-bit ints', () => {
|
||||
assert.strictEqual(readUnsigned(viewFrom([0x07, 0x07])), 1799);
|
||||
});
|
||||
it('handles 32-bit ints', () => {
|
||||
assert.strictEqual(
|
||||
readUnsigned(viewFrom([0x07, 0x07, 0x07, 0x07])),
|
||||
117901063
|
||||
);
|
||||
});
|
||||
it('handles ints smaller than 49 bits as numbers', () => {
|
||||
assert.strictEqual(
|
||||
readUnsigned(viewFrom([0x07, 0x07, 0x07, 0x07, 0x07])),
|
||||
30182672135
|
||||
);
|
||||
assert.strictEqual(
|
||||
readUnsigned(viewFrom([0x07, 0x07, 0x07, 0x07, 0x07, 0x07])),
|
||||
7726764066567
|
||||
);
|
||||
});
|
||||
it('returns ints larger than the max safe number size as bigint', () => {
|
||||
assert.strictEqual(
|
||||
readUnsigned(viewFrom([0x1, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07])),
|
||||
74035645638969095n
|
||||
);
|
||||
assert.strictEqual(
|
||||
typeof readUnsigned(
|
||||
viewFrom([0x1, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07])
|
||||
),
|
||||
'bigint'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#readSigned', () => {
|
||||
it('handles 8-bit ints', () => {
|
||||
assert.strictEqual(readSigned(viewFrom([0x07])), 7);
|
||||
});
|
||||
it('handles 16-bit ints', () => {
|
||||
assert.strictEqual(readSigned(viewFrom([0x07, 0x07])), 1799);
|
||||
});
|
||||
it('handles 32-bit ints', () => {
|
||||
assert.strictEqual(
|
||||
readSigned(viewFrom([0x07, 0x07, 0x07, 0x07])),
|
||||
117901063
|
||||
);
|
||||
});
|
||||
it('handles 32 ~ 64bit ints', () => {
|
||||
assert.strictEqual(readSigned(viewFrom([0x40, 0x20, 0x00])), 4202496);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#readFloat', () => {
|
||||
it('can read 32-bit floats', () => {
|
||||
assert.strictEqual(readFloat(viewFrom([0x40, 0x20, 0x00, 0x00])), 2.5);
|
||||
});
|
||||
it('can read 64-bit floats', () => {
|
||||
assert.strictEqual(
|
||||
readFloat(viewFrom([0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])),
|
||||
2.5
|
||||
);
|
||||
});
|
||||
it('should throw for invalid sized float arrays', () => {
|
||||
assert.throws(() => {
|
||||
readFloat(viewFrom([0x40, 0x20, 0x00]));
|
||||
}, /length should be/);
|
||||
});
|
||||
});
|
||||
describe('#readUtf8', () => {
|
||||
it('can read valid utf-8 strings', () => {
|
||||
assert.strictEqual(readUtf8(viewFrom([97, 98, 99])), 'abc');
|
||||
assert.strictEqual(
|
||||
readUtf8(viewFrom([240, 159, 164, 163, 240, 159, 152, 133])),
|
||||
'🤣😅'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#readAscii', () => {
|
||||
it('can read valid ascii strings', () => {
|
||||
assert.strictEqual(readAscii(viewFrom([97, 98, 99])), 'abc');
|
||||
});
|
||||
|
||||
it('can not read valid ascii strings', () => {
|
||||
assert.notStrictEqual(
|
||||
readAscii(viewFrom([240, 159, 164, 163, 240, 159, 152, 133])),
|
||||
'🤣😅'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
197
tests/value.spec.ts
Normal file
197
tests/value.spec.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import fs from 'node:fs';
|
||||
import { assert, describe, it } from 'vitest';
|
||||
import {
|
||||
EbmlStreamDecoder,
|
||||
EbmlTagIdEnum,
|
||||
EbmlSimpleBlockTag as SimpleBlock,
|
||||
EbmlDataTag,
|
||||
type EbmlMasterTag,
|
||||
} from 'konoebml';
|
||||
import { Readable } from 'node:stream';
|
||||
import { WritableStream } from 'node:stream/web';
|
||||
|
||||
process.setMaxListeners(Number.POSITIVE_INFINITY);
|
||||
|
||||
const createReadStream = (file: string) =>
|
||||
Readable.toWeb(fs.createReadStream(file), {
|
||||
strategy: { highWaterMark: 100, size: (chunk) => chunk.byteLength },
|
||||
}) as ReadableStream<ArrayBuffer>;
|
||||
|
||||
const makeDataStreamTest =
|
||||
(stream: () => ReadableStream<ArrayBuffer>) =>
|
||||
async (cb: (tag: EbmlMasterTag | EbmlDataTag, done: () => void) => void) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
stream()
|
||||
.pipeThrough(new EbmlStreamDecoder())
|
||||
.pipeTo(
|
||||
new WritableStream({
|
||||
write: async (tag) => {
|
||||
cb(tag as EbmlMasterTag | EbmlDataTag, () => resolve(true));
|
||||
},
|
||||
close: () => {
|
||||
reject('hit end of file without calling done');
|
||||
},
|
||||
})
|
||||
)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
describe('EBML Values in tags', () => {
|
||||
describe('AVC1', () => {
|
||||
const makeAVC1StreamTest = makeDataStreamTest(() =>
|
||||
createReadStream('media/video-webm-codecs-avc1-42E01E.webm')
|
||||
);
|
||||
|
||||
it('should get a correct PixelWidth value from a file (2-byte unsigned int)', async () =>
|
||||
await makeAVC1StreamTest((tag, done) => {
|
||||
if (tag instanceof EbmlDataTag && tag.id === EbmlTagIdEnum.PixelWidth) {
|
||||
assert.strictEqual(tag.data, 352);
|
||||
done();
|
||||
}
|
||||
}));
|
||||
|
||||
it('should get a correct EBMLVersion value from a file (one-byte unsigned int)', async () =>
|
||||
await makeAVC1StreamTest((tag, done) => {
|
||||
if (
|
||||
tag instanceof EbmlDataTag &&
|
||||
tag.id === EbmlTagIdEnum.EBMLVersion
|
||||
) {
|
||||
assert.strictEqual(tag.data, 1);
|
||||
done();
|
||||
}
|
||||
}));
|
||||
|
||||
it('should get a correct TimeCodeScale value from a file (3-byte unsigned int)', () =>
|
||||
makeAVC1StreamTest((tag, done) => {
|
||||
if (
|
||||
tag instanceof EbmlDataTag &&
|
||||
tag.id === EbmlTagIdEnum.TimecodeScale
|
||||
) {
|
||||
assert.strictEqual(tag.data, 1000000);
|
||||
done();
|
||||
}
|
||||
}));
|
||||
|
||||
it('should get a correct TrackUID value from a file (56-bit integer in hex)', () =>
|
||||
makeAVC1StreamTest((tag, done) => {
|
||||
if (tag instanceof EbmlDataTag && tag.id === EbmlTagIdEnum.TrackUID) {
|
||||
assert.strictEqual(tag.data, 7990710658693702);
|
||||
done();
|
||||
}
|
||||
}));
|
||||
|
||||
it('should get a correct DocType value from a file (ASCII text)', () =>
|
||||
makeAVC1StreamTest((tag, done) => {
|
||||
if (tag instanceof EbmlDataTag && tag.id === EbmlTagIdEnum.DocType) {
|
||||
assert.strictEqual(tag.data, 'matroska');
|
||||
done();
|
||||
}
|
||||
}));
|
||||
|
||||
it('should get a correct MuxingApp value from a file (utf8 text)', () =>
|
||||
makeAVC1StreamTest((tag, done) => {
|
||||
if (tag instanceof EbmlDataTag && tag.id === EbmlTagIdEnum.MuxingApp) {
|
||||
assert.strictEqual(tag.data, 'Chrome', JSON.stringify(tag));
|
||||
done();
|
||||
}
|
||||
}));
|
||||
|
||||
it('should get a correct SimpleBlock time payload from a file (binary)', () =>
|
||||
makeAVC1StreamTest((tag, done) => {
|
||||
if (!(tag instanceof SimpleBlock)) {
|
||||
return;
|
||||
}
|
||||
if (tag.value <= 0 || tag.value >= 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* look at second simpleBlock */
|
||||
assert.strictEqual(tag.track, 1, 'track');
|
||||
assert.strictEqual(tag.value, 191, 'value (timestamp)');
|
||||
assert.strictEqual(
|
||||
tag.payload.byteLength,
|
||||
169,
|
||||
JSON.stringify(tag.payload)
|
||||
);
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('VP8', () => {
|
||||
const makeVP8StreamTest = makeDataStreamTest(() =>
|
||||
createReadStream('media/video-webm-codecs-vp8.webm')
|
||||
);
|
||||
|
||||
it('should get a correct PixelWidth value from a video/webm; codecs="vp8" file (2-byte unsigned int)', () =>
|
||||
makeVP8StreamTest((tag, done) => {
|
||||
if (tag instanceof EbmlDataTag && tag.id === EbmlTagIdEnum.PixelWidth) {
|
||||
assert.strictEqual(tag.data, 352);
|
||||
done();
|
||||
}
|
||||
}));
|
||||
|
||||
it('should get a correct EBMLVersion value from a video/webm; codecs="vp8" file (one-byte unsigned int)', () =>
|
||||
makeVP8StreamTest((tag, done) => {
|
||||
if (
|
||||
tag instanceof EbmlDataTag &&
|
||||
tag.id === EbmlTagIdEnum.EBMLVersion
|
||||
) {
|
||||
assert.strictEqual(tag.data, 1);
|
||||
done();
|
||||
}
|
||||
}));
|
||||
|
||||
it('should get a correct TimeCodeScale value from a video/webm; codecs="vp8" file (3-byte unsigned int)', () =>
|
||||
makeVP8StreamTest((tag, done) => {
|
||||
if (
|
||||
tag instanceof EbmlDataTag &&
|
||||
tag.id === EbmlTagIdEnum.TimecodeScale
|
||||
) {
|
||||
assert.strictEqual(tag.data, 1000000);
|
||||
done();
|
||||
}
|
||||
}));
|
||||
|
||||
it('should get a correct TrackUID value from a video/webm; codecs="vp8" file (56-bit integer in hex)', () =>
|
||||
makeVP8StreamTest((tag, done) => {
|
||||
if (tag instanceof EbmlDataTag && tag.id === EbmlTagIdEnum.TrackUID) {
|
||||
assert.strictEqual(tag.data, 13630657102564614n);
|
||||
done();
|
||||
}
|
||||
}));
|
||||
|
||||
it('should get a correct DocType value from a video/webm; codecs="vp8" file (ASCII text)', () =>
|
||||
makeVP8StreamTest((tag, done) => {
|
||||
if (tag instanceof EbmlDataTag && tag.id === EbmlTagIdEnum.DocType) {
|
||||
assert.strictEqual(tag.data, 'webm');
|
||||
done();
|
||||
}
|
||||
}));
|
||||
|
||||
it('should get a correct MuxingApp value from a video/webm; codecs="vp8" file (utf8 text)', () =>
|
||||
makeVP8StreamTest((tag, done) => {
|
||||
if (tag instanceof EbmlDataTag && tag.id === EbmlTagIdEnum.MuxingApp) {
|
||||
assert.strictEqual(tag.data, 'Chrome');
|
||||
done();
|
||||
}
|
||||
}));
|
||||
|
||||
it('should get a correct SimpleBlock time payload from a file (binary)', () =>
|
||||
makeVP8StreamTest((tag, done) => {
|
||||
if (!(tag instanceof SimpleBlock)) {
|
||||
return;
|
||||
}
|
||||
if (tag.value <= 0 || tag.value >= 100) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert.strictEqual(tag.track, 1, 'track');
|
||||
assert.strictEqual(tag.value, 96, JSON.stringify(tag));
|
||||
/* look at second simpleBlock */
|
||||
assert.strictEqual(tag.payload.byteLength, 43, JSON.stringify(tag));
|
||||
assert.strictEqual(tag.discardable, false, 'discardable');
|
||||
done();
|
||||
}));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user