Compare commits

..

No commits in common. "ee4eee473d76e901d7ef0bbe9e2e9a4539907292" and "0b681d4fd1231e76ea33ff41ba8110eb989b19d8" have entirely different histories.

93 changed files with 1133 additions and 5658 deletions

View File

@ -1,9 +1,5 @@
{ {
"recommendations": [ "recommendations": [
"runem.lit-plugin", "runem.lit-plugin"
"vitest.explorer",
"biomejs.biome",
"hbenl.vscode-test-explorer",
"zerotaskx.rust-extension-pack"
] ]
} }

409
Cargo.lock generated
View File

@ -2,75 +2,24 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.6" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "bindgen"
version = "0.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
dependencies = [
"bitflags 2.9.0",
"cexpr",
"clang-sys",
"itertools",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.22.0" version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
[[package]]
name = "cc"
version = "1.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
dependencies = [
"shlex",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -78,22 +27,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clang-sys" name = "demuxing"
version = "1.8.1" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [ dependencies = [
"glob", "symphonia-format-mkv",
"libc",
"libloading",
] ]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@ -103,247 +42,18 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "extended"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
[[package]]
name = "ffmpeg-sys-next"
version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bc3234d0a4b2f7d083699d0860c6c9dd83713908771b60f94a96f8704adfe45"
dependencies = [
"bindgen",
"cc",
"libc",
"num_cpus",
"pkg-config",
"vcpkg",
]
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "konoplayer-ffmpeg"
version = "0.1.0"
dependencies = [
"ffmpeg-sys-next",
]
[[package]]
name = "konoplayer-symphonia"
version = "0.1.0"
dependencies = [
"symphonia",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "libloading"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.26" version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "symphonia"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9"
dependencies = [
"lazy_static",
"symphonia-bundle-flac",
"symphonia-codec-adpcm",
"symphonia-codec-pcm",
"symphonia-codec-vorbis",
"symphonia-core",
"symphonia-format-mkv",
"symphonia-format-ogg",
"symphonia-format-riff",
"symphonia-metadata",
]
[[package]]
name = "symphonia-bundle-flac"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-codec-adpcm"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f"
dependencies = [
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-codec-pcm"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b"
dependencies = [
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-codec-vorbis"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30"
dependencies = [
"log",
"symphonia-core",
"symphonia-utils-xiph",
]
[[package]] [[package]]
name = "symphonia-core" name = "symphonia-core"
version = "0.5.4" version = "0.5.4"
@ -351,7 +61,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bitflags 1.3.2", "bitflags",
"bytemuck", "bytemuck",
"lazy_static", "lazy_static",
"log", "log",
@ -370,30 +80,6 @@ dependencies = [
"symphonia-utils-xiph", "symphonia-utils-xiph",
] ]
[[package]]
name = "symphonia-format-ogg"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-format-riff"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50"
dependencies = [
"extended",
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]] [[package]]
name = "symphonia-metadata" name = "symphonia-metadata"
version = "0.5.4" version = "0.5.4"
@ -415,90 +101,3 @@ dependencies = [
"symphonia-core", "symphonia-core",
"symphonia-metadata", "symphonia-metadata",
] ]
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View File

@ -1,3 +1,3 @@
[workspace] [workspace]
members = ["packages/symphonia", "packages/ffmpeg"] members = ["packages/demuxing"]
resolver = "3" resolver = "2"

View File

@ -1,40 +1 @@
# konoplayer # konoplayer
**A project initially launched solely to watch animations in the widely used but poorly supported MKV format in browsers, just for fun.**
## State of Prototype
- [x] Matroska support
- [x] Parse EBML and demux (Done / Typescript)
- [x] Data validating fit matroska v4 doc (Done / Typescript)
- [x] WebCodecs decode + Canvas rendering (Prototyping / Typescript)
- [x] Parsing track CodecId/Private and generate Codec String (Partial / Typescript)
- Video:
- [x] VP9
- [x] VP8
- [x] AVC
- [x] HEVC
- [x] AV1
- Audio:
- [x] AAC
- [x] MP3
- [x] AC3
- [ ] OPUS (not tested, need more work)
- [ ] VORBIS (need fix)
- [ ] EAC-3 (need fix)
- [ ] PCM (need tested)
- [ ] ALAC (need tested)
- [ ] FLAC (need tested)
- [ ] Wrap video element with customElements (Prototyping / Lit-html + Typescript)
- [ ] Add WebCodecs polyfill with ffmpeg or libav (Todo / WASM)
- [x] Chrome/Edge/Android Webview: WebCodecs Native support
- [ ] FIREFOX
- [x] VP8/VP9/AV1 native support
- [x] AVC/HEVC 8bit native support
- [ ] AVC/HEVC >= 10bit polyfill needed
- [ ] Firefox Android not support
- [ ] Safari
- [x] VP8/VP9/AV1 native support
- [x] AVC/HEVC 8bit native support
- [ ] AVC/HEVC >= 10bit polyfill needed for some devices
- [ ] Audio Decoder polyfill needed
- [ ] Danmuku integration (Todo / Typescript)

1
apps/mock/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
public/video-sample/huge/*

View File

@ -1,5 +1,5 @@
{ {
"name": "@konoplayer/mock", "name": "mock",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@ -1,2 +0,0 @@
video/huge/*
!video/huge/.gitkeep

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,8 +4,10 @@
"composite": true, "composite": true,
"module": "CommonJS", "module": "CommonJS",
"moduleResolution": "node", "moduleResolution": "node",
"declaration": true,
"emitDeclarationOnly": false, "emitDeclarationOnly": false,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowImportingTsExtensions": false, "allowImportingTsExtensions": false,
"outDir": "./dist", "outDir": "./dist",
"rootDir": ".", "rootDir": ".",
@ -21,6 +23,6 @@
"node_modules", "node_modules",
"dist", "dist",
"test", "test",
"**/*spec" "**/*spec.ts"
] ]
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@konoplayer/playground", "name": "playground",
"version": "0.1.0", "version": "1.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@ -9,11 +9,11 @@
"preview": "rsbuild preview" "preview": "rsbuild preview"
}, },
"dependencies": { "dependencies": {
"lit": "^3.2.1", "konoebml": "0.1.1",
"@konoplayer/core": "workspace:*", "lit": "^3.2.1"
"@konoplayer/matroska": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"@rsbuild/core": "^1.2.14" "@rsbuild/core": "^1.2.14",
"typescript": "^5.8.2"
} }
} }

View File

@ -5,17 +5,11 @@ export interface RangedStream {
totalSize?: number; totalSize?: number;
} }
export interface CreateRangedStreamOptions { export async function createRangedStream(
url: string; url: string,
byteStart?: number;
byteEnd?: number;
}
export async function createRangedStream({
url,
byteStart = 0, byteStart = 0,
byteEnd, byteEnd?: number
}: CreateRangedStreamOptions) { ) {
const controller = new AbortController(); const controller = new AbortController();
const signal = controller.signal; const signal = controller.signal;
const headers = new Headers(); const headers = new Headers();

View File

@ -0,0 +1,60 @@
export interface RangedStream {
controller: AbortController;
response: Response;
body: ReadableStream<Uint8Array>;
totalSize?: number;
}
export async function createRangedStream(
url: string,
byteStart = 0,
byteEnd?: number
) {
const controller = new AbortController();
const signal = controller.signal;
const headers = new Headers();
headers.append(
'Range',
typeof byteEnd === 'number'
? `bytes=${byteStart}-${byteEnd}`
: `bytes=${byteStart}-`
);
const response = await fetch(url, { signal, headers });
if (!response.ok) {
throw new Error('fetch video stream failed');
}
const acceptRanges = response.headers.get('Accept-Ranges');
if (acceptRanges !== 'bytes') {
throw new Error('video server does not support byte ranges');
}
const body = response.body;
if (!(body instanceof ReadableStream)) {
throw new Error('can not get readable stream from response.body');
}
const contentRange = response.headers.get('Content-Range');
//
// Content-Range Header Syntax:
// Content-Range: <unit> <range-start>-<range-end>/<size>
// Content-Range: <unit> <range-start>-<range-end>/*
// Content-Range: <unit> */<size>
//
const totalSize = contentRange
? Number.parseInt(contentRange.split('/')[1], 10)
: undefined;
return {
controller,
response,
body,
totalSize,
};
}

View File

@ -3,8 +3,7 @@
<head></head> <head></head>
<body> <body>
<!-- <my-element />--> <my-element />
<!-- <video-pipeline-demo src="/api/static/video/test-hevc.mkv" width="800" height="450"></video-pipeline-demo> --> <video-pipeline-demo src="/api/static/video-sample/test.webm" />
<video-pipeline-demo src="/api/static/video/huge/test8.mkv" width="800" height="450"></video-pipeline-demo> <!-- <video-pipeline-demo src="/api/static/video-sample/huge/animation.mkv" /> -->
<!-- <video-pipeline-demo src="/api/static/video/huge/[LoliHouse] Amagami-san Chi no Enmusubi - 23 [WebRip 1080p HEVC-10bit AAC SRTx2].mkv" width="800" height="450" /> -->
</body> </body>

View File

@ -0,0 +1,282 @@
import {
type EbmlClusterTagType,
type EbmlCuePointTagType,
type EbmlCuesTagType,
type EbmlInfoTagType,
type EbmlMasterTagType,
type EbmlSeekHeadTagType,
type EbmlSegmentTagType,
EbmlTagIdEnum,
EbmlTagPosition,
type EbmlTagType,
type EbmlTrackEntryTagType,
type EbmlTracksTagType,
} from 'konoebml';
import {convertEbmlTagToComponent, type InferType,} from './util';
import {isEqual, maxBy} from 'lodash-es';
import {ArkErrors, type Type} from 'arktype';
import {
ClusterSchema,
type ClusterType,
CuePointSchema,
type CuePointType,
type CueTrackPositionsType,
InfoSchema,
type InfoType,
SeekHeadSchema,
type SeekHeadType,
TrackEntrySchema,
type TrackEntryType
} from './schema';
export const SEEK_ID_KAX_INFO = new Uint8Array([0x15, 0x49, 0xa9, 0x66]);
export const SEEK_ID_KAX_TRACKS = new Uint8Array([0x16, 0x54, 0xae, 0x6b]);
export const SEEK_ID_KAX_CUES = new Uint8Array([0x1c, 0x53, 0xbb, 0x6b]);
export class SegmentSystem {
startTag: EbmlSegmentTagType;
headTags: EbmlTagType[] = [];
cue: CueSystem;
cluster: ClusterSystem;
seek: SeekSystem;
info: InfoSystem;
track: TrackSystem;
constructor(startNode: EbmlSegmentTagType) {
this.startTag = startNode;
this.cue = new CueSystem(this);
this.cluster = new ClusterSystem(this);
this.seek = new SeekSystem(this);
this.info = new InfoSystem(this);
this.track = new TrackSystem(this);
}
get dataStartOffset() {
return this.startTag.startOffset + this.startTag.headerLength;
}
get startOffset () {
return this.startTag.startOffset;
}
completeHeads () {
const infoTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_INFO);
const tracksTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_TRACKS);
const cuesTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_CUES);
if (cuesTag?.id === EbmlTagIdEnum.Cues) {
this.cue.prepareCuesWithTag(cuesTag)
}
if (infoTag?.id === EbmlTagIdEnum.Info) {
this.info.prepareWithInfoTag(infoTag);
}
if (tracksTag?.id === EbmlTagIdEnum.Tracks) {
this.track.prepareTracksWithTag(tracksTag);
}
return this;
}
scanHead (tag: EbmlTagType) {
if (
tag.id === EbmlTagIdEnum.SeekHead &&
tag.position === EbmlTagPosition.End
) {
this.seek.addSeekHeadTag(tag);
}
this.headTags.push(tag);
this.seek.memoTag(tag);
return this;
}
}
export class SegmentComponentSystemTrait<E extends EbmlMasterTagType, S extends Type<any>> {
segment: SegmentSystem;
get schema(): S {
throw new Error("unimplemented!")
}
constructor(segment: SegmentSystem) {
this.segment = segment;
}
componentFromTag(tag: E): InferType<S> {
const extracted = convertEbmlTagToComponent(tag);
const result = this.schema(extracted);
if (result instanceof ArkErrors) {
const errors = result;
console.error('Parse component from tag error:', tag.toDebugRecord(), errors.flatProblemsByPath)
throw errors;
}
return result as InferType<S>
}
}
export class SeekSystem extends SegmentComponentSystemTrait<EbmlSeekHeadTagType, typeof SeekHeadSchema> {
override get schema() {
return SeekHeadSchema;
}
seekHeads: SeekHeadType[] = [];
offsetToTagMemo: Map<number, EbmlTagType> = new Map();
memoTag (tag: EbmlTagType) {
this.offsetToTagMemo.set(tag.startOffset, tag);
}
addSeekHeadTag (tag: EbmlSeekHeadTagType) {
const seekHead = this.componentFromTag(tag);
this.seekHeads.push(seekHead);
return seekHead;
}
offsetFromSeekPosition (position: number): number {
return position + this.segment.startOffset;
}
offsetFromSeekDataPosition (position: number) : number {
return position + this.segment.dataStartOffset;
}
seekTagByStartOffset (
startOffset: number | undefined
): EbmlTagType | undefined {
return startOffset! >= 0
? this.offsetToTagMemo.get(startOffset!)
: undefined;
}
seekOffsetBySeekId(seekId: Uint8Array): number | undefined {
const seekPosition = this.seekHeads[0]?.Seek?.find((c) => isEqual(c.SeekID, seekId))
?.SeekPosition;
return seekPosition! >= 0 ? this.offsetFromSeekPosition(seekPosition!) : undefined;
}
seekTagBySeekId(seekId: Uint8Array): EbmlTagType | undefined {
return this.seekTagByStartOffset(
this.seekOffsetBySeekId(seekId)
);
}
}
export class InfoSystem extends SegmentComponentSystemTrait<EbmlInfoTagType, typeof InfoSchema> {
override get schema() {
return InfoSchema;
}
info!: InfoType;
prepareWithInfoTag (tag: EbmlInfoTagType) {
this.info = this.componentFromTag(tag);
return this;
}
}
export class ClusterSystem extends SegmentComponentSystemTrait<EbmlClusterTagType, typeof ClusterSchema> {
override get schema() {
return ClusterSchema
}
clustersBuffer: ClusterType[] = [];
addClusterWithTag (tag: EbmlClusterTagType): ClusterType {
const cluster = this.componentFromTag(tag);
this.clustersBuffer.push(cluster);
return cluster;
}
}
export class TrackSystem extends SegmentComponentSystemTrait<EbmlTrackEntryTagType, typeof TrackEntrySchema> {
override get schema() {
return TrackEntrySchema;
}
tracks = new Map<number, TrackEntryType>();
prepareTracksWithTag (tag: EbmlTracksTagType) {
this.tracks.clear();
for (const c of tag.children) {
if (c.id === EbmlTagIdEnum.TrackEntry) {
const trackEntry = this.componentFromTag(c);
this.tracks.set(trackEntry.TrackNumber, trackEntry);
}
}
return this;
}
}
export class CueSystem extends SegmentComponentSystemTrait<
EbmlCuePointTagType,
typeof CuePointSchema
> {
override get schema () {
return CuePointSchema
};
cues: CuePointType[] = [];
prepareCuesWithTag (tag: EbmlCuesTagType) {
this.cues = tag.children
.filter(c => c.id === EbmlTagIdEnum.CuePoint)
.map(this.componentFromTag.bind(this));
return this;
}
findClosestCue(seekTime: number): CuePointType | undefined {
const cues = this.cues;
if (!cues || cues.length === 0) {
return undefined;
}
let left = 0;
let right = cues.length - 1;
if (seekTime <= cues[0].CueTime) {
return cues[0];
}
if (seekTime >= cues[right].CueTime) {
return cues[right];
}
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (cues[mid].CueTime === seekTime) {
return cues[mid];
}
if (cues[mid].CueTime < seekTime) {
left = mid + 1;
} else {
right = mid - 1;
}
}
const before = cues[right];
const after = cues[left];
return Math.abs(before.CueTime - seekTime) <
Math.abs(after.CueTime - seekTime)
? before
: after;
}
getCueTrackPositions (cuePoint: CuePointType, track?: number): CueTrackPositionsType {
let cueTrackPositions: CueTrackPositionsType | undefined;
if (track! >= 0) {
cueTrackPositions = cuePoint.CueTrackPositions.find(c => c.CueTrack === track);
}
if (!cueTrackPositions) {
cueTrackPositions = maxBy(cuePoint.CueTrackPositions, c => c.CueClusterPosition)!;
}
return cueTrackPositions;
}
get prepared (): boolean {
return this.cues.length > 0;
}
}

View File

@ -0,0 +1,319 @@
import {
type EbmlTagType,
EbmlStreamDecoder,
EbmlTagIdEnum,
EbmlTagPosition,
} from 'konoebml';
import {
Observable,
from,
switchMap,
share,
defer,
EMPTY,
of,
filter,
finalize,
isEmpty,
map,
merge,
raceWith,
reduce,
scan,
shareReplay,
take,
takeUntil,
withLatestFrom,
} from 'rxjs';
import { createRangedStream } from '@/fetch';
import { SegmentSystem, SEEK_ID_KAX_CUES, type CueSystem } from './model';
import { isTagIdPos } from './util';
import type { ClusterType } from "./schema";
export function createRangedEbmlStream(
url: string,
byteStart = 0,
byteEnd?: number
): Observable<{
ebml$: Observable<EbmlTagType>;
totalSize?: number;
response: Response;
body: ReadableStream<Uint8Array>;
controller: AbortController;
}> {
const stream$ = from(createRangedStream(url, byteStart, byteEnd));
return stream$.pipe(
switchMap(({ controller, body, totalSize, response }) => {
let requestCompleted = false;
const originRequest$ = new Observable<EbmlTagType>((subscriber) => {
body
.pipeThrough(
new EbmlStreamDecoder({
streamStartOffset: byteStart,
collectChild: (child) => child.id !== EbmlTagIdEnum.Cluster,
})
)
.pipeTo(
new WritableStream({
write: (tag) => subscriber.next(tag),
close: () => {
if (!requestCompleted) {
subscriber.complete();
}
},
})
)
.catch((error) => {
if (requestCompleted && error?.name === 'AbortError') {
return;
}
subscriber.error(error);
});
return () => {
requestCompleted = true;
controller.abort();
};
}).pipe(
share({
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: true,
})
);
const ebml$ = defer(() =>
requestCompleted ? EMPTY : originRequest$
).pipe(
share({
resetOnError: false,
resetOnComplete: true,
resetOnRefCountZero: true,
})
);
return of({
ebml$,
totalSize,
response,
body,
controller,
});
})
);
}
export function createEbmlController(src: string) {
const request$ = createRangedEbmlStream(src, 0);
const controller$ = request$.pipe(
map(({ totalSize, ebml$, response, controller }) => {
const head$ = ebml$.pipe(
filter(isTagIdPos(EbmlTagIdEnum.EBML, EbmlTagPosition.End)),
take(1),
shareReplay(1)
);
console.debug(
`stream of video "${src}" created, total size is ${totalSize ?? 'unknown'}`
);
const segmentStart$ = ebml$.pipe(
filter((s) => s.position === EbmlTagPosition.Start),
filter((tag) => tag.id === EbmlTagIdEnum.Segment)
);
const segments$ = segmentStart$.pipe(
map((startTag) => {
const segment = new SegmentSystem(startTag);
const clusterSystem = segment.cluster;
const seekSystem = segment.seek;
const continuousReusedCluster$ = ebml$.pipe(
filter(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)),
filter((s) => s.id === EbmlTagIdEnum.Cluster),
map(clusterSystem.addClusterWithTag.bind(clusterSystem))
);
const segmentEnd$ = ebml$.pipe(
filter(isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End)),
filter((tag) => tag.id === EbmlTagIdEnum.Segment),
take(1)
);
const clusterStart$ = ebml$.pipe(
filter(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.Start)),
take(1),
shareReplay(1)
);
const meta$ = ebml$.pipe(
takeUntil(clusterStart$.pipe(raceWith(segmentEnd$))),
share({
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: true,
})
);
const withMeta$ = meta$.pipe(
reduce((segment, meta) => segment.scanHead(meta), segment),
map(segment.completeHeads.bind(segment)),
take(1),
shareReplay(1)
);
const withRemoteCues$ = withMeta$.pipe(
switchMap((s) => {
const cueSystem = s.cue;
const seekSystem = s.seek;
if (cueSystem.prepared) {
return EMPTY;
}
const remoteCuesTagStartOffset = seekSystem.seekOffsetBySeekId(SEEK_ID_KAX_CUES);
if (remoteCuesTagStartOffset! >= 0) {
return createRangedEbmlStream(src, remoteCuesTagStartOffset).pipe(
switchMap((req) => req.ebml$),
filter(isTagIdPos(EbmlTagIdEnum.Cues, EbmlTagPosition.End)),
withLatestFrom(withMeta$),
map(([cues, withMeta]) => {
withMeta.cue.prepareCuesWithTag(cues);
return withMeta;
})
);
}
return EMPTY;
}),
take(1),
shareReplay(1)
);
const withLocalCues$ = withMeta$.pipe(
switchMap((s) => s.cue.prepared ? of(s) : EMPTY),
shareReplay(1)
);
const withCues$ = merge(withLocalCues$, withRemoteCues$).pipe(
take(1)
);
const withoutCues$ = withCues$.pipe(
isEmpty(),
switchMap((empty) => (empty ? withMeta$ : EMPTY))
);
const seekWithoutCues = (seekTime: number): Observable<ClusterType> => {
const cluster$ = continuousReusedCluster$.pipe(
isEmpty(),
switchMap((empty) => {
return empty
? clusterStart$.pipe(
switchMap((startTag) =>
createRangedEbmlStream(src, startTag.startOffset)
),
switchMap((req) => req.ebml$),
filter(
isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)
),
map((tag) => clusterSystem.addClusterWithTag(tag))
)
: continuousReusedCluster$;
})
);
if (seekTime === 0) {
return cluster$;
}
return cluster$.pipe(
scan(
(prev, curr) =>
[prev?.[1], curr] as [
ClusterType | undefined,
ClusterType | undefined,
],
[undefined, undefined] as [
ClusterType | undefined,
ClusterType | undefined,
]
),
filter((c) => c[1]?.Timestamp! > seekTime),
map((c) => c[0] ?? c[1]!)
);
};
const seekWithCues = (
cues: CueSystem,
seekTime: number
): Observable<ClusterType> => {
if (seekTime === 0) {
return seekWithoutCues(seekTime);
}
const cuePoint = cues.findClosestCue(seekTime);
if (!cuePoint) {
return seekWithoutCues(seekTime);
}
return createRangedEbmlStream(
src,
seekSystem.offsetFromSeekDataPosition(cues.getCueTrackPositions(cuePoint).CueClusterPosition)
).pipe(
switchMap((req) => req.ebml$),
filter(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)),
map(clusterSystem.addClusterWithTag.bind(clusterSystem))
);
};
const seek = (seekTime: number): Observable<ClusterType> => {
if (seekTime === 0) {
const subscription = merge(withCues$, withoutCues$).subscribe();
// if seekTime equals to 0 at start, reuse the initialize stream
return seekWithoutCues(seekTime).pipe(
finalize(() => {
subscription.unsubscribe();
})
);
}
return merge(
withCues$.pipe(
switchMap((s) =>
seekWithCues(s.cue, seekTime)
)
),
withoutCues$.pipe(switchMap((_) => seekWithoutCues(seekTime)))
);
};
return {
startTag,
head$,
segment,
meta$,
withMeta$,
withCues$,
withoutCues$,
seekWithCues,
seekWithoutCues,
seek,
};
})
);
return {
segments$,
head$,
totalSize,
ebml$,
controller,
response,
};
})
);
return {
controller$,
request$,
};
}

View File

@ -2,27 +2,24 @@ import { type, match } from 'arktype';
import { EbmlTagIdEnum, EbmlSimpleBlockTag ,EbmlBlockTag } from 'konoebml'; import { EbmlTagIdEnum, EbmlSimpleBlockTag ,EbmlBlockTag } from 'konoebml';
export const BinarySchema = type.instanceOf(Uint8Array); export const BinarySchema = type.instanceOf(Uint8Array);
export type BinaryType = typeof BinarySchema.infer;
export const SimpleBlockSchema = type.instanceOf(EbmlSimpleBlockTag); export const SimpleBlockSchema = type.instanceOf(EbmlSimpleBlockTag);
export const BlockSchema = type.instanceOf(EbmlBlockTag); export const BlockSchema = type.instanceOf(EbmlBlockTag);
export type SimpleBlockType = typeof SimpleBlockSchema.infer;
export type BlockType = typeof BlockSchema.infer;
export const DocTypeExtensionSchema = type({ export const DocTypeExtensionSchema = type({
DocTypeExtensionName: type.string, DocTypeExtensionName: type.string,
DocTypeExtensionVersion: type.number.or(type.bigint), DocTypeExtensionVersion: type.number,
}); });
export type DocTypeExtensionType = typeof DocTypeExtensionSchema.infer; export type DocTypeExtensionType = typeof DocTypeExtensionSchema.infer;
export const EBMLSchema = type({ export const EBMLSchema = type({
EBMLVersion: type.number.or(type.bigint).default(1), EBMLVersion: type.number.default(1),
EBMLReadVersion: type.number.or(type.bigint).default(1), EBMLReadVersion: type.number.default(1),
EBMLMaxIDLength: type.number.or(type.bigint).default(4), EBMLMaxIDLength: type.number.default(4),
EBMLMaxSizeLength: type.number.or(type.bigint).default(8), EBMLMaxSizeLength: type.number.default(8),
DocType: type.string, DocType: type.string,
DocTypeVersion: type.number.or(type.bigint).default(1), DocTypeVersion: type.number.default(1),
DocTypeReadVersion: type.number.or(type.bigint).default(1), DocTypeReadVersion: type.number.default(1),
DocTypeExtension: DocTypeExtensionSchema.array().optional(), DocTypeExtension: DocTypeExtensionSchema.array().optional(),
}); });
@ -30,7 +27,7 @@ export type EBMLType = typeof EBMLSchema.infer;
export const SeekSchema = type({ export const SeekSchema = type({
SeekID: BinarySchema, SeekID: BinarySchema,
SeekPosition: type.number.or(type.bigint), SeekPosition: type.number,
}); });
export type SeekType = typeof SeekSchema.infer; export type SeekType = typeof SeekSchema.infer;
@ -43,8 +40,8 @@ export type SeekHeadType = typeof SeekHeadSchema.infer;
export const ChapterTranslateSchema = type({ export const ChapterTranslateSchema = type({
ChapterTranslateID: BinarySchema, ChapterTranslateID: BinarySchema,
ChapterTranslateCodec: type.number.or(type.bigint), ChapterTranslateCodec: type.number,
ChapterTranslateEditionUID: type.number.or(type.bigint).array().optional(), ChapterTranslateEditionUID: type.number.array().optional(),
}); });
export type ChapterTranslateType = typeof ChapterTranslateSchema.infer; export type ChapterTranslateType = typeof ChapterTranslateSchema.infer;
@ -58,7 +55,7 @@ export const InfoSchema = type({
NextFilename: type.string.optional(), NextFilename: type.string.optional(),
SegmentFamily: BinarySchema.array().optional(), SegmentFamily: BinarySchema.array().optional(),
ChapterTranslate: ChapterTranslateSchema.array().optional(), ChapterTranslate: ChapterTranslateSchema.array().optional(),
TimestampScale: type.number.or(type.bigint).default(1000000), TimestampScale: type.number.default(1000000),
Duration: type.number.optional(), Duration: type.number.optional(),
DateUTC: BinarySchema.optional(), DateUTC: BinarySchema.optional(),
Title: type.string.optional(), Title: type.string.optional(),
@ -69,14 +66,14 @@ export const InfoSchema = type({
export type InfoType = typeof InfoSchema.infer; export type InfoType = typeof InfoSchema.infer;
export const SilentTracksSchema = type({ export const SilentTracksSchema = type({
SilentTrackNumber: type.number.or(type.bigint).array().optional(), SilentTrackNumber: type.number.array().optional(),
}); });
export type SilentTracksType = typeof SilentTracksSchema.infer; export type SilentTracksType = typeof SilentTracksSchema.infer;
export const BlockMoreSchema = type({ export const BlockMoreSchema = type({
BlockAdditional: BinarySchema, BlockAdditional: BinarySchema,
BlockAddID: type.number.or(type.bigint).default(1), BlockAddID: type.number.default(1),
}); });
export type BlockMoreType = typeof BlockMoreSchema.infer; export type BlockMoreType = typeof BlockMoreSchema.infer;
@ -88,11 +85,11 @@ export const BlockAdditionsSchema = type({
export type BlockAdditionsType = typeof BlockAdditionsSchema.infer; export type BlockAdditionsType = typeof BlockAdditionsSchema.infer;
export const TimeSliceSchema = type({ export const TimeSliceSchema = type({
LaceNumber: type.number.or(type.bigint).optional(), LaceNumber: type.number.optional(),
FrameNumber: type.number.or(type.bigint).default(0), FrameNumber: type.number.default(0),
BlockAdditionID: type.number.or(type.bigint).default(0), BlockAdditionID: type.number.default(0),
Delay: type.number.or(type.bigint).default(0), Delay: type.number.default(0),
SliceDuration: type.number.or(type.bigint).default(0), SliceDuration: type.number.default(0),
}); });
export type TimeSliceType = typeof TimeSliceSchema.infer; export type TimeSliceType = typeof TimeSliceSchema.infer;
@ -104,8 +101,8 @@ export const SlicesSchema = type({
export type SlicesType = typeof SlicesSchema.infer; export type SlicesType = typeof SlicesSchema.infer;
export const ReferenceFrameSchema = type({ export const ReferenceFrameSchema = type({
ReferenceOffset: type.number.or(type.bigint), ReferenceOffset: type.number,
ReferenceTimestamp: type.number.or(type.bigint), ReferenceTimestamp: type.number,
}); });
export type ReferenceFrameType = typeof ReferenceFrameSchema.infer; export type ReferenceFrameType = typeof ReferenceFrameSchema.infer;
@ -114,12 +111,12 @@ export const BlockGroupSchema = type({
Block: BlockSchema, Block: BlockSchema,
BlockVirtual: BinarySchema.optional(), BlockVirtual: BinarySchema.optional(),
BlockAdditions: BlockAdditionsSchema.optional(), BlockAdditions: BlockAdditionsSchema.optional(),
BlockDuration: type.number.or(type.bigint).optional(), BlockDuration: type.number.optional(),
ReferencePriority: type.number.or(type.bigint).default(0), ReferencePriority: type.number.default(0),
ReferenceBlock: type.number.or(type.bigint).array().optional(), ReferenceBlock: type.number.array().optional(),
ReferenceVirtual: type.number.or(type.bigint).optional(), ReferenceVirtual: type.number.optional(),
CodecState: BinarySchema.optional(), CodecState: BinarySchema.optional(),
DiscardPadding: type.number.or(type.bigint).optional(), DiscardPadding: type.number.optional(),
Slices: SlicesSchema.optional(), Slices: SlicesSchema.optional(),
ReferenceFrame: ReferenceFrameSchema.optional(), ReferenceFrame: ReferenceFrameSchema.optional(),
}); });
@ -127,10 +124,10 @@ export const BlockGroupSchema = type({
export type BlockGroupType = typeof BlockGroupSchema.infer; export type BlockGroupType = typeof BlockGroupSchema.infer;
export const ClusterSchema = type({ export const ClusterSchema = type({
Timestamp: type.number.or(type.bigint), Timestamp: type.number,
SilentTracks: SilentTracksSchema.optional(), SilentTracks: SilentTracksSchema.optional(),
Position: type.number.or(type.bigint).optional(), Position: type.number.optional(),
PrevSize: type.number.or(type.bigint).optional(), PrevSize: type.number.optional(),
SimpleBlock: SimpleBlockSchema.array().optional(), SimpleBlock: SimpleBlockSchema.array().optional(),
BlockGroup: BlockGroupSchema.array().optional(), BlockGroup: BlockGroupSchema.array().optional(),
EncryptedBlock: BinarySchema.array().optional(), EncryptedBlock: BinarySchema.array().optional(),
@ -139,9 +136,9 @@ export const ClusterSchema = type({
export type ClusterType = typeof ClusterSchema.infer; export type ClusterType = typeof ClusterSchema.infer;
export const BlockAdditionMappingSchema = type({ export const BlockAdditionMappingSchema = type({
BlockAddIDValue: type.number.or(type.bigint).optional(), BlockAddIDValue: type.number.optional(),
BlockAddIDName: type.string.optional(), BlockAddIDName: type.string.optional(),
BlockAddIDType: type.number.or(type.bigint).default(0), BlockAddIDType: type.number.default(0),
BlockAddIDExtraData: BinarySchema.optional(), BlockAddIDExtraData: BinarySchema.optional(),
}); });
@ -149,8 +146,8 @@ export type BlockAdditionMappingType = typeof BlockAdditionMappingSchema.infer;
export const TrackTranslateSchema = type({ export const TrackTranslateSchema = type({
TrackTranslateTrackID: BinarySchema, TrackTranslateTrackID: BinarySchema,
TrackTranslateCodec: type.number.or(type.bigint), TrackTranslateCodec: type.number,
TrackTranslateEditionUID: type.number.or(type.bigint).array().optional(), TrackTranslateEditionUID: type.number.array().optional(),
}); });
export type TrackTranslateType = typeof TrackTranslateSchema.infer; export type TrackTranslateType = typeof TrackTranslateSchema.infer;
@ -201,12 +198,9 @@ export enum MatrixCoefficientsRestrictionEnum {
CHROMA_DERIVED_CONSTANT_LUMINANCE = 13, CHROMA_DERIVED_CONSTANT_LUMINANCE = 13,
// ITU-R BT.2100-0 // ITU-R BT.2100-0
ITU_R_BT_2100_0 = 14, ITU_R_BT_2100_0 = 14,
} };
export const MatrixCoefficientsRestriction = type( export const MatrixCoefficientsRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14');
'0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14' export type MatrixCoefficientsRestrictionType = typeof MatrixCoefficientsRestriction.infer;
);
export type MatrixCoefficientsRestrictionType =
typeof MatrixCoefficientsRestriction.infer;
export enum ChromaSitingHorzRestrictionEnum { export enum ChromaSitingHorzRestrictionEnum {
// unspecified // unspecified
@ -215,10 +209,9 @@ export enum ChromaSitingHorzRestrictionEnum {
LEFT_COLLOCATED = 1, LEFT_COLLOCATED = 1,
// half // half
HALF = 2, HALF = 2,
} };
export const ChromaSitingHorzRestriction = type('0 | 1 | 2'); export const ChromaSitingHorzRestriction = type('0 | 1 | 2');
export type ChromaSitingHorzRestrictionType = export type ChromaSitingHorzRestrictionType = typeof ChromaSitingHorzRestriction.infer;
typeof ChromaSitingHorzRestriction.infer;
export enum ChromaSitingVertRestrictionEnum { export enum ChromaSitingVertRestrictionEnum {
// unspecified // unspecified
@ -227,10 +220,9 @@ export enum ChromaSitingVertRestrictionEnum {
TOP_COLLOCATED = 1, TOP_COLLOCATED = 1,
// half // half
HALF = 2, HALF = 2,
} };
export const ChromaSitingVertRestriction = type('0 | 1 | 2'); export const ChromaSitingVertRestriction = type('0 | 1 | 2');
export type ChromaSitingVertRestrictionType = export type ChromaSitingVertRestrictionType = typeof ChromaSitingVertRestriction.infer;
typeof ChromaSitingVertRestriction.infer;
export enum RangeRestrictionEnum { export enum RangeRestrictionEnum {
// unspecified // unspecified
@ -241,7 +233,7 @@ export enum RangeRestrictionEnum {
FULL_RANGE_NO_CLIPPING = 2, FULL_RANGE_NO_CLIPPING = 2,
// defined by MatrixCoefficients / TransferCharacteristics // defined by MatrixCoefficients / TransferCharacteristics
DEFINED_BY_MATRIX_COEFFICIENTS_TRANSFER_CHARACTERISTICS = 3, DEFINED_BY_MATRIX_COEFFICIENTS_TRANSFER_CHARACTERISTICS = 3,
} };
export const RangeRestriction = type('0 | 1 | 2 | 3'); export const RangeRestriction = type('0 | 1 | 2 | 3');
export type RangeRestrictionType = typeof RangeRestriction.infer; export type RangeRestrictionType = typeof RangeRestriction.infer;
@ -284,12 +276,9 @@ export enum TransferCharacteristicsRestrictionEnum {
SMPTE_ST_428_1 = 17, SMPTE_ST_428_1 = 17,
// ARIB STD-B67 (HLG) // ARIB STD-B67 (HLG)
ARIB_STD_B67_HLG = 18, ARIB_STD_B67_HLG = 18,
} };
export const TransferCharacteristicsRestriction = type( export const TransferCharacteristicsRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18');
'0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18' export type TransferCharacteristicsRestrictionType = typeof TransferCharacteristicsRestriction.infer;
);
export type TransferCharacteristicsRestrictionType =
typeof TransferCharacteristicsRestriction.infer;
export enum PrimariesRestrictionEnum { export enum PrimariesRestrictionEnum {
// reserved // reserved
@ -320,26 +309,24 @@ export enum PrimariesRestrictionEnum {
SMPTE_EG_432_2 = 12, SMPTE_EG_432_2 = 12,
// EBU Tech. 3213-E - JEDEC P22 phosphors // EBU Tech. 3213-E - JEDEC P22 phosphors
EBU_TECH_3213_E_JEDEC_P22_PHOSPHORS = 22, EBU_TECH_3213_E_JEDEC_P22_PHOSPHORS = 22,
} };
export const PrimariesRestriction = type( export const PrimariesRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22');
'0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22'
);
export type PrimariesRestrictionType = typeof PrimariesRestriction.infer; export type PrimariesRestrictionType = typeof PrimariesRestriction.infer;
export const ColourSchema = type({ export const ColourSchema = type({
MatrixCoefficients: MatrixCoefficientsRestriction.default(2), MatrixCoefficients: MatrixCoefficientsRestriction.default(2),
BitsPerChannel: type.number.or(type.bigint).default(0), BitsPerChannel: type.number.default(0),
ChromaSubsamplingHorz: type.number.or(type.bigint).optional(), ChromaSubsamplingHorz: type.number.optional(),
ChromaSubsamplingVert: type.number.or(type.bigint).optional(), ChromaSubsamplingVert: type.number.optional(),
CbSubsamplingHorz: type.number.or(type.bigint).optional(), CbSubsamplingHorz: type.number.optional(),
CbSubsamplingVert: type.number.or(type.bigint).optional(), CbSubsamplingVert: type.number.optional(),
ChromaSitingHorz: ChromaSitingHorzRestriction.default(0), ChromaSitingHorz: ChromaSitingHorzRestriction.default(0),
ChromaSitingVert: ChromaSitingVertRestriction.default(0), ChromaSitingVert: ChromaSitingVertRestriction.default(0),
Range: RangeRestriction.default(0), Range: RangeRestriction.default(0),
TransferCharacteristics: TransferCharacteristicsRestriction.default(2), TransferCharacteristics: TransferCharacteristicsRestriction.default(2),
Primaries: PrimariesRestriction.default(2), Primaries: PrimariesRestriction.default(2),
MaxCLL: type.number.or(type.bigint).optional(), MaxCLL: type.number.optional(),
MaxFALL: type.number.or(type.bigint).optional(), MaxFALL: type.number.optional(),
MasteringMetadata: MasteringMetadataSchema.optional(), MasteringMetadata: MasteringMetadataSchema.optional(),
}); });
@ -354,10 +341,9 @@ export enum ProjectionTypeRestrictionEnum {
CUBEMAP = 2, CUBEMAP = 2,
// mesh // mesh
MESH = 3, MESH = 3,
} };
export const ProjectionTypeRestriction = type('0 | 1 | 2 | 3'); export const ProjectionTypeRestriction = type('0 | 1 | 2 | 3');
export type ProjectionTypeRestrictionType = export type ProjectionTypeRestrictionType = typeof ProjectionTypeRestriction.infer;
typeof ProjectionTypeRestriction.infer;
export const ProjectionSchema = type({ export const ProjectionSchema = type({
ProjectionType: ProjectionTypeRestriction.default(0), ProjectionType: ProjectionTypeRestriction.default(0),
@ -376,10 +362,9 @@ export enum FlagInterlacedRestrictionEnum {
INTERLACED = 1, INTERLACED = 1,
// progressive // progressive
PROGRESSIVE = 2, PROGRESSIVE = 2,
} };
export const FlagInterlacedRestriction = type('0 | 1 | 2'); export const FlagInterlacedRestriction = type('0 | 1 | 2');
export type FlagInterlacedRestrictionType = export type FlagInterlacedRestrictionType = typeof FlagInterlacedRestriction.infer;
typeof FlagInterlacedRestriction.infer;
export enum FieldOrderRestrictionEnum { export enum FieldOrderRestrictionEnum {
// progressive // progressive
@ -394,7 +379,7 @@ export enum FieldOrderRestrictionEnum {
TFF_INTERLEAVED = 9, TFF_INTERLEAVED = 9,
// bff (interleaved) // bff (interleaved)
BFF_INTERLEAVED = 14, BFF_INTERLEAVED = 14,
} };
export const FieldOrderRestriction = type('0 | 1 | 2 | 6 | 9 | 14'); export const FieldOrderRestriction = type('0 | 1 | 2 | 6 | 9 | 14');
export type FieldOrderRestrictionType = typeof FieldOrderRestriction.infer; export type FieldOrderRestrictionType = typeof FieldOrderRestriction.infer;
@ -429,10 +414,8 @@ export enum StereoModeRestrictionEnum {
BOTH_EYES_LACED_IN_ONE_BLOCK_LEFT_EYE_IS_FIRST = 13, BOTH_EYES_LACED_IN_ONE_BLOCK_LEFT_EYE_IS_FIRST = 13,
// both eyes laced in one Block (right eye is first) // both eyes laced in one Block (right eye is first)
BOTH_EYES_LACED_IN_ONE_BLOCK_RIGHT_EYE_IS_FIRST = 14, BOTH_EYES_LACED_IN_ONE_BLOCK_RIGHT_EYE_IS_FIRST = 14,
} };
export const StereoModeRestriction = type( export const StereoModeRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14');
'0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14'
);
export type StereoModeRestrictionType = typeof StereoModeRestriction.infer; export type StereoModeRestrictionType = typeof StereoModeRestriction.infer;
export enum AlphaModeRestrictionEnum { export enum AlphaModeRestrictionEnum {
@ -440,7 +423,7 @@ export enum AlphaModeRestrictionEnum {
NONE = 0, NONE = 0,
// present // present
PRESENT = 1, PRESENT = 1,
} };
export const AlphaModeRestriction = type('0 | 1'); export const AlphaModeRestriction = type('0 | 1');
export type AlphaModeRestrictionType = typeof AlphaModeRestriction.infer; export type AlphaModeRestrictionType = typeof AlphaModeRestriction.infer;
@ -453,10 +436,9 @@ export enum OldStereoModeRestrictionEnum {
LEFT_EYE = 2, LEFT_EYE = 2,
// both eyes // both eyes
BOTH_EYES = 3, BOTH_EYES = 3,
} };
export const OldStereoModeRestriction = type('0 | 1 | 2 | 3'); export const OldStereoModeRestriction = type('0 | 1 | 2 | 3');
export type OldStereoModeRestrictionType = export type OldStereoModeRestrictionType = typeof OldStereoModeRestriction.infer;
typeof OldStereoModeRestriction.infer;
export enum DisplayUnitRestrictionEnum { export enum DisplayUnitRestrictionEnum {
// pixels // pixels
@ -469,7 +451,7 @@ export enum DisplayUnitRestrictionEnum {
DISPLAY_ASPECT_RATIO = 3, DISPLAY_ASPECT_RATIO = 3,
// unknown // unknown
UNKNOWN = 4, UNKNOWN = 4,
} };
export const DisplayUnitRestriction = type('0 | 1 | 2 | 3 | 4'); export const DisplayUnitRestriction = type('0 | 1 | 2 | 3 | 4');
export type DisplayUnitRestrictionType = typeof DisplayUnitRestriction.infer; export type DisplayUnitRestrictionType = typeof DisplayUnitRestriction.infer;
@ -480,10 +462,9 @@ export enum AspectRatioTypeRestrictionEnum {
KEEP_ASPECT_RATIO = 1, KEEP_ASPECT_RATIO = 1,
// fixed // fixed
FIXED = 2, FIXED = 2,
} };
export const AspectRatioTypeRestriction = type('0 | 1 | 2'); export const AspectRatioTypeRestriction = type('0 | 1 | 2');
export type AspectRatioTypeRestrictionType = export type AspectRatioTypeRestrictionType = typeof AspectRatioTypeRestriction.infer;
typeof AspectRatioTypeRestriction.infer;
export const VideoSchema = type({ export const VideoSchema = type({
FlagInterlaced: FlagInterlacedRestriction.default(0), FlagInterlaced: FlagInterlacedRestriction.default(0),
@ -491,14 +472,14 @@ export const VideoSchema = type({
StereoMode: StereoModeRestriction.default(0), StereoMode: StereoModeRestriction.default(0),
AlphaMode: AlphaModeRestriction.default(0), AlphaMode: AlphaModeRestriction.default(0),
OldStereoMode: OldStereoModeRestriction.optional(), OldStereoMode: OldStereoModeRestriction.optional(),
PixelWidth: type.number.or(type.bigint), PixelWidth: type.number,
PixelHeight: type.number.or(type.bigint), PixelHeight: type.number,
PixelCropBottom: type.number.or(type.bigint).default(0), PixelCropBottom: type.number.default(0),
PixelCropTop: type.number.or(type.bigint).default(0), PixelCropTop: type.number.default(0),
PixelCropLeft: type.number.or(type.bigint).default(0), PixelCropLeft: type.number.default(0),
PixelCropRight: type.number.or(type.bigint).default(0), PixelCropRight: type.number.default(0),
DisplayWidth: type.number.or(type.bigint).optional(), DisplayWidth: type.number.optional(),
DisplayHeight: type.number.or(type.bigint).optional(), DisplayHeight: type.number.optional(),
DisplayUnit: DisplayUnitRestriction.default(0), DisplayUnit: DisplayUnitRestriction.default(0),
AspectRatioType: AspectRatioTypeRestriction.default(0), AspectRatioType: AspectRatioTypeRestriction.default(0),
UncompressedFourCC: BinarySchema.optional(), UncompressedFourCC: BinarySchema.optional(),
@ -537,18 +518,16 @@ export enum EmphasisRestrictionEnum {
PHONO_LONDON = 15, PHONO_LONDON = 15,
// Phono NARTB // Phono NARTB
PHONO_NARTB = 16, PHONO_NARTB = 16,
} };
export const EmphasisRestriction = type( export const EmphasisRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16');
'0 | 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16'
);
export type EmphasisRestrictionType = typeof EmphasisRestriction.infer; export type EmphasisRestrictionType = typeof EmphasisRestriction.infer;
export const AudioSchema = type({ export const AudioSchema = type({
SamplingFrequency: type.number.default(0), SamplingFrequency: type.number.default(0),
OutputSamplingFrequency: type.number.optional(), OutputSamplingFrequency: type.number.optional(),
Channels: type.number.or(type.bigint).default(1), Channels: type.number.default(1),
ChannelPositions: BinarySchema.optional(), ChannelPositions: BinarySchema.optional(),
BitDepth: type.number.or(type.bigint).optional(), BitDepth: type.number.optional(),
Emphasis: EmphasisRestriction.default(0), Emphasis: EmphasisRestriction.default(0),
}); });
@ -561,13 +540,12 @@ export enum TrackPlaneTypeRestrictionEnum {
RIGHT_EYE = 1, RIGHT_EYE = 1,
// background // background
BACKGROUND = 2, BACKGROUND = 2,
} };
export const TrackPlaneTypeRestriction = type('0 | 1 | 2'); export const TrackPlaneTypeRestriction = type('0 | 1 | 2');
export type TrackPlaneTypeRestrictionType = export type TrackPlaneTypeRestrictionType = typeof TrackPlaneTypeRestriction.infer;
typeof TrackPlaneTypeRestriction.infer;
export const TrackPlaneSchema = type({ export const TrackPlaneSchema = type({
TrackPlaneUID: type.number.or(type.bigint), TrackPlaneUID: type.number,
TrackPlaneType: TrackPlaneTypeRestriction, TrackPlaneType: TrackPlaneTypeRestriction,
}); });
@ -580,7 +558,7 @@ export const TrackCombinePlanesSchema = type({
export type TrackCombinePlanesType = typeof TrackCombinePlanesSchema.infer; export type TrackCombinePlanesType = typeof TrackCombinePlanesSchema.infer;
export const TrackJoinBlocksSchema = type({ export const TrackJoinBlocksSchema = type({
TrackJoinUID: type.number.or(type.bigint).array().atLeastLength(1), TrackJoinUID: type.number.array().atLeastLength(1),
}); });
export type TrackJoinBlocksType = typeof TrackJoinBlocksSchema.infer; export type TrackJoinBlocksType = typeof TrackJoinBlocksSchema.infer;
@ -601,10 +579,9 @@ export enum ContentCompAlgoRestrictionEnum {
LZO1X = 2, LZO1X = 2,
// Header Stripping // Header Stripping
HEADER_STRIPPING = 3, HEADER_STRIPPING = 3,
} };
export const ContentCompAlgoRestriction = type('0 | 1 | 2 | 3'); export const ContentCompAlgoRestriction = type('0 | 1 | 2 | 3');
export type ContentCompAlgoRestrictionType = export type ContentCompAlgoRestrictionType = typeof ContentCompAlgoRestriction.infer;
typeof ContentCompAlgoRestriction.infer;
export const ContentCompressionSchema = type({ export const ContentCompressionSchema = type({
ContentCompAlgo: ContentCompAlgoRestriction.default(0), ContentCompAlgo: ContentCompAlgoRestriction.default(0),
@ -618,17 +595,15 @@ export enum AESSettingsCipherModeRestrictionEnum {
AES_CTR = 1, AES_CTR = 1,
// AES-CBC // AES-CBC
AES_CBC = 2, AES_CBC = 2,
} };
export const AESSettingsCipherModeRestriction = type('1 | 2'); export const AESSettingsCipherModeRestriction = type('1 | 2');
export type AESSettingsCipherModeRestrictionType = export type AESSettingsCipherModeRestrictionType = typeof AESSettingsCipherModeRestriction.infer;
typeof AESSettingsCipherModeRestriction.infer;
export const ContentEncAESSettingsSchema = type({ export const ContentEncAESSettingsSchema = type({
AESSettingsCipherMode: AESSettingsCipherModeRestriction, AESSettingsCipherMode: AESSettingsCipherModeRestriction,
}); });
export type ContentEncAESSettingsType = export type ContentEncAESSettingsType = typeof ContentEncAESSettingsSchema.infer;
typeof ContentEncAESSettingsSchema.infer;
export enum ContentEncAlgoRestrictionEnum { export enum ContentEncAlgoRestrictionEnum {
// Not encrypted // Not encrypted
@ -643,20 +618,18 @@ export enum ContentEncAlgoRestrictionEnum {
BLOWFISH = 4, BLOWFISH = 4,
// AES // AES
AES = 5, AES = 5,
} };
export const ContentEncAlgoRestriction = type('0 | 1 | 2 | 3 | 4 | 5'); export const ContentEncAlgoRestriction = type('0 | 1 | 2 | 3 | 4 | 5');
export type ContentEncAlgoRestrictionType = export type ContentEncAlgoRestrictionType = typeof ContentEncAlgoRestriction.infer;
typeof ContentEncAlgoRestriction.infer;
export enum ContentSigAlgoRestrictionEnum { export enum ContentSigAlgoRestrictionEnum {
// Not signed // Not signed
NOT_SIGNED = 0, NOT_SIGNED = 0,
// RSA // RSA
RSA = 1, RSA = 1,
} };
export const ContentSigAlgoRestriction = type('0 | 1'); export const ContentSigAlgoRestriction = type('0 | 1');
export type ContentSigAlgoRestrictionType = export type ContentSigAlgoRestrictionType = typeof ContentSigAlgoRestriction.infer;
typeof ContentSigAlgoRestriction.infer;
export enum ContentSigHashAlgoRestrictionEnum { export enum ContentSigHashAlgoRestrictionEnum {
// Not signed // Not signed
@ -665,10 +638,9 @@ export enum ContentSigHashAlgoRestrictionEnum {
SHA1_160 = 1, SHA1_160 = 1,
// MD5 // MD5
MD5 = 2, MD5 = 2,
} };
export const ContentSigHashAlgoRestriction = type('0 | 1 | 2'); export const ContentSigHashAlgoRestriction = type('0 | 1 | 2');
export type ContentSigHashAlgoRestrictionType = export type ContentSigHashAlgoRestrictionType = typeof ContentSigHashAlgoRestriction.infer;
typeof ContentSigHashAlgoRestriction.infer;
export const ContentEncryptionSchema = type({ export const ContentEncryptionSchema = type({
ContentEncAlgo: ContentEncAlgoRestriction.default(0), ContentEncAlgo: ContentEncAlgoRestriction.default(0),
@ -689,23 +661,21 @@ export enum ContentEncodingScopeRestrictionEnum {
PRIVATE = 2, PRIVATE = 2,
// Next // Next
NEXT = 4, NEXT = 4,
} };
export const ContentEncodingScopeRestriction = type('1 | 2 | 4'); export const ContentEncodingScopeRestriction = type('1 | 2 | 4');
export type ContentEncodingScopeRestrictionType = export type ContentEncodingScopeRestrictionType = typeof ContentEncodingScopeRestriction.infer;
typeof ContentEncodingScopeRestriction.infer;
export enum ContentEncodingTypeRestrictionEnum { export enum ContentEncodingTypeRestrictionEnum {
// Compression // Compression
COMPRESSION = 0, COMPRESSION = 0,
// Encryption // Encryption
ENCRYPTION = 1, ENCRYPTION = 1,
} };
export const ContentEncodingTypeRestriction = type('0 | 1'); export const ContentEncodingTypeRestriction = type('0 | 1');
export type ContentEncodingTypeRestrictionType = export type ContentEncodingTypeRestrictionType = typeof ContentEncodingTypeRestriction.infer;
typeof ContentEncodingTypeRestriction.infer;
export const ContentEncodingSchema = type({ export const ContentEncodingSchema = type({
ContentEncodingOrder: type.number.or(type.bigint).default(0), ContentEncodingOrder: type.number.default(0),
ContentEncodingScope: ContentEncodingScopeRestriction.default(1), ContentEncodingScope: ContentEncodingScopeRestriction.default(1),
ContentEncodingType: ContentEncodingTypeRestriction.default(0), ContentEncodingType: ContentEncodingTypeRestriction.default(0),
ContentCompression: ContentCompressionSchema.optional(), ContentCompression: ContentCompressionSchema.optional(),
@ -737,53 +707,53 @@ export enum TrackTypeRestrictionEnum {
CONTROL = 32, CONTROL = 32,
// metadata // metadata
METADATA = 33, METADATA = 33,
} };
export const TrackTypeRestriction = type('1 | 2 | 3 | 16 | 17 | 18 | 32 | 33'); export const TrackTypeRestriction = type('1 | 2 | 3 | 16 | 17 | 18 | 32 | 33');
export type TrackTypeRestrictionType = typeof TrackTypeRestriction.infer; export type TrackTypeRestrictionType = typeof TrackTypeRestriction.infer;
export const TrackEntrySchema = type({ export const TrackEntrySchema = type({
TrackNumber: type.number.or(type.bigint), TrackNumber: type.number,
TrackUID: type.number.or(type.bigint), TrackUID: type.number,
TrackType: TrackTypeRestriction, TrackType: TrackTypeRestriction,
FlagEnabled: type.number.or(type.bigint).default(1), FlagEnabled: type.number.default(1),
FlagDefault: type.number.or(type.bigint).default(1), FlagDefault: type.number.default(1),
FlagForced: type.number.or(type.bigint).default(0), FlagForced: type.number.default(0),
FlagHearingImpaired: type.number.or(type.bigint).optional(), FlagHearingImpaired: type.number.optional(),
FlagVisualImpaired: type.number.or(type.bigint).optional(), FlagVisualImpaired: type.number.optional(),
FlagTextDescriptions: type.number.or(type.bigint).optional(), FlagTextDescriptions: type.number.optional(),
FlagOriginal: type.number.or(type.bigint).optional(), FlagOriginal: type.number.optional(),
FlagCommentary: type.number.or(type.bigint).optional(), FlagCommentary: type.number.optional(),
FlagLacing: type.number.or(type.bigint).default(1), FlagLacing: type.number.default(1),
MinCache: type.number.or(type.bigint).default(0), MinCache: type.number.default(0),
MaxCache: type.number.or(type.bigint).optional(), MaxCache: type.number.optional(),
DefaultDuration: type.number.or(type.bigint).optional(), DefaultDuration: type.number.optional(),
DefaultDecodedFieldDuration: type.number.or(type.bigint).optional(), DefaultDecodedFieldDuration: type.number.optional(),
TrackTimestampScale: type.number.default(0), TrackTimestampScale: type.number.default(0),
TrackOffset: type.number.or(type.bigint).default(0), TrackOffset: type.number.default(0),
MaxBlockAdditionID: type.number.or(type.bigint).default(0), MaxBlockAdditionID: type.number.default(0),
BlockAdditionMapping: BlockAdditionMappingSchema.array().optional(), BlockAdditionMapping: BlockAdditionMappingSchema.array().optional(),
Name: type.string.optional(), Name: type.string.optional(),
Language: type.string.default('eng'), Language: type.string.default("eng"),
LanguageBCP47: type.string.optional(), LanguageBCP47: type.string.optional(),
CodecID: type.string, CodecID: type.string,
CodecPrivate: BinarySchema.optional(), CodecPrivate: BinarySchema.optional(),
CodecName: type.string.optional(), CodecName: type.string.optional(),
AttachmentLink: type.number.or(type.bigint).optional(), AttachmentLink: type.number.optional(),
CodecSettings: type.string.optional(), CodecSettings: type.string.optional(),
CodecInfoURL: type.string.array().optional(), CodecInfoURL: type.string.array().optional(),
CodecDownloadURL: type.string.array().optional(), CodecDownloadURL: type.string.array().optional(),
CodecDecodeAll: type.number.or(type.bigint).default(1), CodecDecodeAll: type.number.default(1),
TrackOverlay: type.number.or(type.bigint).array().optional(), TrackOverlay: type.number.array().optional(),
CodecDelay: type.number.or(type.bigint).default(0), CodecDelay: type.number.default(0),
SeekPreRoll: type.number.or(type.bigint).default(0), SeekPreRoll: type.number.default(0),
TrackTranslate: TrackTranslateSchema.array().optional(), TrackTranslate: TrackTranslateSchema.array().optional(),
Video: VideoSchema.optional(), Video: VideoSchema.optional(),
Audio: AudioSchema.optional(), Audio: AudioSchema.optional(),
TrackOperation: TrackOperationSchema.optional(), TrackOperation: TrackOperationSchema.optional(),
TrickTrackUID: type.number.or(type.bigint).optional(), TrickTrackUID: type.number.optional(),
TrickTrackSegmentUID: BinarySchema.optional(), TrickTrackSegmentUID: BinarySchema.optional(),
TrickTrackFlag: type.number.or(type.bigint).default(0), TrickTrackFlag: type.number.default(0),
TrickMasterTrackUID: type.number.or(type.bigint).optional(), TrickMasterTrackUID: type.number.optional(),
TrickMasterTrackSegmentUID: BinarySchema.optional(), TrickMasterTrackSegmentUID: BinarySchema.optional(),
ContentEncodings: ContentEncodingsSchema.optional(), ContentEncodings: ContentEncodingsSchema.optional(),
}); });
@ -797,28 +767,28 @@ export const TracksSchema = type({
export type TracksType = typeof TracksSchema.infer; export type TracksType = typeof TracksSchema.infer;
export const CueReferenceSchema = type({ export const CueReferenceSchema = type({
CueRefTime: type.number.or(type.bigint), CueRefTime: type.number,
CueRefCluster: type.number.or(type.bigint), CueRefCluster: type.number,
CueRefNumber: type.number.or(type.bigint).default(1), CueRefNumber: type.number.default(1),
CueRefCodecState: type.number.or(type.bigint).default(0), CueRefCodecState: type.number.default(0),
}); });
export type CueReferenceType = typeof CueReferenceSchema.infer; export type CueReferenceType = typeof CueReferenceSchema.infer;
export const CueTrackPositionsSchema = type({ export const CueTrackPositionsSchema = type({
CueTrack: type.number.or(type.bigint), CueTrack: type.number,
CueClusterPosition: type.number.or(type.bigint), CueClusterPosition: type.number,
CueRelativePosition: type.number.or(type.bigint).optional(), CueRelativePosition: type.number.optional(),
CueDuration: type.number.or(type.bigint).optional(), CueDuration: type.number.optional(),
CueBlockNumber: type.number.or(type.bigint).optional(), CueBlockNumber: type.number.optional(),
CueCodecState: type.number.or(type.bigint).default(0), CueCodecState: type.number.default(0),
CueReference: CueReferenceSchema.array().optional(), CueReference: CueReferenceSchema.array().optional(),
}); });
export type CueTrackPositionsType = typeof CueTrackPositionsSchema.infer; export type CueTrackPositionsType = typeof CueTrackPositionsSchema.infer;
export const CuePointSchema = type({ export const CuePointSchema = type({
CueTime: type.number.or(type.bigint), CueTime: type.number,
CueTrackPositions: CueTrackPositionsSchema.array().atLeastLength(1), CueTrackPositions: CueTrackPositionsSchema.array().atLeastLength(1),
}); });
@ -835,10 +805,10 @@ export const AttachedFileSchema = type({
FileName: type.string, FileName: type.string,
FileMediaType: type.string, FileMediaType: type.string,
FileData: BinarySchema, FileData: BinarySchema,
FileUID: type.number.or(type.bigint), FileUID: type.number,
FileReferral: BinarySchema.optional(), FileReferral: BinarySchema.optional(),
FileUsedStartTime: type.number.or(type.bigint).optional(), FileUsedStartTime: type.number.optional(),
FileUsedEndTime: type.number.or(type.bigint).optional(), FileUsedEndTime: type.number.optional(),
}); });
export type AttachedFileType = typeof AttachedFileSchema.infer; export type AttachedFileType = typeof AttachedFileSchema.infer;
@ -856,110 +826,13 @@ export const EditionDisplaySchema = type({
export type EditionDisplayType = typeof EditionDisplaySchema.infer; export type EditionDisplayType = typeof EditionDisplaySchema.infer;
export const ChapterTrackSchema = type({
ChapterTrackUID: type.number.or(type.bigint).array().atLeastLength(1),
});
export type ChapterTrackType = typeof ChapterTrackSchema.infer;
export const ChapLanguageSchema = match({
'string[]': (v) => (v.length > 0 ? v : ['eng']),
default: () => ['eng'],
}).optional();
export const ChapterDisplaySchema = type({
ChapString: type.string,
ChapLanguage: ChapLanguageSchema,
ChapLanguageBCP47: type.string.array().optional(),
ChapCountry: type.string.array().optional(),
});
export type ChapterDisplayType = typeof ChapterDisplaySchema.infer;
export enum ChapProcessTimeRestrictionEnum {
// during the whole chapter
DURING_THE_WHOLE_CHAPTER = 0,
// before starting playback
BEFORE_STARTING_PLAYBACK = 1,
// after playback of the chapter
AFTER_PLAYBACK_OF_THE_CHAPTER = 2,
}
export const ChapProcessTimeRestriction = type('0 | 1 | 2');
export type ChapProcessTimeRestrictionType =
typeof ChapProcessTimeRestriction.infer;
export const ChapProcessCommandSchema = type({
ChapProcessTime: ChapProcessTimeRestriction,
ChapProcessData: BinarySchema,
});
export type ChapProcessCommandType = typeof ChapProcessCommandSchema.infer;
export enum ChapProcessCodecIDRestrictionEnum {
// Matroska Script
MATROSKA_SCRIPT = 0,
// DVD-menu
DVD_MENU = 1,
}
export const ChapProcessCodecIDRestriction = type('0 | 1');
export type ChapProcessCodecIDRestrictionType =
typeof ChapProcessCodecIDRestriction.infer;
export const ChapProcessSchema = type({
ChapProcessCodecID: ChapProcessCodecIDRestriction.default(0),
ChapProcessPrivate: BinarySchema.optional(),
ChapProcessCommand: ChapProcessCommandSchema.array().optional(),
});
export type ChapProcessType = typeof ChapProcessSchema.infer;
export enum ChapterSkipTypeRestrictionEnum {
// No Skipping
NO_SKIPPING = 0,
// Opening Credits
OPENING_CREDITS = 1,
// End Credits
END_CREDITS = 2,
// Recap
RECAP = 3,
// Next Preview
NEXT_PREVIEW = 4,
// Preview
PREVIEW = 5,
// Advertisement
ADVERTISEMENT = 6,
// Intermission
INTERMISSION = 7,
}
export const ChapterSkipTypeRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 6 | 7');
export type ChapterSkipTypeRestrictionType =
typeof ChapterSkipTypeRestriction.infer;
export const ChapterAtomSchema = type({
ChapterUID: type.number.or(type.bigint),
ChapterStringUID: type.string.optional(),
ChapterTimeStart: type.number.or(type.bigint),
ChapterTimeEnd: type.number.or(type.bigint).optional(),
ChapterFlagHidden: type.number.or(type.bigint).default(0),
ChapterFlagEnabled: type.number.or(type.bigint).default(1),
ChapterSegmentUUID: BinarySchema.optional(),
ChapterSkipType: ChapterSkipTypeRestriction.optional(),
ChapterSegmentEditionUID: type.number.or(type.bigint).optional(),
ChapterPhysicalEquiv: type.number.or(type.bigint).optional(),
ChapterTrack: ChapterTrackSchema.optional(),
ChapterDisplay: ChapterDisplaySchema.array().optional(),
ChapProcess: ChapProcessSchema.array().optional(),
});
export type ChapterAtomType = typeof ChapterAtomSchema.infer;
export const EditionEntrySchema = type({ export const EditionEntrySchema = type({
EditionUID: type.number.or(type.bigint).optional(), EditionUID: type.number.optional(),
EditionFlagHidden: type.number.or(type.bigint).default(0), EditionFlagHidden: type.number.default(0),
EditionFlagDefault: type.number.or(type.bigint).default(0), EditionFlagDefault: type.number.default(0),
EditionFlagOrdered: type.number.or(type.bigint).default(0), EditionFlagOrdered: type.number.default(0),
EditionDisplay: EditionDisplaySchema.array().optional(), EditionDisplay: EditionDisplaySchema.array().optional(),
ChapterAtom: ChapterAtomSchema.array().atLeastLength(1),
}); });
export type EditionEntryType = typeof EditionEntrySchema.infer; export type EditionEntryType = typeof EditionEntrySchema.infer;
@ -971,24 +844,28 @@ export const ChaptersSchema = type({
export type ChaptersType = typeof ChaptersSchema.infer; export type ChaptersType = typeof ChaptersSchema.infer;
export const TagTrackUIDSchema = match({ export const TagTrackUIDSchema = match({
'(number | bigint)[]': (v) => (v.length > 0 ? v : [0]), "number[]": v => v.length > 0 ? v : [0],
default: () => [0], "undefined": () => [0],
}).optional(); default: "assert"
});
export const TagEditionUIDSchema = match({ export const TagEditionUIDSchema = match({
'(number | bigint)[]': (v) => (v.length > 0 ? v : [0]), "number[]": v => v.length > 0 ? v : [0],
default: () => [0], "undefined": () => [0],
}).optional(); default: "assert"
});
export const TagChapterUIDSchema = match({ export const TagChapterUIDSchema = match({
'(number | bigint)[]': (v) => (v.length > 0 ? v : [0]), "number[]": v => v.length > 0 ? v : [0],
default: () => [0], "undefined": () => [0],
}).optional(); default: "assert"
});
export const TagAttachmentUIDSchema = match({ export const TagAttachmentUIDSchema = match({
'(number | bigint)[]': (v) => (v.length > 0 ? v : [0]), "number[]": v => v.length > 0 ? v : [0],
default: () => [0], "undefined": () => [0],
}).optional(); default: "assert"
});
export enum TargetTypeValueRestrictionEnum { export enum TargetTypeValueRestrictionEnum {
// SHOT // SHOT
@ -1005,60 +882,55 @@ export enum TargetTypeValueRestrictionEnum {
EDITION_ISSUE_VOLUME_OPUS_SEASON_SEQUEL = 60, EDITION_ISSUE_VOLUME_OPUS_SEASON_SEQUEL = 60,
// COLLECTION // COLLECTION
COLLECTION = 70, COLLECTION = 70,
} };
export const TargetTypeValueRestriction = type( export const TargetTypeValueRestriction = type('10 | 20 | 30 | 40 | 50 | 60 | 70');
'10 | 20 | 30 | 40 | 50 | 60 | 70' export type TargetTypeValueRestrictionType = typeof TargetTypeValueRestriction.infer;
);
export type TargetTypeValueRestrictionType =
typeof TargetTypeValueRestriction.infer;
export enum TargetTypeRestrictionEnum { export enum TargetTypeRestrictionEnum {
// TargetTypeValue 70 // TargetTypeValue 70
COLLECTION = 'COLLECTION', COLLECTION = "COLLECTION",
// TargetTypeValue 60 // TargetTypeValue 60
EDITION = 'EDITION', EDITION = "EDITION",
// TargetTypeValue 60 // TargetTypeValue 60
ISSUE = 'ISSUE', ISSUE = "ISSUE",
// TargetTypeValue 60 // TargetTypeValue 60
VOLUME = 'VOLUME', VOLUME = "VOLUME",
// TargetTypeValue 60 // TargetTypeValue 60
OPUS = 'OPUS', OPUS = "OPUS",
// TargetTypeValue 60 // TargetTypeValue 60
SEASON = 'SEASON', SEASON = "SEASON",
// TargetTypeValue 60 // TargetTypeValue 60
SEQUEL = 'SEQUEL', SEQUEL = "SEQUEL",
// TargetTypeValue 50 // TargetTypeValue 50
ALBUM = 'ALBUM', ALBUM = "ALBUM",
// TargetTypeValue 50 // TargetTypeValue 50
OPERA = 'OPERA', OPERA = "OPERA",
// TargetTypeValue 50 // TargetTypeValue 50
CONCERT = 'CONCERT', CONCERT = "CONCERT",
// TargetTypeValue 50 // TargetTypeValue 50
MOVIE = 'MOVIE', MOVIE = "MOVIE",
// TargetTypeValue 50 // TargetTypeValue 50
EPISODE = 'EPISODE', EPISODE = "EPISODE",
// TargetTypeValue 40 // TargetTypeValue 40
PART = 'PART', PART = "PART",
// TargetTypeValue 40 // TargetTypeValue 40
SESSION = 'SESSION', SESSION = "SESSION",
// TargetTypeValue 30 // TargetTypeValue 30
TRACK = 'TRACK', TRACK = "TRACK",
// TargetTypeValue 30 // TargetTypeValue 30
SONG = 'SONG', SONG = "SONG",
// TargetTypeValue 30 // TargetTypeValue 30
CHAPTER = 'CHAPTER', CHAPTER = "CHAPTER",
// TargetTypeValue 20 // TargetTypeValue 20
SUBTRACK = 'SUBTRACK', SUBTRACK = "SUBTRACK",
// TargetTypeValue 20 // TargetTypeValue 20
MOVEMENT = 'MOVEMENT', MOVEMENT = "MOVEMENT",
// TargetTypeValue 20 // TargetTypeValue 20
SCENE = 'SCENE', SCENE = "SCENE",
// TargetTypeValue 10 // TargetTypeValue 10
SHOT = 'SHOT', SHOT = "SHOT",
} };
export const TargetTypeRestriction = type( export const TargetTypeRestriction = type('"COLLECTION" | "EDITION" | "ISSUE" | "VOLUME" | "OPUS" | "SEASON" | "SEQUEL" | "ALBUM" | "OPERA" | "CONCERT" | "MOVIE" | "EPISODE" | "PART" | "SESSION" | "TRACK" | "SONG" | "CHAPTER" | "SUBTRACK" | "MOVEMENT" | "SCENE" | "SHOT"');
'"COLLECTION" | "EDITION" | "ISSUE" | "VOLUME" | "OPUS" | "SEASON" | "SEQUEL" | "ALBUM" | "OPERA" | "CONCERT" | "MOVIE" | "EPISODE" | "PART" | "SESSION" | "TRACK" | "SONG" | "CHAPTER" | "SUBTRACK" | "MOVEMENT" | "SCENE" | "SHOT"'
);
export type TargetTypeRestrictionType = typeof TargetTypeRestriction.infer; export type TargetTypeRestrictionType = typeof TargetTypeRestriction.infer;
export const TargetsSchema = type({ export const TargetsSchema = type({
@ -1072,21 +944,9 @@ export const TargetsSchema = type({
export type TargetsType = typeof TargetsSchema.infer; export type TargetsType = typeof TargetsSchema.infer;
export const SimpleTagSchema = type({
TagName: type.string,
TagLanguage: type.string.default('und'),
TagLanguageBCP47: type.string.optional(),
TagDefault: type.number.or(type.bigint).default(1),
TagDefaultBogus: type.number.or(type.bigint).default(1),
TagString: type.string.optional(),
TagBinary: BinarySchema.optional(),
});
export type SimpleTagType = typeof SimpleTagSchema.infer;
export const TagSchema = type({ export const TagSchema = type({
Targets: TargetsSchema, Targets: TargetsSchema,
SimpleTag: SimpleTagSchema.array().atLeastLength(1),
}); });
export type TagType = typeof TagSchema.infer; export type TagType = typeof TagSchema.infer;
@ -1138,23 +998,14 @@ export const IdMultiSet = new Set([
EbmlTagIdEnum.CuePoint, EbmlTagIdEnum.CuePoint,
EbmlTagIdEnum.AttachedFile, EbmlTagIdEnum.AttachedFile,
EbmlTagIdEnum.EditionLanguageIETF, EbmlTagIdEnum.EditionLanguageIETF,
EbmlTagIdEnum.ChapterTrackUID,
EbmlTagIdEnum.ChapLanguage,
EbmlTagIdEnum.ChapLanguageBCP47,
EbmlTagIdEnum.ChapCountry,
EbmlTagIdEnum.ChapProcessCommand,
EbmlTagIdEnum.ChapterDisplay,
EbmlTagIdEnum.ChapProcess,
EbmlTagIdEnum.EditionDisplay, EbmlTagIdEnum.EditionDisplay,
EbmlTagIdEnum.ChapterAtom,
EbmlTagIdEnum.EditionEntry, EbmlTagIdEnum.EditionEntry,
EbmlTagIdEnum.TagTrackUID, EbmlTagIdEnum.TagTrackUID,
EbmlTagIdEnum.TagEditionUID, EbmlTagIdEnum.TagEditionUID,
EbmlTagIdEnum.TagChapterUID, EbmlTagIdEnum.TagChapterUID,
EbmlTagIdEnum.TagAttachmentUID, EbmlTagIdEnum.TagAttachmentUID,
EbmlTagIdEnum.SimpleTag,
EbmlTagIdEnum.Tag, EbmlTagIdEnum.Tag,
EbmlTagIdEnum.SeekHead, EbmlTagIdEnum.SeekHead,
EbmlTagIdEnum.Cluster, EbmlTagIdEnum.Cluster,
EbmlTagIdEnum.Tags, EbmlTagIdEnum.Tags
]); ])

View File

@ -51,12 +51,3 @@ export function convertEbmlTagToComponent(tag: EbmlTagType) {
} }
return tag.data; return tag.data;
} }
export function waitTick() {
return new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
resolve();
timeout && clearTimeout(timeout);
}, 0);
});
}

View File

@ -1,358 +1,49 @@
import { html, css, LitElement } from 'lit'; import { html, css, LitElement } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { import { type Subscription, switchMap, take } from 'rxjs';
animationFrames, import { createEbmlController } from './media/mkv/reactive';
BehaviorSubject,
combineLatest,
EMPTY,
map,
Subject,
switchMap,
take,
distinctUntilChanged,
fromEvent,
share,
takeUntil,
firstValueFrom,
tap,
throwIfEmpty,
ReplaySubject,
} from 'rxjs';
import { createMatroska } from '@konoplayer/matroska/model';
import { createRef, ref, type Ref } from 'lit/directives/ref.js';
import { Queue } from 'mnemonist';
import type {
AudioTrackContext,
VideoTrackContext,
} from '@konoplayer/matroska/systems';
import {
captureCanvasAsVideoSrcObject,
createRenderingContext,
renderBitmapAtRenderingContext,
} from '@konoplayer/core/graphics';
export class VideoPipelineDemo extends LitElement { export class VideoPipelineDemo extends LitElement {
static styles = css``;
@property() @property()
src!: string; src!: string;
@property({ type: Number }) subscripton?: Subscription;
width = 1280;
@property({ type: Number }) static styles = css``;
height = 720;
destroyRef$ = new Subject<void>(); async prepareVideoPipeline() {
if (!this.src) {
videoRef: Ref<HTMLVideoElement> = createRef();
renderingContext = createRenderingContext();
audioContext = new AudioContext({});
seeked$ = new ReplaySubject<number>(1);
videoFrameBuffer$ = new BehaviorSubject(new Queue<VideoFrame>());
audioFrameBuffer$ = new BehaviorSubject(new Queue<AudioData>());
paused$ = new BehaviorSubject<boolean>(false);
ended$ = new BehaviorSubject<boolean>(false);
currentTime$ = new BehaviorSubject<number>(0);
duration$ = new BehaviorSubject<number>(0);
frameRate$ = new BehaviorSubject<number>(30);
videoTrack$ = new BehaviorSubject<VideoTrackContext | undefined>(undefined);
audioTrack$ = new BehaviorSubject<AudioTrackContext | undefined>(undefined);
private async preparePipeline() {
const src = this.src;
const destroyRef$ = this.destroyRef$;
if (!src) {
return; return;
} }
const { const { controller$ } = createEbmlController(this.src);
segment: {
seek, this.subscripton = controller$
defaultVideoTrack$, .pipe(
defaultAudioTrack$, switchMap(({ segments$ }) => segments$.pipe(take(1))),
videoTrackDecoder, switchMap(({ seek }) => seek(0))
audioTrackDecoder, )
.subscribe((cluster) => console.log(cluster));
const videoDecoder = new VideoDecoder({
output: (frame) => {},
error: (e) => {
e;
}, },
totalSize,
} = await firstValueFrom(
createMatroska({
url: src,
}).pipe(throwIfEmpty(() => new Error('failed to extract matroska')))
);
console.debug(`[MATROSKA]: loaded metadata, total size ${totalSize} bytes`);
const currentCluster$ = this.seeked$.pipe(
switchMap((seekTime) => seek(seekTime)),
share({
resetOnRefCountZero: false,
resetOnError: false,
resetOnComplete: false,
})
);
defaultVideoTrack$
.pipe(
take(1),
takeUntil(destroyRef$),
tap((track) => console.debug('[MATROSKA]: video track loaded,', track))
)
.subscribe(this.videoTrack$.next.bind(this.videoTrack$));
defaultAudioTrack$
.pipe(
take(1),
takeUntil(destroyRef$),
tap((track) => console.debug('[MATROSKA]: audio track loaded,', track))
)
.subscribe(this.audioTrack$.next.bind(this.audioTrack$));
this.videoTrack$
.pipe(
takeUntil(this.destroyRef$),
switchMap((track) =>
track?.configuration
? videoTrackDecoder(track, currentCluster$)
: EMPTY
),
switchMap(({ frame$ }) => frame$)
)
.subscribe((frame) => {
const buffer = this.videoFrameBuffer$.value;
buffer.enqueue(frame);
this.videoFrameBuffer$.next(buffer);
});
this.audioTrack$
.pipe(
takeUntil(this.destroyRef$),
switchMap((track) =>
track?.configuration
? audioTrackDecoder(track, currentCluster$)
: EMPTY
),
switchMap(({ frame$ }) => frame$)
)
.subscribe((frame) => {
const buffer = this.audioFrameBuffer$.value;
buffer.enqueue(frame);
this.audioFrameBuffer$.next(buffer);
});
let playableStartTime = 0;
const playable = combineLatest({
paused: this.paused$,
ended: this.ended$,
audioBuffered: this.audioFrameBuffer$.pipe(
map((q) => q.size >= 1),
distinctUntilChanged()
),
videoBuffered: this.videoFrameBuffer$.pipe(
map((q) => q.size >= 1),
distinctUntilChanged()
),
}).pipe(
takeUntil(this.destroyRef$),
map(
({ ended, paused, videoBuffered, audioBuffered }) =>
!paused && !ended && !!(videoBuffered || audioBuffered)
),
tap((enabled) => {
if (enabled) {
playableStartTime = performance.now();
}
}),
share()
);
let nextAudioStartTime = 0;
playable
.pipe(
tap(() => {
nextAudioStartTime = 0;
}),
switchMap((enabled) => (enabled ? animationFrames() : EMPTY))
)
.subscribe(() => {
const audioFrameBuffer = this.audioFrameBuffer$.getValue();
const audioContext = this.audioContext;
const nowTime = performance.now();
const accTime = nowTime - playableStartTime;
let audioChanged = false;
while (audioFrameBuffer.size > 0) {
const firstAudio = audioFrameBuffer.peek();
if (firstAudio && firstAudio.timestamp / 1000 <= accTime) {
const audioFrame = audioFrameBuffer.dequeue()!;
audioChanged = true;
if (audioContext) {
const numberOfChannels = audioFrame.numberOfChannels;
const sampleRate = audioFrame.sampleRate;
const numberOfFrames = audioFrame.numberOfFrames;
const audioBuffer = audioContext.createBuffer(
numberOfChannels,
numberOfFrames,
sampleRate
);
// add fade-in-out
const fadeLength = Math.min(50, audioFrame.numberOfFrames);
for (let channel = 0; channel < numberOfChannels; channel++) {
const channelData = new Float32Array(numberOfFrames);
audioFrame.copyTo(channelData, {
planeIndex: channel,
frameCount: numberOfFrames,
});
for (let i = 0; i < fadeLength; i++) {
channelData[i] *= i / fadeLength; // fade-in
channelData[audioFrame.numberOfFrames - 1 - i] *=
i / fadeLength; // fade-out
}
audioBuffer.copyToChannel(channelData, channel);
}
/**
* @TODO: ADD TIME SYNC
*/
const audioTime = audioFrame.timestamp / 1_000_000;
audioFrame.close();
if (audioContext.state === 'running') {
const audioSource = audioContext.createBufferSource();
audioSource.buffer = audioBuffer;
audioSource.connect(audioContext.destination);
const currentTime = audioContext.currentTime;
nextAudioStartTime = Math.max(nextAudioStartTime, currentTime); // 确保不早于当前时间
audioSource.start(nextAudioStartTime);
nextAudioStartTime += audioBuffer.duration;
}
}
} else {
break;
}
}
if (audioChanged) {
this.audioFrameBuffer$.next(this.audioFrameBuffer$.getValue());
}
});
playable
.pipe(switchMap((enabled) => (enabled ? animationFrames() : EMPTY)))
.subscribe(async () => {
const renderingContext = this.renderingContext;
const videoFrameBuffer = this.videoFrameBuffer$.getValue();
let videoChanged = false;
const nowTime = performance.now();
const accTime = nowTime - playableStartTime;
while (videoFrameBuffer.size > 0) {
const firstVideo = videoFrameBuffer.peek();
if (firstVideo && firstVideo.timestamp / 1000 <= accTime) {
const videoFrame = videoFrameBuffer.dequeue()!;
videoChanged = true;
if (renderingContext) {
const bitmap = await createImageBitmap(videoFrame);
renderBitmapAtRenderingContext(renderingContext, bitmap);
}
videoFrame.close();
} else {
break;
}
}
if (videoChanged) {
this.videoFrameBuffer$.next(videoFrameBuffer);
}
});
fromEvent(document.body, 'click')
.pipe(takeUntil(this.destroyRef$))
.subscribe(async () => {
const permissionStatus = await navigator.permissions.query({
name: 'microphone',
});
if (permissionStatus.state === 'prompt') {
await navigator.mediaDevices.getUserMedia({
audio: true,
}); });
} }
this.audioContext.resume();
this.audioFrameBuffer$.next(this.audioFrameBuffer$.getValue());
});
const permissionStatus = await navigator.permissions.query({ connectedCallback(): void {
name: 'microphone',
});
if (permissionStatus.state === 'granted') {
await navigator.mediaDevices.getUserMedia({
audio: true,
});
this.audioContext.resume();
}
this.seeked$.next(0);
}
async connectedCallback() {
super.connectedCallback(); super.connectedCallback();
await this.preparePipeline(); this.prepareVideoPipeline();
} }
disconnectedCallback(): void { disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
this.destroyRef$.next(undefined); this.subscripton?.unsubscribe();
}
firstUpdated() {
const video = this.videoRef.value;
const context = this.renderingContext;
const frameRate$ = this.frameRate$;
const destroyRef$ = this.destroyRef$;
const currentTime$ = this.currentTime$;
const duration$ = this.duration$;
const seeked$ = this.seeked$;
if (!video) {
return;
}
const canvas = context.canvas as HTMLCanvasElement;
Object.defineProperty(video, 'duration', {
get: () => duration$.value,
set: (val: number) => {
duration$.next(val);
},
configurable: true,
});
Object.defineProperty(video, 'currentTime', {
get: () => currentTime$.value,
set: (val: number) => {
currentTime$.next(val);
seeked$.next(val);
},
configurable: true,
});
frameRate$
.pipe(takeUntil(destroyRef$), distinctUntilChanged())
.subscribe((frameRate) => {
canvas.width = this.width || 1;
canvas.height = this.height || 1;
captureCanvasAsVideoSrcObject(video, canvas, frameRate);
});
} }
render() { render() {
return html` return html`<video />`;
<video ref=${ref(this.videoRef)} width=${this.width} height=${this.height} autoplay muted></video>
`;
} }
} }

View File

@ -2,25 +2,19 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"target": "ES2020",
"outDir": "./dist", "outDir": "./dist",
"experimentalDecorators": true,
"module": "ESNext",
"moduleResolution": "bundler",
"useDefineForClassFields": false,
"paths": { "paths": {
"@konoplayer/core/*": [ "@/*": [
"../../packages/core/src/*" "./src/*"
],
"@konoplayer/matroska/*": [
"../../packages/matroska/src/*"
] ]
} }
}, },
"include": [ "include": [
"src" "src"
],
"references": [
{
"path": "../../packages/core"
},
{
"path": "../../packages/matroska"
}
] ]
} }

View File

@ -5,7 +5,7 @@
} }
``` ```
#^https://konoplayer.com/api/static/*** resSpeed://10240+ # ^https://konoplayer.com/api/static/*** resSpeed://1024K
^https://konoplayer.com/api*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5001/api$1 ^https://konoplayer.com/api*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5001/api$1
^https://konoplayer.com/*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5000/$1 excludeFilter://^https://konoplayer.com/api weinre://test ^https://konoplayer.com/*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5000/$1 excludeFilter://^https://konoplayer.com/api
^wss://konoplayer.com/*** reqHeaders://{x-forwarded.json} ws://127.0.0.1:5000/$1 excludeFilter://^wss://konoplayer.com/api ^wss://konoplayer.com/*** reqHeaders://{x-forwarded.json} ws://127.0.0.1:5000/$1 excludeFilter://^wss://konoplayer.com/api

View File

@ -1,5 +1,5 @@
{ {
"name": "@konoplayer/proxy", "name": "proxy",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@ -1 +0,0 @@
{"version":"3.0.9","results":[[":src/matroska/codecs/av1.spec",{"duration":52.71331099999952,"failed":false}]]}

View File

@ -1,17 +0,0 @@
{
"name": "@konoplayer/test",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {},
"dependencies": {
"@konoplayer/core": "workspace:*",
"@konoplayer/matroska": "workspace:*",
"konoebml": "^0.1.2"
},
"devDependencies": {
"unplugin-swc": "^1.5.1",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.9"
}
}

View File

@ -1,2 +0,0 @@
video/huge/*
!video/huge/.gitkeep

Binary file not shown.

View File

@ -1,47 +0,0 @@
import { SegmentSchema, SegmentType } from '@konoplayer/matroska/schema';
import { VideoCodecId } from '@konoplayer/matroska/codecs';
import {
parseAV1DecoderConfigurationRecord,
genCodecStringByAV1DecoderConfigurationRecord,
} from '@konoplayer/matroska/codecs/av1';
import { loadComponentFromRangedResource } from '../utils/data';
import { EbmlTagIdEnum, EbmlTagPosition } from 'konoebml';
import { isTagIdPos } from '@konoplayer/matroska/util';
describe('AV1 code test', () => {
it('should parse av1 meta from track entry', async () => {
const [segment] = await loadComponentFromRangedResource<SegmentType>({
resource: 'video/test-av1.mkv',
predicate: isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End),
schema: SegmentSchema,
});
const av1Track = segment.Tracks?.TrackEntry.find(
(t) => t.CodecID === VideoCodecId.AV1
)!;
expect(av1Track).toBeDefined();
expect(av1Track.CodecPrivate).toBeDefined();
const meta = parseAV1DecoderConfigurationRecord(av1Track)!;
expect(meta).toBeDefined();
const codecStr = genCodecStringByAV1DecoderConfigurationRecord(meta);
expect(meta.marker).toBe(1);
expect(meta.version).toBe(1);
expect(meta.seqProfile).toBe(0);
expect(meta.seqLevelIdx0).toBe(1);
expect(meta.seqTier0).toBe(0);
expect(meta.highBitdepth).toBe(0);
expect(meta.monochrome).toBe(0);
expect(
`${meta.chromaSubsamplingX}${meta.chromaSubsamplingY}${meta.chromaSamplePosition}`
).toBe('110');
expect(meta.initialPresentationDelayMinus1).toBeUndefined();
expect(codecStr).toBe('av01.0.01M.08.0.110');
});
});

View File

@ -1,40 +0,0 @@
import { SegmentSchema, SegmentType } from '@konoplayer/matroska/schema';
import { VideoCodecId } from '@konoplayer/matroska/codecs';
import {
parseAVCDecoderConfigurationRecord,
genCodecStringByAVCDecoderConfigurationRecord,
} from '@konoplayer/matroska/codecs/avc';
import { loadComponentFromRangedResource } from '../utils/data';
import { EbmlTagIdEnum, EbmlTagPosition } from 'konoebml';
import { isTagIdPos } from '@konoplayer/matroska/util';
describe('AVC code test', () => {
it('should parse avc meta from track entry', async () => {
const [segment] = await loadComponentFromRangedResource<SegmentType>({
resource: 'video/test-avc.mkv',
predicate: isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End),
schema: SegmentSchema,
});
const avcTrack = segment.Tracks?.TrackEntry.find(
(t) => t.CodecID === VideoCodecId.H264
)!;
expect(avcTrack).toBeDefined();
expect(avcTrack.CodecPrivate).toBeDefined();
const meta = parseAVCDecoderConfigurationRecord(avcTrack)!;
expect(meta).toBeDefined();
const codecStr = genCodecStringByAVCDecoderConfigurationRecord(meta);
expect(meta.configurationVersion).toBe(1);
expect(meta.avcProfileIndication).toBe(100);
expect(meta.profileCompatibility).toBe(0);
expect(meta.avcLevelIndication).toBe(30);
expect(codecStr).toBe('avc1.64001e');
});
});

View File

@ -1,106 +0,0 @@
import { SegmentSchema, SegmentType } from '@konoplayer/matroska/schema';
import { VideoCodecId } from '@konoplayer/matroska/codecs';
import {
parseHEVCDecoderConfigurationRecord,
genCodecStringByHEVCDecoderConfigurationRecord,
HEVCDecoderConfigurationRecordType,
} from '@konoplayer/matroska/codecs/hevc';
import { loadComponentFromRangedResource } from '../utils/data';
import { EbmlTagIdEnum, EbmlTagPosition } from 'konoebml';
import { isTagIdPos } from '@konoplayer/matroska/util';
import { assert } from 'vitest';
describe('HEVC codec test', () => {
it('should parse hevc meta from track entry', async () => {
const [segment] = await loadComponentFromRangedResource<SegmentType>({
resource: 'video/test-hevc.mkv',
predicate: isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End),
schema: SegmentSchema,
});
const hevcTrack = segment.Tracks?.TrackEntry.find(
(t) => t.CodecID === VideoCodecId.HEVC
)!;
expect(hevcTrack).toBeDefined();
expect(hevcTrack.CodecPrivate).toBeDefined();
const meta = parseHEVCDecoderConfigurationRecord(hevcTrack);
expect(meta).toBeDefined();
const codecStr = genCodecStringByHEVCDecoderConfigurationRecord(meta);
expect(codecStr).toBe('hev1.1.6.L63.90');
});
it('should match chrome test suite', () => {
function makeHEVCParameterSet(
generalProfileSpace: number,
generalProfileIDC: number,
generalProfileCompatibilityFlags: number,
generalTierFlag: number,
generalConstraintIndicatorFlags: [
number,
number,
number,
number,
number,
number,
],
generalLevelIDC: number
) {
return {
generalProfileSpace: generalProfileSpace,
generalProfileIdc: generalProfileIDC,
generalProfileCompatibilityFlags: generalProfileCompatibilityFlags,
generalTierFlag: generalTierFlag,
generalConstraintIndicatorFlags: Number(
new DataView(
new Uint8Array([0, 0, ...generalConstraintIndicatorFlags]).buffer
).getBigUint64(0, false)
),
generalLevelIdc: generalLevelIDC,
} as unknown as HEVCDecoderConfigurationRecordType;
}
assert(
genCodecStringByHEVCDecoderConfigurationRecord(
makeHEVCParameterSet(0, 1, 0x60000000, 0, [0, 0, 0, 0, 0, 0], 93)
),
'hev1.1.6.L93'
);
assert(
genCodecStringByHEVCDecoderConfigurationRecord(
makeHEVCParameterSet(1, 4, 0x82000000, 1, [0, 0, 0, 0, 0, 0], 120)
),
'hev1.A4.41.H120'
);
assert(
genCodecStringByHEVCDecoderConfigurationRecord(
makeHEVCParameterSet(0, 1, 0x60000000, 0, [176, 0, 0, 0, 0, 0], 93)
),
'hev1.1.6.L93.B0'
);
assert(
genCodecStringByHEVCDecoderConfigurationRecord(
makeHEVCParameterSet(1, 4, 0x82000000, 1, [176, 35, 0, 0, 0, 0], 120)
),
'hev1.A4.41.H120.B0.23'
);
assert(
genCodecStringByHEVCDecoderConfigurationRecord(
makeHEVCParameterSet(
2,
1,
0xf77db57b,
1,
[18, 52, 86, 120, 154, 188],
254
)
),
'hev1.B1.DEADBEEF.H254.12.34.56.78.9A.BC'
);
});
});

View File

@ -1,54 +0,0 @@
import { SegmentSchema, SegmentType } from '@konoplayer/matroska/schema';
import { VideoCodecId } from '@konoplayer/matroska/codecs';
import {
genCodecStringByVP9DecoderConfigurationRecord,
parseVP9DecoderConfigurationRecord,
VP9ColorSpaceEnum,
VP9Subsampling,
} from '@konoplayer/matroska/codecs/vp9';
import { loadComponentFromRangedResource } from '../utils/data';
import { EbmlTagIdEnum, EbmlTagPosition } from 'konoebml';
import { isTagIdPos } from '@konoplayer/matroska/util';
describe('VP9 code test', () => {
it('should parse vp9 meta from track entry and keyframe', async () => {
const [segment] = await loadComponentFromRangedResource<SegmentType>({
resource: 'video/test-vp9.mkv',
predicate: isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.End),
schema: SegmentSchema,
});
const vp9Track = segment.Tracks?.TrackEntry.find(
(t) => t.CodecID === VideoCodecId.VP9
)!;
expect(vp9Track).toBeDefined();
expect(vp9Track.CodecPrivate).toBeFalsy();
const keyframe = segment
.Cluster!.flatMap((c) => c.SimpleBlock || [])
.find((b) => b.keyframe && b.track === vp9Track.TrackNumber)!;
expect(keyframe).toBeDefined();
expect(keyframe.frames.length).toBe(1);
const meta = parseVP9DecoderConfigurationRecord(
vp9Track,
keyframe.frames[0]
)!;
expect(meta).toBeDefined();
expect(meta.bitDepth).toBe(8);
expect(meta.subsampling).toBe(VP9Subsampling.YUV420);
expect(meta.width).toBe(640);
expect(meta.height).toBe(360);
expect(meta.colorSpace).toBe(VP9ColorSpaceEnum.BT_601);
expect(meta.profile).toBe(0);
const codecStr = genCodecStringByVP9DecoderConfigurationRecord(meta);
expect(codecStr).toBe('vp09.00.21.08');
});
});

View File

@ -1,56 +0,0 @@
import { Type } from 'arktype';
import { EbmlStreamDecoder, EbmlTagPosition, EbmlTagType } from 'konoebml';
import { convertEbmlTagToComponent } from '@konoplayer/matroska/util';
import fs from 'node:fs';
import { Readable } from 'node:stream';
import { TransformStream } from 'node:stream/web';
import path from 'node:path';
export interface LoadRangedResourceOptions<S extends Type<any> = any> {
resource: string;
byteStart?: number;
byteEnd?: number;
schema?: S;
predicate?: (tag: EbmlTagType) => boolean;
}
export async function loadComponentFromRangedResource<
T,
S extends Type<any> = any,
>({
resource,
byteStart,
byteEnd,
predicate = (tag) => !tag?.parent && tag.position !== EbmlTagPosition.Start,
schema,
}: LoadRangedResourceOptions<S>): Promise<T[]> {
const input = Readable.toWeb(
fs.createReadStream(
path.join(import.meta.dirname, '..', '..', '..', 'resources', resource),
{
start: byteStart,
end: byteEnd,
}
)
);
const output = input.pipeThrough(
new EbmlStreamDecoder({
streamStartOffset: byteStart,
collectChild: true,
}) as unknown as TransformStream<Uint8Array, EbmlTagType>
);
const result: T[] = [];
for await (const t of output) {
if (predicate(t)) {
let component = convertEbmlTagToComponent(t) as T;
if (schema) {
component = schema.assert(component);
}
result.push(component);
}
}
return result;
}

View File

@ -1,30 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"types": [
"vitest/globals",
"node"
],
"paths": {
"@konoplayer/core/*": [
"../../packages/core/src/*"
],
"@konoplayer/matroska/*": [
"../../packages/matroska/src/*"
]
}
},
"include": [
"src"
],
"references": [
{
"path": "../../packages/core"
},
{
"path": "../../packages/matroska"
}
]
}

View File

@ -1,33 +0,0 @@
import swc from 'unplugin-swc';
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
cacheDir: '.vitest',
test: {
setupFiles: ['src/init-test'],
environment: 'happy-dom',
include: ['src/**/*.spec'],
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,
include: ['../../packages/core/src/**', '../../packages/matroska/src/**'],
},
},
plugins: [
tsconfigPaths(),
swc.vite({
include: /\.[mc]?[jt]sx?$/,
// for git+ package only
exclude: [
/node_modules\/(?!@konoplayer|\.pnpm)/,
/node_modules\/\.pnpm\/(?!@konoplayer)/,
] as any,
tsconfigFile: './tsconfig.json',
}),
],
});

View File

@ -6,7 +6,6 @@
"linter": { "linter": {
"rules": { "rules": {
"style": { "style": {
"useSingleCaseStatement": "off",
"noParameterProperties": "off", "noParameterProperties": "off",
"noNonNullAssertion": "off" "noNonNullAssertion": "off"
}, },
@ -17,13 +16,7 @@
"noSvgWithoutTitle": "off" "noSvgWithoutTitle": "off"
}, },
"complexity": { "complexity": {
"noBannedTypes": "off", "noBannedTypes": "off"
"noExcessiveCognitiveComplexity": {
"level": "warn",
"options": {
"maxAllowedComplexity": 40
}
}
}, },
"nursery": { "nursery": {
"noEnum": "off", "noEnum": "off",
@ -58,34 +51,6 @@
], ],
"linter": { "linter": {
"rules": { "rules": {
"suspicious": {
"noConsole": "off",
"noConsoleLog": "off"
},
"performance": {
"useTopLevelRegex": "off"
}
}
}
},
{
"include": [
"apps/test/**"
],
"javascript": {
"globals": [
"describe",
"beforeEach",
"it",
"expect",
"afterEach"
]
},
"linter": {
"rules": {
"style": {
"useImportType": "off"
},
"suspicious": { "suspicious": {
"noConsole": "off" "noConsole": "off"
}, },

View File

@ -2,10 +2,7 @@ set windows-shell := ["pwsh.exe", "-c"]
set dotenv-load := true set dotenv-load := true
dev-playground: dev-playground:
pnpm run --filter=@konoplayer/playground dev pnpm run --filter=playground dev
dev-proxy: dev-proxy:
pnpm run --filter=@konoplayer/proxy --filter=@konoplayer/mock dev pnpm run --filter proxy --filter mock dev
download-samples:
pnpm run download-samples

View File

@ -3,8 +3,7 @@
"version": "0.0.1", "version": "0.0.1",
"description": "A strange player, like the dumtruck, taking you to Isekai.", "description": "A strange player, like the dumtruck, taking you to Isekai.",
"scripts": { "scripts": {
"codegen-mkv": "tsx --tsconfig=./tsconfig.scripts.json ./scripts/codegen-mkv", "codegen-mkv": "tsx --tsconfig=./tsconfig.scripts.json ./scripts/codegen-mkv.ts"
"download-samples": "tsx --tsconfig=./tsconfig.scripts.json ./scripts/download-samples"
}, },
"keywords": [], "keywords": [],
"author": "lonelyhentxi", "author": "lonelyhentxi",
@ -16,19 +15,17 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@types/node": "^22.13.11", "@types/node": "^22.13.8",
"@webgpu/types": "^0.1.59",
"change-case": "^5.4.4", "change-case": "^5.4.4",
"happy-dom": "^17.4.4", "happy-dom": "^17.4.4",
"tsx": "^4.19.3", "tsx": "^4.19.2",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"ultracite": "^4.2.0" "ultracite": "^4.1.15"
}, },
"dependencies": { "dependencies": {
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"arktype": "^2.1.10", "arktype": "^2.1.10",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"media-codecs": "^2.0.2",
"mnemonist": "^0.40.3", "mnemonist": "^0.40.3",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"type-fest": "^4.37.0" "type-fest": "^4.37.0"

View File

@ -1,8 +0,0 @@
{
"name": "@konoplayer/core",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {},
"dependencies": {}
}

View File

@ -1,40 +0,0 @@
import {map, Observable, Subject} from 'rxjs';
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
export function createAudioDecodeStream(configuration: AudioDecoderConfig): Observable<{
decoder: AudioDecoder;
frame$: Observable<AudioData>;
}> {
const frame$ = new Subject<AudioData>()
const decoder$ = new Observable<AudioDecoder>((subscriber) => {
let isFinalized = false;
const decoder = new AudioDecoder({
output: (frame) => frame$.next(frame),
error: (e) => {
if (!isFinalized) {
isFinalized = true;
frame$.error(e);
subscriber.error(e);
}
},
});
decoder.configure(configuration);
subscriber.next(decoder);
return () => {
if (!isFinalized) {
isFinalized = true;
frame$.complete();
decoder.close();
}
};
})
return decoder$.pipe(map((decoder) => ({
decoder,
frame$
})));
}

View File

@ -1,32 +0,0 @@
export enum AudioCodec {
Unknown = 0,
AAC = 1,
MP3 = 2,
PCM = 3,
Vorbis = 4,
FLAC = 5,
AMR_NB = 6,
AMR_WB = 7,
PCM_MULAW = 8,
GSM_MS = 9,
PCM_S16BE = 10,
PCM_S24BE = 11,
Opus = 12,
EAC3 = 13,
PCM_ALAW = 14,
ALAC = 15,
AC3 = 16,
MpegHAudio = 17,
DTS = 18,
DTSXP2 = 19,
DTSE = 20,
AC4 = 21,
IAMF = 22,
PCM_S32BE = 23,
PCM_S32LE = 24,
PCM_S24LE = 25,
PCM_S16LE = 26,
PCM_F32BE = 27,
PCM_F32LE = 28,
MaxValue = PCM_F32LE, // Must equal the last "real" codec above.
}

View File

@ -1,2 +0,0 @@
export { AudioCodec } from './audio-codecs';
export { VideoCodec } from './video-codecs';

View File

@ -1,97 +0,0 @@
export enum VideoCodec {
Unknown = 0,
H264 = 1,
VC1 = 2,
MPEG2 = 3,
MPEG4 = 4,
Theora = 5,
VP8 = 6,
VP9 = 7,
HEVC = 8,
DolbyVision = 9,
AV1 = 10,
MaxValue = AV1, // Must equal the last "real" codec above.
}
export enum VideoCodecProfile {
VIDEO_CODEC_PROFILE_UNKNOWN = -1,
VIDEO_CODEC_PROFILE_MIN = VIDEO_CODEC_PROFILE_UNKNOWN,
H264PROFILE_MIN = 0,
H264PROFILE_BASELINE = H264PROFILE_MIN,
H264PROFILE_MAIN = 1,
H264PROFILE_EXTENDED = 2,
H264PROFILE_HIGH = 3,
H264PROFILE_HIGH10PROFILE = 4,
H264PROFILE_HIGH422PROFILE = 5,
H264PROFILE_HIGH444PREDICTIVEPROFILE = 6,
H264PROFILE_SCALABLEBASELINE = 7,
H264PROFILE_SCALABLEHIGH = 8,
H264PROFILE_STEREOHIGH = 9,
H264PROFILE_MULTIVIEWHIGH = 10,
H264PROFILE_MAX = H264PROFILE_MULTIVIEWHIGH,
VP8PROFILE_MIN = 11,
VP8PROFILE_ANY = VP8PROFILE_MIN,
VP8PROFILE_MAX = VP8PROFILE_ANY,
VP9PROFILE_MIN = 12,
VP9PROFILE_PROFILE0 = VP9PROFILE_MIN,
VP9PROFILE_PROFILE1 = 13,
VP9PROFILE_PROFILE2 = 14,
VP9PROFILE_PROFILE3 = 15,
VP9PROFILE_MAX = VP9PROFILE_PROFILE3,
HEVCPROFILE_MIN = 16,
HEVCPROFILE_MAIN = HEVCPROFILE_MIN,
HEVCPROFILE_MAIN10 = 17,
HEVCPROFILE_MAIN_STILL_PICTURE = 18,
HEVCPROFILE_MAX = HEVCPROFILE_MAIN_STILL_PICTURE,
DOLBYVISION_PROFILE0 = 19,
// Deprecated: DOLBYVISION_PROFILE4 = 20,
DOLBYVISION_PROFILE5 = 21,
DOLBYVISION_PROFILE7 = 22,
THEORAPROFILE_MIN = 23,
THEORAPROFILE_ANY = THEORAPROFILE_MIN,
THEORAPROFILE_MAX = THEORAPROFILE_ANY,
AV1PROFILE_MIN = 24,
AV1PROFILE_PROFILE_MAIN = AV1PROFILE_MIN,
AV1PROFILE_PROFILE_HIGH = 25,
AV1PROFILE_PROFILE_PRO = 26,
AV1PROFILE_MAX = AV1PROFILE_PROFILE_PRO,
DOLBYVISION_PROFILE8 = 27,
DOLBYVISION_PROFILE9 = 28,
HEVCPROFILE_EXT_MIN = 29,
HEVCPROFILE_REXT = HEVCPROFILE_EXT_MIN,
HEVCPROFILE_HIGH_THROUGHPUT = 30,
HEVCPROFILE_MULTIVIEW_MAIN = 31,
HEVCPROFILE_SCALABLE_MAIN = 32,
HEVCPROFILE_3D_MAIN = 33,
HEVCPROFILE_SCREEN_EXTENDED = 34,
HEVCPROFILE_SCALABLE_REXT = 35,
HEVCPROFILE_HIGH_THROUGHPUT_SCREEN_EXTENDED = 36,
HEVCPROFILE_EXT_MAX = HEVCPROFILE_HIGH_THROUGHPUT_SCREEN_EXTENDED,
VVCPROFILE_MIN = 37,
VVCPROFILE_MAIN10 = VVCPROFILE_MIN,
VVCPROFILE_MAIN12 = 38,
VVCPROFILE_MAIN12_INTRA = 39,
VVCPROIFLE_MULTILAYER_MAIN10 = 40,
VVCPROFILE_MAIN10_444 = 41,
VVCPROFILE_MAIN12_444 = 42,
VVCPROFILE_MAIN16_444 = 43,
VVCPROFILE_MAIN12_444_INTRA = 44,
VVCPROFILE_MAIN16_444_INTRA = 45,
VVCPROFILE_MULTILAYER_MAIN10_444 = 46,
VVCPROFILE_MAIN10_STILL_PICTURE = 47,
VVCPROFILE_MAIN12_STILL_PICTURE = 48,
VVCPROFILE_MAIN10_444_STILL_PICTURE = 49,
VVCPROFILE_MAIN12_444_STILL_PICTURE = 50,
VVCPROFILE_MAIN16_444_STILL_PICTURE = 51,
VVCPROFILE_MAX = VVCPROFILE_MAIN16_444_STILL_PICTURE,
VIDEO_CODEC_PROFILE_MAX = VVCPROFILE_MAIN16_444_STILL_PICTURE,
}
export type VideoCodecLevel = number; // uint32
export const NoVideoCodecLevel: VideoCodecLevel = 0;
export type VideoCodecProfileLevel = {
codec: VideoCodec;
profile: VideoCodecProfile;
level: VideoCodecLevel;
};

View File

@ -1,39 +0,0 @@
export class BitReader {
private data: Uint8Array;
private byteOffset = 0;
private bitOffset = 0;
constructor(data: Uint8Array) {
this.data = data;
}
readBits(numBits: number): number {
let value = 0;
for (let i = 0; i < numBits; i++) {
const bit = (this.data[this.byteOffset] >> (7 - this.bitOffset)) & 1;
value = (value << 1) | bit;
this.bitOffset++;
if (this.bitOffset === 8) {
this.bitOffset = 0;
this.byteOffset++;
}
}
return value;
}
skipBits(numBits: number): void {
this.bitOffset += numBits;
while (this.bitOffset >= 8) {
this.bitOffset -= 8;
this.byteOffset++;
}
}
hasData(): boolean {
return this.byteOffset < this.data.length;
}
getRemainingBytes(): Uint8Array {
return this.data.slice(this.byteOffset);
}
}

View File

@ -1,6 +0,0 @@
export {
type RangedStream,
type CreateRangedStreamOptions,
createRangedStream,
} from './fetch';
export { BitReader } from './bit';

View File

@ -1,25 +0,0 @@
export class UnsupportedCodecError extends Error {
constructor(codec: string, context: string) {
super(`codec ${codec} is not supported in ${context} context`);
}
}
export class ParseCodecError extends Error {
constructor(codec: string, detail: string) {
super(`code ${codec} private parse failed: ${detail}`);
}
}
export class UnreachableOrLogicError extends Error {
constructor(detail: string) {
super(`unreachable or logic error: ${detail}`);
}
}
export class ParseCodecErrors extends Error {
cause: Error[] = [];
constructor() {
super('failed to parse codecs');
}
}

View File

@ -1,80 +0,0 @@
import {map, Observable, Subject} from 'rxjs';
export type RenderingContext =
| ImageBitmapRenderingContext
| CanvasRenderingContext2D;
export function createRenderingContext(): RenderingContext {
const canvas = document.createElement('canvas');
const context =
canvas.getContext('bitmaprenderer') || canvas.getContext('2d');
if (!context) {
throw new DOMException(
'can not get rendering context of canvas',
'CanvasException'
);
}
return context;
}
export function renderBitmapAtRenderingContext(
context: RenderingContext,
bitmap: ImageBitmap
) {
const canvas = context.canvas;
if (bitmap.width !== canvas.width || bitmap.height !== canvas.height) {
canvas.width = bitmap.width;
canvas.height = bitmap.height;
}
if (context instanceof ImageBitmapRenderingContext) {
context.transferFromImageBitmap(bitmap);
} else {
context.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height);
bitmap.close();
}
}
export function captureCanvasAsVideoSrcObject(
video: HTMLVideoElement,
canvas: HTMLCanvasElement,
frameRate: number
) {
video.srcObject = canvas.captureStream(frameRate);
}
export function createVideoDecodeStream(configuration: VideoDecoderConfig): Observable<{
decoder: VideoDecoder;
frame$: Observable<VideoFrame>;
}> {
const frame$ = new Subject<VideoFrame>()
const decoder$ = new Observable<VideoDecoder>((subscriber) => {
let isFinalized = false;
const decoder = new VideoDecoder({
output: (frame) => frame$.next(frame),
error: (e) => {
if (!isFinalized) {
isFinalized = true;
frame$.error(e);
subscriber.error(e);
}
},
});
decoder.configure(configuration);
subscriber.next(decoder);
return () => {
if (!isFinalized) {
isFinalized = true;
frame$.complete();
decoder.close();
}
};
})
return decoder$.pipe(map((decoder) => ({
decoder,
frame$
})));
}

View File

@ -1,10 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist"
},
"include": [
"src"
]
}

View File

@ -1,7 +1,7 @@
[package] [package]
name = "konoplayer-symphonia" name = "demuxing"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
symphonia = "0.5.4" symphonia-format-mkv = "0.5.4"

View File

@ -1,7 +0,0 @@
[package]
name = "konoplayer-ffmpeg"
version = "0.1.0"
edition = "2024"
[dependencies]
ffmpeg-sys-next = { version = "7.1.0", features = ["avcodec"] }

View File

@ -1,85 +0,0 @@
use ffmpeg_sys_next as ffmpeg;
fn get_webcodecs_codec(codec_id: &str, codec_private: &[u8]) -> Result<String, String> {
unsafe {
// 根据 CodecID 获取 AVCodec
let codec = match codec_id {
"V_VP8" => ffmpeg::AVCodecID::AV_CODEC_ID_VP8,
"V_VP9" => ffmpeg::AVCodecID::AV_CODEC_ID_VP9,
"V_MPEG4/ISO/AVC" => ffmpeg::AVCodecID::AV_CODEC_ID_H264,
"V_MPEGH/ISO/HEVC" => ffmpeg::AVCodecID::AV_CODEC_ID_HEVC,
_ => return Err(format!("Unsupported CodecID: {}", codec_id)),
};
let av_codec = ffmpeg::avcodec_find_decoder(codec);
if av_codec.is_null() {
return Err("Codec not found".to_string());
}
let context = ffmpeg::avcodec_alloc_context3(av_codec);
if context.is_null() {
return Err("Failed to allocate context".to_string());
}
// 设置 CodecPrivate 数据
(*context).extradata = codec_private.as_ptr() as *mut u8;
(*context).extradata_size = codec_private.len() as i32;
// 解析参数
match codec_id {
"V_VP9" => {
// VP9: 假设默认值,实际需解析帧数据
Ok("vp09.00.10.08".to_string())
}
"V_MPEG4/ISO/AVC" => {
let profile = (*context).profile; // FFmpeg 提供 profile
let level = (*context).level;
Ok(format!("avc1.{:02x}00{:02x}", profile, level))
}
"V_MPEGH/ISO/HEVC" => {
let profile = (*context).profile;
let level = (*context).level;
Ok(format!("hev1.{}.0.{}.B0", profile, level))
}
_ => unreachable!(),
}
}
}
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
fn main() {
let codec_id = "V_MPEGH/ISO/HEVC";
let codec_private = vec![
1, 2, 32, 0, 0, 0, 144, 0, 0, 0, 0, 0, 120, 240, 0, 252, 253, 250, 250, 0, 0, 15, 4,
160, 0, 1, 0, 25, 64, 1, 12, 1, 255, 255, 2, 32, 0, 0, 3, 0, 144, 0, 0, 3, 0, 0, 3, 0,
120, 153, 138, 2, 64, 161, 0, 1, 0, 44, 66, 1, 1, 2, 32, 0, 0, 3, 0, 144, 0, 0, 3, 0,
0, 3, 0, 120, 160, 3, 192, 128, 16, 228, 217, 102, 98, 174, 70, 194, 166, 160, 32, 32,
60, 32, 0, 0, 125, 32, 0, 11, 184, 1, 162, 0, 1, 0, 9, 68, 1, 193, 114, 138, 86, 113,
178, 64, 167, 0, 1, 0, 121, 78, 1, 5, 116, 44, 162, 222, 9, 181, 23, 71, 219, 187, 85,
164, 254, 127, 194, 252, 78, 120, 50, 54, 53, 32, 45, 32, 45, 32, 72, 46, 50, 54, 53,
47, 72, 69, 86, 67, 32, 99, 111, 100, 101, 99, 32, 45, 32, 67, 111, 112, 121, 114, 105,
103, 104, 116, 32, 50, 48, 49, 51, 45, 50, 48, 49, 56, 32, 40, 99, 41, 32, 77, 117,
108, 116, 105, 99, 111, 114, 101, 119, 97, 114, 101, 44, 32, 73, 110, 99, 32, 45, 32,
104, 116, 116, 112, 58, 47, 47, 120, 50, 54, 53, 46, 111, 114, 103, 32, 45, 32, 111,
112, 116, 105, 111, 110, 115, 58, 32, 128,
];
match get_webcodecs_codec(codec_id, &codec_private) {
Ok(codec) => println!("WebCodecs codec: {}", codec),
Err(e) => eprintln!("Error: {}", e),
}
}
}

View File

@ -1,11 +0,0 @@
{
"name": "@konoplayer/matroska",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {},
"dependencies": {
"@konoplayer/core": "workspace:*",
"konoebml": "^0.1.2"
}
}

View File

@ -1,124 +0,0 @@
import { ParseCodecError } from '@konoplayer/core/errors';
import { ArkErrors, type } from 'arktype';
export const AAC_CODEC_TYPE = 'AAC';
export const AudioObjectTypeSchema = type('1 | 2 | 3 | 4 | 5 | 29 | 67 | 23');
export const SamplingFrequencyIndexSchema = type(
'1 | 2 | 3 | 4 |5|6|7|8|9|10|11|12'
);
export const ChannelConfigurationSchema = type('1 | 2 | 3 | 4 | 5 | 6 | 7');
export const AudioSpecificConfigSchema = type({
audioObjectType: AudioObjectTypeSchema, // AAC profiles: Main, LC, SSR, LTP, HE, HE v2
samplingFrequencyIndex: SamplingFrequencyIndexSchema.optional(), // Sampling rate index
channelConfiguration: ChannelConfigurationSchema, // Channel config (1-7)
sbrPresent: type.boolean.optional(), // Optional: Indicates SBR presence
psPresent: type.boolean.optional(), // Optional: Indicates PS presence (for HE-AAC v2)
});
export type AudioSpecificConfigType = typeof AudioSpecificConfigSchema.infer;
/**
* Parse AudioSpecificConfig from codec_private Uint8Array
* @param codecPrivate - Uint8Array containing codec_private data
* @returns Parsed AudioSpecificConfig or throws an error if invalid
*/
export function parseAudioSpecificConfig(
codecPrivate: Uint8Array
): AudioSpecificConfigType {
if (codecPrivate.length < 2) {
throw new ParseCodecError(AAC_CODEC_TYPE, 'codec_private data too short');
}
// Create a DataView for bit-level manipulation
const view = new DataView(
codecPrivate.buffer,
codecPrivate.byteOffset,
codecPrivate.byteLength
);
let byteOffset = 0;
let bitOffset = 0;
// Helper function to read specific number of bits
function readBits(bits: number): number {
let value = 0;
for (let i = 0; i < bits; i++) {
const byte = view.getUint8(byteOffset);
const bit = (byte >> (7 - bitOffset)) & 1;
value = (value << 1) | bit;
bitOffset++;
if (bitOffset === 8) {
bitOffset = 0;
byteOffset++;
}
}
return value;
}
// Read 5 bits for audioObjectType
const audioObjectType = readBits(5);
// Read 4 bits for samplingFrequencyIndex
const samplingFrequencyIndex = readBits(4);
// Read 4 bits for channelConfiguration
const channelConfiguration = readBits(4);
// Check for SBR/PS extension (if audioObjectType indicates HE-AAC)
let sbrPresent = false;
let psPresent = false;
if (audioObjectType === 5 || audioObjectType === 29) {
sbrPresent = true;
if (audioObjectType === 29) {
psPresent = true; // HE-AAC v2 includes Parametric Stereo
}
// Skip extension-specific bits if present (simplified here)
// In real cases, additional parsing may be needed
}
// Construct the result object
const config: AudioSpecificConfigType = {
audioObjectType:
audioObjectType as AudioSpecificConfigType['audioObjectType'],
samplingFrequencyIndex:
samplingFrequencyIndex as AudioSpecificConfigType['samplingFrequencyIndex'],
channelConfiguration:
channelConfiguration as AudioSpecificConfigType['channelConfiguration'],
...(sbrPresent && { sbrPresent }),
...(psPresent && { psPresent }),
};
// Validate with arktype
const validation = AudioSpecificConfigSchema(config);
if (validation instanceof ArkErrors) {
const error = new ParseCodecError(
AAC_CODEC_TYPE,
'Invalid AudioSpecificConfig'
);
error.cause = validation;
throw error;
}
return config;
}
export function genCodecIdByAudioSpecificConfig(
config: AudioSpecificConfigType
) {
return `mp4a.40.${config.audioObjectType}`;
}
export function samplesPerFrameByAACAudioObjectType(audioObjectType: number) {
switch (audioObjectType) {
case 5:
case 29:
return 2048;
case 23:
return 512;
default:
return 1024;
}
}

View File

@ -1,167 +0,0 @@
import { BitReader } from '@konoplayer/core/data';
import { type } from 'arktype';
import type { TrackEntryType } from '../schema';
import { ParseCodecError } from '@konoplayer/core/errors';
export const AV1_CODEC_TYPE = 'AV1';
export const AV1DecoderConfigurationRecordSchema = type({
marker: type.number, // 1 bit, must be 1
version: type.number, // 7 bits, must be 1
seqProfile: type.number, // 3 bits, seq profile (0-7)
seqLevelIdx0: type.number, // 5 bits, seq level (0-31)
seqTier0: type.number, // 1 bit, tier (0 or 1)
highBitdepth: type.number, // 1 bit, high or low
twelveBit: type.number, // 1 bit, if 12-bit
monochrome: type.number, // 1 bit, if mono chrome
chromaSubsamplingX: type.number, // 1 bit, sub sampling X
chromaSubsamplingY: type.number, // 1 bit, sub sampling Y
chromaSamplePosition: type.number, // 2 bits
initialPresentationDelayPresent: type.number, // 1 bit
initialPresentationDelayMinus1: type.number.optional(), // 4 bits, optoinal
configOBUs: type.instanceOf(Uint8Array<ArrayBufferLike>), // remain OBU data
});
export type AV1DecoderConfigurationRecordType =
typeof AV1DecoderConfigurationRecordSchema.infer;
/**
* [webkit impl](https://github.com/movableink/webkit/blob/7e43fe7000b319ce68334c09eed1031642099726/Source/WebCore/platform/graphics/AV1Utilities.cpp#L48)
*/
export function parseAV1DecoderConfigurationRecord(
track: TrackEntryType
): AV1DecoderConfigurationRecordType {
const codecPrivate = track.CodecPrivate;
if (!codecPrivate) {
throw new ParseCodecError(
AV1_CODEC_TYPE,
'CodecPrivate of AVC Track is missing'
);
}
if (codecPrivate.length < 4) {
throw new ParseCodecError(
AV1_CODEC_TYPE,
'Input data too short for AV1DecoderConfigurationRecord'
);
}
const reader = new BitReader(codecPrivate);
// Byte 0
const marker = reader.readBits(1);
const version = reader.readBits(7);
if (marker !== 1 || version !== 1) {
throw new ParseCodecError(
AV1_CODEC_TYPE,
`Invalid marker (${marker}) or version (${version})`
);
}
const seqProfile = reader.readBits(3);
const seqLevelIdx0 = reader.readBits(5);
// Byte 1
const seqTier0 = reader.readBits(1);
const highBitdepth = reader.readBits(1);
const twelveBit = reader.readBits(1);
const monochrome = reader.readBits(1);
const chromaSubsamplingX = reader.readBits(1);
const chromaSubsamplingY = reader.readBits(1);
const chromaSamplePosition = reader.readBits(2);
// Byte 2
const reserved1 = reader.readBits(3);
if (reserved1 !== 0) {
throw new ParseCodecError(
AV1_CODEC_TYPE,
`Reserved bits must be 0, got ${reserved1}`
);
}
const initialPresentationDelayPresent = reader.readBits(1);
let initialPresentationDelayMinus1: number | undefined;
if (initialPresentationDelayPresent) {
initialPresentationDelayMinus1 = reader.readBits(4);
} else {
const reserved2 = reader.readBits(4);
if (reserved2 !== 0) {
throw new ParseCodecError(
AV1_CODEC_TYPE,
`Reserved bits must be 0, got ${reserved2}`
);
}
}
// remain bytes as configOBUs
const configOBUs = reader.getRemainingBytes();
return {
marker,
version,
seqProfile,
seqLevelIdx0,
seqTier0,
highBitdepth,
twelveBit,
monochrome,
chromaSubsamplingX,
chromaSubsamplingY,
chromaSamplePosition,
initialPresentationDelayPresent,
initialPresentationDelayMinus1,
configOBUs,
};
}
/**
* [webkit impl](https://github.com/movableink/webkit/blob/7e43fe7000b319ce68334c09eed1031642099726/Source/WebCore/platform/graphics/AV1Utilities.cpp#L197)
*/
export function genCodecStringByAV1DecoderConfigurationRecord(
config: AV1DecoderConfigurationRecordType
): string {
const parts: string[] = [];
// Prefix
parts.push('av01');
// Profile
parts.push(config.seqProfile.toString());
// Level and Tier
const levelStr = config.seqLevelIdx0.toString().padStart(2, '0');
const tierStr = config.seqTier0 === 0 ? 'M' : 'H';
parts.push(`${levelStr}${tierStr}`);
// Bit Depth
let bitDepthStr: string;
if (config.highBitdepth === 0) {
bitDepthStr = '08'; // 8-bit
} else if (config.twelveBit === 0) {
bitDepthStr = '10'; // 10-bit
} else {
bitDepthStr = '12'; // 12-bit
}
parts.push(bitDepthStr);
// Monochrome
parts.push(config.monochrome.toString());
// Chroma Subsampling
const chromaSubsampling = `${config.chromaSubsamplingX}${config.chromaSubsamplingY}${config.chromaSamplePosition}`;
parts.push(chromaSubsampling);
// Initial Presentation Delayoptional
if (
config.initialPresentationDelayPresent === 1 &&
config.initialPresentationDelayMinus1 !== undefined
) {
const delay = (config.initialPresentationDelayMinus1 + 1)
.toString()
.padStart(2, '0');
parts.push(delay);
}
// joined
return parts.join('.');
}

View File

@ -1,148 +0,0 @@
import { ParseCodecError } from '@konoplayer/core/errors';
import { type } from 'arktype';
import type { TrackEntryType } from '../schema';
export const AVC_CODEC_TYPE = 'h264(AVC)';
export const AVCDecoderConfigurationRecordSchema = type({
configurationVersion: type.number, // Configuration version, typically 1
avcProfileIndication: type.number, // AVC profile
profileCompatibility: type.number, // Profile compatibility
avcLevelIndication: type.number, // AVC level
lengthSizeMinusOne: type.number, // NAL unit length field size minus 1
sps: type
.instanceOf(Uint8Array<ArrayBufferLike>)
.array()
.atLeastLength(1), // Sequence Parameter Sets (SPS)
pps: type
.instanceOf(Uint8Array<ArrayBufferLike>)
.array()
.atLeastLength(1), // Picture Parameter Sets (PPS)
});
export type AVCDecoderConfigurationRecordType =
typeof AVCDecoderConfigurationRecordSchema.infer;
/**
*
* @see [webkit](https://github.com/movableink/webkit/blob/7e43fe7000b319ce68334c09eed1031642099726/Source/WebCore/platform/graphics/HEVCUtilities.cpp#L84)
*/
export function parseAVCDecoderConfigurationRecord(
track: TrackEntryType
): AVCDecoderConfigurationRecordType {
// ISO/IEC 14496-10:2014
// 7.3.2.1.1 Sequence parameter set data syntax
const codecPrivate = track.CodecPrivate;
if (!codecPrivate) {
throw new ParseCodecError(
AVC_CODEC_TYPE,
'CodecPrivate of AVC Track is missing'
);
}
// AVCDecoderConfigurationRecord is at a minimum 24 bytes long
if (codecPrivate.length < 24) {
throw new ParseCodecError(
AVC_CODEC_TYPE,
'Input data too short for AVCDecoderConfigurationRecord'
);
}
const view = new DataView(codecPrivate.buffer);
let offset = 0;
const readUint8 = (move: boolean) => {
const result = view.getUint8(offset);
if (move) {
offset += 1;
}
return result;
};
const readUint16 = (move: boolean) => {
const result = view.getUint16(offset, false);
if (move) {
offset += 2;
}
return result;
};
const configurationVersion = readUint8(true);
const avcProfileIndication = readUint8(true);
const profileCompatibility = readUint8(true);
const avcLevelIndication = readUint8(true);
// Read lengthSizeMinusOne (first 6 bits are reserved, typically 0xFF, last 2 bits are the value)
const lengthSizeMinusOne = readUint8(true) & 0x03;
// Read number of SPS (first 3 bits are reserved, typically 0xE0, last 5 bits are SPS count)
const numOfSPS = readUint8(true) & 0x1f;
const sps: Uint8Array[] = [];
// Parse SPS
for (let i = 0; i < numOfSPS; i++) {
if (offset + 2 > codecPrivate.length) {
throw new ParseCodecError(AVC_CODEC_TYPE, 'Invalid SPS length');
}
const spsLength = readUint16(true);
if (offset + spsLength > codecPrivate.length) {
throw new ParseCodecError(
AVC_CODEC_TYPE,
'SPS data exceeds buffer length'
);
}
sps.push(codecPrivate.subarray(offset, offset + spsLength));
offset += spsLength;
}
// Read number of PPS
if (offset >= codecPrivate.length) {
throw new ParseCodecError(AVC_CODEC_TYPE, 'No space for PPS count');
}
const numOfPPS = readUint8(true);
const pps: Uint8Array[] = [];
// Parse PPS
for (let i = 0; i < numOfPPS; i++) {
if (offset + 2 > codecPrivate.length) {
throw new ParseCodecError(AVC_CODEC_TYPE, 'Invalid PPS length');
}
const ppsLength = readUint16(true);
if (offset + ppsLength > codecPrivate.length) {
throw new ParseCodecError(
AVC_CODEC_TYPE,
'PPS data exceeds buffer length'
);
}
pps.push(codecPrivate.subarray(offset, offset + ppsLength));
offset += ppsLength;
}
return {
configurationVersion,
avcProfileIndication,
profileCompatibility,
avcLevelIndication,
lengthSizeMinusOne,
sps,
pps,
};
}
export function genCodecStringByAVCDecoderConfigurationRecord(
config: AVCDecoderConfigurationRecordType
): string {
const profileHex = config.avcProfileIndication.toString(16).padStart(2, '0');
const profileCompatHex = config.profileCompatibility
.toString(16)
.padStart(2, '0');
const levelHex = config.avcLevelIndication.toString(16).padStart(2, '0');
return `avc1.${profileHex}${profileCompatHex}${levelHex}`;
}

View File

@ -1,214 +0,0 @@
import { ParseCodecError } from '@konoplayer/core/errors';
import { ArkErrors, type } from 'arktype';
import type { TrackEntryType } from '../schema';
export const HEVC_CODEC_TYPE = 'h265(HEVC)';
export const HEVCDecoderConfigurationRecordArraySchema = type({
arrayCompleteness: type.number,
nalUnitType: type.number,
numNalus: type.number,
nalUnit: type.instanceOf(Uint8Array<ArrayBufferLike>).array(),
});
export type HEVCDecoderConfigurationRecordArrayType =
typeof HEVCDecoderConfigurationRecordArraySchema.infer;
// Define the schema for HEVCDecoderConfigurationRecord
export const HEVCDecoderConfigurationRecordSchema = type({
configurationVersion: type.number, // Must be 1
generalProfileSpace: type.number,
generalTierFlag: type.number,
generalProfileIdc: type.number,
generalProfileCompatibilityFlags: type.number,
generalConstraintIndicatorFlags: type.number,
generalLevelIdc: type.number,
minSpatialSegmentationIdc: type.number,
parallelismType: type.number,
chromaFormat: type.number,
bitDepthLumaMinus8: type.number,
bitDepthChromaMinus8: type.number,
avgFrameRate: type.number,
constantFrameRate: type.number,
numTemporalLayers: type.number,
temporalIdNested: type.number,
lengthSizeMinusOne: type.number,
numOfArrays: type.number,
nalUnits: HEVCDecoderConfigurationRecordArraySchema.array(),
});
export type HEVCDecoderConfigurationRecordType =
typeof HEVCDecoderConfigurationRecordSchema.infer;
export function parseHEVCDecoderConfigurationRecord(
track: TrackEntryType
): HEVCDecoderConfigurationRecordType {
const codecPrivate = track.CodecPrivate;
if (!codecPrivate) {
throw new ParseCodecError(
HEVC_CODEC_TYPE,
'CodecPrivate of HEVC Track is missing'
);
}
const view = new DataView(codecPrivate.buffer);
let offset = 0;
const readUint8 = (move: boolean) => {
const result = view.getUint8(offset);
if (move) {
offset += 1;
}
return result;
};
const readUint16 = (move: boolean) => {
const result = view.getUint16(offset, false);
if (move) {
offset += 2;
}
return result;
};
const readUint48 = (move: boolean) => {
const result =
view.getUint16(offset, false) * 2 ** 32 +
view.getUint32(offset + 2, false);
if (move) {
offset += 6;
}
return result;
};
const readUint32 = (move: boolean) => {
const result = view.getUint32(offset, false);
if (move) {
offset += 4;
}
return result;
};
// Read and validate basic fields
const config: HEVCDecoderConfigurationRecordType = {
configurationVersion: readUint8(true),
generalProfileSpace: (readUint8(false) & 0xc0) >> 6,
generalTierFlag: (readUint8(false) & 0x20) >> 5,
generalProfileIdc: readUint8(true) & 0x1f,
generalProfileCompatibilityFlags: readUint32(true),
generalConstraintIndicatorFlags: readUint48(true),
generalLevelIdc: readUint8(true),
minSpatialSegmentationIdc: readUint16(true) & 0x0fff,
parallelismType: readUint8(true) & 0x03,
chromaFormat: readUint8(true) & 0x03,
bitDepthLumaMinus8: readUint8(true) & 0x07,
bitDepthChromaMinus8: readUint8(true) & 0x07,
avgFrameRate: readUint16(true),
constantFrameRate: (readUint8(false) & 0xc0) >> 6,
numTemporalLayers: (readUint8(false) & 0x38) >> 3,
temporalIdNested: (readUint8(false) & 0x04) >> 2,
lengthSizeMinusOne: readUint8(true) & 0x03,
numOfArrays: readUint8(true),
nalUnits: [],
};
// Parse NAL unit arrays
const arrays = config.nalUnits;
for (let i = 0; i < config.numOfArrays; i++) {
const array: HEVCDecoderConfigurationRecordArrayType = {
arrayCompleteness: (readUint8(false) & 0x80) >> 7,
nalUnitType: readUint8(true) & 0x3f,
numNalus: readUint16(true),
nalUnit: [] as Uint8Array<ArrayBufferLike>[],
};
for (let j = 0; j < array.numNalus; j++) {
const nalUnitLength = readUint16(true);
array.nalUnit.push(codecPrivate.subarray(offset, offset + nalUnitLength));
offset += nalUnitLength;
}
arrays.push(array);
}
// Validate using arktype
const validation = HEVCDecoderConfigurationRecordSchema(config);
if (validation instanceof ArkErrors) {
const error = new ParseCodecError(
HEVC_CODEC_TYPE,
'Invalid HEVC configuration record'
);
error.cause = validation;
throw error;
}
return validation;
}
function reverseBits32(value: number): number {
let result = 0;
for (let i = 0; i < 32; i++) {
result = (result << 1) | ((value >> i) & 1);
}
return result;
}
/**
* @see[webkit implementation](https://github.com/movableink/webkit/blob/7e43fe7000b319ce68334c09eed1031642099726/Source/WebCore/platform/graphics/HEVCUtilities.cpp#L204)
*/
export function genCodecStringByHEVCDecoderConfigurationRecord(
config: HEVCDecoderConfigurationRecordType
) {
const result: string[] = [];
// prefix
result.push(`hev${config.configurationVersion}`);
// Profile Space
if (config.generalProfileSpace > 0) {
const profileSpaceChar = String.fromCharCode(
'A'.charCodeAt(0) + config.generalProfileSpace - 1
);
result.push(profileSpaceChar + config.generalProfileIdc.toString());
} else {
result.push(config.generalProfileIdc.toString());
}
// Profile Compatibility Flags
const compatFlags = reverseBits32(config.generalProfileCompatibilityFlags)
.toString(16)
.toUpperCase();
result.push(compatFlags);
// Tier Flag and Level IDC
const tierPrefix = config.generalTierFlag ? 'H' : 'L';
result.push(tierPrefix + config.generalLevelIdc.toString());
// Constraint Indicator Flags
let constraintBytes: number[];
if (Array.isArray(config.generalConstraintIndicatorFlags)) {
constraintBytes = config.generalConstraintIndicatorFlags as number[];
} else {
// split 48 bit integer into 6 byte
const flags = BigInt(config.generalConstraintIndicatorFlags);
constraintBytes = [];
for (let i = 5; i >= 0; i--) {
constraintBytes.push(Number((flags >> BigInt(8 * i)) & BigInt(0xff)));
}
}
// find last non-zero byte
const lastNonZeroIndex = constraintBytes.reduce(
(last, byte, i) => (byte ? i : last),
-1
);
if (lastNonZeroIndex >= 0) {
for (let i = 0; i <= lastNonZeroIndex; i++) {
const byteHex = constraintBytes[i]
.toString(16)
.padStart(2, '0')
.toUpperCase();
result.push(byteHex);
}
}
return result.join('.');
}

View File

@ -1,436 +0,0 @@
import {
ParseCodecError,
UnsupportedCodecError,
} from '@konoplayer/core/errors';
import { VideoCodec, AudioCodec } from '@konoplayer/core/codecs';
import type { TrackEntryType } from '../schema';
import {
genCodecIdByAudioSpecificConfig,
parseAudioSpecificConfig,
samplesPerFrameByAACAudioObjectType,
} from './aac';
import {
genCodecStringByAVCDecoderConfigurationRecord,
parseAVCDecoderConfigurationRecord,
} from './avc';
import {
genCodecStringByAV1DecoderConfigurationRecord,
parseAV1DecoderConfigurationRecord,
} from './av1';
import {
genCodecStringByHEVCDecoderConfigurationRecord,
parseHEVCDecoderConfigurationRecord,
} from './hevc';
import {
genCodecStringByVP9DecoderConfigurationRecord,
parseVP9DecoderConfigurationRecord,
VP9_CODEC_TYPE,
} from './vp9';
export const VideoCodecId = {
VCM: 'V_MS/VFW/FOURCC',
UNCOMPRESSED: 'V_UNCOMPRESSED',
MPEG4_ISO_SP: 'V_MPEG4/ISO/SP',
MPEG4_ISO_ASP: 'V_MPEG4/ISO/ASP',
MPEG4_ISO_AP: 'V_MPEG4/ISO/AP',
MPEG4_MS_V3: 'V_MPEG4/MS/V3',
MPEG1: 'V_MPEG1',
MPEG2: 'V_MPEG2',
H264: 'V_MPEG4/ISO/AVC',
HEVC: 'V_MPEGH/ISO/HEVC',
AVS2: 'V_AVS2',
AVS3: 'V_AVS3',
RV10: 'V_REAL/RV10',
RV20: 'V_REAL/RV20',
RV30: 'V_REAL/RV30',
RV40: 'V_REAL/RV40',
QUICKTIME: 'V_QUICKTIME',
THEORA: 'V_THEORA',
PROPRES: 'V_PRORES',
VP8: 'V_VP8',
VP9: 'V_VP9',
FFV1: 'V_FFV1',
AV1: 'V_AV1',
} as const;
export type VideoCodecIdType =
| `${(typeof VideoCodecId)[keyof typeof VideoCodecId]}`
| string;
export const AudioCodecId = {
MPEG_L3: 'A_MPEG/L3',
MPEG_L2: 'A_MPEG/L2',
MPEG_L1: 'A_MPEG/L1',
PCM_INT_BIG: 'A_PCM/INT/BIG',
PCM_INT_LIT: 'A_PCM/INT/LIT',
PCM_FLOAT_IEEE: 'A_PCM/FLOAT/IEEE',
MPC: 'A_MPC',
AC3: 'A_AC3',
AC3_BSID9: 'A_AC3/BSID9',
AC3_BSID10: 'A_AC3/BSID10',
ALAC: 'A_ALAC',
DTS: 'A_DTS',
DTS_EXPRESS: 'A_DTS/EXPRESS',
DTS_LOSSLESS: 'A_DTS/LOSSLESS',
VORBIS: 'A_VORBIS',
OPUS: 'A_OPUS',
FLAC: 'A_FLAC',
EAC3: 'A_EAC3',
REAL_14_4: 'A_REAL/14_4',
REAL_28_8: 'A_REAL/28_8',
REAL_COOK: 'A_REAL/COOK',
REAL_SIPR: 'A_REAL/SIPR',
REAL_RALF: 'A_REAL/RALF',
REAL_ATRC: 'A_REAL/ATRC',
MS_ACM: 'A_MS/ACM',
AAC: 'A_AAC',
AAC_MPEG2_MAIN: 'A_AAC/MPEG2/MAIN',
AAC_MPEG2_LC: 'A_AAC/MPEG2/LC',
AAC_MPEG2_LC_SBR: 'A_AAC/MPEG2/LC/SBR',
AAC_MPEG2_SSR: 'A_AAC/MPEG2/SSR',
AAC_MPEG4_MAIN: 'A_AAC/MPEG4/MAIN',
AAC_MPEG4_LC: 'A_AAC/MPEG4/LC',
AAC_MPEG4_SBR: 'A_AAC/MPEG4/LC/SBR',
AAC_MPEG4_SSR: 'A_AAC/MPEG4/SSR',
AAC_MPEG4_LTP: 'A_AAC/MPEG4/LTP',
QUICKTIME: 'A_QUICKTIME',
QDMC: 'A_QUICKTIME/QDMC',
QDM2: 'A_QUICKTIME/QDM2',
TTA1: 'A_TTA1',
WAVEPACK4: 'A_WAVPACK4',
ATRAC: 'A_ATRAC/AT1',
} as const;
export type AudioCodecIdType =
| `${(typeof AudioCodecId)[keyof typeof AudioCodecId]}`
| string;
export const SubtitleCodecId = {
UTF8: 'S_TEXT/UTF8',
SSA: 'S_TEXT/SSA',
ASS: 'S_TEXT/ASS',
WEBVTT: 'S_TEXT/WEBVTT',
BMP: 'S_IMAGE/BMP',
DVBSUB: 'S_DVBSUB',
VOBSUB: 'S_VOBSUB',
HDMV_PGS: 'S_HDMV/PGS',
HDMV_TEXTST: 'S_HDMV/TEXTST',
KATE: 'S_KATE',
ARIBSUB: 'S_ARIBSUB',
} as const;
export type SubtitleCodecIdType =
| `${(typeof SubtitleCodecId)[keyof typeof SubtitleCodecId]}`
| string;
export interface VideoDecoderConfigExt extends VideoDecoderConfig {
codecType: VideoCodec;
}
export function videoCodecIdRequirePeekingKeyframe(codecId: VideoCodecIdType) {
return codecId === VideoCodecId.VP9;
}
export function videoCodecIdToWebCodecs(
track: TrackEntryType,
keyframe: Uint8Array | undefined
): VideoDecoderConfigExt {
const codecId = track.CodecID;
const codecPrivate = track.CodecPrivate;
const shareOptions = {
description: codecPrivate,
};
switch (codecId) {
case VideoCodecId.HEVC:
return {
...shareOptions,
codecType: VideoCodec.HEVC,
codec: genCodecStringByHEVCDecoderConfigurationRecord(
parseHEVCDecoderConfigurationRecord(track)
),
};
case VideoCodecId.VP9:
if (!keyframe) {
throw new ParseCodecError(
VP9_CODEC_TYPE,
'keyframe is required to parse VP9 codec'
);
}
return {
...shareOptions,
codecType: VideoCodec.VP9,
codec: genCodecStringByVP9DecoderConfigurationRecord(
parseVP9DecoderConfigurationRecord(track, keyframe)
),
};
case VideoCodecId.AV1:
return {
...shareOptions,
codecType: VideoCodec.AV1,
codec: genCodecStringByAV1DecoderConfigurationRecord(
parseAV1DecoderConfigurationRecord(track)
),
};
case VideoCodecId.H264:
return {
...shareOptions,
codecType: VideoCodec.H264,
codec: genCodecStringByAVCDecoderConfigurationRecord(
parseAVCDecoderConfigurationRecord(track)
),
};
case VideoCodecId.THEORA:
return { ...shareOptions, codecType: VideoCodec.Theora, codec: 'theora' };
case VideoCodecId.VP8:
return { ...shareOptions, codecType: VideoCodec.VP8, codec: 'vp8' };
case VideoCodecId.MPEG4_ISO_SP:
return {
...shareOptions,
codecType: VideoCodec.MPEG4,
codec: 'mp4v.01.3',
};
case VideoCodecId.MPEG4_ISO_ASP:
return {
...shareOptions,
codecType: VideoCodec.MPEG4,
codec: 'mp4v.20.9',
};
case VideoCodecId.MPEG4_ISO_AP:
return {
...shareOptions,
codecType: VideoCodec.MPEG4,
codec: 'mp4v.20.9',
};
default:
throw new UnsupportedCodecError(codecId, 'web codecs video decoder');
}
}
export interface AudioDecoderConfigExt extends AudioDecoderConfig {
codecType: AudioCodec;
samplesPerFrame?: number;
}
export function isAudioCodecIdRequirePeekingKeyframe(_track: TrackEntryType) {
return false;
}
export function audioCodecIdToWebCodecs(
track: TrackEntryType,
_keyframe: Uint8Array | undefined
): AudioDecoderConfigExt {
const codecId = track.CodecID;
const codecPrivate = track.CodecPrivate;
const bitDepth = track.Audio?.BitDepth;
const numberOfChannels = Number(track.Audio?.Channels);
const sampleRate = Number(track.Audio?.SamplingFrequency);
const shareOptions = {
numberOfChannels,
sampleRate,
description: codecPrivate,
};
switch (track.CodecID) {
case AudioCodecId.AAC_MPEG4_MAIN:
case AudioCodecId.AAC_MPEG2_MAIN:
return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.1',
samplesPerFrame: 1024,
};
case AudioCodecId.AAC_MPEG2_LC:
case AudioCodecId.AAC_MPEG4_LC:
return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.2',
samplesPerFrame: 1024,
};
case AudioCodecId.AAC_MPEG2_SSR:
case AudioCodecId.AAC_MPEG4_SSR:
return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.3',
samplesPerFrame: 1024,
};
case AudioCodecId.AAC_MPEG4_LTP:
return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.4',
samplesPerFrame: 1024,
};
case AudioCodecId.AAC_MPEG2_LC_SBR:
case AudioCodecId.AAC_MPEG4_SBR:
return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.5',
samplesPerFrame: 2048,
};
case AudioCodecId.AAC:
if (codecPrivate) {
const config = parseAudioSpecificConfig(codecPrivate);
return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: genCodecIdByAudioSpecificConfig(config),
samplesPerFrame: samplesPerFrameByAACAudioObjectType(
config.audioObjectType
),
};
}
return {
...shareOptions,
codecType: AudioCodec.AAC,
codec: 'mp4a.40.2',
samplesPerFrame: 1024,
};
case AudioCodecId.AC3:
case AudioCodecId.AC3_BSID9:
return {
...shareOptions,
codecType: AudioCodec.AC3,
codec: 'ac-3',
samplesPerFrame: 1536,
};
case AudioCodecId.EAC3:
case AudioCodecId.AC3_BSID10:
return {
...shareOptions,
codecType: AudioCodec.EAC3,
codec: 'ec-3',
// TODO: FIXME
// parse frame header
// samples per frame = numblkscod * 256
// most time numblkscod = 6
// samplesPerFrame: 1536,
};
case AudioCodecId.MPEG_L3:
return {
...shareOptions,
codecType: AudioCodec.MP3,
codec: 'mp3',
samplesPerFrame: 1152,
};
case AudioCodecId.VORBIS:
return {
...shareOptions,
codecType: AudioCodec.Vorbis,
codec: 'vorbis',
/**
* TODO: FIXME
* read code private
* prase setup header
* ShortBlockSize = 2 ^ blocksize_0
* LongBlockSize = 2 ^ blocksize_1
*/
samplesPerFrame: 2048,
};
case AudioCodecId.FLAC:
return {
...shareOptions,
codecType: AudioCodec.FLAC,
codec: 'flac',
/**
* TODO: FIXME
* read code private
* get block size
*/
// samplesPerFrame: 4096,
};
case AudioCodecId.OPUS:
return {
...shareOptions,
codecType: AudioCodec.Opus,
codec: 'opus',
/**
* TODO: FIXME
* Read TOC header from frame data
*/
// samplesPerFrame: 960,
};
case AudioCodecId.ALAC:
return {
...shareOptions,
codecType: AudioCodec.ALAC,
codec: 'alac',
/**
* TODO: FIXME
* parse private data and get frame length
* 00 00 10 00 // Frame Length (4096)
00 00 00 00 // Compatible Version (0)
00 10 // Bit Depth (16-bit)
40 00 // PB (like 40)
00 00 // MB (like 0)
00 00 // KB (like 0)
00 02 // Channels (2)
00 00 AC 44 // Sample Rate (44100Hz)
*/
// samplesPerFrame: 4096,
};
case AudioCodecId.PCM_INT_BIG:
if (bitDepth === 16) {
return {
...shareOptions,
codecType: AudioCodec.PCM_S16BE,
codec: 'pcm-s16be',
};
}
if (bitDepth === 24) {
return {
...shareOptions,
codecType: AudioCodec.PCM_S24BE,
codec: 'pcm-s24be',
};
}
if (bitDepth === 32) {
return {
...shareOptions,
codecType: AudioCodec.PCM_S32BE,
codec: 'pcm-s32be',
};
}
throw new UnsupportedCodecError(
`${codecId}(${bitDepth}b)`,
'web codecs audio decoder'
);
case AudioCodecId.PCM_INT_LIT:
if (bitDepth === 16) {
return {
...shareOptions,
codecType: AudioCodec.PCM_S16LE,
codec: 'pcm-s16le',
};
}
if (bitDepth === 24) {
return {
...shareOptions,
codecType: AudioCodec.PCM_S24LE,
codec: 'pcm-s24le',
};
}
if (bitDepth === 32) {
return {
...shareOptions,
codecType: AudioCodec.PCM_S32LE,
codec: 'pcm-s32le',
};
}
throw new UnsupportedCodecError(
`${codecId}(${bitDepth}b)`,
'web codecs audio decoder'
);
case AudioCodecId.PCM_FLOAT_IEEE:
return {
...shareOptions,
codecType: AudioCodec.PCM_F32LE,
codec: 'pcm-f32le',
};
default:
throw new UnsupportedCodecError(codecId, 'web codecs audio decoder');
}
}

View File

@ -1,232 +0,0 @@
import { type } from 'arktype';
import type { TrackEntryType } from '../schema';
import { BitReader } from '@konoplayer/core/data';
import { ParseCodecError } from '@konoplayer/core/errors';
export const VP9_CODEC_TYPE = 'vp9';
export enum VP9ColorSpaceEnum {
UNKNOWN = 0,
BT_601 = 1, // eq bt_470bg
BT_709 = 2,
SMPTE_170 = 3,
SMPTE_240 = 4,
BT_2020 = 5,
RESERVED = 6,
SRGB = 7,
}
export enum VP9YUVRange {
STUDIO_SWING = 0,
FULL_SWING = 1,
}
export enum VP9Subsampling {
UNKNOWN = 0,
YUV420 = 1,
YUV422 = 2,
YUV440 = 3,
YUV444 = 4,
}
export const VP9PerformenceLevel = [
{ level: '10', maxSampleRate: 829440, maxResolution: 36864 }, // Level 1
{ level: '11', maxSampleRate: 2764800, maxResolution: 73728 }, // Level 1
{ level: '20', maxSampleRate: 4608000, maxResolution: 122880 }, // Level 2
{ level: '21', maxSampleRate: 9216000, maxResolution: 245760 }, // Level 2.1
{ level: '30', maxSampleRate: 20736000, maxResolution: 552960 }, // Level 3
{ level: '31', maxSampleRate: 36864000, maxResolution: 983040 }, // Level 3.1
{ level: '40', maxSampleRate: 83558400, maxResolution: 2228224 }, // Level 4
{ level: '41', maxSampleRate: 160432128, maxResolution: 2228224 }, // Level 4.1
{ level: '50', maxSampleRate: 311951360, maxResolution: 8912896 }, // Level 5
{ level: '51', maxSampleRate: 588251136, maxResolution: 8912896 }, // Level 5.1
{ level: '52', maxSampleRate: 1176502272, maxResolution: 8912896 }, // Level 5.2
{ level: '60', maxSampleRate: 1176502272, maxResolution: 35651584 }, // Level 6
{ level: '61', maxSampleRate: 2353004544, maxResolution: 35651584 }, // Level 6.1
{ level: '62', maxSampleRate: 4706009088, maxResolution: 35651584 }, // Level 6.2
];
export const VP9DecoderConfigurationRecordSchema = type({
profile: type.number, // 0 | 1 | 2 | 3,
bitDepth: type.number, // 8 | 10 | 12
colorSpace: type.number,
subsampling: type.number, // 420 | 422 | 444
width: type.number,
height: type.number,
yuvRangeFlag: type.number.optional(),
hasScaling: type.boolean,
renderWidth: type.number,
renderHeight: type.number,
frameRate: type.number, // frame per second
estimateLevel: type.string,
});
export type VP9DecoderConfigurationRecordType =
typeof VP9DecoderConfigurationRecordSchema.infer;
export function parseVP9DecoderConfigurationRecord(
track: TrackEntryType,
keyframe: Uint8Array
): VP9DecoderConfigurationRecordType {
const reader = new BitReader(keyframe);
const frameRate = 1_000_000_000 / Number(track.DefaultDuration) || 30;
// Frame Marker: 2 bits, must be 0b10
const frameMarker = reader.readBits(2);
if (frameMarker !== 2) {
throw new ParseCodecError(VP9_CODEC_TYPE, 'invalid frame marker');
}
// Profile: 2 bits
const version = reader.readBits(1);
const high = reader.readBits(1);
const profile = (high << 1) + version;
let reservedZero = 0;
if (profile === 3) {
reservedZero = reader.readBits(1);
if (reservedZero !== 0) {
throw new ParseCodecError(
VP9_CODEC_TYPE,
'Invalid reserved zero bit for profile 3'
);
}
}
// Show Existing Frame: 1 bit
const showExistingFrame = reader.readBits(1);
if (showExistingFrame === 1) {
throw new ParseCodecError(VP9_CODEC_TYPE, 'not a keyframe to parse');
}
// Frame Type: 1 bit (0 = keyframe)
const frameType = reader.readBits(1);
if (frameType !== 0) {
throw new ParseCodecError(VP9_CODEC_TYPE, 'not a keyframe to parse');
}
// Show Frame and Error Resilient
const _showFrame = reader.readBits(1);
const _errorResilient = reader.readBits(1);
// Sync Code: 3 bytes (0x49, 0x83, 0x42)
const syncCode =
(reader.readBits(8) << 16) | (reader.readBits(8) << 8) | reader.readBits(8);
if (syncCode !== 0x498342) {
throw new ParseCodecError(VP9_CODEC_TYPE, 'Invalid sync code');
}
// Bit Depth
let bitDepth: number;
if (profile >= 2) {
const tenOrTwelveBit = reader.readBits(1);
bitDepth = tenOrTwelveBit === 0 ? 10 : 12;
} else {
bitDepth = 8;
}
const colorSpace = reader.readBits(3);
let subsamplingX: number;
let subsamplingY: number;
let yuvRangeFlag: number | undefined;
if (colorSpace !== VP9ColorSpaceEnum.SRGB) {
yuvRangeFlag = reader.readBits(1);
if (profile === 1 || profile === 3) {
subsamplingX = reader.readBits(1);
subsamplingY = reader.readBits(1);
reservedZero = reader.readBits(1);
} else {
subsamplingX = 1;
subsamplingY = 1;
}
} else {
if (profile !== 1 && profile !== 3) {
throw new ParseCodecError(
VP9_CODEC_TYPE,
'VP9 profile with sRGB ColorSpace must be 1 or 3'
);
}
subsamplingX = 0;
subsamplingY = 0;
reservedZero = reader.readBits(1);
}
let subsampling: VP9Subsampling;
if (!subsamplingX && subsamplingY) {
subsampling = VP9Subsampling.YUV440;
} else if (subsamplingX && !subsamplingY) {
subsampling = VP9Subsampling.YUV422;
} else if (subsamplingX && subsamplingY) {
subsampling = VP9Subsampling.YUV420;
} else if (!subsamplingX && !subsamplingY) {
subsampling = VP9Subsampling.YUV444;
} else {
subsampling = VP9Subsampling.UNKNOWN;
}
// Frame Size (resolution)
const widthMinus1 = reader.readBits(16);
const heightMinus1 = reader.readBits(16);
const hasScaling = !!reader.readBits(1);
let renderWidthMinus1 = widthMinus1;
let renderHeightMinus1 = heightMinus1;
if (hasScaling) {
renderWidthMinus1 = reader.readBits(16);
renderHeightMinus1 = reader.readBits(16);
}
const width = widthMinus1 + 1;
const height = heightMinus1 + 1;
const sampleRate = width * height * frameRate;
const resolution = width * height;
let estimateLevel = '62';
for (const { level, maxSampleRate, maxResolution } of VP9PerformenceLevel) {
if (sampleRate <= maxSampleRate && resolution <= maxResolution) {
// 检查 profile 和 bitDepth 的额外要求
if (profile >= 2 && bitDepth > 8 && Number.parseFloat(level) < 20) {
continue;
}
estimateLevel = level;
break;
}
}
return {
profile,
bitDepth,
colorSpace,
subsampling,
yuvRangeFlag,
width,
height,
hasScaling,
renderWidth: renderWidthMinus1 + 1,
renderHeight: renderHeightMinus1 + 1,
frameRate,
estimateLevel,
};
}
// The format of the 'vp09' codec string is specified in the webm GitHub repo:
// <https://github.com/webmproject/vp9-dash/blob/master/VPCodecISOMediaFileFormatBinding.md#codecs-parameter-string>
//
// The codecs parameter string for the VP codec family is as follows:
// <sample entry 4CC>.<profile>.<level>.<bitDepth>.<chromaSubsampling>.
// <colourPrimaries>.<transferCharacteristics>.<matrixCoefficients>.
// <videoFullRangeFlag>
// All parameter values are expressed as double-digit decimals.
// sample entry 4CC, profile, level, and bitDepth are all mandatory fields.
export function genCodecStringByVP9DecoderConfigurationRecord(
config: VP9DecoderConfigurationRecordType
): string {
const profileStr = config.profile.toString().padStart(2, '0');
const bitDepthStr = config.bitDepth.toString().padStart(2, '0');
const levelStr = config.estimateLevel;
return `vp09.${profileStr}.${levelStr}.${bitDepthStr}`;
}

View File

@ -1,14 +0,0 @@
import type { ClusterType } from '../schema';
export function* clusterBlocks(cluster: ClusterType) {
if (cluster.SimpleBlock) {
for (const simpleBlock of cluster.SimpleBlock) {
yield simpleBlock;
}
}
if (cluster.BlockGroup) {
for (const block of cluster.BlockGroup) {
yield block;
}
}
}

View File

@ -1,70 +0,0 @@
import { type EbmlEBMLTagType, EbmlTagIdEnum, EbmlTagPosition } from 'konoebml';
import {
switchMap,
filter,
take,
shareReplay,
map,
combineLatest,
of, type Observable, delayWhen, pipe, finalize, tap, throwIfEmpty,
} from 'rxjs';
import { isTagIdPos } from '../util';
import {createRangedEbmlStream, type CreateRangedEbmlStreamOptions} from './resource';
import { type MatroskaSegmentModel, createMatroskaSegment } from './segment';
export type CreateMatroskaOptions = Omit<
CreateRangedEbmlStreamOptions,
'byteStart' | 'byteEnd'
>;
export interface MatroskaModel {
totalSize?: number;
initResponse: Response;
head: EbmlEBMLTagType;
segment: MatroskaSegmentModel;
}
export function createMatroska(options: CreateMatroskaOptions): Observable<MatroskaModel> {
const metadataRequest$ = createRangedEbmlStream({
...options,
byteStart: 0,
});
return metadataRequest$.pipe(
switchMap(({ totalSize, ebml$, response }) => {
/**
* while [matroska v4](https://www.matroska.org/technical/elements.html) doc tell that there is only one segment in a file
* some mkv generated by strange tools will emit several
*/
const segment$ = ebml$.pipe(
filter(isTagIdPos(EbmlTagIdEnum.Segment, EbmlTagPosition.Start)),
map((startTag) => createMatroskaSegment({
startTag,
matroskaOptions: options,
ebml$,
})),
delayWhen(
({ loadedMetadata$ }) => loadedMetadata$
),
take(1),
shareReplay(1)
);
const head$ = ebml$.pipe(
filter(isTagIdPos(EbmlTagIdEnum.EBML, EbmlTagPosition.End)),
take(1),
shareReplay(1),
throwIfEmpty(() => new Error("failed to find head tag"))
);
return combineLatest({
segment: segment$,
head: head$,
totalSize: of(totalSize),
initResponse: of(response),
});
}),
shareReplay(1)
);
}

View File

@ -1,89 +0,0 @@
import {
type CreateRangedStreamOptions,
createRangedStream,
} from '@konoplayer/core/data';
import { type EbmlTagType, EbmlStreamDecoder, EbmlTagIdEnum } from 'konoebml';
import {Observable, from, switchMap, share, defer, EMPTY, of, tap} from 'rxjs';
import { waitTick } from '../util';
export interface CreateRangedEbmlStreamOptions extends CreateRangedStreamOptions {
refCount?: boolean
}
export function createRangedEbmlStream({
url,
byteStart = 0,
byteEnd
}: CreateRangedEbmlStreamOptions): Observable<{
ebml$: Observable<EbmlTagType>;
totalSize?: number;
response: Response;
body: ReadableStream<Uint8Array>;
controller: AbortController;
}> {
const stream$ = from(createRangedStream({ url, byteStart, byteEnd }));
return stream$.pipe(
switchMap(({ controller, body, totalSize, response }) => {
let requestCompleted = false;
const ebml$ = new Observable<EbmlTagType>((subscriber) => {
if (requestCompleted) {
subscriber.complete();
}
body
.pipeThrough(
new EbmlStreamDecoder({
streamStartOffset: byteStart,
collectChild: (child) => child.id !== EbmlTagIdEnum.Cluster,
backpressure: {
eventLoop: waitTick,
},
})
)
.pipeTo(
new WritableStream({
write: async (tag) => {
await waitTick();
subscriber.next(tag);
},
close: () => {
if (!requestCompleted) {
requestCompleted = true;
subscriber.complete();
}
},
})
)
.catch((error) => {
if (requestCompleted && error?.name === 'AbortError') {
return;
}
requestCompleted = true;
subscriber.error(error);
});
return () => {
if (!requestCompleted) {
requestCompleted = true;
controller.abort();
}
};
}).pipe(
share({
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: true,
})
);
return of({
totalSize,
response,
body,
controller,
ebml$
});
})
);
}

View File

@ -1,427 +0,0 @@
import { createAudioDecodeStream } from '@konoplayer/core/audition';
import { createVideoDecodeStream } from '@konoplayer/core/graphics';
import {
type EbmlSegmentTagType,
type EbmlTagType,
EbmlTagIdEnum,
EbmlTagPosition,
} from 'konoebml';
import {
type Observable,
scan,
takeWhile,
share,
map,
switchMap,
shareReplay,
EMPTY,
filter,
withLatestFrom,
take,
of,
merge,
isEmpty,
finalize,
delayWhen,
from,
} from 'rxjs';
import type { CreateMatroskaOptions } from '.';
import { type ClusterType, TrackTypeRestrictionEnum } from '../schema';
import {
SegmentSystem,
type SegmentComponent,
type VideoTrackContext,
type AudioTrackContext,
SEEK_ID_KAX_CUES,
SEEK_ID_KAX_TAGS,
type CueSystem,
} from '../systems';
import {
standardTrackPredicate,
standardTrackPriority,
} from '../systems/track';
import { isTagIdPos } from '../util';
import { createRangedEbmlStream } from './resource';
export interface CreateMatroskaSegmentOptions {
matroskaOptions: CreateMatroskaOptions;
startTag: EbmlSegmentTagType;
ebml$: Observable<EbmlTagType>;
}
export interface MatroskaSegmentModel {
startTag: EbmlSegmentTagType;
segment: SegmentSystem;
loadedMetadata$: Observable<SegmentSystem>;
loadedTags$: Observable<SegmentSystem>;
loadedCues$: Observable<SegmentSystem>;
seek: (seekTime: number) => Observable<SegmentComponent<ClusterType>>;
videoTrackDecoder: (
track: VideoTrackContext,
cluster$: Observable<ClusterType>
) => Observable<{
track: VideoTrackContext;
decoder: VideoDecoder;
frame$: Observable<VideoFrame>;
}>;
audioTrackDecoder: (
track: AudioTrackContext,
cluster$: Observable<ClusterType>
) => Observable<{
track: AudioTrackContext;
decoder: AudioDecoder;
frame$: Observable<AudioData>;
}>;
defaultVideoTrack$: Observable<VideoTrackContext | undefined>;
defaultAudioTrack$: Observable<AudioTrackContext | undefined>;
}
export function createMatroskaSegment({
matroskaOptions,
startTag,
ebml$,
}: CreateMatroskaSegmentOptions): MatroskaSegmentModel {
const segment = new SegmentSystem(startTag);
const clusterSystem = segment.cluster;
const seekSystem = segment.seek;
const metaScan$ = ebml$.pipe(
scan(
(acc, tag) => {
const segment = acc.segment;
segment.scanMeta(tag);
acc.tag = tag;
acc.canComplete = segment.canCompleteMeta();
return acc;
},
{
segment,
tag: undefined as unknown as EbmlTagType,
canComplete: false,
}
),
takeWhile(({ canComplete }) => !canComplete, true),
delayWhen(({ segment }) => from(segment.completeMeta())),
share({
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: true,
})
);
const loadedMetadata$ = metaScan$.pipe(
filter(({ canComplete }) => canComplete),
map(({ segment }) => segment),
take(1),
shareReplay(1),
);
const loadedRemoteCues$ = loadedMetadata$.pipe(
switchMap((s) => {
const cueSystem = s.cue;
const seekSystem = s.seek;
if (cueSystem.prepared) {
return EMPTY;
}
const remoteCuesTagStartOffset =
seekSystem.seekOffsetBySeekId(SEEK_ID_KAX_CUES);
if (remoteCuesTagStartOffset! >= 0) {
return createRangedEbmlStream({
...matroskaOptions,
byteStart: remoteCuesTagStartOffset,
}).pipe(
switchMap((req) => req.ebml$),
filter(isTagIdPos(EbmlTagIdEnum.Cues, EbmlTagPosition.End)),
withLatestFrom(loadedMetadata$),
map(([cues, withMeta]) => {
withMeta.cue.prepareCuesWithTag(cues);
return withMeta;
})
);
}
return EMPTY;
}),
take(1),
shareReplay(1)
);
const loadedLocalCues$ = loadedMetadata$.pipe(
switchMap((s) => (s.cue.prepared ? of(s) : EMPTY)),
shareReplay(1)
);
const loadedEmptyCues$ = merge(loadedLocalCues$, loadedRemoteCues$).pipe(
isEmpty(),
switchMap((empty) => (empty ? loadedMetadata$ : EMPTY))
);
const loadedCues$ = merge(
loadedLocalCues$,
loadedRemoteCues$,
loadedEmptyCues$
).pipe(take(1));
const loadedRemoteTags$ = loadedMetadata$.pipe(
switchMap((s) => {
const tagSystem = s.tag;
const seekSystem = s.seek;
if (tagSystem.prepared) {
return EMPTY;
}
const remoteTagsTagStartOffset =
seekSystem.seekOffsetBySeekId(SEEK_ID_KAX_TAGS);
if (remoteTagsTagStartOffset! >= 0) {
return createRangedEbmlStream({
...matroskaOptions,
byteStart: remoteTagsTagStartOffset,
}).pipe(
switchMap((req) => req.ebml$),
filter(isTagIdPos(EbmlTagIdEnum.Tags, EbmlTagPosition.End)),
withLatestFrom(loadedMetadata$),
map(([tags, withMeta]) => {
withMeta.tag.prepareTagsWithTag(tags);
return withMeta;
})
);
}
return EMPTY;
}),
take(1),
shareReplay(1)
);
const loadedLocalTags$ = loadedMetadata$.pipe(
switchMap((s) => (s.tag.prepared ? of(s) : EMPTY)),
shareReplay(1)
);
const loadedEmptyTags$ = merge(loadedRemoteTags$, loadedLocalTags$).pipe(
isEmpty(),
switchMap((empty) => (empty ? loadedMetadata$ : EMPTY))
);
const loadedTags$ = merge(
loadedLocalTags$,
loadedRemoteTags$,
loadedEmptyTags$
).pipe(take(1));
const seekWithoutCues = (
seekTime: number
): Observable<SegmentComponent<ClusterType>> => {
const request$ = loadedMetadata$.pipe(
switchMap(() =>
createRangedEbmlStream({
...matroskaOptions,
byteStart: seekSystem.firstClusterOffset,
})
)
);
const cluster$ = request$.pipe(
switchMap((req) => req.ebml$),
filter(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)),
map((tag) => clusterSystem.addClusterWithTag(tag))
);
if (seekTime === 0) {
return cluster$;
}
return cluster$.pipe(
scan(
(acc, curr) => {
// avoid object recreation
acc.prev = acc.next;
acc.next = curr;
return acc;
},
{
prev: undefined as SegmentComponent<ClusterType> | undefined,
next: undefined as SegmentComponent<ClusterType> | undefined,
}
),
filter((c) => c.next?.Timestamp! > seekTime),
map((c) => c.prev ?? c.next!)
);
};
const seekWithCues = (
cueSystem: CueSystem,
seekTime: number
): Observable<SegmentComponent<ClusterType>> => {
if (seekTime === 0) {
return seekWithoutCues(seekTime);
}
const cuePoint = cueSystem.findClosestCue(seekTime);
if (!cuePoint) {
return seekWithoutCues(seekTime);
}
return createRangedEbmlStream({
...matroskaOptions,
byteStart: seekSystem.offsetFromSeekPosition(
cueSystem.getCueTrackPositions(cuePoint).CueClusterPosition as number
),
}).pipe(
switchMap((req) => req.ebml$),
filter(isTagIdPos(EbmlTagIdEnum.Cluster, EbmlTagPosition.End)),
map(clusterSystem.addClusterWithTag.bind(clusterSystem))
);
};
const seek = (
seekTime: number
): Observable<SegmentComponent<ClusterType>> => {
if (seekTime === 0) {
const subscription = loadedCues$.subscribe();
// if seekTime equals to 0 at start, reuse the initialize stream
return seekWithoutCues(seekTime).pipe(
finalize(() => {
subscription.unsubscribe();
})
);
}
return loadedCues$.pipe(
switchMap((segment) => {
const cueSystem = segment.cue;
if (cueSystem.prepared) {
return seekWithCues(cueSystem, seekTime);
}
return seekWithoutCues(seekTime);
})
);
};
const videoTrackDecoder = (
track: VideoTrackContext,
cluster$: Observable<ClusterType>
) => {
return createVideoDecodeStream(track.configuration).pipe(
map(({ decoder, frame$ }) => {
const clusterSystem = segment.cluster;
const infoSystem = segment.info;
const timestampScale = Number(infoSystem.info.TimestampScale) / 1000;
const decodeSubscription = cluster$.subscribe((cluster) => {
for (const block of clusterSystem.enumerateBlocks(
cluster,
track.trackEntry
)) {
const blockTime = (Number(cluster.Timestamp) + block.relTime) * timestampScale;
const blockDuration =
frames.length > 1 ? track.predictBlockDuration(blockTime) * timestampScale : 0;
const perFrameDuration =
frames.length > 1 && blockDuration
? blockDuration / block.frames.length
: 0;
for (const frame of block.frames) {
const chunk = new EncodedVideoChunk({
type: block.keyframe ? 'key' : 'delta',
data: frame,
timestamp: blockTime + perFrameDuration,
});
decoder.decode(chunk);
}
}
});
return {
track,
decoder,
frame$: frame$
.pipe(
finalize(() => {
decodeSubscription.unsubscribe();
})
)
}
})
);
};
const audioTrackDecoder = (
track: AudioTrackContext,
cluster$: Observable<ClusterType>
) => {
return createAudioDecodeStream(track.configuration).pipe(
map(({ decoder, frame$ }) => {
const clusterSystem = segment.cluster;
const infoSystem = segment.info;
const timestampScale = Number(infoSystem.info.TimestampScale) / 1000;
const decodeSubscription = cluster$.subscribe((cluster) => {
for (const block of clusterSystem.enumerateBlocks(
cluster,
track.trackEntry
)) {
const blockTime = (Number(cluster.Timestamp) + block.relTime) * timestampScale;
const blockDuration =
frames.length > 1 ? track.predictBlockDuration(blockTime) : 0;
const perFrameDuration =
frames.length > 1 && blockDuration
? blockDuration / block.frames.length
: 0;
let i = 0;
for (const frame of block.frames) {
const chunk = new EncodedAudioChunk({
type: block.keyframe ? 'key' : 'delta',
data: frame,
timestamp: blockTime + perFrameDuration * i,
});
i++;
decoder.decode(chunk);
}
}
});
return {
track,
decoder,
frame$: frame$.pipe(finalize(() => decodeSubscription.unsubscribe())),
};
}));
};
const defaultVideoTrack$ = loadedMetadata$.pipe(
map((segment) =>
segment.track.getTrackContext<VideoTrackContext>({
predicate: (track) =>
track.TrackType === TrackTypeRestrictionEnum.VIDEO &&
standardTrackPredicate(track),
priority: standardTrackPriority,
})
)
);
const defaultAudioTrack$ = loadedMetadata$.pipe(
map((segment) =>
segment.track.getTrackContext<AudioTrackContext>({
predicate: (track) =>
track.TrackType === TrackTypeRestrictionEnum.AUDIO &&
standardTrackPredicate(track),
priority: standardTrackPriority,
})
)
);
return {
startTag,
segment,
loadedMetadata$,
loadedTags$,
loadedCues$,
seek,
videoTrackDecoder,
audioTrackDecoder,
defaultVideoTrack$,
defaultAudioTrack$
};
}

View File

@ -1,100 +0,0 @@
import type { EbmlClusterTagType } from 'konoebml';
import {
ClusterSchema,
type SimpleBlockType,
type ClusterType,
type BlockGroupType,
type TrackEntryType,
} from '../schema';
import { type SegmentComponent } from './segment';
import {SegmentComponentSystemTrait} from "./segment-component";
export abstract class BlockViewTrait {
abstract get keyframe(): boolean;
abstract get frames(): Uint8Array[];
abstract get trackNum(): number | bigint;
abstract get relTime(): number;
}
export class SimpleBlockView extends BlockViewTrait {
constructor(public readonly block: SimpleBlockType) {
super();
}
get keyframe() {
return !!this.block.keyframe;
}
get frames(): Uint8Array<ArrayBufferLike>[] {
return this.block.frames;
}
get trackNum() {
return this.block.track;
}
get relTime() {
return this.block.value;
}
}
export class BlockGroupView extends BlockViewTrait {
constructor(public readonly block: BlockGroupType) {
super();
}
get keyframe() {
return !this.block.ReferenceBlock;
}
get frames(): Uint8Array<ArrayBufferLike>[] {
return this.block.Block.frames;
}
get trackNum() {
return this.block.Block.track;
}
get relTime() {
return this.block.Block.value;
}
}
export class ClusterSystem extends SegmentComponentSystemTrait<
EbmlClusterTagType,
typeof ClusterSchema
> {
override get schema() {
return ClusterSchema;
}
clustersBuffer: SegmentComponent<ClusterType>[] = [];
addClusterWithTag(tag: EbmlClusterTagType) {
const cluster = this.componentFromTag(tag);
// this.clustersBuffer.push(cluster);
return cluster;
}
*enumerateBlocks(
cluster: ClusterType,
track: TrackEntryType
): Generator<BlockViewTrait> {
if (cluster.SimpleBlock) {
for (const block of cluster.SimpleBlock) {
if (block.track === track.TrackNumber) {
yield new SimpleBlockView(block);
}
}
}
if (cluster.BlockGroup) {
for (const block of cluster.BlockGroup) {
if (block.Block.track === track.TrackNumber) {
yield new BlockGroupView(block);
}
}
}
}
}

View File

@ -1,85 +0,0 @@
import {type EbmlCuePointTagType, type EbmlCuesTagType, EbmlTagIdEnum} from "konoebml";
import {CuePointSchema, type CuePointType, type CueTrackPositionsType} from "../schema";
import {maxBy} from "lodash-es";
import type {SegmentComponent} from "./segment";
import {SegmentComponentSystemTrait} from "./segment-component";
export class CueSystem extends SegmentComponentSystemTrait<
EbmlCuePointTagType,
typeof CuePointSchema
> {
override get schema() {
return CuePointSchema;
}
cues: SegmentComponent<CuePointType>[] = [];
prepareCuesWithTag(tag: EbmlCuesTagType) {
this.cues = tag.children
.filter((c) => c.id === EbmlTagIdEnum.CuePoint)
.map(this.componentFromTag.bind(this));
return this;
}
findClosestCue(seekTime: number): CuePointType | undefined {
const cues = this.cues;
if (!cues || cues.length === 0) {
return undefined;
}
let left = 0;
let right = cues.length - 1;
if (seekTime <= cues[0].CueTime) {
return cues[0];
}
if (seekTime >= cues[right].CueTime) {
return cues[right];
}
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (cues[mid].CueTime === seekTime) {
return cues[mid];
}
if (cues[mid].CueTime < seekTime) {
left = mid + 1;
} else {
right = mid - 1;
}
}
const before = cues[right];
const after = cues[left];
return Math.abs((before.CueTime as number) - seekTime) <
Math.abs((after.CueTime as number) - seekTime)
? before
: after;
}
getCueTrackPositions(
cuePoint: CuePointType,
track?: number
): CueTrackPositionsType {
let cueTrackPositions: CueTrackPositionsType | undefined;
if (track! >= 0) {
cueTrackPositions = cuePoint.CueTrackPositions.find(
(c) => c.CueTrack === track
);
}
if (!cueTrackPositions) {
cueTrackPositions = maxBy(
cuePoint.CueTrackPositions,
(c) => c.CueClusterPosition
)!;
}
return cueTrackPositions;
}
get prepared(): boolean {
return this.cues.length > 0;
}
}

View File

@ -1,8 +0,0 @@
export { TrackContext, AudioTrackContext, VideoTrackContext, DefaultTrackContext, type GetTrackEntryOptions, TrackSystem } from './track';
export { CueSystem } from './cue';
export { TagSystem } from './tag';
export { ClusterSystem } from './cluster';
export { InfoSystem } from './info';
export { type SegmentComponent, SegmentSystem, withSegment } from './segment';
export { SeekSystem, SEEK_ID_KAX_CUES, SEEK_ID_KAX_INFO, SEEK_ID_KAX_TAGS, SEEK_ID_KAX_TRACKS } from './seek';
export {SegmentComponentSystemTrait} from "./segment-component";

View File

@ -1,20 +0,0 @@
import type {EbmlInfoTagType} from "konoebml";
import {InfoSchema, type InfoType} from "../schema";
import type {SegmentComponent} from "./segment";
import {SegmentComponentSystemTrait} from "./segment-component";
export class InfoSystem extends SegmentComponentSystemTrait<
EbmlInfoTagType,
typeof InfoSchema
> {
override get schema() {
return InfoSchema;
}
info!: SegmentComponent<InfoType>;
prepareWithInfoTag(tag: EbmlInfoTagType) {
this.info = this.componentFromTag(tag);
return this;
}
}

View File

@ -1,64 +0,0 @@
import type {EbmlSeekHeadTagType, EbmlTagType} from "konoebml";
import {SeekHeadSchema, type SeekHeadType} from "../schema";
import {isEqual} from "lodash-es";
import {UnreachableOrLogicError} from "@konoplayer/core/errors";
import {SegmentComponentSystemTrait} from "./segment-component";
export const SEEK_ID_KAX_INFO = new Uint8Array([0x15, 0x49, 0xa9, 0x66]);
export const SEEK_ID_KAX_TRACKS = new Uint8Array([0x16, 0x54, 0xae, 0x6b]);
export const SEEK_ID_KAX_CUES = new Uint8Array([0x1c, 0x53, 0xbb, 0x6b]);
export const SEEK_ID_KAX_TAGS = new Uint8Array([0x12, 0x54, 0xc3, 0x67]);
export class SeekSystem extends SegmentComponentSystemTrait<
EbmlSeekHeadTagType,
typeof SeekHeadSchema
> {
override get schema() {
return SeekHeadSchema;
}
seekHeads: SeekHeadType[] = [];
private offsetToTagMemo: Map<number, EbmlTagType> = new Map();
memoOffset(tag: EbmlTagType) {
this.offsetToTagMemo.set(tag.startOffset, tag);
}
addSeekHeadTag(tag: EbmlSeekHeadTagType) {
const seekHead = this.componentFromTag(tag);
this.seekHeads.push(seekHead);
return seekHead;
}
offsetFromSeekPosition(position: number): number {
return position + this.segment.contentStartOffset;
}
seekTagByStartOffset(
startOffset: number | undefined
): EbmlTagType | undefined {
return startOffset! >= 0
? this.offsetToTagMemo.get(startOffset!)
: undefined;
}
seekOffsetBySeekId(seekId: Uint8Array): number | undefined {
const seekPosition = this.seekHeads[0]?.Seek?.find((c) =>
isEqual(c.SeekID, seekId)
)?.SeekPosition;
return seekPosition! >= 0
? this.offsetFromSeekPosition(seekPosition! as number)
: undefined;
}
seekTagBySeekId(seekId: Uint8Array): EbmlTagType | undefined {
return this.seekTagByStartOffset(this.seekOffsetBySeekId(seekId));
}
get firstClusterOffset() {
if (!this.segment.firstCluster) {
throw new UnreachableOrLogicError('first cluster not found');
}
return this.segment.firstCluster.startOffset;
}
}

View File

@ -1,37 +0,0 @@
import type {EbmlMasterTagType} from "konoebml";
import {ArkErrors, type Type} from "arktype";
import {convertEbmlTagToComponent, type InferType} from "../util";
import type {SegmentComponent, SegmentSystem} from "./segment";
export class SegmentComponentSystemTrait<
E extends EbmlMasterTagType,
S extends Type<any>,
> {
segment: SegmentSystem;
get schema(): S {
throw new Error('unimplemented!');
}
constructor(segment: SegmentSystem) {
this.segment = segment;
}
componentFromTag(tag: E): SegmentComponent<InferType<S>> {
const extracted = convertEbmlTagToComponent(tag);
const result = this.schema(extracted) as
| (InferType<S> & { segment: SegmentSystem })
| ArkErrors;
if (result instanceof ArkErrors) {
const errors = result;
console.error(
'Parse component from tag error:',
tag.toDebugRecord(),
errors.flatProblemsByPath
);
throw errors;
}
result.segment = this.segment;
return result;
}
}

View File

@ -1,124 +0,0 @@
import {
type EbmlClusterTagType,
type EbmlSegmentTagType,
EbmlTagIdEnum,
EbmlTagPosition,
type EbmlTagType
} from "konoebml";
import {convertEbmlTagToComponent} from "../util";
import {CueSystem} from "./cue";
import {ClusterSystem} from "./cluster";
import {SEEK_ID_KAX_CUES, SEEK_ID_KAX_INFO, SEEK_ID_KAX_TAGS, SEEK_ID_KAX_TRACKS, SeekSystem} from "./seek";
import {InfoSystem} from "./info";
import {TrackSystem} from "./track";
import {TagSystem} from "./tag";
import type {BlockGroupType} from "../schema";
export class SegmentSystem {
startTag: EbmlSegmentTagType;
metaTags: EbmlTagType[] = [];
firstCluster: EbmlClusterTagType | undefined;
cue: CueSystem;
cluster: ClusterSystem;
seek: SeekSystem;
info: InfoSystem;
track: TrackSystem;
tag: TagSystem;
constructor(startNode: EbmlSegmentTagType) {
this.startTag = startNode;
this.cue = new CueSystem(this);
this.cluster = new ClusterSystem(this);
this.seek = new SeekSystem(this);
this.info = new InfoSystem(this);
this.track = new TrackSystem(this);
this.tag = new TagSystem(this);
}
get contentStartOffset() {
return this.startTag.startOffset + this.startTag.headerLength;
}
private seekLocal() {
const infoTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_INFO);
const tracksTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_TRACKS);
const cuesTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_CUES);
const tagsTag = this.seek.seekTagBySeekId(SEEK_ID_KAX_TAGS);
if (cuesTag?.id === EbmlTagIdEnum.Cues) {
this.cue.prepareCuesWithTag(cuesTag);
}
if (infoTag?.id === EbmlTagIdEnum.Info) {
this.info.prepareWithInfoTag(infoTag);
}
if (tracksTag?.id === EbmlTagIdEnum.Tracks) {
this.track.prepareTracksWithTag(tracksTag);
}
if (tagsTag?.id === EbmlTagIdEnum.Tags) {
this.tag.prepareTagsWithTag(tagsTag);
}
}
scanMeta(tag: EbmlTagType) {
if (
tag.id === EbmlTagIdEnum.SeekHead &&
tag.position === EbmlTagPosition.End
) {
this.seek.addSeekHeadTag(tag);
}
this.metaTags.push(tag);
if (tag.position !== EbmlTagPosition.Start) {
this.seek.memoOffset(tag);
}
if (tag.id === EbmlTagIdEnum.Cluster && !this.firstCluster) {
this.firstCluster = tag;
this.seekLocal();
}
if (this.firstCluster) {
if (tag.id === EbmlTagIdEnum.SimpleBlock && tag.keyframe) {
this.track.tryPeekKeyframe(tag);
} else if (tag.id === EbmlTagIdEnum.BlockGroup) {
const blockGroup = convertEbmlTagToComponent(tag) as BlockGroupType;
// keep frame
if (blockGroup && !blockGroup.ReferenceBlock && blockGroup.Block) {
this.track.tryPeekKeyframe(blockGroup.Block);
}
}
}
return this;
}
canCompleteMeta() {
const lastTag = this.metaTags.at(-1);
if (!lastTag) {
return false;
}
if (lastTag.id === EbmlTagIdEnum.Segment && lastTag.position === EbmlTagPosition.End) {
return true;
}
return (!!this.firstCluster && this.track.preparedToConfigureTracks());
}
async completeMeta() {
this.seekLocal();
await this.track.buildTracksConfiguration();
return this;
}
}
export type SegmentComponent<T> = T & {
get segment(): SegmentSystem;
};
export function withSegment<T extends object>(
component: T,
segment: SegmentSystem
): SegmentComponent<T> {
const component_ = component as T & { segment: SegmentSystem };
component_.segment = segment;
return component_;
}

View File

@ -1,27 +0,0 @@
import {EbmlTagIdEnum, type EbmlTagsTagType, type EbmlTagTagType} from "konoebml";
import {TagSchema, type TagType} from "../schema";
import type {SegmentComponent} from "./segment";
import {SegmentComponentSystemTrait} from "./segment-component";
export class TagSystem extends SegmentComponentSystemTrait<
EbmlTagTagType,
typeof TagSchema
> {
override get schema() {
return TagSchema;
}
tags: SegmentComponent<TagType>[] = [];
prepareTagsWithTag(tag: EbmlTagsTagType) {
this.tags = tag.children
.filter((c) => c.id === EbmlTagIdEnum.Tag)
.map((c) => this.componentFromTag(c));
return this;
}
get prepared(): boolean {
return this.tags.length > 0;
}
}

View File

@ -1,229 +0,0 @@
import {
ParseCodecErrors,
UnsupportedCodecError,
} from '@konoplayer/core/errors';
import {
EbmlTagIdEnum,
type EbmlTrackEntryTagType,
type EbmlTracksTagType,
} from 'konoebml';
import {
audioCodecIdToWebCodecs,
videoCodecIdRequirePeekingKeyframe,
videoCodecIdToWebCodecs,
type AudioDecoderConfigExt,
type VideoDecoderConfigExt,
} from '../codecs';
import {
TrackEntrySchema,
type TrackEntryType,
TrackTypeRestrictionEnum,
} from '../schema';
import type { SegmentComponent } from './segment';
import {SegmentComponentSystemTrait} from "./segment-component";
import {pick} from "lodash-es";
export interface GetTrackEntryOptions {
priority?: (v: SegmentComponent<TrackEntryType>) => number;
predicate: (v: SegmentComponent<TrackEntryType>) => boolean;
}
export abstract class TrackContext {
peekingKeyframe?: Uint8Array;
trackEntry: TrackEntryType;
timestampScale: number;
lastBlockTimestamp = Number.NaN;
averageBlockDuration = Number.NaN;
constructor(trackEntry: TrackEntryType, timestampScale: number) {
this.trackEntry = trackEntry;
this.timestampScale = Number(timestampScale);
}
peekKeyframe(payload: Uint8Array) {
this.peekingKeyframe = payload;
}
preparedToConfigure() {
if (this.requirePeekKeyframe()) {
return !!this.peekingKeyframe;
}
return true;
}
abstract requirePeekKeyframe(): boolean;
abstract buildConfiguration(): Promise<void>;
predictBlockDuration(blockTimestamp: number): number {
if (this.trackEntry.DefaultDuration) {
return Number(this.trackEntry.DefaultDuration);
}
const delta = blockTimestamp - this.lastBlockTimestamp;
this.lastBlockTimestamp = blockTimestamp;
this.averageBlockDuration = this.averageBlockDuration
? this.averageBlockDuration * 0.5 + delta * 0.5
: delta;
return this.averageBlockDuration;
}
}
export class DefaultTrackContext extends TrackContext {
override requirePeekKeyframe(): boolean {
return false;
}
// biome-ignore lint/suspicious/noEmptyBlockStatements: <explanation>
override async buildConfiguration(): Promise<void> {}
}
export class VideoTrackContext extends TrackContext {
configuration!: VideoDecoderConfigExt;
override requirePeekKeyframe(): boolean {
return videoCodecIdRequirePeekingKeyframe(this.trackEntry.CodecID);
}
async buildConfiguration() {
const configuration = videoCodecIdToWebCodecs(
this.trackEntry,
this.peekingKeyframe
);
const checkResult = await VideoDecoder?.isConfigSupported?.(configuration);
if (!checkResult?.supported) {
throw new UnsupportedCodecError(configuration.codec, 'video decoder');
}
this.configuration = configuration;
}
}
export class AudioTrackContext extends TrackContext {
configuration!: AudioDecoderConfigExt;
override requirePeekKeyframe(): boolean {
return videoCodecIdRequirePeekingKeyframe(this.trackEntry.CodecID);
}
async buildConfiguration() {
const configuration = audioCodecIdToWebCodecs(
this.trackEntry,
this.peekingKeyframe
);
const checkResult = await AudioDecoder?.isConfigSupported?.(configuration);
if (!checkResult?.supported) {
throw new UnsupportedCodecError(configuration.codec, 'audio decoder');
}
this.configuration = configuration;
}
override predictBlockDuration(blockTimestamp: number): number {
if (this.trackEntry.DefaultDuration) {
return Number(this.trackEntry.DefaultDuration);
}
if (this.configuration.samplesPerFrame) {
return (
Number(
this.configuration.samplesPerFrame / this.configuration.sampleRate
) * this.timestampScale
);
}
const delta = blockTimestamp - this.lastBlockTimestamp;
this.lastBlockTimestamp = blockTimestamp;
this.averageBlockDuration = this.averageBlockDuration
? this.averageBlockDuration * 0.5 + delta * 0.5
: delta;
return this.averageBlockDuration;
}
}
export function standardTrackPredicate(track: TrackEntryType) {
return track.FlagEnabled !== 0;
}
export function standardTrackPriority(track: TrackEntryType) {
return (Number(!!track.FlagForced) << 8) + (Number(!!track.FlagDefault) << 4);
}
export class TrackSystem extends SegmentComponentSystemTrait<
EbmlTrackEntryTagType,
typeof TrackEntrySchema
> {
override get schema() {
return TrackEntrySchema;
}
tracks: SegmentComponent<TrackEntryType>[] = [];
trackContexts: Map<number | bigint, TrackContext> = new Map();
getTrackEntry({
priority = standardTrackPriority,
predicate,
}: GetTrackEntryOptions) {
return this.tracks
.filter(predicate)
.toSorted((a, b) => priority(b) - priority(a))
.at(0);
}
getTrackContext<T extends TrackContext>(
options: GetTrackEntryOptions
): T | undefined {
const trackEntry = this.getTrackEntry(options);
const trackNum = trackEntry?.TrackNumber!;
return this.trackContexts.get(trackNum) as T | undefined;
}
prepareTracksWithTag(tag: EbmlTracksTagType) {
const infoSystem = this.segment.info;
this.tracks = tag.children
.filter((c) => c.id === EbmlTagIdEnum.TrackEntry)
.map((c) => this.componentFromTag(c));
for (const track of this.tracks) {
if (track.TrackType === TrackTypeRestrictionEnum.VIDEO) {
this.trackContexts.set(
track.TrackNumber,
new VideoTrackContext(track, Number(infoSystem.info.TimestampScale))
);
} else if (track.TrackType === TrackTypeRestrictionEnum.AUDIO) {
this.trackContexts.set(
track.TrackNumber,
new AudioTrackContext(track, Number(infoSystem.info.TimestampScale))
);
}
}
return this;
}
async buildTracksConfiguration() {
const parseErrors = new ParseCodecErrors();
for (const context of this.trackContexts.values()) {
try {
await context.buildConfiguration();
} catch (e) {
parseErrors.cause.push(e as Error);
}
}
if (parseErrors.cause.length > 0) {
console.error(parseErrors, parseErrors.cause);
}
}
tryPeekKeyframe(tag: { track: number | bigint; frames: Uint8Array[] }) {
for (const c of this.trackContexts.values()) {
if (c.trackEntry.TrackNumber === tag.track) {
c.peekKeyframe(tag.frames?.[0]);
}
}
}
preparedToConfigureTracks(): boolean {
for (const c of this.trackContexts.values()) {
if (!c.preparedToConfigure()) {
return false;
}
}
return true;
}
}

View File

@ -1,20 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"paths": {
"@konoplayer/core/*": [
"../core/src/*"
]
}
},
"include": [
"src"
],
"references": [
{
"path": "../core"
}
]
}

1090
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -43,15 +43,15 @@ const EbmlTypeMetas = {
}, },
uinteger: { uinteger: {
code: 'Uint', code: 'Uint',
primitive: () => 'type.number.or(type.bigint)', primitive: () => 'type.number',
default: (d: string): string => d, default: (d: string): string => d,
primitiveStr: () => '(number | bigint)', primitiveStr: () => 'number',
}, },
integer: { integer: {
code: 'Int', code: 'Int',
primitive: () => 'type.number.or(type.bigint)', primitive: () => 'type.number',
default: (d: string) => d, default: (d: string) => d,
primitiveStr: () => '(number | bigint)', primitiveStr: () => 'number',
}, },
float: { float: {
code: 'Float', code: 'Float',
@ -160,7 +160,7 @@ function extractElement(element: Element) {
); );
assert(typeof path_ === 'string', `path of ${name} is not string ${element}`); assert(typeof path_ === 'string', `path of ${name} is not string ${element}`);
const path = path_.replace(/\\\+/g, '\\').split('\\').filter(Boolean); const path = path_.split('\\').filter(Boolean);
const parentPath = path.at(-2); const parentPath = path.at(-2);
const prefix = path.slice(0, -1); const prefix = path.slice(0, -1);
const level = path.length - 1; const level = path.length - 1;
@ -342,15 +342,10 @@ function generateMkvSchemaHierarchy(elements_: EbmlElementType[]) {
const idMulti = new Set<string>(); const idMulti = new Set<string>();
const preDefs = [ const preDefs = [
'export const BinarySchema = type.instanceOf(Uint8Array);', 'export const BinarySchema = type.instanceOf(Uint8Array);',
'export type BinaryType = typeof BinarySchema.infer;',
...Object.entries(AdHocType).map( ...Object.entries(AdHocType).map(
([name, meta]) => ([name, meta]) =>
`export const ${meta.primitive()} = type.instanceOf(Ebml${name}Tag);` `export const ${meta.primitive()} = type.instanceOf(Ebml${name}Tag);`
), ),
...Object.entries(AdHocType).map(
([name, meta]) =>
`export type ${name}Type = typeof ${meta.primitive()}.infer;`
),
]; ];
const generateAssociated = (el: EbmlElementType): string | undefined => { const generateAssociated = (el: EbmlElementType): string | undefined => {
@ -368,6 +363,7 @@ function generateMkvSchemaHierarchy(elements_: EbmlElementType[]) {
const selfSchema = [ const selfSchema = [
`export const ${el.name}Schema = type({`, `export const ${el.name}Schema = type({`,
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
...associated.map((v) => { ...associated.map((v) => {
let meta: any; let meta: any;
const restriction = generateRestriction(v); const restriction = generateRestriction(v);
@ -395,7 +391,7 @@ function generateMkvSchemaHierarchy(elements_: EbmlElementType[]) {
if (v.maxOccurs !== 1) { if (v.maxOccurs !== 1) {
expr = `${expr}.array()`; expr = `${expr}.array()`;
if (v.maxOccurs !== 1 && v.minOccurs === 1 && !v.default) { if (v.maxOccurs !== 1 && v.minOccurs === 1 && !v.default) {
expr = `${expr}.atLeastLength(1)`; expr = `${expr}.atLeastLength(1)`
} }
idMulti.add(v.name); idMulti.add(v.name);
} }
@ -405,8 +401,9 @@ function generateMkvSchemaHierarchy(elements_: EbmlElementType[]) {
} else { } else {
childrenSchema.push(`export const ${v.name}Schema = match({ childrenSchema.push(`export const ${v.name}Schema = match({
"${meta.primitiveStr(v.name)}[]": v => v.length > 0 ? v : [${meta.default(v.default)}], "${meta.primitiveStr(v.name)}[]": v => v.length > 0 ? v : [${meta.default(v.default)}],
default: () => [${meta.default(v.default)}], "undefined": () => [${meta.default(v.default)}],
}).optional();`); default: "assert"
});`);
expr = `${v.name}Schema`; expr = `${v.name}Schema`;
} }
} else if (!v.minOccurs) { } else if (!v.minOccurs) {
@ -437,7 +434,7 @@ function main() {
const elementSchemas = extractElementAll(); const elementSchemas = extractElementAll();
const files = { const files = {
'schema': [ 'schema.ts': [
generateMkvSchemaImports(elementSchemas), generateMkvSchemaImports(elementSchemas),
generateMkvSchemaHierarchy(elementSchemas), generateMkvSchemaHierarchy(elementSchemas),
], ],

View File

@ -1,54 +0,0 @@
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import path from 'node:path';
import os from 'node:os';
import fsp from 'node:fs/promises';
async function downloadAndExtract() {
try {
// 目标目录
const targetDir = path.join(import.meta.dirname, '..', 'apps', 'mock', 'public', 'video', 'huge');
const url = 'https://sourceforge.net/projects/matroska/files/test_files/matroska_test_w1_1.zip/download';
const zipFile = 'matroska_test_w1_1.zip';
const platform = os.platform();
const execPromise = (cmd: string) => promisify(exec)(cmd, {
cwd: targetDir,
shell: platform === 'win32' ? 'powershell' : undefined
});
await fsp.mkdir(targetDir, { recursive: true })
console.log(`Working directory switched to: ${targetDir}`);
if (platform === 'win32') {
// Windows: 使用 PowerShell 的 Invoke-WebRequest 和 Expand-Archive
console.log('Downloading on Windows...');
await execPromise(`Invoke-WebRequest -Uri '${url}' -OutFile '${zipFile}' -UserAgent "wget"`);
console.log('Extracting on Windows...');
await execPromise(`Expand-Archive -Path '${zipFile}' -DestinationPath '.' -Force`);
console.log('Cleaning up...');
await execPromise(`rm '${zipFile}'`);
} else {
// *nix: 使用 curl 和 unzip
console.log('Downloading on *nix...');
await execPromise(`curl -L "${url}" -o "${zipFile}"`);
console.log('Extracting on *nix...');
await execPromise(`unzip -o "${zipFile}"`);
console.log('Cleaning up...');
await execPromise(`rm "${zipFile}"`);
}
console.log('Download and extraction completed successfully!');
} catch (error) {
console.error('An error occurred:', error);
throw error;
}
}
// 执行
downloadAndExtract().catch((err) => {
console.error(err)
process.exit(1);
});

View File

@ -14,22 +14,19 @@
"DOM.AsyncIterable", "DOM.AsyncIterable",
"DOM.Iterable" "DOM.Iterable"
], ],
"types": [
"@webgpu/types",
"@types/node"
],
"module": "ESNext", "module": "ESNext",
"moduleDetection": "force", "moduleDetection": "force",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"allowImportingTsExtensions": false, "allowImportingTsExtensions": true,
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"skipLibCheck": true, "skipLibCheck": true,
"target": "ES2021", "target": "ES2021",
"strictNullChecks": true, "strictNullChecks": true,
"strict": true, "strict": true,
"useDefineForClassFields": false, "noUnusedLocals": true,
"noUnusedParameters": true,
"useDefineForClassFields": true,
"exactOptionalPropertyTypes": false, "exactOptionalPropertyTypes": false,
"experimentalDecorators": true
} }
} }

View File

@ -9,15 +9,6 @@
}, },
{ {
"path": "./tsconfig.scripts.json" "path": "./tsconfig.scripts.json"
},
{
"path": "./packages/matroska"
},
{
"path": "./packages/core"
},
{
"path": "./apps/test"
} }
] ]
} }