Compare commits
No commits in common. "ee4eee473d76e901d7ef0bbe9e2e9a4539907292" and "0b681d4fd1231e76ea33ff41ba8110eb989b19d8" have entirely different histories.
ee4eee473d
...
0b681d4fd1
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
@ -1,9 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"runem.lit-plugin",
|
||||
"vitest.explorer",
|
||||
"biomejs.biome",
|
||||
"hbenl.vscode-test-explorer",
|
||||
"zerotaskx.rust-extension-pack"
|
||||
"runem.lit-plugin"
|
||||
]
|
||||
}
|
409
Cargo.lock
generated
409
Cargo.lock
generated
@ -2,75 +2,24 @@
|
||||
# It is not intended for manual editing.
|
||||
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]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@ -78,22 +27,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
name = "demuxing"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
"symphonia-format-mkv",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
@ -103,247 +42,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "log"
|
||||
version = "0.4.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "symphonia-core"
|
||||
version = "0.5.4"
|
||||
@ -351,7 +61,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags 1.3.2",
|
||||
"bitflags",
|
||||
"bytemuck",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@ -370,30 +80,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "symphonia-metadata"
|
||||
version = "0.5.4"
|
||||
@ -415,90 +101,3 @@ dependencies = [
|
||||
"symphonia-core",
|
||||
"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"
|
||||
|
@ -1,3 +1,3 @@
|
||||
[workspace]
|
||||
members = ["packages/symphonia", "packages/ffmpeg"]
|
||||
resolver = "3"
|
||||
members = ["packages/demuxing"]
|
||||
resolver = "2"
|
||||
|
39
README.md
39
README.md
@ -1,40 +1 @@
|
||||
# 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
1
apps/mock/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
public/video-sample/huge/*
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@konoplayer/mock",
|
||||
"name": "mock",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
2
apps/mock/public/.gitignore
vendored
2
apps/mock/public/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
video/huge/*
|
||||
!video/huge/.gitkeep
|
BIN
apps/mock/public/video-sample/audiosample.webm
Normal file
BIN
apps/mock/public/video-sample/audiosample.webm
Normal file
Binary file not shown.
BIN
apps/mock/public/video-sample/video-webm-codecs-avc1-42E01E.webm
Normal file
BIN
apps/mock/public/video-sample/video-webm-codecs-avc1-42E01E.webm
Normal file
Binary file not shown.
BIN
apps/mock/public/video-sample/video-webm-codecs-vp8.webm
Normal file
BIN
apps/mock/public/video-sample/video-webm-codecs-vp8.webm
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -4,8 +4,10 @@
|
||||
"composite": true,
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
@ -21,6 +23,6 @@
|
||||
"node_modules",
|
||||
"dist",
|
||||
"test",
|
||||
"**/*spec"
|
||||
"**/*spec.ts"
|
||||
]
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@konoplayer/playground",
|
||||
"version": "0.1.0",
|
||||
"name": "playground",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@ -9,11 +9,11 @@
|
||||
"preview": "rsbuild preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"lit": "^3.2.1",
|
||||
"@konoplayer/core": "workspace:*",
|
||||
"@konoplayer/matroska": "workspace:*"
|
||||
"konoebml": "0.1.1",
|
||||
"lit": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rsbuild/core": "^1.2.14"
|
||||
"@rsbuild/core": "^1.2.14",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
@ -5,17 +5,11 @@ export interface RangedStream {
|
||||
totalSize?: number;
|
||||
}
|
||||
|
||||
export interface CreateRangedStreamOptions {
|
||||
url: string;
|
||||
byteStart?: number;
|
||||
byteEnd?: number;
|
||||
}
|
||||
|
||||
export async function createRangedStream({
|
||||
url,
|
||||
export async function createRangedStream(
|
||||
url: string,
|
||||
byteStart = 0,
|
||||
byteEnd,
|
||||
}: CreateRangedStreamOptions) {
|
||||
byteEnd?: number
|
||||
) {
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
const headers = new Headers();
|
60
apps/playground/src/fetch/index.ts
Normal file
60
apps/playground/src/fetch/index.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
|
@ -3,8 +3,7 @@
|
||||
<head></head>
|
||||
|
||||
<body>
|
||||
<!-- <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/huge/test8.mkv" width="800" height="450"></video-pipeline-demo>
|
||||
<!-- <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" /> -->
|
||||
<my-element />
|
||||
<video-pipeline-demo src="/api/static/video-sample/test.webm" />
|
||||
<!-- <video-pipeline-demo src="/api/static/video-sample/huge/animation.mkv" /> -->
|
||||
</body>
|
282
apps/playground/src/media/mkv/model.ts
Normal file
282
apps/playground/src/media/mkv/model.ts
Normal 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;
|
||||
}
|
||||
}
|
319
apps/playground/src/media/mkv/reactive.ts
Normal file
319
apps/playground/src/media/mkv/reactive.ts
Normal 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$,
|
||||
};
|
||||
}
|
@ -2,27 +2,24 @@ import { type, match } from 'arktype';
|
||||
import { EbmlTagIdEnum, EbmlSimpleBlockTag ,EbmlBlockTag } from 'konoebml';
|
||||
|
||||
export const BinarySchema = type.instanceOf(Uint8Array);
|
||||
export type BinaryType = typeof BinarySchema.infer;
|
||||
export const SimpleBlockSchema = type.instanceOf(EbmlSimpleBlockTag);
|
||||
export const BlockSchema = type.instanceOf(EbmlBlockTag);
|
||||
export type SimpleBlockType = typeof SimpleBlockSchema.infer;
|
||||
export type BlockType = typeof BlockSchema.infer;
|
||||
|
||||
export const DocTypeExtensionSchema = type({
|
||||
DocTypeExtensionName: type.string,
|
||||
DocTypeExtensionVersion: type.number.or(type.bigint),
|
||||
DocTypeExtensionVersion: type.number,
|
||||
});
|
||||
|
||||
export type DocTypeExtensionType = typeof DocTypeExtensionSchema.infer;
|
||||
|
||||
export const EBMLSchema = type({
|
||||
EBMLVersion: type.number.or(type.bigint).default(1),
|
||||
EBMLReadVersion: type.number.or(type.bigint).default(1),
|
||||
EBMLMaxIDLength: type.number.or(type.bigint).default(4),
|
||||
EBMLMaxSizeLength: type.number.or(type.bigint).default(8),
|
||||
EBMLVersion: type.number.default(1),
|
||||
EBMLReadVersion: type.number.default(1),
|
||||
EBMLMaxIDLength: type.number.default(4),
|
||||
EBMLMaxSizeLength: type.number.default(8),
|
||||
DocType: type.string,
|
||||
DocTypeVersion: type.number.or(type.bigint).default(1),
|
||||
DocTypeReadVersion: type.number.or(type.bigint).default(1),
|
||||
DocTypeVersion: type.number.default(1),
|
||||
DocTypeReadVersion: type.number.default(1),
|
||||
DocTypeExtension: DocTypeExtensionSchema.array().optional(),
|
||||
});
|
||||
|
||||
@ -30,7 +27,7 @@ export type EBMLType = typeof EBMLSchema.infer;
|
||||
|
||||
export const SeekSchema = type({
|
||||
SeekID: BinarySchema,
|
||||
SeekPosition: type.number.or(type.bigint),
|
||||
SeekPosition: type.number,
|
||||
});
|
||||
|
||||
export type SeekType = typeof SeekSchema.infer;
|
||||
@ -43,8 +40,8 @@ export type SeekHeadType = typeof SeekHeadSchema.infer;
|
||||
|
||||
export const ChapterTranslateSchema = type({
|
||||
ChapterTranslateID: BinarySchema,
|
||||
ChapterTranslateCodec: type.number.or(type.bigint),
|
||||
ChapterTranslateEditionUID: type.number.or(type.bigint).array().optional(),
|
||||
ChapterTranslateCodec: type.number,
|
||||
ChapterTranslateEditionUID: type.number.array().optional(),
|
||||
});
|
||||
|
||||
export type ChapterTranslateType = typeof ChapterTranslateSchema.infer;
|
||||
@ -58,7 +55,7 @@ export const InfoSchema = type({
|
||||
NextFilename: type.string.optional(),
|
||||
SegmentFamily: BinarySchema.array().optional(),
|
||||
ChapterTranslate: ChapterTranslateSchema.array().optional(),
|
||||
TimestampScale: type.number.or(type.bigint).default(1000000),
|
||||
TimestampScale: type.number.default(1000000),
|
||||
Duration: type.number.optional(),
|
||||
DateUTC: BinarySchema.optional(),
|
||||
Title: type.string.optional(),
|
||||
@ -69,14 +66,14 @@ export const InfoSchema = type({
|
||||
export type InfoType = typeof InfoSchema.infer;
|
||||
|
||||
export const SilentTracksSchema = type({
|
||||
SilentTrackNumber: type.number.or(type.bigint).array().optional(),
|
||||
SilentTrackNumber: type.number.array().optional(),
|
||||
});
|
||||
|
||||
export type SilentTracksType = typeof SilentTracksSchema.infer;
|
||||
|
||||
export const BlockMoreSchema = type({
|
||||
BlockAdditional: BinarySchema,
|
||||
BlockAddID: type.number.or(type.bigint).default(1),
|
||||
BlockAddID: type.number.default(1),
|
||||
});
|
||||
|
||||
export type BlockMoreType = typeof BlockMoreSchema.infer;
|
||||
@ -88,11 +85,11 @@ export const BlockAdditionsSchema = type({
|
||||
export type BlockAdditionsType = typeof BlockAdditionsSchema.infer;
|
||||
|
||||
export const TimeSliceSchema = type({
|
||||
LaceNumber: type.number.or(type.bigint).optional(),
|
||||
FrameNumber: type.number.or(type.bigint).default(0),
|
||||
BlockAdditionID: type.number.or(type.bigint).default(0),
|
||||
Delay: type.number.or(type.bigint).default(0),
|
||||
SliceDuration: type.number.or(type.bigint).default(0),
|
||||
LaceNumber: type.number.optional(),
|
||||
FrameNumber: type.number.default(0),
|
||||
BlockAdditionID: type.number.default(0),
|
||||
Delay: type.number.default(0),
|
||||
SliceDuration: type.number.default(0),
|
||||
});
|
||||
|
||||
export type TimeSliceType = typeof TimeSliceSchema.infer;
|
||||
@ -104,8 +101,8 @@ export const SlicesSchema = type({
|
||||
export type SlicesType = typeof SlicesSchema.infer;
|
||||
|
||||
export const ReferenceFrameSchema = type({
|
||||
ReferenceOffset: type.number.or(type.bigint),
|
||||
ReferenceTimestamp: type.number.or(type.bigint),
|
||||
ReferenceOffset: type.number,
|
||||
ReferenceTimestamp: type.number,
|
||||
});
|
||||
|
||||
export type ReferenceFrameType = typeof ReferenceFrameSchema.infer;
|
||||
@ -114,12 +111,12 @@ export const BlockGroupSchema = type({
|
||||
Block: BlockSchema,
|
||||
BlockVirtual: BinarySchema.optional(),
|
||||
BlockAdditions: BlockAdditionsSchema.optional(),
|
||||
BlockDuration: type.number.or(type.bigint).optional(),
|
||||
ReferencePriority: type.number.or(type.bigint).default(0),
|
||||
ReferenceBlock: type.number.or(type.bigint).array().optional(),
|
||||
ReferenceVirtual: type.number.or(type.bigint).optional(),
|
||||
BlockDuration: type.number.optional(),
|
||||
ReferencePriority: type.number.default(0),
|
||||
ReferenceBlock: type.number.array().optional(),
|
||||
ReferenceVirtual: type.number.optional(),
|
||||
CodecState: BinarySchema.optional(),
|
||||
DiscardPadding: type.number.or(type.bigint).optional(),
|
||||
DiscardPadding: type.number.optional(),
|
||||
Slices: SlicesSchema.optional(),
|
||||
ReferenceFrame: ReferenceFrameSchema.optional(),
|
||||
});
|
||||
@ -127,10 +124,10 @@ export const BlockGroupSchema = type({
|
||||
export type BlockGroupType = typeof BlockGroupSchema.infer;
|
||||
|
||||
export const ClusterSchema = type({
|
||||
Timestamp: type.number.or(type.bigint),
|
||||
Timestamp: type.number,
|
||||
SilentTracks: SilentTracksSchema.optional(),
|
||||
Position: type.number.or(type.bigint).optional(),
|
||||
PrevSize: type.number.or(type.bigint).optional(),
|
||||
Position: type.number.optional(),
|
||||
PrevSize: type.number.optional(),
|
||||
SimpleBlock: SimpleBlockSchema.array().optional(),
|
||||
BlockGroup: BlockGroupSchema.array().optional(),
|
||||
EncryptedBlock: BinarySchema.array().optional(),
|
||||
@ -139,9 +136,9 @@ export const ClusterSchema = type({
|
||||
export type ClusterType = typeof ClusterSchema.infer;
|
||||
|
||||
export const BlockAdditionMappingSchema = type({
|
||||
BlockAddIDValue: type.number.or(type.bigint).optional(),
|
||||
BlockAddIDValue: type.number.optional(),
|
||||
BlockAddIDName: type.string.optional(),
|
||||
BlockAddIDType: type.number.or(type.bigint).default(0),
|
||||
BlockAddIDType: type.number.default(0),
|
||||
BlockAddIDExtraData: BinarySchema.optional(),
|
||||
});
|
||||
|
||||
@ -149,8 +146,8 @@ export type BlockAdditionMappingType = typeof BlockAdditionMappingSchema.infer;
|
||||
|
||||
export const TrackTranslateSchema = type({
|
||||
TrackTranslateTrackID: BinarySchema,
|
||||
TrackTranslateCodec: type.number.or(type.bigint),
|
||||
TrackTranslateEditionUID: type.number.or(type.bigint).array().optional(),
|
||||
TrackTranslateCodec: type.number,
|
||||
TrackTranslateEditionUID: type.number.array().optional(),
|
||||
});
|
||||
|
||||
export type TrackTranslateType = typeof TrackTranslateSchema.infer;
|
||||
@ -201,12 +198,9 @@ export enum MatrixCoefficientsRestrictionEnum {
|
||||
CHROMA_DERIVED_CONSTANT_LUMINANCE = 13,
|
||||
// ITU-R BT.2100-0
|
||||
ITU_R_BT_2100_0 = 14,
|
||||
}
|
||||
export const MatrixCoefficientsRestriction = type(
|
||||
'0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14'
|
||||
);
|
||||
export type MatrixCoefficientsRestrictionType =
|
||||
typeof MatrixCoefficientsRestriction.infer;
|
||||
};
|
||||
export const MatrixCoefficientsRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14');
|
||||
export type MatrixCoefficientsRestrictionType = typeof MatrixCoefficientsRestriction.infer;
|
||||
|
||||
export enum ChromaSitingHorzRestrictionEnum {
|
||||
// unspecified
|
||||
@ -215,10 +209,9 @@ export enum ChromaSitingHorzRestrictionEnum {
|
||||
LEFT_COLLOCATED = 1,
|
||||
// half
|
||||
HALF = 2,
|
||||
}
|
||||
};
|
||||
export const ChromaSitingHorzRestriction = type('0 | 1 | 2');
|
||||
export type ChromaSitingHorzRestrictionType =
|
||||
typeof ChromaSitingHorzRestriction.infer;
|
||||
export type ChromaSitingHorzRestrictionType = typeof ChromaSitingHorzRestriction.infer;
|
||||
|
||||
export enum ChromaSitingVertRestrictionEnum {
|
||||
// unspecified
|
||||
@ -227,10 +220,9 @@ export enum ChromaSitingVertRestrictionEnum {
|
||||
TOP_COLLOCATED = 1,
|
||||
// half
|
||||
HALF = 2,
|
||||
}
|
||||
};
|
||||
export const ChromaSitingVertRestriction = type('0 | 1 | 2');
|
||||
export type ChromaSitingVertRestrictionType =
|
||||
typeof ChromaSitingVertRestriction.infer;
|
||||
export type ChromaSitingVertRestrictionType = typeof ChromaSitingVertRestriction.infer;
|
||||
|
||||
export enum RangeRestrictionEnum {
|
||||
// unspecified
|
||||
@ -241,7 +233,7 @@ export enum RangeRestrictionEnum {
|
||||
FULL_RANGE_NO_CLIPPING = 2,
|
||||
// defined by MatrixCoefficients / TransferCharacteristics
|
||||
DEFINED_BY_MATRIX_COEFFICIENTS_TRANSFER_CHARACTERISTICS = 3,
|
||||
}
|
||||
};
|
||||
export const RangeRestriction = type('0 | 1 | 2 | 3');
|
||||
export type RangeRestrictionType = typeof RangeRestriction.infer;
|
||||
|
||||
@ -284,12 +276,9 @@ export enum TransferCharacteristicsRestrictionEnum {
|
||||
SMPTE_ST_428_1 = 17,
|
||||
// ARIB STD-B67 (HLG)
|
||||
ARIB_STD_B67_HLG = 18,
|
||||
}
|
||||
export const TransferCharacteristicsRestriction = type(
|
||||
'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 const TransferCharacteristicsRestriction = type('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 enum PrimariesRestrictionEnum {
|
||||
// reserved
|
||||
@ -320,26 +309,24 @@ export enum PrimariesRestrictionEnum {
|
||||
SMPTE_EG_432_2 = 12,
|
||||
// EBU Tech. 3213-E - JEDEC P22 phosphors
|
||||
EBU_TECH_3213_E_JEDEC_P22_PHOSPHORS = 22,
|
||||
}
|
||||
export const PrimariesRestriction = type(
|
||||
'0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22'
|
||||
);
|
||||
};
|
||||
export const PrimariesRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22');
|
||||
export type PrimariesRestrictionType = typeof PrimariesRestriction.infer;
|
||||
|
||||
export const ColourSchema = type({
|
||||
MatrixCoefficients: MatrixCoefficientsRestriction.default(2),
|
||||
BitsPerChannel: type.number.or(type.bigint).default(0),
|
||||
ChromaSubsamplingHorz: type.number.or(type.bigint).optional(),
|
||||
ChromaSubsamplingVert: type.number.or(type.bigint).optional(),
|
||||
CbSubsamplingHorz: type.number.or(type.bigint).optional(),
|
||||
CbSubsamplingVert: type.number.or(type.bigint).optional(),
|
||||
BitsPerChannel: type.number.default(0),
|
||||
ChromaSubsamplingHorz: type.number.optional(),
|
||||
ChromaSubsamplingVert: type.number.optional(),
|
||||
CbSubsamplingHorz: type.number.optional(),
|
||||
CbSubsamplingVert: type.number.optional(),
|
||||
ChromaSitingHorz: ChromaSitingHorzRestriction.default(0),
|
||||
ChromaSitingVert: ChromaSitingVertRestriction.default(0),
|
||||
Range: RangeRestriction.default(0),
|
||||
TransferCharacteristics: TransferCharacteristicsRestriction.default(2),
|
||||
Primaries: PrimariesRestriction.default(2),
|
||||
MaxCLL: type.number.or(type.bigint).optional(),
|
||||
MaxFALL: type.number.or(type.bigint).optional(),
|
||||
MaxCLL: type.number.optional(),
|
||||
MaxFALL: type.number.optional(),
|
||||
MasteringMetadata: MasteringMetadataSchema.optional(),
|
||||
});
|
||||
|
||||
@ -354,10 +341,9 @@ export enum ProjectionTypeRestrictionEnum {
|
||||
CUBEMAP = 2,
|
||||
// mesh
|
||||
MESH = 3,
|
||||
}
|
||||
};
|
||||
export const ProjectionTypeRestriction = type('0 | 1 | 2 | 3');
|
||||
export type ProjectionTypeRestrictionType =
|
||||
typeof ProjectionTypeRestriction.infer;
|
||||
export type ProjectionTypeRestrictionType = typeof ProjectionTypeRestriction.infer;
|
||||
|
||||
export const ProjectionSchema = type({
|
||||
ProjectionType: ProjectionTypeRestriction.default(0),
|
||||
@ -376,10 +362,9 @@ export enum FlagInterlacedRestrictionEnum {
|
||||
INTERLACED = 1,
|
||||
// progressive
|
||||
PROGRESSIVE = 2,
|
||||
}
|
||||
};
|
||||
export const FlagInterlacedRestriction = type('0 | 1 | 2');
|
||||
export type FlagInterlacedRestrictionType =
|
||||
typeof FlagInterlacedRestriction.infer;
|
||||
export type FlagInterlacedRestrictionType = typeof FlagInterlacedRestriction.infer;
|
||||
|
||||
export enum FieldOrderRestrictionEnum {
|
||||
// progressive
|
||||
@ -394,7 +379,7 @@ export enum FieldOrderRestrictionEnum {
|
||||
TFF_INTERLEAVED = 9,
|
||||
// bff (interleaved)
|
||||
BFF_INTERLEAVED = 14,
|
||||
}
|
||||
};
|
||||
export const FieldOrderRestriction = type('0 | 1 | 2 | 6 | 9 | 14');
|
||||
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 (right eye is first)
|
||||
BOTH_EYES_LACED_IN_ONE_BLOCK_RIGHT_EYE_IS_FIRST = 14,
|
||||
}
|
||||
export const StereoModeRestriction = type(
|
||||
'0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14'
|
||||
);
|
||||
};
|
||||
export const StereoModeRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14');
|
||||
export type StereoModeRestrictionType = typeof StereoModeRestriction.infer;
|
||||
|
||||
export enum AlphaModeRestrictionEnum {
|
||||
@ -440,7 +423,7 @@ export enum AlphaModeRestrictionEnum {
|
||||
NONE = 0,
|
||||
// present
|
||||
PRESENT = 1,
|
||||
}
|
||||
};
|
||||
export const AlphaModeRestriction = type('0 | 1');
|
||||
export type AlphaModeRestrictionType = typeof AlphaModeRestriction.infer;
|
||||
|
||||
@ -453,10 +436,9 @@ export enum OldStereoModeRestrictionEnum {
|
||||
LEFT_EYE = 2,
|
||||
// both eyes
|
||||
BOTH_EYES = 3,
|
||||
}
|
||||
};
|
||||
export const OldStereoModeRestriction = type('0 | 1 | 2 | 3');
|
||||
export type OldStereoModeRestrictionType =
|
||||
typeof OldStereoModeRestriction.infer;
|
||||
export type OldStereoModeRestrictionType = typeof OldStereoModeRestriction.infer;
|
||||
|
||||
export enum DisplayUnitRestrictionEnum {
|
||||
// pixels
|
||||
@ -469,7 +451,7 @@ export enum DisplayUnitRestrictionEnum {
|
||||
DISPLAY_ASPECT_RATIO = 3,
|
||||
// unknown
|
||||
UNKNOWN = 4,
|
||||
}
|
||||
};
|
||||
export const DisplayUnitRestriction = type('0 | 1 | 2 | 3 | 4');
|
||||
export type DisplayUnitRestrictionType = typeof DisplayUnitRestriction.infer;
|
||||
|
||||
@ -480,10 +462,9 @@ export enum AspectRatioTypeRestrictionEnum {
|
||||
KEEP_ASPECT_RATIO = 1,
|
||||
// fixed
|
||||
FIXED = 2,
|
||||
}
|
||||
};
|
||||
export const AspectRatioTypeRestriction = type('0 | 1 | 2');
|
||||
export type AspectRatioTypeRestrictionType =
|
||||
typeof AspectRatioTypeRestriction.infer;
|
||||
export type AspectRatioTypeRestrictionType = typeof AspectRatioTypeRestriction.infer;
|
||||
|
||||
export const VideoSchema = type({
|
||||
FlagInterlaced: FlagInterlacedRestriction.default(0),
|
||||
@ -491,14 +472,14 @@ export const VideoSchema = type({
|
||||
StereoMode: StereoModeRestriction.default(0),
|
||||
AlphaMode: AlphaModeRestriction.default(0),
|
||||
OldStereoMode: OldStereoModeRestriction.optional(),
|
||||
PixelWidth: type.number.or(type.bigint),
|
||||
PixelHeight: type.number.or(type.bigint),
|
||||
PixelCropBottom: type.number.or(type.bigint).default(0),
|
||||
PixelCropTop: type.number.or(type.bigint).default(0),
|
||||
PixelCropLeft: type.number.or(type.bigint).default(0),
|
||||
PixelCropRight: type.number.or(type.bigint).default(0),
|
||||
DisplayWidth: type.number.or(type.bigint).optional(),
|
||||
DisplayHeight: type.number.or(type.bigint).optional(),
|
||||
PixelWidth: type.number,
|
||||
PixelHeight: type.number,
|
||||
PixelCropBottom: type.number.default(0),
|
||||
PixelCropTop: type.number.default(0),
|
||||
PixelCropLeft: type.number.default(0),
|
||||
PixelCropRight: type.number.default(0),
|
||||
DisplayWidth: type.number.optional(),
|
||||
DisplayHeight: type.number.optional(),
|
||||
DisplayUnit: DisplayUnitRestriction.default(0),
|
||||
AspectRatioType: AspectRatioTypeRestriction.default(0),
|
||||
UncompressedFourCC: BinarySchema.optional(),
|
||||
@ -537,18 +518,16 @@ export enum EmphasisRestrictionEnum {
|
||||
PHONO_LONDON = 15,
|
||||
// Phono NARTB
|
||||
PHONO_NARTB = 16,
|
||||
}
|
||||
export const EmphasisRestriction = type(
|
||||
'0 | 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16'
|
||||
);
|
||||
};
|
||||
export const EmphasisRestriction = type('0 | 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16');
|
||||
export type EmphasisRestrictionType = typeof EmphasisRestriction.infer;
|
||||
|
||||
export const AudioSchema = type({
|
||||
SamplingFrequency: type.number.default(0),
|
||||
OutputSamplingFrequency: type.number.optional(),
|
||||
Channels: type.number.or(type.bigint).default(1),
|
||||
Channels: type.number.default(1),
|
||||
ChannelPositions: BinarySchema.optional(),
|
||||
BitDepth: type.number.or(type.bigint).optional(),
|
||||
BitDepth: type.number.optional(),
|
||||
Emphasis: EmphasisRestriction.default(0),
|
||||
});
|
||||
|
||||
@ -561,13 +540,12 @@ export enum TrackPlaneTypeRestrictionEnum {
|
||||
RIGHT_EYE = 1,
|
||||
// background
|
||||
BACKGROUND = 2,
|
||||
}
|
||||
};
|
||||
export const TrackPlaneTypeRestriction = type('0 | 1 | 2');
|
||||
export type TrackPlaneTypeRestrictionType =
|
||||
typeof TrackPlaneTypeRestriction.infer;
|
||||
export type TrackPlaneTypeRestrictionType = typeof TrackPlaneTypeRestriction.infer;
|
||||
|
||||
export const TrackPlaneSchema = type({
|
||||
TrackPlaneUID: type.number.or(type.bigint),
|
||||
TrackPlaneUID: type.number,
|
||||
TrackPlaneType: TrackPlaneTypeRestriction,
|
||||
});
|
||||
|
||||
@ -580,7 +558,7 @@ export const TrackCombinePlanesSchema = type({
|
||||
export type TrackCombinePlanesType = typeof TrackCombinePlanesSchema.infer;
|
||||
|
||||
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;
|
||||
@ -601,10 +579,9 @@ export enum ContentCompAlgoRestrictionEnum {
|
||||
LZO1X = 2,
|
||||
// Header Stripping
|
||||
HEADER_STRIPPING = 3,
|
||||
}
|
||||
};
|
||||
export const ContentCompAlgoRestriction = type('0 | 1 | 2 | 3');
|
||||
export type ContentCompAlgoRestrictionType =
|
||||
typeof ContentCompAlgoRestriction.infer;
|
||||
export type ContentCompAlgoRestrictionType = typeof ContentCompAlgoRestriction.infer;
|
||||
|
||||
export const ContentCompressionSchema = type({
|
||||
ContentCompAlgo: ContentCompAlgoRestriction.default(0),
|
||||
@ -618,17 +595,15 @@ export enum AESSettingsCipherModeRestrictionEnum {
|
||||
AES_CTR = 1,
|
||||
// AES-CBC
|
||||
AES_CBC = 2,
|
||||
}
|
||||
};
|
||||
export const AESSettingsCipherModeRestriction = type('1 | 2');
|
||||
export type AESSettingsCipherModeRestrictionType =
|
||||
typeof AESSettingsCipherModeRestriction.infer;
|
||||
export type AESSettingsCipherModeRestrictionType = typeof AESSettingsCipherModeRestriction.infer;
|
||||
|
||||
export const ContentEncAESSettingsSchema = type({
|
||||
AESSettingsCipherMode: AESSettingsCipherModeRestriction,
|
||||
});
|
||||
|
||||
export type ContentEncAESSettingsType =
|
||||
typeof ContentEncAESSettingsSchema.infer;
|
||||
export type ContentEncAESSettingsType = typeof ContentEncAESSettingsSchema.infer;
|
||||
|
||||
export enum ContentEncAlgoRestrictionEnum {
|
||||
// Not encrypted
|
||||
@ -643,20 +618,18 @@ export enum ContentEncAlgoRestrictionEnum {
|
||||
BLOWFISH = 4,
|
||||
// AES
|
||||
AES = 5,
|
||||
}
|
||||
};
|
||||
export const ContentEncAlgoRestriction = type('0 | 1 | 2 | 3 | 4 | 5');
|
||||
export type ContentEncAlgoRestrictionType =
|
||||
typeof ContentEncAlgoRestriction.infer;
|
||||
export type ContentEncAlgoRestrictionType = typeof ContentEncAlgoRestriction.infer;
|
||||
|
||||
export enum ContentSigAlgoRestrictionEnum {
|
||||
// Not signed
|
||||
NOT_SIGNED = 0,
|
||||
// RSA
|
||||
RSA = 1,
|
||||
}
|
||||
};
|
||||
export const ContentSigAlgoRestriction = type('0 | 1');
|
||||
export type ContentSigAlgoRestrictionType =
|
||||
typeof ContentSigAlgoRestriction.infer;
|
||||
export type ContentSigAlgoRestrictionType = typeof ContentSigAlgoRestriction.infer;
|
||||
|
||||
export enum ContentSigHashAlgoRestrictionEnum {
|
||||
// Not signed
|
||||
@ -665,10 +638,9 @@ export enum ContentSigHashAlgoRestrictionEnum {
|
||||
SHA1_160 = 1,
|
||||
// MD5
|
||||
MD5 = 2,
|
||||
}
|
||||
};
|
||||
export const ContentSigHashAlgoRestriction = type('0 | 1 | 2');
|
||||
export type ContentSigHashAlgoRestrictionType =
|
||||
typeof ContentSigHashAlgoRestriction.infer;
|
||||
export type ContentSigHashAlgoRestrictionType = typeof ContentSigHashAlgoRestriction.infer;
|
||||
|
||||
export const ContentEncryptionSchema = type({
|
||||
ContentEncAlgo: ContentEncAlgoRestriction.default(0),
|
||||
@ -689,23 +661,21 @@ export enum ContentEncodingScopeRestrictionEnum {
|
||||
PRIVATE = 2,
|
||||
// Next
|
||||
NEXT = 4,
|
||||
}
|
||||
};
|
||||
export const ContentEncodingScopeRestriction = type('1 | 2 | 4');
|
||||
export type ContentEncodingScopeRestrictionType =
|
||||
typeof ContentEncodingScopeRestriction.infer;
|
||||
export type ContentEncodingScopeRestrictionType = typeof ContentEncodingScopeRestriction.infer;
|
||||
|
||||
export enum ContentEncodingTypeRestrictionEnum {
|
||||
// Compression
|
||||
COMPRESSION = 0,
|
||||
// Encryption
|
||||
ENCRYPTION = 1,
|
||||
}
|
||||
};
|
||||
export const ContentEncodingTypeRestriction = type('0 | 1');
|
||||
export type ContentEncodingTypeRestrictionType =
|
||||
typeof ContentEncodingTypeRestriction.infer;
|
||||
export type ContentEncodingTypeRestrictionType = typeof ContentEncodingTypeRestriction.infer;
|
||||
|
||||
export const ContentEncodingSchema = type({
|
||||
ContentEncodingOrder: type.number.or(type.bigint).default(0),
|
||||
ContentEncodingOrder: type.number.default(0),
|
||||
ContentEncodingScope: ContentEncodingScopeRestriction.default(1),
|
||||
ContentEncodingType: ContentEncodingTypeRestriction.default(0),
|
||||
ContentCompression: ContentCompressionSchema.optional(),
|
||||
@ -737,53 +707,53 @@ export enum TrackTypeRestrictionEnum {
|
||||
CONTROL = 32,
|
||||
// metadata
|
||||
METADATA = 33,
|
||||
}
|
||||
};
|
||||
export const TrackTypeRestriction = type('1 | 2 | 3 | 16 | 17 | 18 | 32 | 33');
|
||||
export type TrackTypeRestrictionType = typeof TrackTypeRestriction.infer;
|
||||
|
||||
export const TrackEntrySchema = type({
|
||||
TrackNumber: type.number.or(type.bigint),
|
||||
TrackUID: type.number.or(type.bigint),
|
||||
TrackNumber: type.number,
|
||||
TrackUID: type.number,
|
||||
TrackType: TrackTypeRestriction,
|
||||
FlagEnabled: type.number.or(type.bigint).default(1),
|
||||
FlagDefault: type.number.or(type.bigint).default(1),
|
||||
FlagForced: type.number.or(type.bigint).default(0),
|
||||
FlagHearingImpaired: type.number.or(type.bigint).optional(),
|
||||
FlagVisualImpaired: type.number.or(type.bigint).optional(),
|
||||
FlagTextDescriptions: type.number.or(type.bigint).optional(),
|
||||
FlagOriginal: type.number.or(type.bigint).optional(),
|
||||
FlagCommentary: type.number.or(type.bigint).optional(),
|
||||
FlagLacing: type.number.or(type.bigint).default(1),
|
||||
MinCache: type.number.or(type.bigint).default(0),
|
||||
MaxCache: type.number.or(type.bigint).optional(),
|
||||
DefaultDuration: type.number.or(type.bigint).optional(),
|
||||
DefaultDecodedFieldDuration: type.number.or(type.bigint).optional(),
|
||||
FlagEnabled: type.number.default(1),
|
||||
FlagDefault: type.number.default(1),
|
||||
FlagForced: type.number.default(0),
|
||||
FlagHearingImpaired: type.number.optional(),
|
||||
FlagVisualImpaired: type.number.optional(),
|
||||
FlagTextDescriptions: type.number.optional(),
|
||||
FlagOriginal: type.number.optional(),
|
||||
FlagCommentary: type.number.optional(),
|
||||
FlagLacing: type.number.default(1),
|
||||
MinCache: type.number.default(0),
|
||||
MaxCache: type.number.optional(),
|
||||
DefaultDuration: type.number.optional(),
|
||||
DefaultDecodedFieldDuration: type.number.optional(),
|
||||
TrackTimestampScale: type.number.default(0),
|
||||
TrackOffset: type.number.or(type.bigint).default(0),
|
||||
MaxBlockAdditionID: type.number.or(type.bigint).default(0),
|
||||
TrackOffset: type.number.default(0),
|
||||
MaxBlockAdditionID: type.number.default(0),
|
||||
BlockAdditionMapping: BlockAdditionMappingSchema.array().optional(),
|
||||
Name: type.string.optional(),
|
||||
Language: type.string.default('eng'),
|
||||
Language: type.string.default("eng"),
|
||||
LanguageBCP47: type.string.optional(),
|
||||
CodecID: type.string,
|
||||
CodecPrivate: BinarySchema.optional(),
|
||||
CodecName: type.string.optional(),
|
||||
AttachmentLink: type.number.or(type.bigint).optional(),
|
||||
AttachmentLink: type.number.optional(),
|
||||
CodecSettings: type.string.optional(),
|
||||
CodecInfoURL: type.string.array().optional(),
|
||||
CodecDownloadURL: type.string.array().optional(),
|
||||
CodecDecodeAll: type.number.or(type.bigint).default(1),
|
||||
TrackOverlay: type.number.or(type.bigint).array().optional(),
|
||||
CodecDelay: type.number.or(type.bigint).default(0),
|
||||
SeekPreRoll: type.number.or(type.bigint).default(0),
|
||||
CodecDecodeAll: type.number.default(1),
|
||||
TrackOverlay: type.number.array().optional(),
|
||||
CodecDelay: type.number.default(0),
|
||||
SeekPreRoll: type.number.default(0),
|
||||
TrackTranslate: TrackTranslateSchema.array().optional(),
|
||||
Video: VideoSchema.optional(),
|
||||
Audio: AudioSchema.optional(),
|
||||
TrackOperation: TrackOperationSchema.optional(),
|
||||
TrickTrackUID: type.number.or(type.bigint).optional(),
|
||||
TrickTrackUID: type.number.optional(),
|
||||
TrickTrackSegmentUID: BinarySchema.optional(),
|
||||
TrickTrackFlag: type.number.or(type.bigint).default(0),
|
||||
TrickMasterTrackUID: type.number.or(type.bigint).optional(),
|
||||
TrickTrackFlag: type.number.default(0),
|
||||
TrickMasterTrackUID: type.number.optional(),
|
||||
TrickMasterTrackSegmentUID: BinarySchema.optional(),
|
||||
ContentEncodings: ContentEncodingsSchema.optional(),
|
||||
});
|
||||
@ -797,28 +767,28 @@ export const TracksSchema = type({
|
||||
export type TracksType = typeof TracksSchema.infer;
|
||||
|
||||
export const CueReferenceSchema = type({
|
||||
CueRefTime: type.number.or(type.bigint),
|
||||
CueRefCluster: type.number.or(type.bigint),
|
||||
CueRefNumber: type.number.or(type.bigint).default(1),
|
||||
CueRefCodecState: type.number.or(type.bigint).default(0),
|
||||
CueRefTime: type.number,
|
||||
CueRefCluster: type.number,
|
||||
CueRefNumber: type.number.default(1),
|
||||
CueRefCodecState: type.number.default(0),
|
||||
});
|
||||
|
||||
export type CueReferenceType = typeof CueReferenceSchema.infer;
|
||||
|
||||
export const CueTrackPositionsSchema = type({
|
||||
CueTrack: type.number.or(type.bigint),
|
||||
CueClusterPosition: type.number.or(type.bigint),
|
||||
CueRelativePosition: type.number.or(type.bigint).optional(),
|
||||
CueDuration: type.number.or(type.bigint).optional(),
|
||||
CueBlockNumber: type.number.or(type.bigint).optional(),
|
||||
CueCodecState: type.number.or(type.bigint).default(0),
|
||||
CueTrack: type.number,
|
||||
CueClusterPosition: type.number,
|
||||
CueRelativePosition: type.number.optional(),
|
||||
CueDuration: type.number.optional(),
|
||||
CueBlockNumber: type.number.optional(),
|
||||
CueCodecState: type.number.default(0),
|
||||
CueReference: CueReferenceSchema.array().optional(),
|
||||
});
|
||||
|
||||
export type CueTrackPositionsType = typeof CueTrackPositionsSchema.infer;
|
||||
|
||||
export const CuePointSchema = type({
|
||||
CueTime: type.number.or(type.bigint),
|
||||
CueTime: type.number,
|
||||
CueTrackPositions: CueTrackPositionsSchema.array().atLeastLength(1),
|
||||
});
|
||||
|
||||
@ -835,10 +805,10 @@ export const AttachedFileSchema = type({
|
||||
FileName: type.string,
|
||||
FileMediaType: type.string,
|
||||
FileData: BinarySchema,
|
||||
FileUID: type.number.or(type.bigint),
|
||||
FileUID: type.number,
|
||||
FileReferral: BinarySchema.optional(),
|
||||
FileUsedStartTime: type.number.or(type.bigint).optional(),
|
||||
FileUsedEndTime: type.number.or(type.bigint).optional(),
|
||||
FileUsedStartTime: type.number.optional(),
|
||||
FileUsedEndTime: type.number.optional(),
|
||||
});
|
||||
|
||||
export type AttachedFileType = typeof AttachedFileSchema.infer;
|
||||
@ -856,110 +826,13 @@ export const EditionDisplaySchema = type({
|
||||
|
||||
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({
|
||||
EditionUID: type.number.or(type.bigint).optional(),
|
||||
EditionFlagHidden: type.number.or(type.bigint).default(0),
|
||||
EditionFlagDefault: type.number.or(type.bigint).default(0),
|
||||
EditionFlagOrdered: type.number.or(type.bigint).default(0),
|
||||
EditionUID: type.number.optional(),
|
||||
EditionFlagHidden: type.number.default(0),
|
||||
EditionFlagDefault: type.number.default(0),
|
||||
EditionFlagOrdered: type.number.default(0),
|
||||
EditionDisplay: EditionDisplaySchema.array().optional(),
|
||||
ChapterAtom: ChapterAtomSchema.array().atLeastLength(1),
|
||||
|
||||
});
|
||||
|
||||
export type EditionEntryType = typeof EditionEntrySchema.infer;
|
||||
@ -971,24 +844,28 @@ export const ChaptersSchema = type({
|
||||
export type ChaptersType = typeof ChaptersSchema.infer;
|
||||
|
||||
export const TagTrackUIDSchema = match({
|
||||
'(number | bigint)[]': (v) => (v.length > 0 ? v : [0]),
|
||||
default: () => [0],
|
||||
}).optional();
|
||||
"number[]": v => v.length > 0 ? v : [0],
|
||||
"undefined": () => [0],
|
||||
default: "assert"
|
||||
});
|
||||
|
||||
export const TagEditionUIDSchema = match({
|
||||
'(number | bigint)[]': (v) => (v.length > 0 ? v : [0]),
|
||||
default: () => [0],
|
||||
}).optional();
|
||||
"number[]": v => v.length > 0 ? v : [0],
|
||||
"undefined": () => [0],
|
||||
default: "assert"
|
||||
});
|
||||
|
||||
export const TagChapterUIDSchema = match({
|
||||
'(number | bigint)[]': (v) => (v.length > 0 ? v : [0]),
|
||||
default: () => [0],
|
||||
}).optional();
|
||||
"number[]": v => v.length > 0 ? v : [0],
|
||||
"undefined": () => [0],
|
||||
default: "assert"
|
||||
});
|
||||
|
||||
export const TagAttachmentUIDSchema = match({
|
||||
'(number | bigint)[]': (v) => (v.length > 0 ? v : [0]),
|
||||
default: () => [0],
|
||||
}).optional();
|
||||
"number[]": v => v.length > 0 ? v : [0],
|
||||
"undefined": () => [0],
|
||||
default: "assert"
|
||||
});
|
||||
|
||||
export enum TargetTypeValueRestrictionEnum {
|
||||
// SHOT
|
||||
@ -1005,60 +882,55 @@ export enum TargetTypeValueRestrictionEnum {
|
||||
EDITION_ISSUE_VOLUME_OPUS_SEASON_SEQUEL = 60,
|
||||
// COLLECTION
|
||||
COLLECTION = 70,
|
||||
}
|
||||
export const TargetTypeValueRestriction = type(
|
||||
'10 | 20 | 30 | 40 | 50 | 60 | 70'
|
||||
);
|
||||
export type TargetTypeValueRestrictionType =
|
||||
typeof TargetTypeValueRestriction.infer;
|
||||
};
|
||||
export const TargetTypeValueRestriction = type('10 | 20 | 30 | 40 | 50 | 60 | 70');
|
||||
export type TargetTypeValueRestrictionType = typeof TargetTypeValueRestriction.infer;
|
||||
|
||||
export enum TargetTypeRestrictionEnum {
|
||||
// TargetTypeValue 70
|
||||
COLLECTION = 'COLLECTION',
|
||||
COLLECTION = "COLLECTION",
|
||||
// TargetTypeValue 60
|
||||
EDITION = 'EDITION',
|
||||
EDITION = "EDITION",
|
||||
// TargetTypeValue 60
|
||||
ISSUE = 'ISSUE',
|
||||
ISSUE = "ISSUE",
|
||||
// TargetTypeValue 60
|
||||
VOLUME = 'VOLUME',
|
||||
VOLUME = "VOLUME",
|
||||
// TargetTypeValue 60
|
||||
OPUS = 'OPUS',
|
||||
OPUS = "OPUS",
|
||||
// TargetTypeValue 60
|
||||
SEASON = 'SEASON',
|
||||
SEASON = "SEASON",
|
||||
// TargetTypeValue 60
|
||||
SEQUEL = 'SEQUEL',
|
||||
SEQUEL = "SEQUEL",
|
||||
// TargetTypeValue 50
|
||||
ALBUM = 'ALBUM',
|
||||
ALBUM = "ALBUM",
|
||||
// TargetTypeValue 50
|
||||
OPERA = 'OPERA',
|
||||
OPERA = "OPERA",
|
||||
// TargetTypeValue 50
|
||||
CONCERT = 'CONCERT',
|
||||
CONCERT = "CONCERT",
|
||||
// TargetTypeValue 50
|
||||
MOVIE = 'MOVIE',
|
||||
MOVIE = "MOVIE",
|
||||
// TargetTypeValue 50
|
||||
EPISODE = 'EPISODE',
|
||||
EPISODE = "EPISODE",
|
||||
// TargetTypeValue 40
|
||||
PART = 'PART',
|
||||
PART = "PART",
|
||||
// TargetTypeValue 40
|
||||
SESSION = 'SESSION',
|
||||
SESSION = "SESSION",
|
||||
// TargetTypeValue 30
|
||||
TRACK = 'TRACK',
|
||||
TRACK = "TRACK",
|
||||
// TargetTypeValue 30
|
||||
SONG = 'SONG',
|
||||
SONG = "SONG",
|
||||
// TargetTypeValue 30
|
||||
CHAPTER = 'CHAPTER',
|
||||
CHAPTER = "CHAPTER",
|
||||
// TargetTypeValue 20
|
||||
SUBTRACK = 'SUBTRACK',
|
||||
SUBTRACK = "SUBTRACK",
|
||||
// TargetTypeValue 20
|
||||
MOVEMENT = 'MOVEMENT',
|
||||
MOVEMENT = "MOVEMENT",
|
||||
// TargetTypeValue 20
|
||||
SCENE = 'SCENE',
|
||||
SCENE = "SCENE",
|
||||
// TargetTypeValue 10
|
||||
SHOT = 'SHOT',
|
||||
}
|
||||
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"'
|
||||
);
|
||||
SHOT = "SHOT",
|
||||
};
|
||||
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"');
|
||||
export type TargetTypeRestrictionType = typeof TargetTypeRestriction.infer;
|
||||
|
||||
export const TargetsSchema = type({
|
||||
@ -1072,21 +944,9 @@ export const TargetsSchema = type({
|
||||
|
||||
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({
|
||||
Targets: TargetsSchema,
|
||||
SimpleTag: SimpleTagSchema.array().atLeastLength(1),
|
||||
|
||||
});
|
||||
|
||||
export type TagType = typeof TagSchema.infer;
|
||||
@ -1138,23 +998,14 @@ export const IdMultiSet = new Set([
|
||||
EbmlTagIdEnum.CuePoint,
|
||||
EbmlTagIdEnum.AttachedFile,
|
||||
EbmlTagIdEnum.EditionLanguageIETF,
|
||||
EbmlTagIdEnum.ChapterTrackUID,
|
||||
EbmlTagIdEnum.ChapLanguage,
|
||||
EbmlTagIdEnum.ChapLanguageBCP47,
|
||||
EbmlTagIdEnum.ChapCountry,
|
||||
EbmlTagIdEnum.ChapProcessCommand,
|
||||
EbmlTagIdEnum.ChapterDisplay,
|
||||
EbmlTagIdEnum.ChapProcess,
|
||||
EbmlTagIdEnum.EditionDisplay,
|
||||
EbmlTagIdEnum.ChapterAtom,
|
||||
EbmlTagIdEnum.EditionEntry,
|
||||
EbmlTagIdEnum.TagTrackUID,
|
||||
EbmlTagIdEnum.TagEditionUID,
|
||||
EbmlTagIdEnum.TagChapterUID,
|
||||
EbmlTagIdEnum.TagAttachmentUID,
|
||||
EbmlTagIdEnum.SimpleTag,
|
||||
EbmlTagIdEnum.Tag,
|
||||
EbmlTagIdEnum.SeekHead,
|
||||
EbmlTagIdEnum.Cluster,
|
||||
EbmlTagIdEnum.Tags,
|
||||
]);
|
||||
EbmlTagIdEnum.Tags
|
||||
])
|
@ -51,12 +51,3 @@ export function convertEbmlTagToComponent(tag: EbmlTagType) {
|
||||
}
|
||||
return tag.data;
|
||||
}
|
||||
|
||||
export function waitTick() {
|
||||
return new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
resolve();
|
||||
timeout && clearTimeout(timeout);
|
||||
}, 0);
|
||||
});
|
||||
}
|
@ -1,358 +1,49 @@
|
||||
import { html, css, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import {
|
||||
animationFrames,
|
||||
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';
|
||||
import { type Subscription, switchMap, take } from 'rxjs';
|
||||
import { createEbmlController } from './media/mkv/reactive';
|
||||
|
||||
export class VideoPipelineDemo extends LitElement {
|
||||
static styles = css``;
|
||||
|
||||
@property()
|
||||
src!: string;
|
||||
|
||||
@property({ type: Number })
|
||||
width = 1280;
|
||||
subscripton?: Subscription;
|
||||
|
||||
@property({ type: Number })
|
||||
height = 720;
|
||||
static styles = css``;
|
||||
|
||||
destroyRef$ = new Subject<void>();
|
||||
|
||||
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) {
|
||||
async prepareVideoPipeline() {
|
||||
if (!this.src) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
segment: {
|
||||
seek,
|
||||
defaultVideoTrack$,
|
||||
defaultAudioTrack$,
|
||||
videoTrackDecoder,
|
||||
audioTrackDecoder,
|
||||
const { controller$ } = createEbmlController(this.src);
|
||||
|
||||
this.subscripton = controller$
|
||||
.pipe(
|
||||
switchMap(({ segments$ }) => segments$.pipe(take(1))),
|
||||
switchMap(({ seek }) => seek(0))
|
||||
)
|
||||
.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({
|
||||
name: 'microphone',
|
||||
});
|
||||
if (permissionStatus.state === 'granted') {
|
||||
await navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
});
|
||||
this.audioContext.resume();
|
||||
}
|
||||
|
||||
this.seeked$.next(0);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
await this.preparePipeline();
|
||||
this.prepareVideoPipeline();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.destroyRef$.next(undefined);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
this.subscripton?.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<video ref=${ref(this.videoRef)} width=${this.width} height=${this.height} autoplay muted></video>
|
||||
`;
|
||||
return html`<video />`;
|
||||
}
|
||||
}
|
||||
|
@ -2,25 +2,19 @@
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"target": "ES2020",
|
||||
"outDir": "./dist",
|
||||
"experimentalDecorators": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"useDefineForClassFields": false,
|
||||
"paths": {
|
||||
"@konoplayer/core/*": [
|
||||
"../../packages/core/src/*"
|
||||
],
|
||||
"@konoplayer/matroska/*": [
|
||||
"../../packages/matroska/src/*"
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/core"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/matroska"
|
||||
}
|
||||
]
|
||||
}
|
@ -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/*** 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
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@konoplayer/proxy",
|
||||
"name": "proxy",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -1 +0,0 @@
|
||||
{"version":"3.0.9","results":[[":src/matroska/codecs/av1.spec",{"duration":52.71331099999952,"failed":false}]]}
|
@ -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"
|
||||
}
|
||||
}
|
2
apps/test/resources/.gitignore
vendored
2
apps/test/resources/.gitignore
vendored
@ -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.
Binary file not shown.
@ -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');
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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',
|
||||
}),
|
||||
],
|
||||
});
|
37
biome.jsonc
37
biome.jsonc
@ -6,7 +6,6 @@
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useSingleCaseStatement": "off",
|
||||
"noParameterProperties": "off",
|
||||
"noNonNullAssertion": "off"
|
||||
},
|
||||
@ -17,13 +16,7 @@
|
||||
"noSvgWithoutTitle": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noBannedTypes": "off",
|
||||
"noExcessiveCognitiveComplexity": {
|
||||
"level": "warn",
|
||||
"options": {
|
||||
"maxAllowedComplexity": 40
|
||||
}
|
||||
}
|
||||
"noBannedTypes": "off"
|
||||
},
|
||||
"nursery": {
|
||||
"noEnum": "off",
|
||||
@ -58,34 +51,6 @@
|
||||
],
|
||||
"linter": {
|
||||
"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": {
|
||||
"noConsole": "off"
|
||||
},
|
||||
|
7
justfile
7
justfile
@ -2,10 +2,7 @@ set windows-shell := ["pwsh.exe", "-c"]
|
||||
set dotenv-load := true
|
||||
|
||||
dev-playground:
|
||||
pnpm run --filter=@konoplayer/playground dev
|
||||
pnpm run --filter=playground dev
|
||||
|
||||
dev-proxy:
|
||||
pnpm run --filter=@konoplayer/proxy --filter=@konoplayer/mock dev
|
||||
|
||||
download-samples:
|
||||
pnpm run download-samples
|
||||
pnpm run --filter proxy --filter mock dev
|
11
package.json
11
package.json
@ -3,8 +3,7 @@
|
||||
"version": "0.0.1",
|
||||
"description": "A strange player, like the dumtruck, taking you to Isekai.",
|
||||
"scripts": {
|
||||
"codegen-mkv": "tsx --tsconfig=./tsconfig.scripts.json ./scripts/codegen-mkv",
|
||||
"download-samples": "tsx --tsconfig=./tsconfig.scripts.json ./scripts/download-samples"
|
||||
"codegen-mkv": "tsx --tsconfig=./tsconfig.scripts.json ./scripts/codegen-mkv.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "lonelyhentxi",
|
||||
@ -16,19 +15,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/node": "^22.13.11",
|
||||
"@webgpu/types": "^0.1.59",
|
||||
"@types/node": "^22.13.8",
|
||||
"change-case": "^5.4.4",
|
||||
"happy-dom": "^17.4.4",
|
||||
"tsx": "^4.19.3",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.8.2",
|
||||
"ultracite": "^4.2.0"
|
||||
"ultracite": "^4.1.15"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"arktype": "^2.1.10",
|
||||
"lodash-es": "^4.17.21",
|
||||
"media-codecs": "^2.0.2",
|
||||
"mnemonist": "^0.40.3",
|
||||
"rxjs": "^7.8.2",
|
||||
"type-fest": "^4.37.0"
|
||||
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "@konoplayer/core",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {},
|
||||
"dependencies": {}
|
||||
}
|
@ -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$
|
||||
})));
|
||||
}
|
@ -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.
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export { AudioCodec } from './audio-codecs';
|
||||
export { VideoCodec } from './video-codecs';
|
@ -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;
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export {
|
||||
type RangedStream,
|
||||
type CreateRangedStreamOptions,
|
||||
createRangedStream,
|
||||
} from './fetch';
|
||||
export { BitReader } from './bit';
|
@ -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');
|
||||
}
|
||||
}
|
@ -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$
|
||||
})));
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "konoplayer-symphonia"
|
||||
name = "demuxing"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
symphonia = "0.5.4"
|
||||
symphonia-format-mkv = "0.5.4"
|
@ -1,7 +0,0 @@
|
||||
[package]
|
||||
name = "konoplayer-ffmpeg"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
ffmpeg-sys-next = { version = "7.1.0", features = ["avcodec"] }
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "@konoplayer/matroska",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"@konoplayer/core": "workspace:*",
|
||||
"konoebml": "^0.1.2"
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 Delay(optional)
|
||||
if (
|
||||
config.initialPresentationDelayPresent === 1 &&
|
||||
config.initialPresentationDelayMinus1 !== undefined
|
||||
) {
|
||||
const delay = (config.initialPresentationDelayMinus1 + 1)
|
||||
.toString()
|
||||
.padStart(2, '0');
|
||||
parts.push(delay);
|
||||
}
|
||||
|
||||
// joined
|
||||
return parts.join('.');
|
||||
}
|
@ -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}`;
|
||||
}
|
@ -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('.');
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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}`;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
);
|
||||
}
|
@ -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$
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
@ -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$
|
||||
};
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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";
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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_;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
1090
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -43,15 +43,15 @@ const EbmlTypeMetas = {
|
||||
},
|
||||
uinteger: {
|
||||
code: 'Uint',
|
||||
primitive: () => 'type.number.or(type.bigint)',
|
||||
primitive: () => 'type.number',
|
||||
default: (d: string): string => d,
|
||||
primitiveStr: () => '(number | bigint)',
|
||||
primitiveStr: () => 'number',
|
||||
},
|
||||
integer: {
|
||||
code: 'Int',
|
||||
primitive: () => 'type.number.or(type.bigint)',
|
||||
primitive: () => 'type.number',
|
||||
default: (d: string) => d,
|
||||
primitiveStr: () => '(number | bigint)',
|
||||
primitiveStr: () => 'number',
|
||||
},
|
||||
float: {
|
||||
code: 'Float',
|
||||
@ -160,7 +160,7 @@ function extractElement(element: 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 prefix = path.slice(0, -1);
|
||||
const level = path.length - 1;
|
||||
@ -342,15 +342,10 @@ function generateMkvSchemaHierarchy(elements_: EbmlElementType[]) {
|
||||
const idMulti = new Set<string>();
|
||||
const preDefs = [
|
||||
'export const BinarySchema = type.instanceOf(Uint8Array);',
|
||||
'export type BinaryType = typeof BinarySchema.infer;',
|
||||
...Object.entries(AdHocType).map(
|
||||
([name, meta]) =>
|
||||
`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 => {
|
||||
@ -368,6 +363,7 @@ function generateMkvSchemaHierarchy(elements_: EbmlElementType[]) {
|
||||
|
||||
const selfSchema = [
|
||||
`export const ${el.name}Schema = type({`,
|
||||
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
|
||||
...associated.map((v) => {
|
||||
let meta: any;
|
||||
const restriction = generateRestriction(v);
|
||||
@ -395,7 +391,7 @@ function generateMkvSchemaHierarchy(elements_: EbmlElementType[]) {
|
||||
if (v.maxOccurs !== 1) {
|
||||
expr = `${expr}.array()`;
|
||||
if (v.maxOccurs !== 1 && v.minOccurs === 1 && !v.default) {
|
||||
expr = `${expr}.atLeastLength(1)`;
|
||||
expr = `${expr}.atLeastLength(1)`
|
||||
}
|
||||
idMulti.add(v.name);
|
||||
}
|
||||
@ -405,8 +401,9 @@ function generateMkvSchemaHierarchy(elements_: EbmlElementType[]) {
|
||||
} else {
|
||||
childrenSchema.push(`export const ${v.name}Schema = match({
|
||||
"${meta.primitiveStr(v.name)}[]": v => v.length > 0 ? v : [${meta.default(v.default)}],
|
||||
default: () => [${meta.default(v.default)}],
|
||||
}).optional();`);
|
||||
"undefined": () => [${meta.default(v.default)}],
|
||||
default: "assert"
|
||||
});`);
|
||||
expr = `${v.name}Schema`;
|
||||
}
|
||||
} else if (!v.minOccurs) {
|
||||
@ -437,7 +434,7 @@ function main() {
|
||||
const elementSchemas = extractElementAll();
|
||||
|
||||
const files = {
|
||||
'schema': [
|
||||
'schema.ts': [
|
||||
generateMkvSchemaImports(elementSchemas),
|
||||
generateMkvSchemaHierarchy(elementSchemas),
|
||||
],
|
||||
|
@ -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);
|
||||
});
|
@ -14,22 +14,19 @@
|
||||
"DOM.AsyncIterable",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"types": [
|
||||
"@webgpu/types",
|
||||
"@types/node"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"allowImportingTsExtensions": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"skipLibCheck": true,
|
||||
"target": "ES2021",
|
||||
"strictNullChecks": true,
|
||||
"strict": true,
|
||||
"useDefineForClassFields": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"useDefineForClassFields": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"experimentalDecorators": true
|
||||
}
|
||||
}
|
@ -9,15 +9,6 @@
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.scripts.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/matroska"
|
||||
},
|
||||
{
|
||||
"path": "./packages/core"
|
||||
},
|
||||
{
|
||||
"path": "./apps/test"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user