feat: first step

This commit is contained in:
master 2025-03-16 00:22:42 +08:00
commit 764addd7f6
41 changed files with 6917 additions and 0 deletions

193
.gitignore vendored Normal file
View File

@ -0,0 +1,193 @@
# Created by https://www.gitignore.io/api/vim,node,jetbrains+all,visualstudiocode
### JetBrains+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
### JetBrains+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
### Vim ###
# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
# Temporary
.netrwhist
*~
# Persistent undo
[._]*.un~
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# End of https://www.gitignore.io/api/vim,node,jetbrains+all,visualstudiocode
# babel generated folder now; no need for it to be kept
lib/
/flow-typed/npm/
# https://atom.io/packages/atomic-management
.atom/*
# Added by cargo
**/target
/.vitest
**/dist

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) 2025 Yeheng Zhou (konoebml)
Copyright (c) 2023 Liam Dyer (ebml-web-stream)
Copyright (c) 2013-2018 Mark Schmale and contributors (ebml-stream)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE

93
README.md Normal file
View File

@ -0,0 +1,93 @@
<h1 align="center">
<img src="./assets/konoebml-512x512.webp" alt="logo" height=180 />
<br />
<b>Konoebml</b>
<div align="center"><img src="https://img.shields.io/badge/status-beta-blue" alt="status-badge" /></div>
</h1>
<p align="center"><b>A modern JavaScript implementation of RFC8794 (EBML). </b></p>
## Note
[EBML][EBML] stands for Extensible Binary Meta-Language and is somewhat of a binary version of XML. It's used for container formats like [WebM][webm] or [MKV][mkv].
This package is serving as a fork with extensive rewrites and enhancements to [ebml-web-stream][ebml-web-stream] and [ebml-stream][ebml-stream], providing:
- better [unknown size vint][unknown size vint] support
- bigint support for vint, unsigned and signed int data type
- better error types
# Install
Install via NPM:
```bash
npm install konoebml
```
# Usage and Examples
This example reads a media file into memory and decodes it.
```js
import fs from 'node:fs/promises';
import {
ReadableStream,
WritableStream,
type TransformStream,
} from 'node:stream/web';
import { EbmlStreamDecoder } from 'konoebml';
async function main() {
const fileBuffer = await fs.readFile('media/test.webm');
await new ReadableStream({
pull(controller) {
controller.enqueue(fileBuffer);
controller.close();
},
})
.pipeThrough(new EbmlStreamDecoder() as unknown as TransformStream)
.pipeTo(new WritableStream({ write: console.log }));
}
main();
```
**Todo: add more docs and tests**
# State of this project
Parsing and writing should both work. If something is broken, please create [an issue][new-issue].
Any additional feature requests can also be submitted as [an issue][new-issue].
If any well-known tags have special parsing/encoding rules or data structures that aren't implemented, pull requests are welcome!
# License
[MIT](./LICENSE)
# Other Contributors
(in alphabetical order)
* [Austin Blake](https://github.com/austinleroy)
* [Chris Price](https://github.com/chrisprice)
* [Davy Van Deursen](https://github.com/dvdeurse)
* [Ed Markowski](https://github.com/siphontv)
* [Jonathan Sifuentes](https://github.com/jayands)
* [Liam Dyer](https://github.com/Saghen)
* [Manuel Wiedenmann](https://github.com/fsmanuel)
* [Mark Schmale](https://github.com/themasch)
* [Mathias Buus](https://github.com/mafintosh)
* [Max Ogden](https://github.com/maxogden)
* [Oliver Jones](https://github.com/OllieJones)
* [Oliver Walzer](https://github.com/owcd)
[EBML]: http://ebml.sourceforge.net/
[mkv]: http://www.matroska.org/technical/specs/index.html
[webm]: https://www.webmproject.org/
[new-issue]: https://github.com/saghen/ebml-web-stream/issues/new
[unknown size vint]: (https://www.rfc-editor.org/rfc/rfc8794.html#name-unknown-data-size)
[ebml-web-stream]: (https://github.com/Saghen/ebml-web-stream)
[ebml-stream]: (https://github.com/austinleroy/node-ebml)

BIN
assets/konoebml-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

62
biome.jsonc Normal file
View File

@ -0,0 +1,62 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"extends": [
"ultracite"
],
"linter": {
"rules": {
"style": {
"noNonNullAssertion": "off",
"noParameterAssign": "off",
"useFilenamingConvention": "off",
"noParameterProperties": "off"
},
"suspicious": {
"noExplicitAny": "off"
},
"complexity": {
"noForEach": "off"
},
"correctness": {
"noUnusedImports": {
"fix": "none",
"level": "warn"
}
},
"nursery": {
"noEnum": "off",
"useConsistentMemberAccessibility": "off"
}
}
},
"files": {
"ignore": [
".vscode/*.json"
]
},
"overrides": [
{
"include": [
"tests/**"
],
"javascript": {
"globals": [
"describe",
"beforeEach",
"it",
"expect"
]
},
"linter": {
"rules": {
"performance": {
"useTopLevelRegex": "off"
},
"suspicious": {
"noMisplacedAssertion": "off"
}
}
}
}
]
}

21
examples/playground.ts Normal file
View File

@ -0,0 +1,21 @@
import fs from 'node:fs/promises';
import {
ReadableStream,
WritableStream,
type TransformStream,
} from 'node:stream/web';
import { EbmlStreamDecoder } from 'konoebml';
async function main() {
const fileBuffer = await fs.readFile('media/test.webm');
await new ReadableStream({
pull(controller) {
controller.enqueue(fileBuffer);
controller.close();
},
})
.pipeThrough(new EbmlStreamDecoder() as unknown as TransformStream)
.pipeTo(new WritableStream({ write: console.log }));
}
main();

BIN
media/audiosample.webm Normal file

Binary file not shown.

BIN
media/test.webm Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

84
package.json Normal file
View File

@ -0,0 +1,84 @@
{
"name": "konoebml",
"version": "0.1.0-rc.1",
"description": "A modern JavaScript implementation of EBML RFC8794",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"maintainers": [
"Yeheng Zhou <master@evernightfireworks.com>"
],
"files": [
"dist",
"LICENSE",
"README.md"
],
"scripts": {
"build": "rslib build",
"dev": "rslib build --watch",
"test": "vitest --coverage",
"test-ci": "vitest --watch=false --coverage",
"prepublishOnly": "npm run build",
"lint": "ultracite lint",
"format": "ultracite format",
"playground": "tsx --tsconfig=./tsconfig.example.json ./examples/playground.ts"
},
"repository": "github:dumtruck/konoebml",
"engines": {
"node": ">= 18.0.0"
},
"keywords": [
"ebml",
"webm",
"mkv",
"matroska",
"format",
"rfc8794"
],
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@rslib/core": "^0.5.3",
"@swc/core": "^1.11.8",
"@types/jasmine": "^5.1.7",
"@types/node": "^22.13.10",
"@vitest/coverage-v8": "^3.0.8",
"cross-env": "^7.0.3",
"happy-dom": "^17.4.3",
"rimraf": "^6.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.8.2",
"ultracite": "^4.1.21",
"unplugin-swc": "^1.5.1",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.8"
},
"contributors": [
"Yeheng Zhou <master@evernightfireworks.com>",
"Liam Dyer <liamcdyer@gmail.com>",
"Austin Blake <austin.leroy@hotmail.com>",
"Chris Price <price.c@gmail.com>",
"Davy Van Deursen <d.vandeursen@evs.com>",
"Ed Markowski <siphon@protonmail.com>",
"Jonathan Sifuentes <jayands.dev@gmail.com>",
"Manuel Wiedenmann <manuel@funkensturm.de>",
"Mathias Buus <mathiasbuus@gmail.com>",
"Max Ogden <max@maxogden.com>",
"Oliver Walzer <walzer@incuray.com>",
"greenkeeperio-bot <support@greenkeeper.io>"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/dumtruck/konoebml/issues"
},
"homepage": "https://github.com/dumtruck/konoebml#readme",
"dependencies": {
"mnemonist": "^0.40.3",
"type-fest": "^4.37.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"@swc/core"
]
}
}

2338
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

38
rslib.config.ts Normal file
View File

@ -0,0 +1,38 @@
import { defineConfig } from '@rslib/core';
export default defineConfig({
source: {
tsconfigPath: './tsconfig.lib.json',
},
lib: [
{
format: 'esm',
syntax: 'es2021',
bundle: false,
dts: {
bundle: false,
build: false,
distPath: './dist',
},
source: {
entry: {
index: ['src/**/*.ts'],
},
},
},
{
format: 'cjs',
syntax: 'es2021',
dts: false,
bundle: true,
source: {
entry: {
index: './src/index.ts',
},
},
},
],
output: {
target: 'web',
},
});

12
src/adapters/index.ts Normal file
View File

@ -0,0 +1,12 @@
import type { Promisable } from 'type-fest';
export type FileDataViewController = {
getOffset(): number;
seek(offset: number): Promisable<number>;
read(
offset: number,
length?: number,
exactLength?: boolean
): Promise<DataView>;
peek(offset: number): Promise<DataView | null>;
};

123
src/decode-utils.ts Normal file
View File

@ -0,0 +1,123 @@
import { type EbmlTagIdType, isEbmlMasterTagId } from './models/enums';
import type { EbmlTagTrait } from './models/tag-trait';
import type { FileDataViewController } from './adapters';
import {
checkVintSafeSize,
dataViewSlice,
readUnsigned,
readVint,
readVintLength,
type SafeSizeVint,
} from './tools';
import { EbmlTagPosition } from './models/enums';
import { createEbmlTag } from './factory';
import { UnreachableOrLogicError } from './errors';
export async function decodeEbmlTagHeader(
controller: FileDataViewController
): Promise<{
sizeVint: SafeSizeVint;
tagVint: { length: number };
tagId: EbmlTagIdType;
}> {
const offset = controller.getOffset();
let view = await controller.read(offset, 1);
const tagVintLength = readVintLength(view);
view =
tagVintLength > view.byteLength
? await controller.read(offset, tagVintLength)
: view;
const tagIdView = dataViewSlice(view, 0, tagVintLength);
view =
tagVintLength + 1 > view.byteLength
? await controller.read(offset, tagVintLength + 1)
: view;
const sizeVintLength = readVintLength(
dataViewSlice(view, tagVintLength, tagVintLength + 1)
);
view =
tagVintLength + sizeVintLength > view.byteLength
? await controller.read(offset, tagVintLength + sizeVintLength)
: view;
const sizeVint = readVint(
dataViewSlice(view, tagVintLength, tagVintLength + sizeVintLength)
)!;
if (!sizeVint) {
throw new UnreachableOrLogicError(
'size vint dataView length is invalid, check code logic!'
);
}
const tagId = readUnsigned(tagIdView);
const safeSizeVint = checkVintSafeSize(sizeVint, tagId);
return {
sizeVint: safeSizeVint,
tagVint: {
length: tagVintLength,
},
tagId,
};
}
export async function* decodeEbmlContent(
controller: FileDataViewController
): AsyncGenerator<EbmlTagTrait, void, unknown> {
while (true) {
const offset = controller.getOffset();
const peeked = await controller.peek(offset);
if (!peeked) {
break;
}
const vints = await decodeEbmlTagHeader(controller);
const { tagId, tagVint, sizeVint } = vints;
const headerLength = tagVint.length + sizeVint.length;
const contentLength = sizeVint.value;
const isMaster = isEbmlMasterTagId(tagId);
if (isMaster) {
const tag: EbmlTagTrait = createEbmlTag(tagId, {
headerLength,
contentLength,
startOffset: offset,
position: EbmlTagPosition.Start,
parent: undefined,
});
yield tag;
}
await controller.seek(offset + headerLength);
const tag: EbmlTagTrait = createEbmlTag(tagId, {
headerLength,
contentLength,
startOffset: offset,
position: isMaster ? EbmlTagPosition.End : EbmlTagPosition.Content,
parent: undefined,
});
for await (const item of tag.decodeContent(controller)) {
yield item;
}
tag.endOffset = controller.getOffset();
yield tag;
}
}

227
src/decoder.ts Normal file
View File

@ -0,0 +1,227 @@
import { Queue } from 'mnemonist';
import type { FileDataViewController } from './adapters';
import type { EbmlTagTrait } from './models/tag-trait';
import { decodeEbmlContent } from './decode-utils';
import { StreamFlushReason, UnreachableOrLogicError } from './errors';
import { dataViewSlice } from './tools';
export class EbmlDecodeStreamTransformer
implements Transformer<ArrayBuffer, EbmlTagTrait>, FileDataViewController
{
private _offset = 0;
private _buffer: Uint8Array = new Uint8Array(0);
private _requests: Queue<
[number, number, (view: DataView) => void, (err: Error) => void]
> = new Queue();
private _tickIdleCallback: VoidFunction | undefined;
private _currentTask: Promise<void> | undefined;
private _writeBuffer = new Queue<EbmlTagTrait>();
public getBuffer(): Uint8Array {
return this._buffer;
}
public getOffset(): number {
return this._offset;
}
public seek(nextOffset: number): number {
const oldOffset = this._offset;
if (this._requests.size > 0) {
throw new UnreachableOrLogicError(
'sequential transformer should not seek before all read requests done'
);
}
if (nextOffset < oldOffset) {
throw new UnreachableOrLogicError(
'sequential transformer should not seek to previous offset'
);
}
this._buffer = this._buffer.slice(nextOffset - oldOffset);
this._offset = nextOffset;
return nextOffset;
}
public async read(
reqStart: number,
reqLength = 0,
exactLength = false
): Promise<DataView> {
const bufferStart = this._offset;
const bufferEnd = bufferStart + this._buffer.byteLength;
const reqEnd = reqStart + reqLength;
if (reqStart < bufferStart) {
throw new UnreachableOrLogicError(
'sequential transformer should not read before current offset'
);
}
let view: DataView;
if (bufferEnd >= reqEnd) {
view = new DataView(this._buffer.buffer, reqStart - bufferStart);
} else {
view = await new Promise<DataView>((resolve, reject) => {
this._requests.enqueue([reqStart, reqEnd, resolve, reject]);
this.notifyIdle();
});
}
view = exactLength ? dataViewSlice(view, 0, reqLength) : view;
return view;
}
public async peek(offset: number): Promise<DataView | null> {
try {
return await this.read(offset, 1);
} catch (e) {
if (e instanceof StreamFlushReason) {
return null;
}
throw e;
}
}
private handleRequests() {
const bufferStart = this._offset;
const bufferEnd = bufferStart + this._buffer.byteLength;
let handleCounts = this._requests.size;
while (handleCounts > 0) {
const req = this._requests.dequeue();
if (!req) {
break;
}
const [reqStart, reqEnd, resolve, _] = req;
if (reqStart < bufferStart) {
throw new UnreachableOrLogicError(
'sequential transformer should not read before current offset'
);
}
if (bufferEnd >= reqEnd) {
resolve(new DataView(this._buffer.buffer, reqStart - bufferStart));
} else {
this.notifyIdle();
this._requests.enqueue(req);
}
handleCounts--;
}
}
private cancelRequests() {
while (true) {
const req = this._requests.dequeue();
if (!req) {
break;
}
const [_reqStart, _reqEnd, _resolve, reject] = req;
reject(new StreamFlushReason());
}
}
notifyIdle() {
if (this._tickIdleCallback) {
this._tickIdleCallback();
}
}
tryEnqueueToBuffer(item: EbmlTagTrait) {
this._writeBuffer.enqueue(item);
}
waitBufferRelease(
ctrl: TransformStreamDefaultController<EbmlTagTrait>,
isFlush: boolean
) {
while (this._writeBuffer.size) {
if (ctrl.desiredSize! <= 0 && !isFlush) {
break;
}
ctrl.enqueue(this._writeBuffer.dequeue());
}
}
async tick(
ctrl: TransformStreamDefaultController<EbmlTagTrait>,
isFlush: boolean
) {
const waitIdle = new Promise<void>((resolve) => {
this._tickIdleCallback = () => {
resolve();
this._tickIdleCallback = undefined;
};
});
if (isFlush) {
this.cancelRequests();
} else {
this.handleRequests();
}
if (!this._currentTask && !isFlush) {
const decode = async () => {
try {
for await (const tag of decodeEbmlContent(this)) {
this.tryEnqueueToBuffer(tag);
}
this._currentTask = undefined;
} catch (err) {
if (!(err instanceof StreamFlushReason)) {
ctrl.error(err);
}
}
};
this._currentTask = decode();
}
await Promise.race([this._currentTask, waitIdle]);
this.waitBufferRelease(ctrl, isFlush);
}
async start(ctrl: TransformStreamDefaultController<EbmlTagTrait>) {
this._offset = 0;
this._buffer = new Uint8Array(0);
this._requests.clear();
this._tickIdleCallback = undefined;
this._currentTask = undefined;
await this.tick(ctrl, false);
}
async transform(
chunk: ArrayBuffer,
ctrl: TransformStreamDefaultController<EbmlTagTrait>
): Promise<void> {
if (chunk.byteLength === 0) {
return;
}
const newBuffer = new Uint8Array(
this._buffer.byteLength + chunk.byteLength
);
newBuffer.set(this._buffer, 0);
newBuffer.set(new Uint8Array(chunk), this._buffer.byteLength);
this._buffer = newBuffer;
await this.tick(ctrl, false);
}
async flush(ctrl: TransformStreamDefaultController<EbmlTagTrait>) {
await this.tick(ctrl, true);
}
}
export class EbmlStreamDecoder extends TransformStream<
ArrayBuffer,
EbmlTagTrait
> {
public readonly transformer: EbmlDecodeStreamTransformer;
constructor() {
const transformer = new EbmlDecodeStreamTransformer();
super(transformer);
this.transformer = transformer;
}
}

93
src/encoder.ts Normal file
View File

@ -0,0 +1,93 @@
import { Queue, Stack } from 'mnemonist';
import { EbmlTagTrait } from './models/tag-trait';
import { EbmlTagPosition } from './models/enums';
import { EbmlMasterTag } from './models/tag-master';
import { EbmlTreeMasterNotMatchError, UnreachableOrLogicError } from './errors';
export class EbmlEncodeStreamTransformer
implements Transformer<EbmlTagTrait, ArrayBuffer>
{
stack = new Stack<[EbmlMasterTag, ArrayBuffer[]]>();
_writeBuffer = new Queue<ArrayBuffer>();
_writeBufferTask: Promise<void> | undefined;
closed = false;
tryEnqueueToBuffer(...frag: ArrayBuffer[]) {
const top = this.stack.peek();
if (top) {
top[1].push(...frag);
} else {
for (const f of frag) {
this._writeBuffer.enqueue(f);
}
}
}
waitBufferRelease(
ctrl: TransformStreamDefaultController<ArrayBuffer>,
isFlush: boolean
) {
while (this._writeBuffer.size) {
if (ctrl.desiredSize! <= 0 && !isFlush) {
break;
}
ctrl.enqueue(this._writeBuffer.dequeue());
}
}
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
async transform(
tag: EbmlTagTrait,
ctrl: TransformStreamDefaultController<ArrayBuffer>
) {
if (!(tag instanceof EbmlTagTrait)) {
throw new UnreachableOrLogicError('should only accept embl tag but not');
}
if (tag instanceof EbmlMasterTag) {
if (tag.contentLength === Number.POSITIVE_INFINITY) {
if (tag.position === EbmlTagPosition.Start) {
this.tryEnqueueToBuffer(...tag.encodeHeader());
}
} else {
// biome-ignore lint/style/useCollapsedElseIf: <explanation>
if (tag.position === EbmlTagPosition.Start) {
this.stack.push([tag, []]);
} else {
const pop = this.stack.pop();
if (!pop) {
throw new EbmlTreeMasterNotMatchError(tag);
}
const [startTag, fragments] = pop;
const size = fragments.reduce(
(acc, curr) => acc + curr.byteLength,
0
);
startTag.contentLength = size;
this.tryEnqueueToBuffer(...startTag.encodeHeader());
this.tryEnqueueToBuffer(...fragments);
}
}
} else {
this.tryEnqueueToBuffer(...tag.encode());
}
this.waitBufferRelease(ctrl, false);
}
flush(ctrl: TransformStreamDefaultController<ArrayBuffer>) {
this.waitBufferRelease(ctrl, true);
}
}
export class EbmlStreamEncoder extends TransformStream<
EbmlTagTrait,
ArrayBuffer
> {
public readonly transformer: EbmlEncodeStreamTransformer;
constructor() {
const transformer = new EbmlEncodeStreamTransformer();
super(transformer);
this.transformer = transformer;
}
}

114
src/errors.ts Normal file
View File

@ -0,0 +1,114 @@
import type { EbmlElementType, EbmlTagIdType } from './models/enums';
import type { EbmlTagTrait } from './models/tag-trait';
export class ElementIdVintDataAllZerosError extends Error {
constructor(value: number | bigint) {
super(
`RFC8794 Element ID VINT_DATA can not be all zeros, but got ${value.toString(16)}`
);
}
}
export class ElementIdVintDataAllOnesError extends Error {
constructor(value: number | bigint) {
super(
`RFC8794 Element ID VINT_DATA can not be all ones, but got ${value.toString(16)}`
);
}
}
export class ElementIdVintDataNotShortestError extends Error {
constructor(value: number | bigint) {
super(
`RFC8794 Element ID VINT_DATA should be shortest, but ${value.toString(16)} is not`
);
}
}
export class VintOutOfRangeError extends Error {
constructor(value: number | bigint) {
super(
`RFC8794 VINT_DATA out of range 0 ~ 2^56-1, but got ${value.toString(16)}`
);
}
}
export class VintLengthOutOfRangeError extends Error {
constructor(length: number) {
super(
`RFC8794 Vint length out of range, valid vint range is 1 ~ 8 octet, but got ${length}`
);
}
}
export class UnsupportLengthForElementTypeError extends Error {
constructor(type: EbmlElementType, expected: string, found: string | number) {
super(
`RFC8794 type ${type} length should be ${expected}, but found ${found}`
);
}
}
export class OutOfRangeForElementTypeError extends Error {
constructor(
type: EbmlElementType,
expected: string,
found: string | number | bigint
) {
super(
`RFC8794 type ${type} value range should be ${expected}, but found ${found}`
);
}
}
export class InconsistentWellKnownEbmlTagTypeError extends Error {
constructor(id: EbmlTagIdType, type: EbmlElementType) {
super(
`Trying to create tag of well-known type "${id.toString(16)}" using content type "${type}" (which is incorrect). Either pass the correct type or ignore the type parameter to EbmlTag.create()`
);
}
}
export class InconsistentOffsetOnDecodingContentError extends Error {
constructor(tag: EbmlTagTrait, endOffset: number) {
super(
`Inconsistent offset on decoding content, startOffset(${tag.startOffset + tag.headerLength}) + contentLength(${tag.contentLength}) != endOffset(${endOffset}) of tag(${JSON.stringify(tag.toDebugRecord())})`
);
}
}
export class SizeUnitOutOfSafeIntegerRangeError extends Error {
constructor(size: number | bigint) {
super(
`Size unit ${size.toString(16)} is a valid vint, but out of ecmascript safe integer range`
);
}
}
export class UnreachableOrLogicError extends Error {
constructor(detail: string) {
super(`Unreachable or Logic Error: ${detail}`);
}
}
export class AbortReason extends Error {
isAbortReason = true;
constructor(reason: string) {
super(`Aborted: ${reason}`);
}
}
export class StreamFlushReason extends AbortReason {
constructor() {
super('stream flushed');
}
}
export class EbmlTreeMasterNotMatchError extends Error {
constructor(tag: EbmlTagTrait) {
super(
`start and end of master tag does not match in ebml tree at ${JSON.stringify(tag)}`
);
}
}

180
src/factory.ts Normal file
View File

@ -0,0 +1,180 @@
import { InconsistentWellKnownEbmlTagTypeError } from './errors';
import {
type EbmlMasterTagIdType,
type EbmlDataTagIdType,
type EbmlBlockTagIdType,
type EbmlSimpleBlockTagIdType,
EbmlElementType,
EbmlTagIdEnum,
isEbmlBlockTagId,
isEbmlSimpleBlockTagId,
isEbmlMasterTagId,
isEbmlUintDataTagId,
isEbmlIntDataTagId,
isEbmlFloatDataTagId,
isEbmlStringDataTagId,
isEbmlUtf8DataTagId,
isEbmlDateDataTagId,
isEbmlBinaryDataTagId,
} from './models/enums';
import {
type CreateEbmlBlockTagOptions,
EbmlBlockTag,
} from './models/tag-block';
import { type CreateEbmlDataTagOptions, EbmlDataTag } from './models/tag-data';
import {
type CreateEbmlMasterTagOptions,
EbmlMasterTag,
} from './models/tag-master';
import {
type CreateEbmlSimpleBlockTagOptions,
EbmlSimpleBlockTag,
} from './models/tag-simple-block';
import type { CreateEbmlTagOptions, EbmlTagTrait } from './models/tag-trait';
export function createEbmlTag(
id: EbmlMasterTagIdType,
options: Omit<CreateEbmlMasterTagOptions, 'id'>
): EbmlMasterTag;
export function createEbmlTag(
id: EbmlDataTagIdType,
options: Omit<CreateEbmlDataTagOptions, 'id'>
): EbmlDataTag;
export function createEbmlTag(
id: EbmlBlockTagIdType,
options: Omit<CreateEbmlBlockTagOptions, 'id'>
): EbmlBlockTag;
export function createEbmlTag(
id: EbmlSimpleBlockTagIdType,
options: Omit<CreateEbmlSimpleBlockTagOptions, 'id'>
): EbmlSimpleBlockTag;
export function createEbmlTag(
id: CreateEbmlTagOptions['id'],
options: Omit<CreateEbmlTagOptions, 'id'>
): EbmlTagTrait;
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
export function createEbmlTag(arg1: unknown, arg2: unknown): EbmlTagTrait {
const id = arg1 as EbmlTagIdEnum;
const options = arg2 as Omit<CreateEbmlTagOptions, 'id'>;
let type: EbmlElementType | undefined = options.type;
if (EbmlTagIdEnum[id] !== undefined) {
let foundType: EbmlElementType | undefined;
if (isEbmlBlockTagId(id)) {
return new EbmlBlockTag({ ...options, id });
}
if (isEbmlSimpleBlockTagId(id)) {
return new EbmlSimpleBlockTag({ ...options });
}
if (isEbmlMasterTagId(id)) {
foundType = EbmlElementType.Master;
} else if (isEbmlUintDataTagId(id)) {
foundType = EbmlElementType.UnsignedInt;
} else if (isEbmlIntDataTagId(id)) {
foundType = EbmlElementType.Integer;
} else if (isEbmlFloatDataTagId(id)) {
foundType = EbmlElementType.Float;
} else if (isEbmlStringDataTagId(id)) {
foundType = EbmlElementType.String;
} else if (isEbmlUtf8DataTagId(id)) {
foundType = EbmlElementType.UTF8;
} else if (isEbmlDateDataTagId(id)) {
foundType = EbmlElementType.Date;
} else if (isEbmlBinaryDataTagId(id)) {
foundType = EbmlElementType.Binary;
}
if (type === undefined) {
type = foundType;
}
if (type !== foundType) {
throw new InconsistentWellKnownEbmlTagTypeError(id, type!);
}
}
if (type === EbmlElementType.Master) {
return new EbmlMasterTag({
...options,
id: id as EbmlMasterTagIdType,
});
}
return new EbmlDataTag({
...options,
id,
type,
});
}
export type EncodeUselessCreateOptionsType =
| 'contentLength'
| 'headerLength'
| 'startOffset';
export function createEbmlTagForManuallyBuild(
id: EbmlMasterTagIdType,
options: Omit<
CreateEbmlMasterTagOptions,
'id' | EncodeUselessCreateOptionsType
> &
Partial<Pick<CreateEbmlMasterTagOptions, EncodeUselessCreateOptionsType>>
): EbmlMasterTag;
export function createEbmlTagForManuallyBuild(
id: EbmlDataTagIdType,
options: Omit<
CreateEbmlDataTagOptions,
'id' | EncodeUselessCreateOptionsType
> &
Partial<Pick<CreateEbmlMasterTagOptions, EncodeUselessCreateOptionsType>>
): EbmlDataTag;
export function createEbmlTagForManuallyBuild(
id: EbmlBlockTagIdType,
options: Omit<
CreateEbmlBlockTagOptions,
'id' | EncodeUselessCreateOptionsType
> &
Partial<Pick<CreateEbmlMasterTagOptions, EncodeUselessCreateOptionsType>>
): EbmlBlockTag;
export function createEbmlTagForManuallyBuild(
id: EbmlSimpleBlockTagIdType,
options: Omit<
CreateEbmlSimpleBlockTagOptions,
'id' | EncodeUselessCreateOptionsType
> &
Partial<Pick<CreateEbmlMasterTagOptions, EncodeUselessCreateOptionsType>>
): EbmlSimpleBlockTag;
export function createEbmlTagForManuallyBuild(
id: CreateEbmlTagOptions['id'],
options: Omit<CreateEbmlTagOptions, 'id' | EncodeUselessCreateOptionsType> &
Partial<Pick<CreateEbmlMasterTagOptions, EncodeUselessCreateOptionsType>>
): EbmlTagTrait;
export function createEbmlTagForManuallyBuild(
arg1: unknown,
arg2: unknown
): EbmlTagTrait {
const id = arg1 as CreateEbmlTagOptions['id'];
const options = arg2 as Omit<
CreateEbmlTagOptions,
'id' | EncodeUselessCreateOptionsType
> &
Partial<Pick<CreateEbmlMasterTagOptions, EncodeUselessCreateOptionsType>>;
return createEbmlTag(
id,
Object.assign(
{
contentLength: Number.NaN,
headerLength: Number.NaN,
startOffset: Number.NaN,
},
options
) as Omit<CreateEbmlTagOptions, 'id'>
);
}

47
src/index.ts Normal file
View File

@ -0,0 +1,47 @@
export { EbmlBlockTag } from './models/tag-block';
export { EbmlDataTag } from './models/tag-data';
export { EbmlMasterTag } from './models/tag-master';
export { EbmlSimpleBlockTag } from './models/tag-simple-block';
export { EbmlTagTrait } from './models/tag-trait';
export {
createEbmlTag,
createEbmlTagForManuallyBuild,
} from './factory';
export {
decodeEbmlTagHeader,
decodeEbmlContent,
} from './decode-utils';
export { EbmlStreamDecoder, EbmlDecodeStreamTransformer } from './decoder';
export {
EbmlStreamEncoder,
EbmlEncodeStreamTransformer,
} from './encoder';
export {
EbmlBlockLacing,
EbmlTagIdEnum,
type EbmlElementType,
type EbmlBinaryDataTagIdType,
type EbmlMasterTagIdType,
type EbmlBlockTagIdType,
type EbmlDataTagIdType,
type EbmlDateDataTagIdType,
type EbmlFloatDataTagIdType,
type EbmlIntDataTagIdType,
type EbmlSimpleBlockTagIdType,
type EbmlStringDataTagIdType,
type EbmlUintDataTagIdType,
type EbmlUtf8DataTagIdType,
type EbmlTagIdType,
EbmlTagPosition,
isEbmlBinaryDataTagId,
isEbmlBlockTagId,
isEbmlDateDataTagId,
isEbmlFloatDataTagId,
isEbmlIntDataTagId,
isEbmlMasterTagId,
isEbmlSimpleBlockTagId,
isEbmlStringDataTagId,
isEbmlUintDataTagId,
isEbmlUtf8DataTagId,
isUnknownDataTagId,
} from './models/enums';

995
src/models/enums.ts Normal file
View File

@ -0,0 +1,995 @@
export enum EbmlElementType {
Master = 'm',
UnsignedInt = 'u',
Integer = 'i',
String = 's',
UTF8 = '8',
Binary = 'b',
Float = 'f',
Date = 'd',
}
export enum EbmlTagPosition {
Start = 1,
Content = 2,
End = 3,
}
export enum EbmlTagIdEnum {
ChapterDisplay = 0x80,
TrackType = 0x83,
ChapString = 0x85,
CodecID = 0x86,
FlagDefault = 0x88,
ChapterTrackNumber = 0x89,
ChapterTimeStart = 0x91,
ChapterTimeEnd = 0x92,
CueRefTime = 0x96,
CueRefCluster = 0x97,
ChapterFlagHidden = 0x98,
ContentCompAlgo = 0x4254,
ContentCompSettings = 0x4255,
DocType = 0x4282,
DocTypeReadVersion = 0x4285,
EBMLVersion = 0x4286,
DocTypeVersion = 0x4287,
SegmentFamily = 0x4444,
DateUTC = 0x4461,
TagDefault = 0x4484,
TagBinary = 0x4485,
TagString = 0x4487,
Duration = 0x4489,
ChapterFlagEnabled = 0x4598,
FileMimeType = 0x4660,
FileUsedStartTime = 0x4661,
FileUsedEndTime = 0x4662,
FileReferral = 0x4675,
ContentEncodingOrder = 0x5031,
ContentEncodingScope = 0x5032,
ContentEncodingType = 0x5033,
ContentCompression = 0x5034,
ContentEncryption = 0x5035,
CueBlockNumber = 0x5378,
ChapterStringUID = 0x5654,
WritingApp = 0x5741,
SilentTracks = 0x5854,
ContentEncoding = 0x6240,
BitDepth = 0x6264,
SignedElement = 0x6532,
TrackTranslate = 0x6624,
ChapProcessCommand = 0x6911,
ChapProcessTime = 0x6922,
ChapterTranslate = 0x6924,
ChapProcessData = 0x6933,
ChapProcess = 0x6944,
ChapProcessCodecID = 0x6955,
Tag = 0x7373,
SegmentFilename = 0x7384,
AttachmentLink = 0x7446,
CodecName = 0x258688,
Segment = 0x18538067,
TagLanguage = 0x447a,
TagName = 0x45a3,
SimpleTag = 0x67c8,
TagAttachmentUID = 0x63c6,
TagChapterUID = 0x63c4,
TagEditionUID = 0x63c9,
TagTrackUID = 0x63c5,
TargetType = 0x63ca,
TargetTypeValue = 0x68ca,
Targets = 0x63c0,
Tags = 0x1254c367,
ChapProcessPrivate = 0x450d,
ChapCountry = 0x437e,
ChapLanguage = 0x437c,
ChapterTrack = 0x8f,
ChapterPhysicalEquiv = 0x63c3,
ChapterSegmentEditionUID = 0x6ebc,
ChapterSegmentUID = 0x6e67,
ChapterUID = 0x73c4,
ChapterAtom = 0xb6,
EditionFlagOrdered = 0x45dd,
EditionFlagDefault = 0x45db,
EditionFlagHidden = 0x45bd,
EditionUID = 0x45bc,
EditionEntry = 0x45b9,
Chapters = 0x1043a770,
FileUID = 0x46ae,
FileData = 0x465c,
FileName = 0x466e,
FileDescription = 0x467e,
AttachedFile = 0x61a7,
Attachments = 0x1941a469,
CueRefCodecState = 0xeb,
CueRefNumber = 0x535f,
CueReference = 0xdb,
CueCodecState = 0xea,
CueDuration = 0xb2,
CueRelativePosition = 0xf0,
CueClusterPosition = 0xf1,
CueTrack = 0xf7,
CueTrackPositions = 0xb7,
CueTime = 0xb3,
CuePoint = 0xbb,
Cues = 0x1c53bb6b,
AESSettingsCipherMode = 0x47e8,
ContentEncAESSettings = 0x47e7,
ContentSigHashAlgo = 0x47e6,
ContentSigAlgo = 0x47e5,
ContentSigKeyID = 0x47e4,
ContentSignature = 0x47e3,
ContentEncKeyID = 0x47e2,
ContentEncAlgo = 0x47e1,
ContentEncodings = 0x6d80,
TrickMasterTrackSegmentUID = 0xc4,
TrickMasterTrackUID = 0xc7,
TrickTrackFlag = 0xc6,
TrickTrackSegmentUID = 0xc1,
TrickTrackUID = 0xc0,
TrackJoinUID = 0xed,
TrackJoinBlocks = 0xe9,
TrackPlaneType = 0xe6,
TrackPlaneUID = 0xe5,
TrackPlane = 0xe4,
TrackCombinePlanes = 0xe3,
TrackOperation = 0xe2,
ChannelPositions = 0x7d7b,
Channels = 0x9f,
OutputSamplingFrequency = 0x78b5,
SamplingFrequency = 0xb5,
Audio = 0xe1,
FrameRate = 0x2383e3,
GammaValue = 0x2fb523,
ColourSpace = 0x2eb524,
AspectRatioType = 0x54b3,
DisplayUnit = 0x54b2,
DisplayHeight = 0x54ba,
DisplayWidth = 0x54b0,
PixelCropRight = 0x54dd,
PixelCropLeft = 0x54cc,
PixelCropTop = 0x54bb,
PixelCropBottom = 0x54aa,
PixelHeight = 0xba,
PixelWidth = 0xb0,
OldStereoMode = 0x53b9,
AlphaMode = 0x53c0,
StereoMode = 0x53b8,
FlagInterlaced = 0x9a,
Video = 0xe0,
TrackTranslateTrackID = 0x66a5,
TrackTranslateCodec = 0x66bf,
TrackTranslateEditionUID = 0x66fc,
SeekPreRoll = 0x56bb,
CodecDelay = 0x56aa,
TrackOverlay = 0x6fab,
CodecDecodeAll = 0xaa,
CodecDownloadURL = 0x26b240,
CodecInfoURL = 0x3b4040,
CodecSettings = 0x3a9697,
CodecPrivate = 0x63a2,
Language = 0x22b59c,
Name = 0x536e,
MaxBlockAdditionID = 0x55ee,
TrackOffset = 0x537f,
TrackTimecodeScale = 0x23314f,
DefaultDecodedFieldDuration = 0x234e7a,
DefaultDuration = 0x23e383,
MaxCache = 0x6df8,
MinCache = 0x6de7,
FlagLacing = 0x9c,
FlagForced = 0x55aa,
FlagEnabled = 0xb9,
TrackUID = 0x73c5,
TrackNumber = 0xd7,
TrackEntry = 0xae,
Tracks = 0x1654ae6b,
EncryptedBlock = 0xaf,
ReferenceTimeCode = 0xca,
ReferenceOffset = 0xc9,
ReferenceFrame = 0xc8,
SliceDuration = 0xcf,
Delay = 0xce,
BlockAdditionID = 0xcb,
FrameNumber = 0xcd,
LaceNumber = 0xcc,
TimeSlice = 0xe8,
Slices = 0x8e,
DiscardPadding = 0x75a2,
CodecState = 0xa4,
ReferenceVirtual = 0xfd,
ReferenceBlock = 0xfb,
ReferencePriority = 0xfa,
BlockDuration = 0x9b,
BlockAdditional = 0xa5,
BlockAddID = 0xee,
BlockMore = 0xa6,
BlockAdditions = 0x75a1,
BlockVirtual = 0xa2,
Block = 0xa1,
BlockGroup = 0xa0,
SimpleBlock = 0xa3,
PrevSize = 0xab,
Position = 0xa7,
SilentTrackNumber = 0x58d7,
Timecode = 0xe7,
Cluster = 0x1f43b675,
MuxingApp = 0x4d80,
Title = 0x7ba9,
TimecodeScaleDenominator = 0x2ad7b2,
TimecodeScale = 0x2ad7b1,
ChapterTranslateID = 0x69a5,
ChapterTranslateCodec = 0x69bf,
ChapterTranslateEditionUID = 0x69fc,
NextFilename = 0x3e83bb,
NextUID = 0x3eb923,
PrevFilename = 0x3c83ab,
PrevUID = 0x3cb923,
SegmentUID = 0x73a4,
Info = 0x1549a966,
SeekPosition = 0x53ac,
SeekID = 0x53ab,
Seek = 0x4dbb,
SeekHead = 0x114d9b74,
SignatureElementList = 0x7e7b,
SignatureElements = 0x7e5b,
Signature = 0x7eb5,
SignaturePublicKey = 0x7ea5,
SignatureHash = 0x7e9a,
SignatureAlgo = 0x7e8a,
SignatureSlot = 0x1b538667,
CRC32 = 0xbf,
Void = 0xec,
EBMLMaxSizeLength = 0x42f3,
EBMLMaxIDLength = 0x42f2,
EBMLReadVersion = 0x42f7,
EBML = 0x1a45dfa3,
}
export type EbmlTagIdType = EbmlTagIdEnum | number | bigint;
export type EbmlMasterTagIdType =
| EbmlTagIdEnum.ChapterDisplay
| EbmlTagIdEnum.ContentCompression
| EbmlTagIdEnum.ContentEncryption
| EbmlTagIdEnum.SilentTracks
| EbmlTagIdEnum.ContentEncoding
| EbmlTagIdEnum.TrackTranslate
| EbmlTagIdEnum.ChapProcessCommand
| EbmlTagIdEnum.ChapterTranslate
| EbmlTagIdEnum.ChapProcess
| EbmlTagIdEnum.Tag
| EbmlTagIdEnum.Segment
| EbmlTagIdEnum.SimpleTag
| EbmlTagIdEnum.Targets
| EbmlTagIdEnum.Tags
| EbmlTagIdEnum.ChapterTrack
| EbmlTagIdEnum.ChapterAtom
| EbmlTagIdEnum.EditionEntry
| EbmlTagIdEnum.Chapters
| EbmlTagIdEnum.AttachedFile
| EbmlTagIdEnum.Attachments
| EbmlTagIdEnum.CueReference
| EbmlTagIdEnum.CueTrackPositions
| EbmlTagIdEnum.CuePoint
| EbmlTagIdEnum.Cues
| EbmlTagIdEnum.ContentEncAESSettings
| EbmlTagIdEnum.ContentEncodings
| EbmlTagIdEnum.TrackJoinBlocks
| EbmlTagIdEnum.TrackPlane
| EbmlTagIdEnum.TrackCombinePlanes
| EbmlTagIdEnum.TrackOperation
| EbmlTagIdEnum.Audio
| EbmlTagIdEnum.Video
| EbmlTagIdEnum.TrackEntry
| EbmlTagIdEnum.Tracks
| EbmlTagIdEnum.ReferenceFrame
| EbmlTagIdEnum.TimeSlice
| EbmlTagIdEnum.Slices
| EbmlTagIdEnum.BlockMore
| EbmlTagIdEnum.BlockAdditions
| EbmlTagIdEnum.BlockGroup
| EbmlTagIdEnum.Cluster
| EbmlTagIdEnum.Info
| EbmlTagIdEnum.Seek
| EbmlTagIdEnum.SeekHead
| EbmlTagIdEnum.SignatureElementList
| EbmlTagIdEnum.SignatureElements
| EbmlTagIdEnum.SignatureSlot
| EbmlTagIdEnum.EBML;
export type EbmlDataTagIdType =
| EbmlTagIdEnum.TrackType
| EbmlTagIdEnum.FlagDefault
| EbmlTagIdEnum.ChapterTrackNumber
| EbmlTagIdEnum.ChapterTimeStart
| EbmlTagIdEnum.ChapterTimeEnd
| EbmlTagIdEnum.CueRefTime
| EbmlTagIdEnum.CueRefCluster
| EbmlTagIdEnum.ChapterFlagHidden
| EbmlTagIdEnum.ContentCompAlgo
| EbmlTagIdEnum.DocTypeReadVersion
| EbmlTagIdEnum.EBMLVersion
| EbmlTagIdEnum.DocTypeVersion
| EbmlTagIdEnum.TagDefault
| EbmlTagIdEnum.ChapterFlagEnabled
| EbmlTagIdEnum.FileUsedStartTime
| EbmlTagIdEnum.FileUsedEndTime
| EbmlTagIdEnum.ContentEncodingOrder
| EbmlTagIdEnum.ContentEncodingScope
| EbmlTagIdEnum.ContentEncodingType
| EbmlTagIdEnum.CueBlockNumber
| EbmlTagIdEnum.BitDepth
| EbmlTagIdEnum.ChapProcessTime
| EbmlTagIdEnum.ChapProcessCodecID
| EbmlTagIdEnum.AttachmentLink
| EbmlTagIdEnum.TagAttachmentUID
| EbmlTagIdEnum.TagChapterUID
| EbmlTagIdEnum.TagEditionUID
| EbmlTagIdEnum.TagTrackUID
| EbmlTagIdEnum.TargetTypeValue
| EbmlTagIdEnum.ChapterPhysicalEquiv
| EbmlTagIdEnum.ChapterSegmentEditionUID
| EbmlTagIdEnum.ChapterUID
| EbmlTagIdEnum.EditionFlagOrdered
| EbmlTagIdEnum.EditionFlagDefault
| EbmlTagIdEnum.EditionFlagHidden
| EbmlTagIdEnum.EditionUID
| EbmlTagIdEnum.FileUID
| EbmlTagIdEnum.CueRefCodecState
| EbmlTagIdEnum.CueRefNumber
| EbmlTagIdEnum.CueCodecState
| EbmlTagIdEnum.CueDuration
| EbmlTagIdEnum.CueRelativePosition
| EbmlTagIdEnum.CueClusterPosition
| EbmlTagIdEnum.CueTrack
| EbmlTagIdEnum.CueTime
| EbmlTagIdEnum.AESSettingsCipherMode
| EbmlTagIdEnum.ContentSigHashAlgo
| EbmlTagIdEnum.ContentSigAlgo
| EbmlTagIdEnum.ContentEncAlgo
| EbmlTagIdEnum.TrickMasterTrackUID
| EbmlTagIdEnum.TrickTrackFlag
| EbmlTagIdEnum.TrickTrackUID
| EbmlTagIdEnum.TrackJoinUID
| EbmlTagIdEnum.TrackPlaneType
| EbmlTagIdEnum.TrackPlaneUID
| EbmlTagIdEnum.Channels
| EbmlTagIdEnum.AspectRatioType
| EbmlTagIdEnum.DisplayUnit
| EbmlTagIdEnum.DisplayHeight
| EbmlTagIdEnum.DisplayWidth
| EbmlTagIdEnum.PixelCropRight
| EbmlTagIdEnum.PixelCropLeft
| EbmlTagIdEnum.PixelCropTop
| EbmlTagIdEnum.PixelCropBottom
| EbmlTagIdEnum.PixelHeight
| EbmlTagIdEnum.PixelWidth
| EbmlTagIdEnum.OldStereoMode
| EbmlTagIdEnum.AlphaMode
| EbmlTagIdEnum.StereoMode
| EbmlTagIdEnum.FlagInterlaced
| EbmlTagIdEnum.TrackTranslateCodec
| EbmlTagIdEnum.TrackTranslateEditionUID
| EbmlTagIdEnum.SeekPreRoll
| EbmlTagIdEnum.CodecDelay
| EbmlTagIdEnum.TrackOverlay
| EbmlTagIdEnum.CodecDecodeAll
| EbmlTagIdEnum.MaxBlockAdditionID
| EbmlTagIdEnum.DefaultDecodedFieldDuration
| EbmlTagIdEnum.DefaultDuration
| EbmlTagIdEnum.MaxCache
| EbmlTagIdEnum.MinCache
| EbmlTagIdEnum.FlagLacing
| EbmlTagIdEnum.FlagForced
| EbmlTagIdEnum.FlagEnabled
| EbmlTagIdEnum.TrackUID
| EbmlTagIdEnum.TrackNumber
| EbmlTagIdEnum.ReferenceTimeCode
| EbmlTagIdEnum.ReferenceOffset
| EbmlTagIdEnum.SliceDuration
| EbmlTagIdEnum.Delay
| EbmlTagIdEnum.BlockAdditionID
| EbmlTagIdEnum.FrameNumber
| EbmlTagIdEnum.LaceNumber
| EbmlTagIdEnum.ReferencePriority
| EbmlTagIdEnum.BlockDuration
| EbmlTagIdEnum.BlockAddID
| EbmlTagIdEnum.PrevSize
| EbmlTagIdEnum.Position
| EbmlTagIdEnum.SilentTrackNumber
| EbmlTagIdEnum.Timecode
| EbmlTagIdEnum.TimecodeScaleDenominator
| EbmlTagIdEnum.TimecodeScale
| EbmlTagIdEnum.ChapterTranslateCodec
| EbmlTagIdEnum.ChapterTranslateEditionUID
| EbmlTagIdEnum.SeekPosition
| EbmlTagIdEnum.SignatureHash
| EbmlTagIdEnum.SignatureAlgo
| EbmlTagIdEnum.EBMLMaxSizeLength
| EbmlTagIdEnum.EBMLMaxIDLength
| EbmlTagIdEnum.EBMLReadVersion
| EbmlTagIdEnum.TrackOffset
| EbmlTagIdEnum.DiscardPadding
| EbmlTagIdEnum.ReferenceVirtual
| EbmlTagIdEnum.ReferenceBlock
| EbmlTagIdEnum.CodecID
| EbmlTagIdEnum.DocType
| EbmlTagIdEnum.FileMimeType
| EbmlTagIdEnum.TagLanguage
| EbmlTagIdEnum.TargetType
| EbmlTagIdEnum.ChapCountry
| EbmlTagIdEnum.ChapLanguage
| EbmlTagIdEnum.CodecDownloadURL
| EbmlTagIdEnum.CodecInfoURL
| EbmlTagIdEnum.Language
| EbmlTagIdEnum.ChapString
| EbmlTagIdEnum.TagString
| EbmlTagIdEnum.ChapterStringUID
| EbmlTagIdEnum.WritingApp
| EbmlTagIdEnum.SegmentFilename
| EbmlTagIdEnum.CodecName
| EbmlTagIdEnum.TagName
| EbmlTagIdEnum.FileName
| EbmlTagIdEnum.FileDescription
| EbmlTagIdEnum.CodecSettings
| EbmlTagIdEnum.Name
| EbmlTagIdEnum.MuxingApp
| EbmlTagIdEnum.Title
| EbmlTagIdEnum.NextFilename
| EbmlTagIdEnum.PrevFilename
| EbmlTagIdEnum.ContentCompSettings
| EbmlTagIdEnum.SegmentFamily
| EbmlTagIdEnum.TagBinary
| EbmlTagIdEnum.FileReferral
| EbmlTagIdEnum.SignedElement
| EbmlTagIdEnum.ChapProcessData
| EbmlTagIdEnum.ChapProcessPrivate
| EbmlTagIdEnum.ChapterSegmentUID
| EbmlTagIdEnum.FileData
| EbmlTagIdEnum.ContentSigKeyID
| EbmlTagIdEnum.ContentSignature
| EbmlTagIdEnum.ContentEncKeyID
| EbmlTagIdEnum.TrickMasterTrackSegmentUID
| EbmlTagIdEnum.TrickTrackSegmentUID
| EbmlTagIdEnum.ChannelPositions
| EbmlTagIdEnum.ColourSpace
| EbmlTagIdEnum.TrackTranslateTrackID
| EbmlTagIdEnum.CodecPrivate
| EbmlTagIdEnum.EncryptedBlock
| EbmlTagIdEnum.CodecState
| EbmlTagIdEnum.BlockAdditional
| EbmlTagIdEnum.BlockVirtual
| EbmlTagIdEnum.ChapterTranslateID
| EbmlTagIdEnum.NextUID
| EbmlTagIdEnum.PrevUID
| EbmlTagIdEnum.SegmentUID
| EbmlTagIdEnum.SeekID
| EbmlTagIdEnum.Signature
| EbmlTagIdEnum.SignaturePublicKey
| EbmlTagIdEnum.CRC32
| EbmlTagIdEnum.Void
| EbmlTagIdEnum.Duration
| EbmlTagIdEnum.OutputSamplingFrequency
| EbmlTagIdEnum.SamplingFrequency
| EbmlTagIdEnum.FrameRate
| EbmlTagIdEnum.GammaValue
| EbmlTagIdEnum.TrackTimecodeScale
| EbmlTagIdEnum.DateUTC;
export type EbmlBlockTagIdType = EbmlTagIdEnum.Block;
export type EbmlSimpleBlockTagIdType = EbmlTagIdEnum.SimpleBlock;
export type EbmlUintDataTagIdType =
| EbmlTagIdEnum.TrackType
| EbmlTagIdEnum.FlagDefault
| EbmlTagIdEnum.ChapterTrackNumber
| EbmlTagIdEnum.ChapterTimeStart
| EbmlTagIdEnum.ChapterTimeEnd
| EbmlTagIdEnum.CueRefTime
| EbmlTagIdEnum.CueRefCluster
| EbmlTagIdEnum.ChapterFlagHidden
| EbmlTagIdEnum.ContentCompAlgo
| EbmlTagIdEnum.DocTypeReadVersion
| EbmlTagIdEnum.EBMLVersion
| EbmlTagIdEnum.DocTypeVersion
| EbmlTagIdEnum.TagDefault
| EbmlTagIdEnum.ChapterFlagEnabled
| EbmlTagIdEnum.FileUsedStartTime
| EbmlTagIdEnum.FileUsedEndTime
| EbmlTagIdEnum.ContentEncodingOrder
| EbmlTagIdEnum.ContentEncodingScope
| EbmlTagIdEnum.ContentEncodingType
| EbmlTagIdEnum.CueBlockNumber
| EbmlTagIdEnum.BitDepth
| EbmlTagIdEnum.ChapProcessTime
| EbmlTagIdEnum.ChapProcessCodecID
| EbmlTagIdEnum.AttachmentLink
| EbmlTagIdEnum.TagAttachmentUID
| EbmlTagIdEnum.TagChapterUID
| EbmlTagIdEnum.TagEditionUID
| EbmlTagIdEnum.TagTrackUID
| EbmlTagIdEnum.TargetTypeValue
| EbmlTagIdEnum.ChapterPhysicalEquiv
| EbmlTagIdEnum.ChapterSegmentEditionUID
| EbmlTagIdEnum.ChapterUID
| EbmlTagIdEnum.EditionFlagOrdered
| EbmlTagIdEnum.EditionFlagDefault
| EbmlTagIdEnum.EditionFlagHidden
| EbmlTagIdEnum.EditionUID
| EbmlTagIdEnum.FileUID
| EbmlTagIdEnum.CueRefCodecState
| EbmlTagIdEnum.CueRefNumber
| EbmlTagIdEnum.CueCodecState
| EbmlTagIdEnum.CueDuration
| EbmlTagIdEnum.CueRelativePosition
| EbmlTagIdEnum.CueClusterPosition
| EbmlTagIdEnum.CueTrack
| EbmlTagIdEnum.CueTime
| EbmlTagIdEnum.AESSettingsCipherMode
| EbmlTagIdEnum.ContentSigHashAlgo
| EbmlTagIdEnum.ContentSigAlgo
| EbmlTagIdEnum.ContentEncAlgo
| EbmlTagIdEnum.TrickMasterTrackUID
| EbmlTagIdEnum.TrickTrackFlag
| EbmlTagIdEnum.TrickTrackUID
| EbmlTagIdEnum.TrackJoinUID
| EbmlTagIdEnum.TrackPlaneType
| EbmlTagIdEnum.TrackPlaneUID
| EbmlTagIdEnum.Channels
| EbmlTagIdEnum.AspectRatioType
| EbmlTagIdEnum.DisplayUnit
| EbmlTagIdEnum.DisplayHeight
| EbmlTagIdEnum.DisplayWidth
| EbmlTagIdEnum.PixelCropRight
| EbmlTagIdEnum.PixelCropLeft
| EbmlTagIdEnum.PixelCropTop
| EbmlTagIdEnum.PixelCropBottom
| EbmlTagIdEnum.PixelHeight
| EbmlTagIdEnum.PixelWidth
| EbmlTagIdEnum.OldStereoMode
| EbmlTagIdEnum.AlphaMode
| EbmlTagIdEnum.StereoMode
| EbmlTagIdEnum.FlagInterlaced
| EbmlTagIdEnum.TrackTranslateCodec
| EbmlTagIdEnum.TrackTranslateEditionUID
| EbmlTagIdEnum.SeekPreRoll
| EbmlTagIdEnum.CodecDelay
| EbmlTagIdEnum.TrackOverlay
| EbmlTagIdEnum.CodecDecodeAll
| EbmlTagIdEnum.MaxBlockAdditionID
| EbmlTagIdEnum.DefaultDecodedFieldDuration
| EbmlTagIdEnum.DefaultDuration
| EbmlTagIdEnum.MaxCache
| EbmlTagIdEnum.MinCache
| EbmlTagIdEnum.FlagLacing
| EbmlTagIdEnum.FlagForced
| EbmlTagIdEnum.FlagEnabled
| EbmlTagIdEnum.TrackUID
| EbmlTagIdEnum.TrackNumber
| EbmlTagIdEnum.ReferenceTimeCode
| EbmlTagIdEnum.ReferenceOffset
| EbmlTagIdEnum.SliceDuration
| EbmlTagIdEnum.Delay
| EbmlTagIdEnum.BlockAdditionID
| EbmlTagIdEnum.FrameNumber
| EbmlTagIdEnum.LaceNumber
| EbmlTagIdEnum.ReferencePriority
| EbmlTagIdEnum.BlockDuration
| EbmlTagIdEnum.BlockAddID
| EbmlTagIdEnum.PrevSize
| EbmlTagIdEnum.Position
| EbmlTagIdEnum.SilentTrackNumber
| EbmlTagIdEnum.Timecode
| EbmlTagIdEnum.TimecodeScaleDenominator
| EbmlTagIdEnum.TimecodeScale
| EbmlTagIdEnum.ChapterTranslateCodec
| EbmlTagIdEnum.ChapterTranslateEditionUID
| EbmlTagIdEnum.SeekPosition
| EbmlTagIdEnum.SignatureHash
| EbmlTagIdEnum.SignatureAlgo
| EbmlTagIdEnum.EBMLMaxSizeLength
| EbmlTagIdEnum.EBMLMaxIDLength
| EbmlTagIdEnum.EBMLReadVersion;
export type EbmlIntDataTagIdType =
| EbmlTagIdEnum.TrackOffset
| EbmlTagIdEnum.DiscardPadding
| EbmlTagIdEnum.ReferenceVirtual
| EbmlTagIdEnum.ReferenceBlock;
export type EbmlStringDataTagIdType =
| EbmlTagIdEnum.CodecID
| EbmlTagIdEnum.DocType
| EbmlTagIdEnum.FileMimeType
| EbmlTagIdEnum.TagLanguage
| EbmlTagIdEnum.TargetType
| EbmlTagIdEnum.ChapCountry
| EbmlTagIdEnum.ChapLanguage
| EbmlTagIdEnum.CodecDownloadURL
| EbmlTagIdEnum.CodecInfoURL
| EbmlTagIdEnum.Language;
export type EbmlUtf8DataTagIdType =
| EbmlTagIdEnum.ChapString
| EbmlTagIdEnum.TagString
| EbmlTagIdEnum.ChapterStringUID
| EbmlTagIdEnum.WritingApp
| EbmlTagIdEnum.SegmentFilename
| EbmlTagIdEnum.CodecName
| EbmlTagIdEnum.TagName
| EbmlTagIdEnum.FileName
| EbmlTagIdEnum.FileDescription
| EbmlTagIdEnum.CodecSettings
| EbmlTagIdEnum.Name
| EbmlTagIdEnum.MuxingApp
| EbmlTagIdEnum.Title
| EbmlTagIdEnum.NextFilename
| EbmlTagIdEnum.PrevFilename;
export type EbmlFloatDataTagIdType =
| EbmlTagIdEnum.Duration
| EbmlTagIdEnum.OutputSamplingFrequency
| EbmlTagIdEnum.SamplingFrequency
| EbmlTagIdEnum.FrameRate
| EbmlTagIdEnum.GammaValue
| EbmlTagIdEnum.TrackTimecodeScale;
export type EbmlDateDataTagIdType = EbmlTagIdEnum.DateUTC;
export type EbmlBinaryDataTagIdType =
| EbmlTagIdEnum.ContentCompSettings
| EbmlTagIdEnum.SegmentFamily
| EbmlTagIdEnum.TagBinary
| EbmlTagIdEnum.FileReferral
| EbmlTagIdEnum.SignedElement
| EbmlTagIdEnum.ChapProcessData
| EbmlTagIdEnum.ChapProcessPrivate
| EbmlTagIdEnum.ChapterSegmentUID
| EbmlTagIdEnum.FileData
| EbmlTagIdEnum.ContentSigKeyID
| EbmlTagIdEnum.ContentSignature
| EbmlTagIdEnum.ContentEncKeyID
| EbmlTagIdEnum.TrickMasterTrackSegmentUID
| EbmlTagIdEnum.TrickTrackSegmentUID
| EbmlTagIdEnum.ChannelPositions
| EbmlTagIdEnum.ColourSpace
| EbmlTagIdEnum.TrackTranslateTrackID
| EbmlTagIdEnum.CodecPrivate
| EbmlTagIdEnum.EncryptedBlock
| EbmlTagIdEnum.CodecState
| EbmlTagIdEnum.BlockAdditional
| EbmlTagIdEnum.BlockVirtual
| EbmlTagIdEnum.ChapterTranslateID
| EbmlTagIdEnum.NextUID
| EbmlTagIdEnum.PrevUID
| EbmlTagIdEnum.SegmentUID
| EbmlTagIdEnum.SeekID
| EbmlTagIdEnum.Signature
| EbmlTagIdEnum.SignaturePublicKey
| EbmlTagIdEnum.CRC32
| EbmlTagIdEnum.Void;
export function isEbmlMasterTagId(
tagId: EbmlTagIdType
): tagId is EbmlMasterTagIdType {
switch (tagId) {
case EbmlTagIdEnum.ChapterDisplay:
case EbmlTagIdEnum.ContentCompression:
case EbmlTagIdEnum.ContentEncryption:
case EbmlTagIdEnum.SilentTracks:
case EbmlTagIdEnum.ContentEncoding:
case EbmlTagIdEnum.TrackTranslate:
case EbmlTagIdEnum.ChapProcessCommand:
case EbmlTagIdEnum.ChapterTranslate:
case EbmlTagIdEnum.ChapProcess:
case EbmlTagIdEnum.Tag:
case EbmlTagIdEnum.Segment:
case EbmlTagIdEnum.SimpleTag:
case EbmlTagIdEnum.Targets:
case EbmlTagIdEnum.Tags:
case EbmlTagIdEnum.ChapterTrack:
case EbmlTagIdEnum.ChapterAtom:
case EbmlTagIdEnum.EditionEntry:
case EbmlTagIdEnum.Chapters:
case EbmlTagIdEnum.AttachedFile:
case EbmlTagIdEnum.Attachments:
case EbmlTagIdEnum.CueReference:
case EbmlTagIdEnum.CueTrackPositions:
case EbmlTagIdEnum.CuePoint:
case EbmlTagIdEnum.Cues:
case EbmlTagIdEnum.ContentEncAESSettings:
case EbmlTagIdEnum.ContentEncodings:
case EbmlTagIdEnum.TrackJoinBlocks:
case EbmlTagIdEnum.TrackPlane:
case EbmlTagIdEnum.TrackCombinePlanes:
case EbmlTagIdEnum.TrackOperation:
case EbmlTagIdEnum.Audio:
case EbmlTagIdEnum.Video:
case EbmlTagIdEnum.TrackEntry:
case EbmlTagIdEnum.Tracks:
case EbmlTagIdEnum.ReferenceFrame:
case EbmlTagIdEnum.TimeSlice:
case EbmlTagIdEnum.Slices:
case EbmlTagIdEnum.BlockMore:
case EbmlTagIdEnum.BlockAdditions:
case EbmlTagIdEnum.BlockGroup:
case EbmlTagIdEnum.Cluster:
case EbmlTagIdEnum.Info:
case EbmlTagIdEnum.Seek:
case EbmlTagIdEnum.SeekHead:
case EbmlTagIdEnum.SignatureElementList:
case EbmlTagIdEnum.SignatureElements:
case EbmlTagIdEnum.SignatureSlot:
case EbmlTagIdEnum.EBML:
return true;
default:
return false;
}
}
export function isEbmlBlockTagId(
tagId: EbmlTagIdType
): tagId is EbmlBlockTagIdType {
return tagId === EbmlTagIdEnum.Block;
}
export function isEbmlSimpleBlockTagId(
tagId: EbmlTagIdType
): tagId is EbmlSimpleBlockTagIdType {
return tagId === EbmlTagIdEnum.SimpleBlock;
}
export function isEbmlUintDataTagId(
tagId: EbmlTagIdType
): tagId is EbmlUintDataTagIdType {
switch (tagId) {
case EbmlTagIdEnum.TrackType:
case EbmlTagIdEnum.FlagDefault:
case EbmlTagIdEnum.ChapterTrackNumber:
case EbmlTagIdEnum.ChapterTimeStart:
case EbmlTagIdEnum.ChapterTimeEnd:
case EbmlTagIdEnum.CueRefTime:
case EbmlTagIdEnum.CueRefCluster:
case EbmlTagIdEnum.ChapterFlagHidden:
case EbmlTagIdEnum.ContentCompAlgo:
case EbmlTagIdEnum.DocTypeReadVersion:
case EbmlTagIdEnum.EBMLVersion:
case EbmlTagIdEnum.DocTypeVersion:
case EbmlTagIdEnum.TagDefault:
case EbmlTagIdEnum.ChapterFlagEnabled:
case EbmlTagIdEnum.FileUsedStartTime:
case EbmlTagIdEnum.FileUsedEndTime:
case EbmlTagIdEnum.ContentEncodingOrder:
case EbmlTagIdEnum.ContentEncodingScope:
case EbmlTagIdEnum.ContentEncodingType:
case EbmlTagIdEnum.CueBlockNumber:
case EbmlTagIdEnum.BitDepth:
case EbmlTagIdEnum.ChapProcessTime:
case EbmlTagIdEnum.ChapProcessCodecID:
case EbmlTagIdEnum.AttachmentLink:
case EbmlTagIdEnum.TagAttachmentUID:
case EbmlTagIdEnum.TagChapterUID:
case EbmlTagIdEnum.TagEditionUID:
case EbmlTagIdEnum.TagTrackUID:
case EbmlTagIdEnum.TargetTypeValue:
case EbmlTagIdEnum.ChapterPhysicalEquiv:
case EbmlTagIdEnum.ChapterSegmentEditionUID:
case EbmlTagIdEnum.ChapterUID:
case EbmlTagIdEnum.EditionFlagOrdered:
case EbmlTagIdEnum.EditionFlagDefault:
case EbmlTagIdEnum.EditionFlagHidden:
case EbmlTagIdEnum.EditionUID:
case EbmlTagIdEnum.FileUID:
case EbmlTagIdEnum.CueRefCodecState:
case EbmlTagIdEnum.CueRefNumber:
case EbmlTagIdEnum.CueCodecState:
case EbmlTagIdEnum.CueDuration:
case EbmlTagIdEnum.CueRelativePosition:
case EbmlTagIdEnum.CueClusterPosition:
case EbmlTagIdEnum.CueTrack:
case EbmlTagIdEnum.CueTime:
case EbmlTagIdEnum.AESSettingsCipherMode:
case EbmlTagIdEnum.ContentSigHashAlgo:
case EbmlTagIdEnum.ContentSigAlgo:
case EbmlTagIdEnum.ContentEncAlgo:
case EbmlTagIdEnum.TrickMasterTrackUID:
case EbmlTagIdEnum.TrickTrackFlag:
case EbmlTagIdEnum.TrickTrackUID:
case EbmlTagIdEnum.TrackJoinUID:
case EbmlTagIdEnum.TrackPlaneType:
case EbmlTagIdEnum.TrackPlaneUID:
case EbmlTagIdEnum.Channels:
case EbmlTagIdEnum.AspectRatioType:
case EbmlTagIdEnum.DisplayUnit:
case EbmlTagIdEnum.DisplayHeight:
case EbmlTagIdEnum.DisplayWidth:
case EbmlTagIdEnum.PixelCropRight:
case EbmlTagIdEnum.PixelCropLeft:
case EbmlTagIdEnum.PixelCropTop:
case EbmlTagIdEnum.PixelCropBottom:
case EbmlTagIdEnum.PixelHeight:
case EbmlTagIdEnum.PixelWidth:
case EbmlTagIdEnum.OldStereoMode:
case EbmlTagIdEnum.AlphaMode:
case EbmlTagIdEnum.StereoMode:
case EbmlTagIdEnum.FlagInterlaced:
case EbmlTagIdEnum.TrackTranslateCodec:
case EbmlTagIdEnum.TrackTranslateEditionUID:
case EbmlTagIdEnum.SeekPreRoll:
case EbmlTagIdEnum.CodecDelay:
case EbmlTagIdEnum.TrackOverlay:
case EbmlTagIdEnum.CodecDecodeAll:
case EbmlTagIdEnum.MaxBlockAdditionID:
case EbmlTagIdEnum.DefaultDecodedFieldDuration:
case EbmlTagIdEnum.DefaultDuration:
case EbmlTagIdEnum.MaxCache:
case EbmlTagIdEnum.MinCache:
case EbmlTagIdEnum.FlagLacing:
case EbmlTagIdEnum.FlagForced:
case EbmlTagIdEnum.FlagEnabled:
case EbmlTagIdEnum.TrackUID:
case EbmlTagIdEnum.TrackNumber:
case EbmlTagIdEnum.ReferenceTimeCode:
case EbmlTagIdEnum.ReferenceOffset:
case EbmlTagIdEnum.SliceDuration:
case EbmlTagIdEnum.Delay:
case EbmlTagIdEnum.BlockAdditionID:
case EbmlTagIdEnum.FrameNumber:
case EbmlTagIdEnum.LaceNumber:
case EbmlTagIdEnum.ReferencePriority:
case EbmlTagIdEnum.BlockDuration:
case EbmlTagIdEnum.BlockAddID:
case EbmlTagIdEnum.PrevSize:
case EbmlTagIdEnum.Position:
case EbmlTagIdEnum.SilentTrackNumber:
case EbmlTagIdEnum.Timecode:
case EbmlTagIdEnum.TimecodeScaleDenominator:
case EbmlTagIdEnum.TimecodeScale:
case EbmlTagIdEnum.ChapterTranslateCodec:
case EbmlTagIdEnum.ChapterTranslateEditionUID:
case EbmlTagIdEnum.SeekPosition:
case EbmlTagIdEnum.SignatureHash:
case EbmlTagIdEnum.SignatureAlgo:
case EbmlTagIdEnum.EBMLMaxSizeLength:
case EbmlTagIdEnum.EBMLMaxIDLength:
case EbmlTagIdEnum.EBMLReadVersion:
return true;
default:
return false;
}
}
export function isEbmlIntDataTagId(
tagId: EbmlTagIdType
): tagId is EbmlIntDataTagIdType {
switch (tagId) {
case EbmlTagIdEnum.TrackOffset:
case EbmlTagIdEnum.DiscardPadding:
case EbmlTagIdEnum.ReferenceVirtual:
case EbmlTagIdEnum.ReferenceBlock:
return true;
default:
return false;
}
}
export function isEbmlFloatDataTagId(
tagId: EbmlTagIdType
): tagId is EbmlFloatDataTagIdType {
switch (tagId) {
case EbmlTagIdEnum.Duration:
case EbmlTagIdEnum.OutputSamplingFrequency:
case EbmlTagIdEnum.SamplingFrequency:
case EbmlTagIdEnum.FrameRate:
case EbmlTagIdEnum.GammaValue:
case EbmlTagIdEnum.TrackTimecodeScale:
return true;
default:
return false;
}
}
export function isEbmlStringDataTagId(
tagId: EbmlTagIdType
): tagId is EbmlStringDataTagIdType {
switch (tagId) {
case EbmlTagIdEnum.CodecID:
case EbmlTagIdEnum.DocType:
case EbmlTagIdEnum.FileMimeType:
case EbmlTagIdEnum.TagLanguage:
case EbmlTagIdEnum.TargetType:
case EbmlTagIdEnum.ChapCountry:
case EbmlTagIdEnum.ChapLanguage:
case EbmlTagIdEnum.CodecDownloadURL:
case EbmlTagIdEnum.CodecInfoURL:
case EbmlTagIdEnum.Language:
return true;
default:
return false;
}
}
export function isEbmlUtf8DataTagId(
tagId: EbmlTagIdType
): tagId is EbmlUtf8DataTagIdType {
switch (tagId) {
case EbmlTagIdEnum.ChapString:
case EbmlTagIdEnum.TagString:
case EbmlTagIdEnum.ChapterStringUID:
case EbmlTagIdEnum.WritingApp:
case EbmlTagIdEnum.SegmentFilename:
case EbmlTagIdEnum.CodecName:
case EbmlTagIdEnum.TagName:
case EbmlTagIdEnum.FileName:
case EbmlTagIdEnum.FileDescription:
case EbmlTagIdEnum.CodecSettings:
case EbmlTagIdEnum.Name:
case EbmlTagIdEnum.MuxingApp:
case EbmlTagIdEnum.Title:
case EbmlTagIdEnum.NextFilename:
case EbmlTagIdEnum.PrevFilename:
return true;
default:
return false;
}
}
export function isEbmlDateDataTagId(
tagId: EbmlTagIdType
): tagId is EbmlDateDataTagIdType {
return tagId === EbmlTagIdEnum.DateUTC;
}
export function isEbmlBinaryDataTagId(
tagId: EbmlTagIdType
): tagId is EbmlBinaryDataTagIdType {
switch (tagId) {
case EbmlTagIdEnum.ContentCompSettings:
case EbmlTagIdEnum.SegmentFamily:
case EbmlTagIdEnum.TagBinary:
case EbmlTagIdEnum.FileReferral:
case EbmlTagIdEnum.SignedElement:
case EbmlTagIdEnum.ChapProcessData:
case EbmlTagIdEnum.ChapProcessPrivate:
case EbmlTagIdEnum.ChapterSegmentUID:
case EbmlTagIdEnum.FileData:
case EbmlTagIdEnum.ContentSigKeyID:
case EbmlTagIdEnum.ContentSignature:
case EbmlTagIdEnum.ContentEncKeyID:
case EbmlTagIdEnum.TrickMasterTrackSegmentUID:
case EbmlTagIdEnum.TrickTrackSegmentUID:
case EbmlTagIdEnum.ChannelPositions:
case EbmlTagIdEnum.ColourSpace:
case EbmlTagIdEnum.TrackTranslateTrackID:
case EbmlTagIdEnum.CodecPrivate:
case EbmlTagIdEnum.EncryptedBlock:
case EbmlTagIdEnum.CodecState:
case EbmlTagIdEnum.BlockAdditional:
case EbmlTagIdEnum.BlockVirtual:
case EbmlTagIdEnum.ChapterTranslateID:
case EbmlTagIdEnum.NextUID:
case EbmlTagIdEnum.PrevUID:
case EbmlTagIdEnum.SegmentUID:
case EbmlTagIdEnum.SeekID:
case EbmlTagIdEnum.Signature:
case EbmlTagIdEnum.SignaturePublicKey:
case EbmlTagIdEnum.CRC32:
case EbmlTagIdEnum.Void:
return true;
default:
return false;
}
}
export function isUnknownDataTagId(
tagId: EbmlDataTagIdType
): tagId is Exclude<EbmlDataTagIdType, EbmlTagIdEnum> {
return tagId in EbmlTagIdEnum;
}
export enum EbmlBlockLacing {
None = 1,
Xiph = 2,
EBML = 3,
FixedSize = 4,
}

123
src/models/tag-block.ts Normal file
View File

@ -0,0 +1,123 @@
import { type CreateEbmlDataTagOptions, EbmlDataTag } from './tag-data';
import { EbmlBlockLacing } from './enums';
import {
dataViewSlice,
dataViewSliceToBuf,
readSigned,
readVint,
writeSigned,
writeVint,
} from '../tools';
import {
type EbmlBlockTagIdType,
type EbmlSimpleBlockTagIdType,
EbmlTagIdEnum,
} from './enums';
import { EbmlElementType } from './enums';
import type { FileDataViewController } from '../adapters';
export interface CreateEbmlBlockTagOptions
extends Omit<CreateEbmlDataTagOptions, 'id' | 'type'> {
id?: EbmlBlockTagIdType | EbmlSimpleBlockTagIdType;
}
export class EbmlBlockTag extends EbmlDataTag {
payload = new Uint8Array(0);
track: number | bigint = 0;
value = 0;
invisible: boolean | undefined;
lacing: EbmlBlockLacing | undefined;
constructor(options: CreateEbmlBlockTagOptions) {
super({
...options,
id: options.id ?? EbmlTagIdEnum.Block,
type: EbmlElementType.Binary,
});
}
protected writeTrackBuffer(): Uint8Array {
return writeVint(this.track);
}
protected writeValueBuffer(): Uint8Array {
return writeSigned(this.value, 2);
}
protected writeFlagsBuffer(): Uint8Array {
let flags = 0x00;
if (this.invisible) {
flags |= 0x10;
}
switch (this.lacing) {
case EbmlBlockLacing.None:
break;
case EbmlBlockLacing.Xiph:
flags |= 0x04;
break;
case EbmlBlockLacing.EBML:
flags |= 0x08;
break;
case EbmlBlockLacing.FixedSize:
flags |= 0x0c;
break;
default:
}
return new Uint8Array([flags % 256]);
}
*encodeContent(): Generator<Uint8Array, void, unknown> {
yield this.writeTrackBuffer();
yield this.writeValueBuffer();
yield this.writeFlagsBuffer();
yield this.payload;
}
// biome-ignore lint/correctness/useYield: <explanation>
async *decodeContentImpl(controller: FileDataViewController) {
const offset = controller.getOffset();
const view = await controller.read(offset, this.contentLength, true);
const track = readVint(view)!;
this.track = track.value;
this.value = Number(
readSigned(dataViewSlice(view, track.length, track.length + 2))
);
const flags: number = view.getUint8(track.length + 2);
this.invisible = Boolean(flags & 0x10);
switch (flags & 0x0c) {
case 0x00:
this.lacing = EbmlBlockLacing.None;
break;
case 0x04:
this.lacing = EbmlBlockLacing.Xiph;
break;
case 0x08:
this.lacing = EbmlBlockLacing.EBML;
break;
case 0x0c:
this.lacing = EbmlBlockLacing.FixedSize;
break;
default:
}
this.payload = dataViewSliceToBuf(view, track.length + 3, undefined);
await controller.seek(offset + view.byteLength);
}
override toDebugRecord() {
const s = super.toDebugRecord();
return {
...s,
payload: this.payload,
track: this.track,
value: this.value,
invisible: this.invisible,
lacing: EbmlBlockLacing[this.lacing!] || this.lacing,
};
}
}

92
src/models/tag-data.ts Normal file
View File

@ -0,0 +1,92 @@
import { type CreateEbmlTagOptions, EbmlTagTrait } from './tag-trait';
import { EbmlElementType } from './enums';
import {
dataViewSlice,
dataViewSliceToBuf,
readAscii,
readFloat,
readSigned,
readUnsigned,
readUtf8,
writeAscii,
writeFloat,
writeSigned,
writeUnsigned,
writeUtf8,
} from '../tools';
import { EbmlTagPosition } from './enums';
import type { FileDataViewController } from 'src/adapters';
export type CreateEbmlDataTagOptions = Omit<CreateEbmlTagOptions, 'position'>;
export class EbmlDataTag extends EbmlTagTrait {
data: number | string | bigint | null | Uint8Array | undefined;
constructor(options: CreateEbmlDataTagOptions) {
super({
...options,
position: EbmlTagPosition.Content,
});
}
// biome-ignore lint/correctness/useYield: <explanation>
override async *decodeContentImpl(controller: FileDataViewController) {
const offset = controller.getOffset();
const view = await controller.read(offset, this.contentLength, true);
switch (this.type) {
case EbmlElementType.UnsignedInt:
this.data = readUnsigned(view);
break;
case EbmlElementType.Float:
this.data = readFloat(view);
break;
case EbmlElementType.Integer:
this.data = readSigned(view);
break;
case EbmlElementType.String:
this.data = readAscii(view);
break;
case EbmlElementType.UTF8:
this.data = readUtf8(view);
break;
default:
this.data = dataViewSliceToBuf(view, undefined, undefined);
break;
}
await controller.seek(offset + view.byteLength);
}
*encodeContent(): Generator<Uint8Array, void, unknown> {
switch (this.type) {
case EbmlElementType.UnsignedInt:
yield writeUnsigned(this.data as any);
break;
case EbmlElementType.Float:
yield writeFloat(this.data as any);
break;
case EbmlElementType.Integer:
yield writeSigned(this.data as any);
break;
case EbmlElementType.String:
yield writeAscii(this.data as any);
break;
case EbmlElementType.UTF8:
yield writeUtf8(this.data as any);
break;
default:
yield this.data as Uint8Array;
break;
}
}
override toDebugRecord() {
return {
...super.toDebugRecord(),
data: this.data,
};
}
toJSON() {
return JSON.stringify(this.toDebugRecord(), null, 2);
}
}

93
src/models/tag-master.ts Normal file
View File

@ -0,0 +1,93 @@
import { type CreateEbmlTagOptions, EbmlTagTrait } from './tag-trait';
import { EbmlElementType, EbmlTagPosition, isEbmlMasterTagId } from './enums';
import { decodeEbmlTagHeader } from '../decode-utils';
import { createEbmlTag } from 'src/factory';
import type { EbmlMasterTagIdType } from './enums';
import type { FileDataViewController } from '../adapters';
export interface CreateEbmlMasterTagOptions
extends Omit<CreateEbmlTagOptions, 'position' | 'type' | 'id'> {
id: EbmlMasterTagIdType;
}
export class EbmlMasterTag extends EbmlTagTrait {
private _children: EbmlTagTrait[] = [];
get children(): EbmlTagTrait[] {
return this._children;
}
set children(value: EbmlTagTrait[]) {
this._children = value;
}
constructor(options: CreateEbmlMasterTagOptions) {
super({
...options,
id: options.id,
type: EbmlElementType.Master,
});
}
*encodeContent(): Generator<Uint8Array, void, unknown> {
for (const child of this.children) {
yield* child.encode();
}
}
async *decodeContentImpl(controller: FileDataViewController) {
while (true) {
const offset = controller.getOffset();
if (offset >= this.endOffset) {
break;
}
const peeked = await controller.peek(offset);
if (!peeked) {
break;
}
const vints = await decodeEbmlTagHeader(controller);
const { tagId, tagVint, sizeVint } = vints;
const headerLength = tagVint.length + sizeVint.length;
const contentLength = sizeVint.value;
const isMaster = isEbmlMasterTagId(tagId);
if (isMaster) {
const tag: EbmlTagTrait = createEbmlTag(tagId, {
headerLength,
contentLength,
startOffset: offset,
position: EbmlTagPosition.Start,
parent: this,
});
yield tag;
}
await controller.seek(offset + headerLength);
const tag: EbmlTagTrait = createEbmlTag(tagId, {
headerLength,
contentLength,
startOffset: offset,
position: isMaster ? EbmlTagPosition.End : EbmlTagPosition.Content,
parent: this,
});
for await (const item of tag.decodeContent(controller)) {
yield item;
}
tag.endOffset = controller.getOffset();
this._children.push(tag);
yield tag;
}
}
}

View File

@ -0,0 +1,54 @@
import { readVint } from '../tools';
import { type CreateEbmlBlockTagOptions, EbmlBlockTag } from './tag-block';
import type { EbmlSimpleBlockTagIdType } from './enums';
import type { FileDataViewController } from '../adapters';
export interface CreateEbmlSimpleBlockTagOptions
extends Omit<CreateEbmlBlockTagOptions, 'id'> {
id?: EbmlSimpleBlockTagIdType;
}
export class EbmlSimpleBlockTag extends EbmlBlockTag {
discardable: boolean | undefined;
keyframe: boolean | undefined;
// biome-ignore lint/complexity/noUselessConstructor: <explanation>
constructor(options: CreateEbmlSimpleBlockTagOptions) {
super(options);
}
*encodeContent(): Generator<Uint8Array, void, unknown> {
const flags = this.writeFlagsBuffer();
if (this.keyframe) {
flags[0] |= 0x80;
}
if (this.discardable) {
flags[0] |= 0x01;
}
yield this.writeTrackBuffer();
yield this.writeValueBuffer();
yield flags;
yield this.payload;
}
async *decodeContentImpl(controller: FileDataViewController) {
const offset = controller.getOffset();
const view = await controller.read(offset, this.contentLength, true);
for await (const item of super.decodeContentImpl(controller)) {
yield item;
}
const trackVint = readVint(view)!;
const flags: number = view.getUint8(trackVint.length + 2);
this.keyframe = Boolean(flags & 0x80);
this.discardable = Boolean(flags & 0x01);
// seeked by block tag
// await controller.seek(offset + this.contentLength);
}
}

202
src/models/tag-trait.ts Normal file
View File

@ -0,0 +1,202 @@
import { EbmlTagPosition } from './enums';
import { EbmlTagIdEnum, type EbmlTagIdType } from './enums';
import type { EbmlElementType } from './enums';
import { hexStringToBuf, UNKNOWN_SIZE_VINT_BUF, writeVint } from '../tools';
import type { FileDataViewController } from '../adapters';
import { InconsistentOffsetOnDecodingContentError } from '../errors';
export interface CreateEbmlTagOptions {
id: EbmlTagIdType;
type?: EbmlElementType;
position?: EbmlTagPosition;
headerLength: number;
contentLength: number;
startOffset: number;
endOffset?: number;
parent?: EbmlTagTrait;
}
export abstract class EbmlTagTrait {
/**
* The id of the EBML tag.
* In most documentation this number is in hexadecimal format
*/
id: EbmlTagIdType;
/**
* The data type of the EBML tag
*/
type?: EbmlElementType;
/**
* The position of this EBML tag.
* Currently, one of "Start", "Content", or "End".
* "Start" and "End" only for Master type
*/
position: EbmlTagPosition;
/**
* Size vint length + tag vint length
*/
headerLength: number;
/**
* Start offset relative to context (stream or file) start
*/
startOffset: number;
/**
* Parent node
*/
parent?: EbmlTagTrait;
/**
* Content length in ebml data
* Return Number.POSITIVE_INFINITY as "unknown"
*/
private _contentLength: number;
/**
* Caculated end offset when
*/
private _endOffset?: number;
constructor(options: CreateEbmlTagOptions) {
this.id = options.id;
this.type = options.type;
this.position = options.position ?? EbmlTagPosition.Content;
this.parent = options.parent;
this.startOffset = options.startOffset;
this.headerLength = options.headerLength;
this._contentLength = options.contentLength;
this._endOffset = options.endOffset;
}
public set contentLength(value: number) {
this._contentLength = value;
}
/**
* After caculated or known, manually set endOffset
*/
public set endOffset(offset: number) {
this._endOffset = offset;
}
/**
* End offset relative to context (stream or file) start
* Calcalate from self _contentLength and parent end offset
* Return Number.POSITIVE_INFINITY as "unknown"
*/
public get endOffset(): number {
if (this._endOffset) {
return this._endOffset;
}
if (this._contentLength === Number.POSITIVE_INFINITY) {
return this.parent?.endOffset ?? Number.POSITIVE_INFINITY;
}
return this.startOffset + this.headerLength + this._contentLength;
}
/**
* Header length + Content Length
* Calcalate from self _contentLength and parent end offset
* Return Number.POSITIVE_INFINITY as "unknown"
*/
public get totalLength(): number {
return this.endOffset - this.startOffset;
}
/**
* Content Length
* Calcalate from self _contentLength and parent end offset
* Return Number.POSITIVE_INFINITY as "unknown"
*/
public get contentLength(): number {
return this.totalLength - this.headerLength;
}
protected abstract encodeContent(): Generator<Uint8Array, void, unknown>;
/**
* Deep traversal and parse all descendants then yield as AsyncGenerator
* @param controller DataView controller, simulate async filesystem file
*/
protected abstract decodeContentImpl(
controller: FileDataViewController
): AsyncGenerator<EbmlTagTrait, void, unknown>;
/**
* Wrap of abstract decode content impl function, add before and after lifecircle check
* @param controller DataView controller, simulate async filesystem file
* @returns Deep traversal async iterators of all descendants
*/
public async *decodeContent(
controller: FileDataViewController
): AsyncGenerator<EbmlTagTrait, void, unknown> {
if (this.contentLength === 0 || this.position === EbmlTagPosition.Start) {
return;
}
const startOffset = controller.getOffset();
for await (const tag of this.decodeContentImpl(controller)) {
yield tag;
}
const endOffset = controller.getOffset();
if (
startOffset + this.contentLength !== endOffset &&
this.contentLength !== Number.POSITIVE_INFINITY
) {
throw new InconsistentOffsetOnDecodingContentError(this, endOffset);
}
}
private getTagDeclaration(): Uint8Array {
let tagHex = this.id.toString(16);
if (tagHex.length % 2 !== 0) {
tagHex = `0${tagHex}`;
}
return hexStringToBuf(tagHex);
}
public *encodeHeader(): Generator<Uint8Array, void, unknown> {
const tagEncoded = this.getTagDeclaration();
yield tagEncoded;
if (this._contentLength === Number.POSITIVE_INFINITY) {
const mayBeSizeLength = this.headerLength - tagEncoded.byteLength;
if (mayBeSizeLength > 0 && mayBeSizeLength <= 8) {
yield UNKNOWN_SIZE_VINT_BUF[mayBeSizeLength];
} else {
yield UNKNOWN_SIZE_VINT_BUF[2];
}
} else {
yield writeVint(this._contentLength);
}
}
public *encode(): Generator<Uint8Array, void, unknown> {
if (this._contentLength === Number.POSITIVE_INFINITY) {
yield* this.encodeHeader();
for (const part of this.encodeContent()) {
yield part;
}
} else {
let size = 0;
const parts: Uint8Array[] = [];
for (const part of this.encodeContent()) {
parts.push(part);
size += part.byteLength;
}
this._contentLength = size;
yield* this.encodeHeader();
for (const part of parts) {
yield part;
}
}
}
public toDebugRecord(): Record<string, any> {
return {
id: EbmlTagIdEnum[this.id as any] || this.id,
type: this.type,
position: EbmlTagPosition[this.position],
contentLength: this.contentLength,
headerLength: this.headerLength,
startOffset: this.startOffset,
endOffset: this.endOffset,
};
}
}

671
src/tools.ts Normal file
View File

@ -0,0 +1,671 @@
import {
VintOutOfRangeError,
VintLengthOutOfRangeError,
ElementIdVintDataAllOnesError,
ElementIdVintDataAllZerosError,
ElementIdVintDataNotShortestError,
UnsupportLengthForElementTypeError,
OutOfRangeForElementTypeError,
SizeUnitOutOfSafeIntegerRangeError,
UnreachableOrLogicError,
} from './errors';
import {
EbmlElementType,
type EbmlTagIdType,
isEbmlMasterTagId,
} from './models/enums';
export const UTF8_DECODER = new TextDecoder('utf-8');
export const ASCII_DECODER = new TextDecoder('ascii');
export const UTF8_ENCODER = new TextEncoder();
export const MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
export const VINT_WIDTH_MARKER_VALUE_TABLE = [
0,
128,
16384,
2097152,
268435456,
34359738368,
4398046511104,
562949953421312,
72057594037927936n,
] as [number, number, number, number, number, number, number, number, bigint];
export const ELEM_ID_VINT_DATA_UP_LIMIT = [
0,
127,
16383,
2097151,
268435455,
34359738367,
4398046511103,
562949953421311,
72057594037927935n,
] as [number, number, number, number, number, number, number, number, bigint];
export const MAX_UINT_TABLE = [
0,
255,
65535,
16777215,
4294967295,
1099511627775,
281474976710655,
72057594037927935n,
18446744073709551615n,
];
export const MIN_INT_TABLE = [
0,
-128,
-32768,
-8388608,
-2147483648,
-549755813888,
-140737488355328,
-36028797018963968n,
-9223372036854775808n,
];
export const MAX_INT_TABLE = [
127,
32767,
8388607,
2147483647,
549755813887,
140737488355327,
36028797018963967n,
9223372036854775807n,
];
export const MAX_UINT32 = 4294967295;
export const MAX_UINT64 = 18446744073709551615n;
export const MIN_INT32 = -2147483648;
export const MAX_INT32 = 2147483647;
export const MIN_INT64 = -9223372036854775808n;
export const MAX_INT64 = 9223372036854775807n;
export const UNKNOWN_SIZE_VINT_BUF = [
new Uint8Array(0),
new Uint8Array([0xff]),
new Uint8Array([0x7f, 0xff]),
new Uint8Array([0x3f, 0xff, 0xff]),
new Uint8Array([0x1f, 0xff, 0xff, 0xff]),
new Uint8Array([0x0f, 0xff, 0xff, 0xff, 0xff]),
new Uint8Array([0x07, 0xff, 0xff, 0xff, 0xff, 0xff]),
new Uint8Array([0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
new Uint8Array([0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
];
export const UNKNOWN_SIZE_VINT_VALUE = [
0,
127,
16383,
2097151,
268435455,
34359738367,
4398046511103,
562949953421311,
72057594037927935n,
] as [number, number, number, number, number, number, number, number, bigint];
export interface Vint {
length: number;
value: number | bigint;
}
export interface SafeSizeVint extends Vint {
length: number;
value: number;
}
export function checkVintSafeSize(
vint: Vint,
tagId: EbmlTagIdType
): SafeSizeVint {
const n = vint.value;
if (typeof n === 'bigint') {
if (vint.length === 8 && UNKNOWN_SIZE_VINT_VALUE[8] === n) {
if (!isEbmlMasterTagId(tagId)) {
throw new UnreachableOrLogicError(
'only master tag size can be unknown (vint_data be all ones)'
);
}
return {
length: vint.length,
value: Number.POSITIVE_INFINITY,
};
}
throw new SizeUnitOutOfSafeIntegerRangeError(vint.value);
}
if (n === 0) {
return vint as SafeSizeVint;
}
if (n === (UNKNOWN_SIZE_VINT_VALUE[vint.length] as number)) {
if (!isEbmlMasterTagId(tagId)) {
throw new UnreachableOrLogicError(
'only master tag size can be unknown (vint_data be all ones)'
);
}
return {
length: vint.length,
value: Number.POSITIVE_INFINITY,
};
}
return vint as SafeSizeVint;
}
export function readVintLength(view: DataView) {
if (view.byteLength < 1) {
throw new VintLengthOutOfRangeError(0);
}
const length = 8 - Math.floor(Math.log2(view.getUint8(0)));
if (length > 8 || length < 0 || Number.isNaN(length)) {
throw new VintLengthOutOfRangeError(length);
}
return length;
}
export function readVint(view: DataView): Vint | null {
if (view.byteLength < 1) {
return null;
}
const length = readVintLength(view);
if (length > view.byteLength) {
return null;
}
let value: number | bigint;
if (length === 8) {
value = view.getBigUint64(0, false) - VINT_WIDTH_MARKER_VALUE_TABLE[8];
if (value <= MAX_SAFE_INTEGER_BIGINT) {
value = Number(value);
}
} else if (length === 4) {
value = view.getUint32(0, false) - VINT_WIDTH_MARKER_VALUE_TABLE[4];
} else if (length === 2) {
value = view.getUint16(0, false) - VINT_WIDTH_MARKER_VALUE_TABLE[2];
} else if (length === 1) {
value = view.getUint8(0) - VINT_WIDTH_MARKER_VALUE_TABLE[1];
} else {
value = 0;
for (let i = 0; i < length; i++) {
value = value * 256 + view.getUint8(i);
}
value -= VINT_WIDTH_MARKER_VALUE_TABLE[length] as number;
}
return {
length,
value,
};
}
export function readElementIdVint(view: DataView): Vint | null {
const vint = readVint(view);
if (!vint) {
return null;
}
const length = vint.length;
const value = vint.value;
if (value <= 0) {
throw new ElementIdVintDataAllZerosError(value);
}
if (value === ELEM_ID_VINT_DATA_UP_LIMIT[length]) {
throw new ElementIdVintDataAllOnesError(value);
}
if (value < ELEM_ID_VINT_DATA_UP_LIMIT[length - 1]) {
throw new ElementIdVintDataNotShortestError(
((value as any) + VINT_WIDTH_MARKER_VALUE_TABLE[length]) as any
);
}
return vint;
}
export function writeVint(
value: number | bigint,
desiredLength?: number
): Uint8Array {
if (value < 0 || value >= VINT_WIDTH_MARKER_VALUE_TABLE[8]) {
throw new VintOutOfRangeError(value);
}
if (desiredLength! > 8 || desiredLength! < 1) {
throw new VintLengthOutOfRangeError(desiredLength!);
}
let length = desiredLength;
if (!length) {
length = 1;
while (value >= ELEM_ID_VINT_DATA_UP_LIMIT[length]) {
length++;
}
}
if (length === 8) {
value = BigInt(value);
} else {
value = Number(value);
}
if (typeof value === 'bigint') {
value = BigInt(value);
const buf = new Uint8Array(8);
new DataView(buf.buffer).setBigUint64(
0,
value + VINT_WIDTH_MARKER_VALUE_TABLE[8],
false
);
return buf;
}
value += VINT_WIDTH_MARKER_VALUE_TABLE[length] as number;
const buffer = new Uint8Array(length);
for (let i = length - 1; i >= 0; i -= 1) {
buffer[i] = value % 256;
value = Math.floor(value / 256);
}
return buffer;
}
export function writeElementIdVint(value: number | bigint): Uint8Array {
if (value >= ELEM_ID_VINT_DATA_UP_LIMIT[8]) {
throw new VintLengthOutOfRangeError(9);
}
if (value <= 0) {
throw new ElementIdVintDataAllZerosError(value);
}
return writeVint(value);
}
export function readUnsigned(view: DataView): number | bigint {
const byteLength = view.byteLength;
let value: bigint | number;
switch (byteLength) {
case 0:
return 0;
case 1:
return view.getUint8(0);
case 2:
return view.getUint16(0, false);
case 3:
return view.getUint8(0) * 2 ** 16 + view.getUint16(1, false);
case 4:
return view.getUint32(0, false);
case 5:
return view.getUint8(0) * 2 ** 32 + view.getUint32(1, false);
case 6:
return view.getUint16(0, false) * 2 ** 32 + view.getUint32(2, false);
case 7:
value =
(BigInt(view.getUint8(0)) << 48n) +
(BigInt(view.getUint16(1, false)) << 32n) +
BigInt(view.getUint32(3, false));
break;
case 8:
value = view.getBigUint64(0, false);
break;
default:
throw new UnsupportLengthForElementTypeError(
EbmlElementType.UnsignedInt,
'0~8',
byteLength
);
}
if (value <= MAX_SAFE_INTEGER_BIGINT) {
return Number(value);
}
return value;
}
export function writeUnsigned(
num: bigint | number,
preferredLength?: number
): Uint8Array {
if (num < 0 || num > MAX_UINT64) {
throw new OutOfRangeForElementTypeError(
EbmlElementType.UnsignedInt,
`0-${MAX_UINT64}`,
num
);
}
let length = preferredLength;
if (!length) {
length = 0;
while (num > MAX_UINT_TABLE[length]) {
length++;
}
}
if (length === 0) {
return new Uint8Array(0);
}
switch (length) {
case 1: {
num = Number(num);
const buf = new Uint8Array(1);
const view = new DataView(buf.buffer);
view.setUint8(0, Number(num));
return buf;
}
case 2: {
num = Number(num);
const buf = new Uint8Array(2);
const view = new DataView(buf.buffer);
view.setUint16(0, Number(num), false);
return buf;
}
case 3: {
num = Number(num);
const buf = new Uint8Array(3);
const view = new DataView(buf.buffer);
const d1 = num % 2 ** 16;
num -= d1;
view.setUint16(1, d1, false);
view.setUint8(0, Math.floor(num / 2 ** 16));
return buf;
}
case 4: {
num = Number(num);
const buf = new Uint8Array(4);
const view = new DataView(buf.buffer);
view.setUint32(0, Number(num), false);
return buf;
}
case 5: {
num = Number(num);
const buf = new Uint8Array(5);
const view = new DataView(buf.buffer);
const d1 = num % 2 ** 32;
num -= d1;
view.setUint32(1, d1, false);
view.setUint8(0, Math.floor(num / 2 ** 32));
return buf;
}
case 6: {
num = BigInt(num);
const buf = new Uint8Array(6);
const view = new DataView(buf.buffer);
const d1 = num % (1n << 32n);
num -= d1;
view.setUint32(2, Number(d1), false);
view.setUint16(0, Number(num >> 32n), false);
return buf;
}
case 7: {
num = BigInt(num);
const buf = new Uint8Array(7);
const view = new DataView(buf.buffer);
const d1 = num % (1n << 32n);
num -= d1;
view.setUint32(3, Number(d1), false);
const d2 = num % (1n << 48n);
num -= d2;
view.setUint16(1, Number(d2), false);
view.setUint8(0, Number(num >> 48n));
return buf;
}
case 8: {
const buf = new Uint8Array(8);
const view = new DataView(buf.buffer);
view.setBigUint64(0, BigInt(num), false);
return buf;
}
default:
throw new UnsupportLengthForElementTypeError(
EbmlElementType.UnsignedInt,
'0~8',
length
);
}
}
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
export function readSigned(view: DataView): number | bigint {
const byteLength = view.byteLength;
if (byteLength < 0 || byteLength > 8) {
throw new UnsupportLengthForElementTypeError(
EbmlElementType.Integer,
'0~8',
byteLength
);
}
if (byteLength === 0) {
return 0;
}
if (byteLength === 1) {
return view.getInt8(0);
}
if (byteLength === 2) {
return view.getInt16(0, false);
}
if (byteLength === 4) {
return view.getInt32(0, false);
}
if (byteLength <= 6) {
let n = 0;
const signBit = view.getUint8(0) & 0x80;
let unignedValue = 0;
for (let i = 0; i < byteLength; i++) {
unignedValue = unignedValue * 256 + view.getUint8(i);
}
if (signBit) {
const bitLength = byteLength * 8;
const maxUnsignedValue = 1 * 2 ** bitLength;
n = unignedValue - maxUnsignedValue;
} else {
n = unignedValue;
}
return n;
}
let n = 0n;
if (byteLength === 8) {
n = view.getBigInt64(0, false);
} else {
const signBit = view.getUint8(0) & 0x80;
let unignedValue = 0n;
for (let i = 0; i < byteLength; i++) {
unignedValue = (unignedValue << 8n) + BigInt(view.getUint8(i));
}
if (signBit) {
n = unignedValue;
} else {
const bitLength = BigInt(byteLength * 8);
const maxUnsignedValue = 1n << bitLength;
n = unignedValue - maxUnsignedValue;
}
}
if (n <= MAX_SAFE_INTEGER_BIGINT) {
return Number(n);
}
return n;
}
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
export function writeSigned(
num: number | bigint,
preferredLength?: number
): Uint8Array {
if (num < MIN_INT64 || num > MAX_INT64) {
throw new OutOfRangeForElementTypeError(
EbmlElementType.Integer,
`${MIN_INT64}~${MAX_INT64}`,
num
);
}
let length = preferredLength;
if (!length) {
length = 0;
if (num > 0) {
while (num > MAX_INT_TABLE[length]) {
length++;
}
} else {
while (num < MIN_INT_TABLE[length]) {
length++;
}
}
}
if (length === 0) {
return new Uint8Array(0);
}
if (length > 8) {
throw new UnsupportLengthForElementTypeError(
EbmlElementType.Integer,
'0~8',
length
);
}
if (num < 0) {
if (length >= 6) {
num = BigInt(num) + BigInt(MAX_UINT_TABLE[length]) + 1n;
} else {
num = Number(num) + Number(MAX_UINT_TABLE[length]) + 1;
}
}
return writeUnsigned(num, length);
}
export function readFloat(view: DataView): number {
const byteLength = view.byteLength;
switch (byteLength) {
case 0:
return 0;
case 4:
return view.getFloat32(0, false);
case 8:
return view.getFloat64(0, false);
default:
throw new UnsupportLengthForElementTypeError(
EbmlElementType.Float,
'0,4,8',
byteLength
);
}
}
export function writeFloat(num: number): Uint8Array {
if (num === 0) {
return new Uint8Array(0);
}
const buf = new Uint8Array(4);
const view = new DataView(buf.buffer);
view.setFloat32(0, num, false);
return buf;
}
export function readUtf8(view: ArrayBufferView): string {
return UTF8_DECODER.decode(view);
}
export function readAscii(view: ArrayBufferView): string {
return ASCII_DECODER.decode(view);
}
export function writeUtf8(str: string): Uint8Array {
return UTF8_ENCODER.encode(str);
}
export function writeAscii(str: string): Uint8Array {
return writeUtf8(str);
}
export function readHexString(view: DataView): string {
const n = readUnsigned(view);
return n.toString(16);
}
export function hexStringToBuf(hex: string): Uint8Array {
// @ts-ignore
if (typeof Uint8Array.fromHex === 'function') {
// @ts-ignore
return Uint8Array.fromHex(hex);
}
return new Uint8Array(
hex.match(/[\da-fA-F]{2}/gi)!.map((h) => Number.parseInt(h, 16))
);
}
export function vintToHexString(num: number, litterEnd = false) {
const buf = writeVint(num);
const bytes = [...buf];
if (litterEnd) {
bytes.reverse();
}
return bytes.map((b) => b.toString(16).padStart(2, '0')).join(' ');
}
export function concatBufs(...bufs: Uint8Array[]): Uint8Array {
const tmp = new Uint8Array(
bufs.reduce((byteLength, buf) => byteLength + buf.byteLength, 0)
);
let byteOffset = 0;
for (const buf of bufs) {
tmp.set(buf, byteOffset);
byteOffset += buf.byteLength;
}
return tmp;
}
export function concatArrayBuffers(...bufs: ArrayBuffer[]): Uint8Array {
return concatBufs(...bufs.map((b) => new Uint8Array(b)));
}
export function dataViewSliceToBuf(
view: DataView,
start: number | undefined,
end: number | undefined
): Uint8Array {
const viewBufferEnd = view.byteOffset + view.byteLength;
const viewBufferStart = view.byteOffset;
let newBufferStart = viewBufferStart;
if (start! >= 0) {
newBufferStart = viewBufferStart + start!;
} else if (start! < 0) {
newBufferStart = viewBufferEnd + start!;
}
let newBufferEnd = viewBufferEnd;
if (end! >= 0) {
newBufferEnd = viewBufferStart + end!;
} else if (end! < 0) {
newBufferEnd = viewBufferEnd + end!;
}
return new Uint8Array(view.buffer.slice(newBufferStart, newBufferEnd));
}
export function dataViewSlice(
view: DataView,
start: number | undefined,
end: number | undefined
) {
const viewBufferEnd = view.byteOffset + view.byteLength;
const viewBufferStart = view.byteOffset;
let newBufferStart = viewBufferStart;
if (start! >= 0) {
newBufferStart = viewBufferStart + start!;
} else if (start! < 0) {
newBufferStart = viewBufferEnd + start!;
}
let newBufferEnd = viewBufferEnd;
if (end! >= 0) {
newBufferEnd = viewBufferStart + end!;
} else if (end! < 0) {
newBufferEnd = viewBufferEnd + end!;
}
if (newBufferStart === viewBufferStart && newBufferEnd === viewBufferEnd) {
return view;
}
const newBufferLength = Math.max(newBufferEnd - newBufferStart, 0);
return new DataView(view.buffer, newBufferStart, newBufferLength);
}

135
tests/decoder.spec.ts Normal file
View 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
View 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
View File

99
tests/pipeline.spec.ts Normal file
View 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
View 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
View 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();
}));
});
});

19
tsconfig.base.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"useDefineForClassFields": true,
"resolveJsonModule": true,
"moduleResolution": "bundler",
"target": "ES2021",
"module": "ESNext",
"lib": [
"ES2021",
"DOM",
"DOM.Iterable"
]
}
}

28
tsconfig.example.json Normal file
View File

@ -0,0 +1,28 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"composite": true,
"rootDir": ".",
"types": [
"node"
],
"noEmit": true,
"paths": {
"konoebml": [
"./src/index.ts"
],
"konoebml/*": [
"./src/*"
]
}
},
"files": [],
"include": [
"examples/*"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "./tsconfig.base.json",
"files": [],
"include": [],
"exclude": [
"node_modules"
],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./tsconfig.example.json"
}
]
}

17
tsconfig.lib.json Normal file
View File

@ -0,0 +1,17 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"composite": true,
"rootDir": "./src",
"baseUrl": ".",
"declarationDir": "./dist",
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true
},
"include": [
"src"
],
"exclude": []
}

1
tsconfig.lib.tsbuildinfo Normal file

File diff suppressed because one or more lines are too long

29
tsconfig.spec.json Normal file
View File

@ -0,0 +1,29 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"composite": true,
"rootDir": ".",
"types": [
"vitest/globals",
"node"
],
"noEmit": true,
"paths": {
"konoebml": [
"./src/index.ts"
],
"konoebml/*": [
"./src/*"
]
}
},
"files": [],
"include": [
"tests/*"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

36
vitest.config.ts Normal file
View File

@ -0,0 +1,36 @@
import swc from 'unplugin-swc';
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
cacheDir: '.vitest',
test: {
setupFiles: ['tests/init-test.ts'],
environment: 'happy-dom',
include: ['tests/**/*.spec.ts'],
globals: true,
restoreMocks: true,
coverage: {
// you can include other reporters, but 'json-summary' is required, json is recommended
reporter: ['text', 'json-summary', 'json'],
// If you want a coverage reports even if your tests are failing, include the reportOnFailure option
reportOnFailure: true,
exclude: [
'vitest.config.ts',
'rslib.config.ts',
'scripts/**',
'examples/**',
'dist/**',
],
},
},
plugins: [
tsconfigPaths(),
swc.vite({
include: /\.[mc]?[jt]sx?$/,
// for git+ package only
exclude: [] as any,
tsconfigFile: './tsconfig.spec.json',
}),
],
});