Compare commits
19 Commits
9b310f0c62
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ea9dbb2f8 | |||
| 9654779f82 | |||
| ee4eee473d | |||
| 9896c4caec | |||
| 8cc1a2bab1 | |||
| 39e17eb6a5 | |||
| 3c317627e7 | |||
| 39a4cf2773 | |||
| 42e36e3c68 | |||
| 53f2bc8ca7 | |||
| 54edfd2fdc | |||
| 0b681d4fd1 | |||
| f921819d2a | |||
| d7b8b57e9c | |||
| 4537190096 | |||
| c0d4de4d28 | |||
| 6da8556f13 | |||
| 16c807b98e | |||
| 595e8d29dc |
195
.gitignore
vendored
Normal file
195
.gitignore
vendored
Normal file
@@ -0,0 +1,195 @@
|
||||
# Created by https://www.gitignore.io/api/vim,node,jetbrains+all,visualstudiocode
|
||||
|
||||
### JetBrains+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
### JetBrains+all Patch ###
|
||||
# Ignores the whole .idea folder and all .iml files
|
||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||
|
||||
.idea/
|
||||
|
||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
### Vim ###
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
*~
|
||||
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
|
||||
# End of https://www.gitignore.io/api/vim,node,jetbrains+all,visualstudiocode
|
||||
|
||||
# babel generated folder now; no need for it to be kept
|
||||
lib/
|
||||
/flow-typed/npm/
|
||||
# https://atom.io/packages/atomic-management
|
||||
.atom/*
|
||||
|
||||
|
||||
# Added by cargo
|
||||
|
||||
**/target
|
||||
/.vitest
|
||||
**/dist
|
||||
/lib
|
||||
**/*.tsbuildinfo
|
||||
**/temp
|
||||
9
.vscode/extensions.json
vendored
Normal file
9
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"runem.lit-plugin",
|
||||
"vitest.explorer",
|
||||
"biomejs.biome",
|
||||
"hbenl.vscode-test-explorer",
|
||||
"zerotaskx.rust-extension-pack"
|
||||
]
|
||||
}
|
||||
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
// allow autocomplete for ArkType expressions like "string | num"
|
||||
"editor.quickSuggestions": {
|
||||
"strings": "on"
|
||||
},
|
||||
// prioritize ArkType's "type" for autoimports
|
||||
"typescript.preferences.autoImportSpecifierExcludeRegexes": [
|
||||
"^(node:)?os$"
|
||||
],
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
504
Cargo.lock
generated
Normal file
504
Cargo.lock
generated
Normal file
@@ -0,0 +1,504 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# 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"
|
||||
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"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags 1.3.2",
|
||||
"bytemuck",
|
||||
"lazy_static",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-format-mkv"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
"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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-utils-xiph"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe"
|
||||
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"
|
||||
3
Cargo.toml
Normal file
3
Cargo.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[workspace]
|
||||
members = ["packages/symphonia", "packages/ffmpeg"]
|
||||
resolver = "3"
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Zhou Yeheng
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
39
README.md
39
README.md
@@ -1 +1,40 @@
|
||||
# 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)
|
||||
8
apps/mock/nest-cli.json
Normal file
8
apps/mock/nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
25
apps/mock/package.json
Normal file
25
apps/mock/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@konoplayer/mock",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "nest start --watch -b swc",
|
||||
"dev": "pnpm run start"
|
||||
},
|
||||
"keywords": [],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.11",
|
||||
"@nestjs/config": "^4.0.1",
|
||||
"@nestjs/core": "^11.0.11",
|
||||
"@nestjs/platform-express": "^11.0.11",
|
||||
"@nestjs/serve-static": "^5.0.3",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@swc/cli": "^0.6.0",
|
||||
"@swc/core": "^1.11.8"
|
||||
}
|
||||
}
|
||||
2
apps/mock/public/.gitignore
vendored
Normal file
2
apps/mock/public/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
video/huge/*
|
||||
!video/huge/.gitkeep
|
||||
0
apps/mock/public/video/huge/.gitkeep
Normal file
0
apps/mock/public/video/huge/.gitkeep
Normal file
BIN
apps/mock/public/video/test-av1.mkv
Normal file
BIN
apps/mock/public/video/test-av1.mkv
Normal file
Binary file not shown.
BIN
apps/mock/public/video/test-avc.mkv
Normal file
BIN
apps/mock/public/video/test-avc.mkv
Normal file
Binary file not shown.
BIN
apps/mock/public/video/test-hevc.mkv
Normal file
BIN
apps/mock/public/video/test-hevc.mkv
Normal file
Binary file not shown.
BIN
apps/mock/public/video/test-theora.mkv
Normal file
BIN
apps/mock/public/video/test-theora.mkv
Normal file
Binary file not shown.
BIN
apps/mock/public/video/test-vp8.mkv
Normal file
BIN
apps/mock/public/video/test-vp8.mkv
Normal file
Binary file not shown.
BIN
apps/mock/public/video/test-vp9.mkv
Normal file
BIN
apps/mock/public/video/test-vp9.mkv
Normal file
Binary file not shown.
BIN
apps/mock/public/video/test.webm
Normal file
BIN
apps/mock/public/video/test.webm
Normal file
Binary file not shown.
19
apps/mock/src/app.module.ts
Normal file
19
apps/mock/src/app.module.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import path from 'node:path';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: path.join(__dirname, '..', 'public'),
|
||||
serveRoot: '/api/static',
|
||||
serveStaticOptions: {
|
||||
cacheControl: true,
|
||||
maxAge: '1d',
|
||||
},
|
||||
})
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
})
|
||||
export class AppModule { }
|
||||
8
apps/mock/src/main.ts
Normal file
8
apps/mock/src/main.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
await app.listen(process.env.port ?? 5001);
|
||||
}
|
||||
bootstrap();
|
||||
26
apps/mock/tsconfig.json
Normal file
26
apps/mock/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"emitDeclarationOnly": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"baseUrl": ".",
|
||||
"lib": [
|
||||
"ES2024"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"test",
|
||||
"**/*spec"
|
||||
]
|
||||
}
|
||||
13
apps/playground/.gitignore
vendored
Normal file
13
apps/playground/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Local
|
||||
.DS_Store
|
||||
*.local
|
||||
*.log*
|
||||
|
||||
# Dist
|
||||
node_modules
|
||||
dist/
|
||||
|
||||
# IDE
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
29
apps/playground/README.md
Normal file
29
apps/playground/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Rsbuild project
|
||||
|
||||
## Setup
|
||||
|
||||
Install the dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Get started
|
||||
|
||||
Start the dev server:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Build the app for production:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Preview the production build locally:
|
||||
|
||||
```bash
|
||||
pnpm preview
|
||||
```
|
||||
19
apps/playground/package.json
Normal file
19
apps/playground/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@konoplayer/playground",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "rsbuild build",
|
||||
"dev": "rsbuild dev",
|
||||
"preview": "rsbuild preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"lit": "^3.2.1",
|
||||
"@konoplayer/core": "workspace:*",
|
||||
"@konoplayer/matroska": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rsbuild/core": "^1.2.14"
|
||||
}
|
||||
}
|
||||
0
apps/playground/public/.gitkeep
Normal file
0
apps/playground/public/.gitkeep
Normal file
18
apps/playground/rsbuild.config.ts
Normal file
18
apps/playground/rsbuild.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from '@rsbuild/core';
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
html: {
|
||||
title: 'Konoplayer Playground',
|
||||
template: './src/index.html',
|
||||
},
|
||||
source: {
|
||||
decorators: {
|
||||
version: 'legacy',
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5000,
|
||||
},
|
||||
});
|
||||
1
apps/playground/src/env.d.ts
vendored
Normal file
1
apps/playground/src/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="@rsbuild/core/types" />
|
||||
4
apps/playground/src/index.css
Normal file
4
apps/playground/src/index.css
Normal file
@@ -0,0 +1,4 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
10
apps/playground/src/index.html
Normal file
10
apps/playground/src/index.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!doctype html>
|
||||
|
||||
<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" /> -->
|
||||
</body>
|
||||
4
apps/playground/src/index.ts
Normal file
4
apps/playground/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import './index.css';
|
||||
import { VideoPipelineDemo } from './video-pipeline-demo';
|
||||
|
||||
customElements.define('video-pipeline-demo', VideoPipelineDemo);
|
||||
359
apps/playground/src/video-pipeline-demo.ts
Normal file
359
apps/playground/src/video-pipeline-demo.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
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';
|
||||
|
||||
export class VideoPipelineDemo extends LitElement {
|
||||
static styles = css``;
|
||||
|
||||
@property()
|
||||
src!: string;
|
||||
|
||||
@property({ type: Number })
|
||||
width = 1280;
|
||||
|
||||
@property({ type: Number })
|
||||
height = 720;
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
segment: {
|
||||
seek,
|
||||
defaultVideoTrack$,
|
||||
defaultAudioTrack$,
|
||||
videoTrackDecoder,
|
||||
audioTrackDecoder,
|
||||
},
|
||||
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, {
|
||||
format: 'f32-planar',
|
||||
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() {
|
||||
super.connectedCallback();
|
||||
await this.preparePipeline();
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<video ref=${ref(this.videoRef)} width=${this.width} height=${this.height} autoplay muted></video>
|
||||
`;
|
||||
}
|
||||
}
|
||||
26
apps/playground/tsconfig.json
Normal file
26
apps/playground/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"paths": {
|
||||
"@konoplayer/core/*": [
|
||||
"../../packages/core/src/*"
|
||||
],
|
||||
"@konoplayer/matroska/*": [
|
||||
"../../packages/matroska/src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/core"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/matroska"
|
||||
}
|
||||
]
|
||||
}
|
||||
5
apps/proxy/.whistle/.gitignore
vendored
Normal file
5
apps/proxy/.whistle/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
**/.backup
|
||||
**/.recycle_bin
|
||||
**/.clientid
|
||||
/properties/*
|
||||
!/properties/properties
|
||||
1
apps/proxy/.whistle/properties/properties
Normal file
1
apps/proxy/.whistle/properties/properties
Normal file
@@ -0,0 +1 @@
|
||||
{"filesOrder":["latestVersion"],"Custom1":"Custom1","Custom2":"Custom2"}
|
||||
11
apps/proxy/.whistle/rules/files/0.konoplayer
Normal file
11
apps/proxy/.whistle/rules/files/0.konoplayer
Normal file
@@ -0,0 +1,11 @@
|
||||
```x-forwarded.json
|
||||
{
|
||||
"X-Forwarded-Host": "konoplayer.com",
|
||||
"X-Forwarded-Proto": "https"
|
||||
}
|
||||
```
|
||||
|
||||
^https://konoplayer.com/api/static/*** resSpeed://10240
|
||||
^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
|
||||
^wss://konoplayer.com/*** reqHeaders://{x-forwarded.json} ws://127.0.0.1:5000/$1 excludeFilter://^wss://konoplayer.com/api
|
||||
1
apps/proxy/.whistle/rules/properties
Normal file
1
apps/proxy/.whistle/rules/properties
Normal file
@@ -0,0 +1 @@
|
||||
{"filesOrder":["konoplayer"],"selectedList":["konoplayer"],"disabledDefalutRules":true}
|
||||
0
apps/proxy/.whistle/values/properties
Normal file
0
apps/proxy/.whistle/values/properties
Normal file
15
apps/proxy/package.json
Normal file
15
apps/proxy/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@konoplayer/proxy",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "cross-env WHISTLE_MODE=\"prod|capture|keepXFF|x-forwarded-host|x-forwarded-proto\" whistle run -p 8899 -t 30000 -D .",
|
||||
"dev": "pnpm run start"
|
||||
},
|
||||
"keywords": [],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"whistle": "^2.9.93"
|
||||
}
|
||||
}
|
||||
1
apps/test/.vitest/results.json
Normal file
1
apps/test/.vitest/results.json
Normal file
@@ -0,0 +1 @@
|
||||
{"version":"3.0.9","results":[[":src/matroska/codecs/av1.spec",{"duration":52.71331099999952,"failed":false}]]}
|
||||
17
apps/test/package.json
Normal file
17
apps/test/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"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
Normal file
2
apps/test/resources/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
video/huge/*
|
||||
!video/huge/.gitkeep
|
||||
BIN
apps/test/resources/video/test-av1.mkv
Normal file
BIN
apps/test/resources/video/test-av1.mkv
Normal file
Binary file not shown.
BIN
apps/test/resources/video/test-avc.mkv
Normal file
BIN
apps/test/resources/video/test-avc.mkv
Normal file
Binary file not shown.
BIN
apps/test/resources/video/test-hevc.mkv
Normal file
BIN
apps/test/resources/video/test-hevc.mkv
Normal file
Binary file not shown.
BIN
apps/test/resources/video/test-theora.mkv
Normal file
BIN
apps/test/resources/video/test-theora.mkv
Normal file
Binary file not shown.
BIN
apps/test/resources/video/test-vp8.mkv
Normal file
BIN
apps/test/resources/video/test-vp8.mkv
Normal file
Binary file not shown.
BIN
apps/test/resources/video/test-vp9.mkv
Normal file
BIN
apps/test/resources/video/test-vp9.mkv
Normal file
Binary file not shown.
BIN
apps/test/resources/video/test.webm
Normal file
BIN
apps/test/resources/video/test.webm
Normal file
Binary file not shown.
0
apps/test/src/init-test.ts
Normal file
0
apps/test/src/init-test.ts
Normal file
47
apps/test/src/matroska/codecs/av1.spec.ts
Normal file
47
apps/test/src/matroska/codecs/av1.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
40
apps/test/src/matroska/codecs/avc.spec.ts
Normal file
40
apps/test/src/matroska/codecs/avc.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
106
apps/test/src/matroska/codecs/hevc.spec.ts
Normal file
106
apps/test/src/matroska/codecs/hevc.spec.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
54
apps/test/src/matroska/codecs/vp9.spec.ts
Normal file
54
apps/test/src/matroska/codecs/vp9.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
56
apps/test/src/matroska/utils/data.ts
Normal file
56
apps/test/src/matroska/utils/data.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
}
|
||||
30
apps/test/tsconfig.json
Normal file
30
apps/test/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
apps/test/vitest.config.ts
Normal file
33
apps/test/vitest.config.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
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',
|
||||
}),
|
||||
],
|
||||
});
|
||||
77
assets/specification/ebml.xml
Normal file
77
assets/specification/ebml.xml
Normal file
@@ -0,0 +1,77 @@
|
||||
<EBMLSchema xmlns="urn:ietf:rfc:8794" docType="ebml" version="1">
|
||||
<element name="EBML" path="\EBML" id="0x1A45DFA3" type="master" minOccurs="1" maxOccurs="1">
|
||||
<documentation lang="en" purpose="definition">Set the EBML characteristics of the data to
|
||||
follow. Each EBML document has to start with this.</documentation>
|
||||
</element>
|
||||
<element name="EBMLVersion" path="\EBML\EBMLVersion" id="0x4286" type="uinteger" range="not 0"
|
||||
default="1" minOccurs="1" maxOccurs="1">
|
||||
<documentation lang="en" purpose="definition">The version of EBML parser used to create the
|
||||
file.</documentation>
|
||||
</element>
|
||||
<element name="EBMLReadVersion" path="\EBML\EBMLReadVersion" id="0x42F7" type="uinteger"
|
||||
range="1" default="1" minOccurs="1" maxOccurs="1">
|
||||
<documentation lang="en" purpose="definition">The minimum EBML version a parser has to
|
||||
support to read this file.</documentation>
|
||||
</element>
|
||||
<element name="EBMLMaxIDLength" path="\EBML\EBMLMaxIDLength" id="0x42F2" type="uinteger"
|
||||
range=">=4" default="4" minOccurs="1" maxOccurs="1">
|
||||
<documentation lang="en" purpose="definition">The maximum length of the IDs you'll find in
|
||||
this file (4 or less in Matroska).</documentation>
|
||||
</element>
|
||||
<element name="EBMLMaxSizeLength" path="\EBML\EBMLMaxSizeLength" id="0x42F3" type="uinteger"
|
||||
range="not 0" default="8" minOccurs="1" maxOccurs="1">
|
||||
<documentation lang="en" purpose="definition">The maximum length of the sizes you'll find in
|
||||
this file (8 or less in Matroska). This does not override the element size indicated at
|
||||
the beginning of an element. Elements that have an indicated size which is larger than
|
||||
what is allowed by EBMLMaxSizeLength shall be considered invalid.</documentation>
|
||||
</element>
|
||||
<element name="DocType" path="\EBML\DocType" id="0x4282" type="string" length=">0"
|
||||
minOccurs="1" maxOccurs="1">
|
||||
<documentation lang="en" purpose="definition">A string that describes the type of document
|
||||
that follows this EBML header, for example 'matroska' or 'webm'.</documentation>
|
||||
</element>
|
||||
<element name="DocTypeVersion" path="\EBML\DocTypeVersion" id="0x4287" type="uinteger"
|
||||
range="not 0" default="1" minOccurs="1" maxOccurs="1">
|
||||
<documentation lang="en" purpose="definition">The version of DocType interpreter used to
|
||||
create the file.</documentation>
|
||||
</element>
|
||||
<element name="DocTypeReadVersion" path="\EBML\DocTypeReadVersion" id="0x4285" type="uinteger"
|
||||
range="not 0" default="1" minOccurs="1" maxOccurs="1">
|
||||
<documentation lang="en" purpose="definition">The minimum DocType version an interpreter has
|
||||
to support to read this file.</documentation>
|
||||
</element>
|
||||
<element name="DocTypeExtension" path="\EBML\DocTypeExtension" id="0x4281" type="master">
|
||||
<documentation lang="en" purpose="definition">A DocTypeExtension adds extra Elements to the
|
||||
main DocType+DocTypeVersion tuple it's attached to. An EBML Reader **MAY** know these
|
||||
extra Elements and how to use them. A DocTypeExtension **MAY** be used to iterate
|
||||
between experimental Elements before they are integrated into a regular DocTypeVersion.
|
||||
Reading one DocTypeExtension version of a DocType+DocTypeVersion tuple doesn't imply one
|
||||
should be able to read upper versions of this DocTypeExtension.</documentation>
|
||||
</element>
|
||||
<element name="DocTypeExtensionName" path="\EBML\DocTypeExtension\DocTypeExtensionName"
|
||||
id="0x4283" type="string" length=">0" minOccurs="1" maxOccurs="1">
|
||||
<documentation lang="en" purpose="definition">The name of the DocTypeExtension to
|
||||
differentiate it from other DocTypeExtensions of the same DocType+DocTypeVersion tuple.
|
||||
A DocTypeExtensionName value **MUST** be unique within the EBML Header.</documentation>
|
||||
</element>
|
||||
<element name="DocTypeExtensionVersion" path="\EBML\DocTypeExtension\DocTypeExtensionVersion"
|
||||
id="0x4284" type="uinteger" range="not 0" minOccurs="1" maxOccurs="1">
|
||||
<documentation lang="en" purpose="definition">The version of the DocTypeExtension. Different
|
||||
DocTypeExtensionVersion values of the same DocType + DocTypeVersion +
|
||||
DocTypeExtensionName tuple **MAY** contain completely different sets of extra Elements.
|
||||
An EBML Reader **MAY** support multiple versions of the same tuple, only one version of
|
||||
the tuple, or not support the tuple at all.</documentation>
|
||||
</element>
|
||||
|
||||
<element name="Void" path="\(-\)Void" id="0xEC" type="binary">
|
||||
<documentation lang="en" purpose="definition">Used to void damaged data, to avoid unexpected
|
||||
behaviors when using damaged data. The content is discarded. Also used to reserve space
|
||||
in a sub-element for later use.</documentation>
|
||||
</element>
|
||||
<element name="CRC-32" path="\(1-\)CRC-32" id="0xBF" type="binary" length="4" maxOccurs="1">
|
||||
<documentation lang="en" purpose="definition">The CRC is computed on all the data of the
|
||||
Master element it's in. The CRC element should be the first in it's parent master for
|
||||
easier reading. All level 1 elements should include a CRC-32. The CRC in use is the IEEE
|
||||
CRC32 Little Endian.</documentation>
|
||||
</element>
|
||||
</EBMLSchema>
|
||||
2477
assets/specification/ebml_mkv.xml
Normal file
2477
assets/specification/ebml_mkv.xml
Normal file
File diff suppressed because it is too large
Load Diff
671
assets/specification/ebml_mkv_legacy.xml
Normal file
671
assets/specification/ebml_mkv_legacy.xml
Normal file
@@ -0,0 +1,671 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<table>
|
||||
<element name="EBML" level="0" id="0x1A45DFA3" type="master" mandatory="1" multiple="1"
|
||||
minver="1">Set the EBML characteristics of the data to follow. Each EBML document has to
|
||||
start with this.</element>
|
||||
<element name="EBMLVersion" level="1" id="0x4286" type="uinteger" mandatory="1" default="1"
|
||||
minver="1">The version of EBML parser used to create the file.</element>
|
||||
<element name="EBMLReadVersion" level="1" id="0x42F7" type="uinteger" mandatory="1" default="1"
|
||||
minver="1">The minimum EBML version a parser has to support to read this file.</element>
|
||||
<element name="EBMLMaxIDLength" level="1" id="0x42F2" type="uinteger" mandatory="1" default="4"
|
||||
minver="1">The maximum length of the IDs you'll find in this file (4 or less in Matroska).</element>
|
||||
<element name="EBMLMaxSizeLength" level="1" id="0x42F3" type="uinteger" mandatory="1"
|
||||
default="8" minver="1">The maximum length of the sizes you'll find in this file (8 or less
|
||||
in Matroska). This does not override the element size indicated at the beginning of an
|
||||
element. Elements that have an indicated size which is larger than what is allowed by
|
||||
EBMLMaxSizeLength shall be considered invalid.</element>
|
||||
<element name="DocType" level="1" id="0x4282" type="string" mandatory="1" default="matroska"
|
||||
minver="1">A string that describes the type of document that follows this EBML header.
|
||||
'matroska' in our case or 'webm' for webm files.</element>
|
||||
<element name="DocTypeVersion" level="1" id="0x4287" type="uinteger" mandatory="1" default="1"
|
||||
minver="1">The version of DocType interpreter used to create the file.</element>
|
||||
<element name="DocTypeReadVersion" level="1" id="0x4285" type="uinteger" mandatory="1"
|
||||
default="1" minver="1">The minimum DocType version an interpreter has to support to read
|
||||
this file.</element>
|
||||
<element name="Void" level="-1" id="0xEC" type="binary" minver="1">Used to void damaged data, to
|
||||
avoid unexpected behaviors when using damaged data. The content is discarded. Also used to
|
||||
reserve space in a sub-element for later use.</element>
|
||||
<element name="CRC-32" level="-1" id="0xBF" type="binary" minver="1" webm="0">The CRC is
|
||||
computed on all the data of the Master element it's in. The CRC element should be the first
|
||||
in it's parent master for easier reading. All level 1 elements should include a CRC-32. The
|
||||
CRC in use is the IEEE CRC32 Little Endian</element>
|
||||
<element name="SignatureSlot" level="-1" id="0x1B538667" type="master" multiple="1" webm="0">Contain
|
||||
signature of some (coming) elements in the stream.</element>
|
||||
<element name="SignatureAlgo" level="1" id="0x7E8A" type="uinteger" webm="0">Signature algorithm
|
||||
used (1=RSA, 2=elliptic).</element>
|
||||
<element name="SignatureHash" level="1" id="0x7E9A" type="uinteger" webm="0">Hash algorithm used
|
||||
(1=SHA1-160, 2=MD5).</element>
|
||||
<element name="SignaturePublicKey" level="1" id="0x7EA5" type="binary" webm="0">The public key
|
||||
to use with the algorithm (in the case of a PKI-based signature).</element>
|
||||
<element name="Signature" level="1" id="0x7EB5" type="binary" webm="0">The signature of the data
|
||||
(until a new.</element>
|
||||
<element name="SignatureElements" level="1" id="0x7E5B" type="master" webm="0">Contains elements
|
||||
that will be used to compute the signature.</element>
|
||||
<element name="SignatureElementList" level="2" id="0x7E7B" type="master" multiple="1" webm="0">A
|
||||
list consists of a number of consecutive elements that represent one case where data is used
|
||||
in signature. Ex: <i>Cluster|Block|BlockAdditional</i> means that the BlockAdditional of all
|
||||
Blocks in all Clusters is used for encryption.</element>
|
||||
<element name="SignedElement" level="3" id="0x6532" type="binary" multiple="1" webm="0">An
|
||||
element ID whose data will be used to compute the signature.</element>
|
||||
<element name="Segment" level="0" id="0x18538067" type="master" mandatory="1" multiple="1"
|
||||
minver="1">This element contains all other top-level (level 1) elements. Typically a
|
||||
Matroska file is composed of 1 segment.</element>
|
||||
<element name="SeekHead" cppname="SeekHeader" level="1" id="0x114D9B74" type="master"
|
||||
multiple="1" minver="1">Contains the <a
|
||||
href="http://www.matroska.org/technical/specs/notes.html#Position_References">position</a>
|
||||
of other level 1 elements.</element>
|
||||
<element name="Seek" cppname="SeekPoint" level="2" id="0x4DBB" type="master" mandatory="1"
|
||||
multiple="1" minver="1">Contains a single seek entry to an EBML element.</element>
|
||||
<element name="SeekID" level="3" id="0x53AB" type="binary" mandatory="1" minver="1">The binary
|
||||
ID corresponding to the element name.</element>
|
||||
<element name="SeekPosition" level="3" id="0x53AC" type="uinteger" mandatory="1" minver="1">The <a
|
||||
href="http://www.matroska.org/technical/specs/notes.html#Position_References">position</a>
|
||||
of the element in the segment in octets (0 = first level 1 element).</element>
|
||||
<element name="Info" level="1" id="0x1549A966" type="master" mandatory="1" multiple="1"
|
||||
minver="1">Contains miscellaneous general information and statistics on the file.</element>
|
||||
<element name="SegmentUID" level="2" id="0x73A4" type="binary" minver="1" webm="0" range="not 0"
|
||||
bytesize="16">A randomly generated unique ID to identify the current segment between many
|
||||
others (128 bits).</element>
|
||||
<element name="SegmentFilename" level="2" id="0x7384" type="utf-8" minver="1" webm="0">A
|
||||
filename corresponding to this segment.</element>
|
||||
<element name="PrevUID" level="2" id="0x3CB923" type="binary" minver="1" webm="0" bytesize="16">A
|
||||
unique ID to identify the previous chained segment (128 bits).</element>
|
||||
<element name="PrevFilename" level="2" id="0x3C83AB" type="utf-8" minver="1" webm="0">An escaped
|
||||
filename corresponding to the previous segment.</element>
|
||||
<element name="NextUID" level="2" id="0x3EB923" type="binary" minver="1" webm="0" bytesize="16">A
|
||||
unique ID to identify the next chained segment (128 bits).</element>
|
||||
<element name="NextFilename" level="2" id="0x3E83BB" type="utf-8" minver="1" webm="0">An escaped
|
||||
filename corresponding to the next segment.</element>
|
||||
<element name="SegmentFamily" level="2" id="0x4444" type="binary" multiple="1" minver="1"
|
||||
webm="0" bytesize="16">A randomly generated unique ID that all segments related to each
|
||||
other must use (128 bits).</element>
|
||||
<element name="ChapterTranslate" level="2" id="0x6924" type="master" multiple="1" minver="1"
|
||||
webm="0">A tuple of corresponding ID used by chapter codecs to represent this segment.</element>
|
||||
<element name="ChapterTranslateEditionUID" level="3" id="0x69FC" type="uinteger" multiple="1"
|
||||
minver="1" webm="0">Specify an edition UID on which this correspondance applies. When not
|
||||
specified, it means for all editions found in the segment.</element>
|
||||
<element name="ChapterTranslateCodec" level="3" id="0x69BF" type="uinteger" mandatory="1"
|
||||
minver="1" webm="0">The <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#ChapProcessCodecID">chapter
|
||||
codec</a> using this ID (0: Matroska Script, 1: DVD-menu).</element>
|
||||
<element name="ChapterTranslateID" level="3" id="0x69A5" type="binary" mandatory="1" minver="1"
|
||||
webm="0">The binary value used to represent this segment in the chapter codec data. The
|
||||
format depends on the <a
|
||||
href="http://www.matroska.org/technical/specs/chapters/index.html#ChapProcessCodecID">
|
||||
ChapProcessCodecID</a> used.</element>
|
||||
<element name="TimecodeScale" level="2" id="0x2AD7B1" type="uinteger" mandatory="1" minver="1"
|
||||
default="1000000">Timecode scale in nanoseconds (1.000.000 means all timecodes in the
|
||||
segment are expressed in milliseconds).</element>
|
||||
<!-- <element name="TimecodeScaleDenominator" level="2" id="0x2AD7B2" type="uinteger"
|
||||
mandatory="1" minver="4" default="1000000000">Timecode scale numerator, see <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#TimecodeScale">TimecodeScale</a>.</element>
|
||||
TimecodeScale When combined with <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#TimecodeScaleDenominator">TimecodeScaleDenominator</a>
|
||||
the Timecode scale is given by the fraction TimecodeScale/TimecodeScaleDenominator in
|
||||
seconds.-->
|
||||
<element name="Duration" level="2" id="0x4489" type="float" minver="1" range="> 0">Duration
|
||||
of the segment (based on TimecodeScale).</element>
|
||||
<element name="DateUTC" level="2" id="0x4461" type="date" minver="1">Date of the origin of
|
||||
timecode (value 0), i.e. production date.</element>
|
||||
<element name="Title" level="2" id="0x7BA9" type="utf-8" minver="1" webm="0">General name of the
|
||||
segment.</element>
|
||||
<element name="MuxingApp" level="2" id="0x4D80" type="utf-8" mandatory="1" minver="1">Muxing
|
||||
application or library ("libmatroska-0.4.3").</element>
|
||||
<element name="WritingApp" level="2" id="0x5741" type="utf-8" mandatory="1" minver="1">Writing
|
||||
application ("mkvmerge-0.3.3").</element>
|
||||
<element name="Cluster" level="1" id="0x1F43B675" type="master" multiple="1" minver="1">The
|
||||
lower level element containing the (monolithic) Block structure.</element>
|
||||
<element name="Timecode" cppname="ClusterTimecode" level="2" id="0xE7" type="uinteger"
|
||||
mandatory="1" minver="1">Absolute timecode of the cluster (based on TimecodeScale).</element>
|
||||
<element name="SilentTracks" cppname="ClusterSilentTracks" level="2" id="0x5854" type="master"
|
||||
minver="1" webm="0">The list of tracks that are not used in that part of the stream. It is
|
||||
useful when using overlay tracks on seeking. Then you should decide what track to use.</element>
|
||||
<element name="SilentTrackNumber" cppname="ClusterSilentTrackNumber" level="3" id="0x58D7"
|
||||
type="uinteger" multiple="1" minver="1" webm="0">One of the track number that are not used
|
||||
from now on in the stream. It could change later if not specified as silent in a further
|
||||
Cluster.</element>
|
||||
<element name="Position" cppname="ClusterPosition" level="2" id="0xA7" type="uinteger"
|
||||
minver="1" webm="0">The <a
|
||||
href="http://www.matroska.org/technical/specs/notes.html#Position_References">Position</a>
|
||||
of the Cluster in the segment (0 in live broadcast streams). It might help to resynchronise
|
||||
offset on damaged streams.</element>
|
||||
<element name="PrevSize" cppname="ClusterPrevSize" level="2" id="0xAB" type="uinteger"
|
||||
minver="1">Size of the previous Cluster, in octets. Can be useful for backward playing.</element>
|
||||
<element name="SimpleBlock" level="2" id="0xA3" type="binary" multiple="1" minver="2" webm="1"
|
||||
divx="1">Similar to <a href="http://www.matroska.org/technical/specs/index.html#Block">Block</a>
|
||||
but without all the extra information, mostly used to reduced overhead when no extra feature
|
||||
is needed. (see <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#simpleblock_structure">SimpleBlock
|
||||
Structure</a>)</element>
|
||||
<element name="BlockGroup" level="2" id="0xA0" type="master" multiple="1" minver="1">Basic
|
||||
container of information containing a single Block or BlockVirtual, and information specific
|
||||
to that Block/VirtualBlock.</element>
|
||||
<element name="Block" level="3" id="0xA1" type="binary" mandatory="1" minver="1">Block
|
||||
containing the actual data to be rendered and a timecode relative to the Cluster Timecode.
|
||||
(see <a href="http://www.matroska.org/technical/specs/index.html#block_structure">Block
|
||||
Structure</a>)</element>
|
||||
<element name="BlockVirtual" level="3" id="0xA2" type="binary" webm="0">A Block with no data. It
|
||||
must be stored in the stream at the place the real Block should be in display order. (see <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#block_virtual">Block Virtual</a>
|
||||
)</element>
|
||||
<element name="BlockAdditions" level="3" id="0x75A1" type="master" minver="1" webm="0">Contain
|
||||
additional blocks to complete the main one. An EBML parser that has no knowledge of the
|
||||
Block structure could still see and use/skip these data.</element>
|
||||
<element name="BlockMore" level="4" id="0xA6" type="master" mandatory="1" multiple="1"
|
||||
minver="1" webm="0">Contain the BlockAdditional and some parameters.</element>
|
||||
<element name="BlockAddID" level="5" id="0xEE" type="uinteger" mandatory="1" minver="1" webm="0"
|
||||
default="1" range="not 0">An ID to identify the BlockAdditional level.</element>
|
||||
<element name="BlockAdditional" level="5" id="0xA5" type="binary" mandatory="1" minver="1"
|
||||
webm="0">Interpreted by the codec as it wishes (using the BlockAddID).</element>
|
||||
<element name="BlockDuration" level="3" id="0x9B" type="uinteger" minver="1"
|
||||
default="TrackDuration">The duration of the Block (based on TimecodeScale). This element is
|
||||
mandatory when DefaultDuration is set for the track (but can be omitted as other default
|
||||
values). When not written and with no DefaultDuration, the value is assumed to be the
|
||||
difference between the timecode of this Block and the timecode of the next Block in
|
||||
"display" order (not coding order). This element can be useful at the end of a Track (as
|
||||
there is not other Block available), or when there is a break in a track like for subtitle
|
||||
tracks. When set to 0 that means the frame is not a keyframe.</element>
|
||||
<element name="ReferencePriority" cppname="FlagReferenced" level="3" id="0xFA" type="uinteger"
|
||||
mandatory="1" minver="1" webm="0" default="0">This frame is referenced and has the specified
|
||||
cache priority. In cache only a frame of the same or higher priority can replace this frame.
|
||||
A value of 0 means the frame is not referenced.</element>
|
||||
<element name="ReferenceBlock" level="3" id="0xFB" type="integer" multiple="1" minver="1">Timecode
|
||||
of another frame used as a reference (ie: B or P frame). The timecode is relative to the
|
||||
block it's attached to.</element>
|
||||
<element name="ReferenceVirtual" level="3" id="0xFD" type="integer" webm="0">Relative <a
|
||||
href="http://www.matroska.org/technical/specs/notes.html#Position_References">position</a>
|
||||
of the data that should be in position of the virtual block.</element>
|
||||
<element name="CodecState" level="3" id="0xA4" type="binary" minver="2" webm="0">The new codec
|
||||
state to use. Data interpretation is private to the codec. This information should always be
|
||||
referenced by a seek entry.</element>
|
||||
<element name="Slices" level="3" id="0x8E" type="master" minver="1" divx="0">Contains slices
|
||||
description.</element>
|
||||
<element name="TimeSlice" level="4" id="0xE8" type="master" multiple="1" minver="1" divx="0">Contains
|
||||
extra time information about the data contained in the Block. While there are a few files in
|
||||
the wild with this element, it is no longer in use and has been deprecated. Being able to
|
||||
interpret this element is not required for playback.</element>
|
||||
<element name="LaceNumber" cppname="SliceLaceNumber" level="5" id="0xCC" type="uinteger"
|
||||
minver="1" default="0" divx="0">The reverse number of the frame in the lace (0 is the last
|
||||
frame, 1 is the next to last, etc). While there are a few files in the wild with this
|
||||
element, it is no longer in use and has been deprecated. Being able to interpret this
|
||||
element is not required for playback.</element>
|
||||
<element name="FrameNumber" cppname="SliceFrameNumber" level="5" id="0xCD" type="uinteger"
|
||||
default="0">The number of the frame to generate from this lace with this delay (allow you to
|
||||
generate many frames from the same Block/Frame).</element>
|
||||
<element name="BlockAdditionID" cppname="SliceBlockAddID" level="5" id="0xCB" type="uinteger"
|
||||
default="0">The ID of the BlockAdditional element (0 is the main Block).</element>
|
||||
<element name="Delay" cppname="SliceDelay" level="5" id="0xCE" type="uinteger" default="0">The
|
||||
(scaled) delay to apply to the element.</element>
|
||||
<element name="SliceDuration" level="5" id="0xCF" type="uinteger" default="0">The (scaled)
|
||||
duration to apply to the element.</element>
|
||||
<element name="ReferenceFrame" level="3" id="0xC8" type="master" multiple="0" minver="0"
|
||||
webm="0" divx="1">
|
||||
<a href="http://developer.divx.com/docs/divx_plus_hd/format_features/Smooth_FF_RW">DivX
|
||||
trick track extenstions</a>
|
||||
</element>
|
||||
<element name="ReferenceOffset" level="4" id="0xC9" type="uinteger" multiple="0" mandatory="1"
|
||||
minver="0" webm="0" divx="1">
|
||||
<a href="http://developer.divx.com/docs/divx_plus_hd/format_features/Smooth_FF_RW">DivX
|
||||
trick track extenstions</a>
|
||||
</element>
|
||||
<element name="ReferenceTimeCode" level="4" id="0xCA" type="uinteger" multiple="0" mandatory="1"
|
||||
minver="0" webm="0" divx="1">
|
||||
<a href="http://developer.divx.com/docs/divx_plus_hd/format_features/Smooth_FF_RW">DivX
|
||||
trick track extenstions</a>
|
||||
</element>
|
||||
<element name="EncryptedBlock" level="2" id="0xAF" type="binary" multiple="1" webm="0">Similar
|
||||
to <a href="http://www.matroska.org/technical/specs/index.html#SimpleBlock">SimpleBlock</a>
|
||||
but the data inside the Block are Transformed (encrypt and/or signed). (see <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#encryptedblock_structure">EncryptedBlock
|
||||
Structure</a>)</element>
|
||||
<element name="Tracks" level="1" id="0x1654AE6B" type="master" multiple="1" minver="1">A
|
||||
top-level block of information with many tracks described.</element>
|
||||
<element name="TrackEntry" level="2" id="0xAE" type="master" mandatory="1" multiple="1"
|
||||
minver="1">Describes a track with all elements.</element>
|
||||
<element name="TrackNumber" level="3" id="0xD7" type="uinteger" mandatory="1" minver="1"
|
||||
range="not 0">The track number as used in the Block Header (using more than 127 tracks is
|
||||
not encouraged, though the design allows an unlimited number).</element>
|
||||
<element name="TrackUID" level="3" id="0x73C5" type="uinteger" mandatory="1" minver="1"
|
||||
range="not 0">A unique ID to identify the Track. This should be kept the same when making a
|
||||
direct stream copy of the Track to another file.</element>
|
||||
<element name="TrackType" level="3" id="0x83" type="uinteger" mandatory="1" minver="1"
|
||||
range="1-254">A set of track types coded on 8 bits (1: video, 2: audio, 3: complex, 0x10:
|
||||
logo, 0x11: subtitle, 0x12: buttons, 0x20: control).</element>
|
||||
<element name="FlagEnabled" cppname="TrackFlagEnabled" level="3" id="0xB9" type="uinteger"
|
||||
mandatory="1" minver="2" webm="1" default="1" range="0-1">Set if the track is usable. (1
|
||||
bit)</element>
|
||||
<element name="FlagDefault" cppname="TrackFlagDefault" level="3" id="0x88" type="uinteger"
|
||||
mandatory="1" minver="1" default="1" range="0-1">Set if that track (audio, video or subs)
|
||||
SHOULD be active if no language found matches the user preference. (1 bit)</element>
|
||||
<element name="FlagForced" cppname="TrackFlagForced" level="3" id="0x55AA" type="uinteger"
|
||||
mandatory="1" minver="1" default="0" range="0-1">Set if that track MUST be active during
|
||||
playback. There can be many forced track for a kind (audio, video or subs), the player
|
||||
should select the one which language matches the user preference or the default + forced
|
||||
track. Overlay MAY happen between a forced and non-forced track of the same kind. (1 bit)</element>
|
||||
<element name="FlagLacing" cppname="TrackFlagLacing" level="3" id="0x9C" type="uinteger"
|
||||
mandatory="1" minver="1" default="1" range="0-1">Set if the track may contain blocks using
|
||||
lacing. (1 bit)</element>
|
||||
<element name="MinCache" cppname="TrackMinCache" level="3" id="0x6DE7" type="uinteger"
|
||||
mandatory="1" minver="1" webm="0" default="0">The minimum number of frames a player should
|
||||
be able to cache during playback. If set to 0, the reference pseudo-cache system is not
|
||||
used.</element>
|
||||
<element name="MaxCache" cppname="TrackMaxCache" level="3" id="0x6DF8" type="uinteger"
|
||||
minver="1" webm="0">The maximum cache size required to store referenced frames in and the
|
||||
current frame. 0 means no cache is needed.</element>
|
||||
<element name="DefaultDuration" cppname="TrackDefaultDuration" level="3" id="0x23E383"
|
||||
type="uinteger" minver="1" range="not 0">Number of nanoseconds (not scaled via
|
||||
TimecodeScale) per frame ('frame' in the Matroska sense -- one element put into a
|
||||
(Simple)Block).</element>
|
||||
<element name="TrackTimecodeScale" level="3" id="0x23314F" type="float" mandatory="1" minver="1"
|
||||
maxver="3" webm="0" default="1.0" range="> 0">DEPRECATED, DO NOT USE. The scale to apply
|
||||
on this track to work at normal speed in relation with other tracks (mostly used to adjust
|
||||
video speed when the audio length differs).</element>
|
||||
<element name="TrackOffset" level="3" id="0x537F" type="integer" webm="0" default="0">A value to
|
||||
add to the Block's Timecode. This can be used to adjust the playback offset of a track.</element>
|
||||
<element name="MaxBlockAdditionID" level="3" id="0x55EE" type="uinteger" mandatory="1"
|
||||
minver="1" webm="0" default="0">The maximum value of <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#BlockAddID">BlockAddID</a>. A
|
||||
value 0 means there is no <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#BlockAdditions">BlockAdditions</a>
|
||||
for this track.</element>
|
||||
<element name="Name" cppname="TrackName" level="3" id="0x536E" type="utf-8" minver="1">A
|
||||
human-readable track name.</element>
|
||||
<element name="Language" cppname="TrackLanguage" level="3" id="0x22B59C" type="string"
|
||||
minver="1" default="eng">Specifies the language of the track in the <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#languages">Matroska languages
|
||||
form</a>.</element>
|
||||
<element name="CodecID" level="3" id="0x86" type="string" mandatory="1" minver="1">An ID
|
||||
corresponding to the codec, see the <a
|
||||
href="http://www.matroska.org/technical/specs/codecid/index.html">codec page</a> for
|
||||
more info.</element>
|
||||
<element name="CodecPrivate" level="3" id="0x63A2" type="binary" minver="1">Private data only
|
||||
known to the codec.</element>
|
||||
<element name="CodecName" level="3" id="0x258688" type="utf-8" minver="1">A human-readable
|
||||
string specifying the codec.</element>
|
||||
<element name="AttachmentLink" cppname="TrackAttachmentLink" level="3" id="0x7446"
|
||||
type="uinteger" minver="1" webm="0" range="not 0">The UID of an attachment that is used by
|
||||
this codec.</element>
|
||||
<element name="CodecSettings" level="3" id="0x3A9697" type="utf-8" webm="0">A string describing
|
||||
the encoding setting used.</element>
|
||||
<element name="CodecInfoURL" level="3" id="0x3B4040" type="string" multiple="1" webm="0">A URL
|
||||
to find information about the codec used.</element>
|
||||
<element name="CodecDownloadURL" level="3" id="0x26B240" type="string" multiple="1" webm="0">A
|
||||
URL to download about the codec used.</element>
|
||||
<element name="CodecDecodeAll" level="3" id="0xAA" type="uinteger" mandatory="1" minver="2"
|
||||
webm="0" default="1" range="0-1">The codec can decode potentially damaged data (1 bit).</element>
|
||||
<element name="TrackOverlay" level="3" id="0x6FAB" type="uinteger" multiple="1" minver="1"
|
||||
webm="0">Specify that this track is an overlay track for the Track specified (in the
|
||||
u-integer). That means when this track has a gap (see <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#SilentTracks">SilentTracks</a>)
|
||||
the overlay track should be used instead. The order of multiple TrackOverlay matters, the
|
||||
first one is the one that should be used. If not found it should be the second, etc.</element>
|
||||
<element name="TrackTranslate" level="3" id="0x6624" type="master" multiple="1" minver="1"
|
||||
webm="0">The track identification for the given Chapter Codec.</element>
|
||||
<element name="TrackTranslateEditionUID" level="4" id="0x66FC" type="uinteger" multiple="1"
|
||||
minver="1" webm="0">Specify an edition UID on which this translation applies. When not
|
||||
specified, it means for all editions found in the segment.</element>
|
||||
<element name="TrackTranslateCodec" level="4" id="0x66BF" type="uinteger" mandatory="1"
|
||||
minver="1" webm="0">The <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#ChapProcessCodecID">chapter
|
||||
codec</a> using this ID (0: Matroska Script, 1: DVD-menu).</element>
|
||||
<element name="TrackTranslateTrackID" level="4" id="0x66A5" type="binary" mandatory="1"
|
||||
minver="1" webm="0">The binary value used to represent this track in the chapter codec data.
|
||||
The format depends on the <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#ChapProcessCodecID">
|
||||
ChapProcessCodecID</a> used.</element>
|
||||
<element name="Video" cppname="TrackVideo" level="3" id="0xE0" type="master" minver="1">Video
|
||||
settings.</element>
|
||||
<element name="FlagInterlaced" cppname="VideoFlagInterlaced" level="4" id="0x9A" type="uinteger"
|
||||
mandatory="1" minver="2" webm="1" default="0" range="0-1">Set if the video is interlaced. (1
|
||||
bit)</element>
|
||||
<element name="StereoMode" cppname="VideoStereoMode" level="4" id="0x53B8" type="uinteger"
|
||||
minver="3" webm="1" default="0">Stereo-3D video mode (0: mono, 1: side by side (left eye is
|
||||
first), 2: top-bottom (right eye is first), 3: top-bottom (left eye is first), 4: checkboard
|
||||
(right is first), 5: checkboard (left is first), 6: row interleaved (right is first), 7: row
|
||||
interleaved (left is first), 8: column interleaved (right is first), 9: column interleaved
|
||||
(left is first), 10: anaglyph (cyan/red), 11: side by side (right eye is first), 12:
|
||||
anaglyph (green/magenta), 13 both eyes laced in one Block (left eye is first), 14 both eyes
|
||||
laced in one Block (right eye is first)) . There are some more details on <a
|
||||
href="http://www.matroska.org/technical/specs/notes.html#3D">3D support in the
|
||||
Specification Notes</a>.</element>
|
||||
<element name="OldStereoMode" level="4" id="0x53B9" type="uinteger" maxver="0" webm="0" divx="0">DEPRECATED,
|
||||
DO NOT USE. Bogus StereoMode value used in old versions of libmatroska. (0: mono, 1: right
|
||||
eye, 2: left eye, 3: both eyes).</element>
|
||||
<element name="PixelWidth" cppname="VideoPixelWidth" level="4" id="0xB0" type="uinteger"
|
||||
mandatory="1" minver="1" range="not 0">Width of the encoded video frames in pixels.</element>
|
||||
<element name="PixelHeight" cppname="VideoPixelHeight" level="4" id="0xBA" type="uinteger"
|
||||
mandatory="1" minver="1" range="not 0">Height of the encoded video frames in pixels.</element>
|
||||
<element name="PixelCropBottom" cppname="VideoPixelCropBottom" level="4" id="0x54AA"
|
||||
type="uinteger" minver="1" default="0">The number of video pixels to remove at the bottom of
|
||||
the image (for HDTV content).</element>
|
||||
<element name="PixelCropTop" cppname="VideoPixelCropTop" level="4" id="0x54BB" type="uinteger"
|
||||
minver="1" default="0">The number of video pixels to remove at the top of the image.</element>
|
||||
<element name="PixelCropLeft" cppname="VideoPixelCropLeft" level="4" id="0x54CC" type="uinteger"
|
||||
minver="1" default="0">The number of video pixels to remove on the left of the image.</element>
|
||||
<element name="PixelCropRight" cppname="VideoPixelCropRight" level="4" id="0x54DD"
|
||||
type="uinteger" minver="1" default="0">The number of video pixels to remove on the right of
|
||||
the image.</element>
|
||||
<element name="DisplayWidth" cppname="VideoDisplayWidth" level="4" id="0x54B0" type="uinteger"
|
||||
minver="1" default="PixelWidth" range="not 0">Width of the video frames to display. The
|
||||
default value is only valid when <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#DisplayUnit">DisplayUnit</a> is
|
||||
0.</element>
|
||||
<element name="DisplayHeight" cppname="VideoDisplayHeight" level="4" id="0x54BA" type="uinteger"
|
||||
minver="1" default="PixelHeight" range="not 0">Height of the video frames to display. The
|
||||
default value is only valid when <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#DisplayUnit">DisplayUnit</a> is
|
||||
0.</element>
|
||||
<element name="DisplayUnit" cppname="VideoDisplayUnit" level="4" id="0x54B2" type="uinteger"
|
||||
minver="1" default="0">How DisplayWidth & DisplayHeight should be interpreted (0:
|
||||
pixels, 1: centimeters, 2: inches, 3: Display Aspect Ratio).</element>
|
||||
<element name="AspectRatioType" cppname="VideoAspectRatio" level="4" id="0x54B3" type="uinteger"
|
||||
minver="1" default="0">Specify the possible modifications to the aspect ratio (0: free
|
||||
resizing, 1: keep aspect ratio, 2: fixed).</element>
|
||||
<element name="ColourSpace" cppname="VideoColourSpace" level="4" id="0x2EB524" type="binary"
|
||||
minver="1" webm="0" bytesize="4">Same value as in AVI (32 bits).</element>
|
||||
<element name="GammaValue" cppname="VideoGamma" level="4" id="0x2FB523" type="float" webm="0"
|
||||
range="> 0">Gamma Value.</element>
|
||||
<element name="FrameRate" cppname="VideoFrameRate" level="4" id="0x2383E3" type="float"
|
||||
range="> 0">Number of frames per second. <strong>Informational</strong> only.</element>
|
||||
<element name="Audio" cppname="TrackAudio" level="3" id="0xE1" type="master" minver="1">Audio
|
||||
settings.</element>
|
||||
<element name="SamplingFrequency" cppname="AudioSamplingFreq" level="4" id="0xB5" type="float"
|
||||
mandatory="1" minver="1" default="8000.0" range="> 0">Sampling frequency in Hz.</element>
|
||||
<element name="OutputSamplingFrequency" cppname="AudioOutputSamplingFreq" level="4" id="0x78B5"
|
||||
type="float" minver="1" default="Sampling Frequency" range="> 0">Real output sampling
|
||||
frequency in Hz (used for SBR techniques).</element>
|
||||
<element name="Channels" cppname="AudioChannels" level="4" id="0x9F" type="uinteger"
|
||||
mandatory="1" minver="1" default="1" range="not 0">Numbers of channels in the track.</element>
|
||||
<element name="ChannelPositions" cppname="AudioPosition" level="4" id="0x7D7B" type="binary"
|
||||
webm="0">Table of horizontal angles for each successive channel, see <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#channelposition">appendix</a>.</element>
|
||||
<element name="BitDepth" cppname="AudioBitDepth" level="4" id="0x6264" type="uinteger"
|
||||
minver="1" range="not 0">Bits per sample, mostly used for PCM.</element>
|
||||
<element name="TrackOperation" level="3" id="0xE2" type="master" minver="3" webm="0">Operation
|
||||
that needs to be applied on tracks to create this virtual track. For more details <a
|
||||
href="http://www.matroska.org/technical/specs/notes.html#TrackOperation">look at the
|
||||
Specification Notes</a> on the subject.</element>
|
||||
<element name="TrackCombinePlanes" level="4" id="0xE3" type="master" minver="3" webm="0">Contains
|
||||
the list of all video plane tracks that need to be combined to create this 3D track</element>
|
||||
<element name="TrackPlane" level="5" id="0xE4" type="master" mandatory="1" multiple="1"
|
||||
minver="3" webm="0">Contains a video plane track that need to be combined to create this 3D
|
||||
track</element>
|
||||
<element name="TrackPlaneUID" level="6" id="0xE5" type="uinteger" mandatory="1" minver="3"
|
||||
webm="0" range="not 0">The trackUID number of the track representing the plane.</element>
|
||||
<element name="TrackPlaneType" level="6" id="0xE6" type="uinteger" mandatory="1" minver="3"
|
||||
webm="0">The kind of plane this track corresponds to (0: left eye, 1: right eye, 2:
|
||||
background).</element>
|
||||
<element name="TrackJoinBlocks" level="4" id="0xE9" type="master" minver="3" webm="0">Contains
|
||||
the list of all tracks whose Blocks need to be combined to create this virtual track</element>
|
||||
<element name="TrackJoinUID" level="5" id="0xED" type="uinteger" mandatory="1" multiple="1"
|
||||
minver="3" webm="0" range="not 0">The trackUID number of a track whose blocks are used to
|
||||
create this virtual track.</element>
|
||||
<element name="TrickTrackUID" level="3" id="0xC0" type="uinteger" divx="1">
|
||||
<a href="http://developer.divx.com/docs/divx_plus_hd/format_features/Smooth_FF_RW">DivX
|
||||
trick track extenstions</a>
|
||||
</element>
|
||||
<element name="TrickTrackSegmentUID" level="3" id="0xC1" type="binary" divx="1" bytesize="16">
|
||||
<a href="http://developer.divx.com/docs/divx_plus_hd/format_features/Smooth_FF_RW">DivX
|
||||
trick track extenstions</a>
|
||||
</element>
|
||||
<element name="TrickTrackFlag" level="3" id="0xC6" type="uinteger" divx="1" default="0">
|
||||
<a href="http://developer.divx.com/docs/divx_plus_hd/format_features/Smooth_FF_RW">DivX
|
||||
trick track extenstions</a>
|
||||
</element>
|
||||
<element name="TrickMasterTrackUID" level="3" id="0xC7" type="uinteger" divx="1">
|
||||
<a href="http://developer.divx.com/docs/divx_plus_hd/format_features/Smooth_FF_RW">DivX
|
||||
trick track extenstions</a>
|
||||
</element>
|
||||
<element name="TrickMasterTrackSegmentUID" level="3" id="0xC4" type="binary" divx="1"
|
||||
bytesize="16">
|
||||
<a href="http://developer.divx.com/docs/divx_plus_hd/format_features/Smooth_FF_RW">DivX
|
||||
trick track extenstions</a>
|
||||
</element>
|
||||
<element name="ContentEncodings" level="3" id="0x6D80" type="master" minver="1" webm="0">Settings
|
||||
for several content encoding mechanisms like compression or encryption.</element>
|
||||
<element name="ContentEncoding" level="4" id="0x6240" type="master" mandatory="1" multiple="1"
|
||||
minver="1" webm="0">Settings for one content encoding like compression or encryption.</element>
|
||||
<element name="ContentEncodingOrder" level="5" id="0x5031" type="uinteger" mandatory="1"
|
||||
minver="1" webm="0" default="0">Tells when this modification was used during encoding/muxing
|
||||
starting with 0 and counting upwards. The decoder/demuxer has to start with the highest
|
||||
order number it finds and work its way down. This value has to be unique over all
|
||||
ContentEncodingOrder elements in the segment.</element>
|
||||
<element name="ContentEncodingScope" level="5" id="0x5032" type="uinteger" mandatory="1"
|
||||
minver="1" webm="0" default="1" range="not 0">A bit field that describes which elements have
|
||||
been modified in this way. Values (big endian) can be OR'ed. Possible values:<br /> 1 - all
|
||||
frame contents,<br /> 2 - the track's private data,<br /> 4 - the next ContentEncoding (next
|
||||
ContentEncodingOrder. Either the data inside ContentCompression and/or ContentEncryption)</element>
|
||||
<element name="ContentEncodingType" level="5" id="0x5033" type="uinteger" mandatory="1"
|
||||
minver="1" webm="0" default="0">A value describing what kind of transformation has been
|
||||
done. Possible values:<br /> 0 - compression,<br /> 1 - encryption</element>
|
||||
<element name="ContentCompression" level="5" id="0x5034" type="master" minver="1" webm="0">Settings
|
||||
describing the compression used. Must be present if the value of ContentEncodingType is 0
|
||||
and absent otherwise. Each block must be decompressable even if no previous block is
|
||||
available in order not to prevent seeking.</element>
|
||||
<element name="ContentCompAlgo" level="6" id="0x4254" type="uinteger" mandatory="1" minver="1"
|
||||
webm="0" default="0">The compression algorithm used. Algorithms that have been specified so
|
||||
far are:<br /> 0 - zlib,<br /> <del>1 - bzlib,</del><br /> <del>2 - lzo1x</del><br /> 3 -
|
||||
Header Stripping</element>
|
||||
<element name="ContentCompSettings" level="6" id="0x4255" type="binary" minver="1" webm="0">Settings
|
||||
that might be needed by the decompressor. For Header Stripping (ContentCompAlgo=3), the
|
||||
bytes that were removed from the beggining of each frames of the track.</element>
|
||||
<element name="ContentEncryption" level="5" id="0x5035" type="master" minver="1" webm="0">Settings
|
||||
describing the encryption used. Must be present if the value of ContentEncodingType is 1 and
|
||||
absent otherwise.</element>
|
||||
<element name="ContentEncAlgo" level="6" id="0x47E1" type="uinteger" minver="1" webm="0"
|
||||
default="0">The encryption algorithm used. The value '0' means that the contents have not
|
||||
been encrypted but only signed. Predefined values:<br /> 1 - DES, 2 - 3DES, 3 - Twofish, 4 -
|
||||
Blowfish, 5 - AES</element>
|
||||
<element name="ContentEncKeyID" level="6" id="0x47E2" type="binary" minver="1" webm="0">For
|
||||
public key algorithms this is the ID of the public key the the data was encrypted with.</element>
|
||||
<element name="ContentSignature" level="6" id="0x47E3" type="binary" minver="1" webm="0">A
|
||||
cryptographic signature of the contents.</element>
|
||||
<element name="ContentSigKeyID" level="6" id="0x47E4" type="binary" minver="1" webm="0">This is
|
||||
the ID of the private key the data was signed with.</element>
|
||||
<element name="ContentSigAlgo" level="6" id="0x47E5" type="uinteger" minver="1" webm="0"
|
||||
default="0">The algorithm used for the signature. A value of '0' means that the contents
|
||||
have not been signed but only encrypted. Predefined values:<br /> 1 - RSA</element>
|
||||
<element name="ContentSigHashAlgo" level="6" id="0x47E6" type="uinteger" minver="1" webm="0"
|
||||
default="0">The hash algorithm used for the signature. A value of '0' means that the
|
||||
contents have not been signed but only encrypted. Predefined values:<br /> 1 - SHA1-160<br />
|
||||
2 - MD5</element>
|
||||
<element name="Cues" level="1" id="0x1C53BB6B" type="master" minver="1">A top-level element to
|
||||
speed seeking access. All entries are local to the segment. Should be mandatory for non <a
|
||||
href="http://www.matroska.org/technical/streaming/index.hmtl">"live" streams</a>.</element>
|
||||
<element name="CuePoint" level="2" id="0xBB" type="master" mandatory="1" multiple="1" minver="1">Contains
|
||||
all information relative to a seek point in the segment.</element>
|
||||
<element name="CueTime" level="3" id="0xB3" type="uinteger" mandatory="1" minver="1">Absolute
|
||||
timecode according to the segment time base.</element>
|
||||
<element name="CueTrackPositions" level="3" id="0xB7" type="master" mandatory="1" multiple="1"
|
||||
minver="1">Contain positions for different tracks corresponding to the timecode.</element>
|
||||
<element name="CueTrack" level="4" id="0xF7" type="uinteger" mandatory="1" minver="1"
|
||||
range="not 0">The track for which a position is given.</element>
|
||||
<element name="CueClusterPosition" level="4" id="0xF1" type="uinteger" mandatory="1" minver="1">
|
||||
The <a href="http://www.matroska.org/technical/specs/notes.html#Position_References">
|
||||
position</a> of the Cluster containing the required Block.</element>
|
||||
<element name="CueRelativePosition" level="4" id="0xF0" type="uinteger" mandatory="0" minver="4"
|
||||
webm="0">The relative position of the referenced block inside the cluster with 0 being the
|
||||
first possible position for an element inside that cluster.</element>
|
||||
<element name="CueDuration" level="4" id="0xB2" type="uinteger" mandatory="0" minver="4"
|
||||
webm="0">The duration of the block according to the segment time base. If missing the
|
||||
track's DefaultDuration does not apply and no duration information is available in terms of
|
||||
the cues.</element>
|
||||
<element name="CueBlockNumber" level="4" id="0x5378" type="uinteger" minver="1" default="1"
|
||||
range="not 0">Number of the Block in the specified Cluster.</element>
|
||||
<element name="CueCodecState" level="4" id="0xEA" type="uinteger" minver="2" webm="0"
|
||||
default="0">The <a
|
||||
href="http://www.matroska.org/technical/specs/notes.html#Position_References">position</a>
|
||||
of the Codec State corresponding to this Cue element. 0 means that the data is taken from
|
||||
the initial Track Entry.</element>
|
||||
<element name="CueReference" level="4" id="0xDB" type="master" multiple="1" minver="2" webm="0">The
|
||||
Clusters containing the required referenced Blocks.</element>
|
||||
<element name="CueRefTime" level="5" id="0x96" type="uinteger" mandatory="1" minver="2" webm="0">Timecode
|
||||
of the referenced Block.</element>
|
||||
<element name="CueRefCluster" level="5" id="0x97" type="uinteger" mandatory="1" webm="0">The <a
|
||||
href="http://www.matroska.org/technical/specs/notes.html#Position_References">Position</a>
|
||||
of the Cluster containing the referenced Block.</element>
|
||||
<element name="CueRefNumber" level="5" id="0x535F" type="uinteger" webm="0" default="1"
|
||||
range="not 0">Number of the referenced Block of Track X in the specified Cluster.</element>
|
||||
<element name="CueRefCodecState" level="5" id="0xEB" type="uinteger" webm="0" default="0">The <a
|
||||
href="http://www.matroska.org/technical/specs/notes.html#Position_References">position</a>
|
||||
of the Codec State corresponding to this referenced element. 0 means that the data is taken
|
||||
from the initial Track Entry.</element>
|
||||
<element name="Attachments" level="1" id="0x1941A469" type="master" minver="1" webm="0">Contain
|
||||
attached files.</element>
|
||||
<element name="AttachedFile" level="2" id="0x61A7" type="master" mandatory="1" multiple="1"
|
||||
minver="1" webm="0">An attached file.</element>
|
||||
<element name="FileDescription" level="3" id="0x467E" type="utf-8" minver="1" webm="0">A
|
||||
human-friendly name for the attached file.</element>
|
||||
<element name="FileName" level="3" id="0x466E" type="utf-8" mandatory="1" minver="1" webm="0">Filename
|
||||
of the attached file.</element>
|
||||
<element name="FileMimeType" level="3" id="0x4660" type="string" mandatory="1" minver="1"
|
||||
webm="0">MIME type of the file.</element>
|
||||
<element name="FileData" level="3" id="0x465C" type="binary" mandatory="1" minver="1" webm="0">The
|
||||
data of the file.</element>
|
||||
<element name="FileUID" level="3" id="0x46AE" type="uinteger" mandatory="1" minver="1" webm="0"
|
||||
range="not 0">Unique ID representing the file, as random as possible.</element>
|
||||
<element name="FileReferral" level="3" id="0x4675" type="binary" webm="0">A binary value that a
|
||||
track/codec can refer to when the attachment is needed.</element>
|
||||
<element name="FileUsedStartTime" level="3" id="0x4661" type="uinteger" divx="1">
|
||||
<a href="http://developer.divx.com/docs/divx_plus_hd/format_features/World_Fonts">DivX font
|
||||
extension</a>
|
||||
</element>
|
||||
<element name="FileUsedEndTime" level="3" id="0x4662" type="uinteger" divx="1">
|
||||
<a href="http://developer.divx.com/docs/divx_plus_hd/format_features/World_Fonts">DivX font
|
||||
extension</a>
|
||||
</element>
|
||||
<element name="Chapters" level="1" id="0x1043A770" type="master" minver="1" webm="1">A system to
|
||||
define basic menus and partition data. For more detailed information, look at the <a
|
||||
href="http://www.matroska.org/technical/specs/chapters/index.html">Chapters Explanation</a>
|
||||
.</element>
|
||||
<element name="EditionEntry" level="2" id="0x45B9" type="master" mandatory="1" multiple="1"
|
||||
minver="1" webm="1">Contains all information about a segment edition.</element>
|
||||
<element name="EditionUID" level="3" id="0x45BC" type="uinteger" minver="1" webm="0"
|
||||
range="not 0">A unique ID to identify the edition. It's useful for tagging an edition.</element>
|
||||
<element name="EditionFlagHidden" level="3" id="0x45BD" type="uinteger" mandatory="1" minver="1"
|
||||
webm="0" default="0" range="0-1">If an edition is hidden (1), it should not be available to
|
||||
the user interface (but still to Control Tracks). (1 bit)</element>
|
||||
<element name="EditionFlagDefault" level="3" id="0x45DB" type="uinteger" mandatory="1"
|
||||
minver="1" webm="0" default="0" range="0-1">If a flag is set (1) the edition should be used
|
||||
as the default one. (1 bit)</element>
|
||||
<element name="EditionFlagOrdered" level="3" id="0x45DD" type="uinteger" minver="1" webm="0"
|
||||
default="0" range="0-1">Specify if the chapters can be defined multiple times and the order
|
||||
to play them is enforced. (1 bit)</element>
|
||||
<element name="ChapterAtom" level="3" recursive="1" id="0xB6" type="master" mandatory="1"
|
||||
multiple="1" minver="1" webm="1">Contains the atom information to use as the chapter atom
|
||||
(apply to all tracks).</element>
|
||||
<element name="ChapterUID" level="4" id="0x73C4" type="uinteger" mandatory="1" minver="1"
|
||||
webm="1" range="not 0">A unique ID to identify the Chapter.</element>
|
||||
<element name="ChapterStringUID" level="4" id="0x5654" type="utf-8" mandatory="0" minver="3"
|
||||
webm="1">A unique string ID to identify the Chapter. Use for <a
|
||||
href="http://dev.w3.org/html5/webvtt/#webvtt-cue-identifier">WebVTT cue identifier
|
||||
storage</a>.</element>
|
||||
<element name="ChapterTimeStart" level="4" id="0x91" type="uinteger" mandatory="1" minver="1"
|
||||
webm="1">Timecode of the start of Chapter (not scaled).</element>
|
||||
<element name="ChapterTimeEnd" level="4" id="0x92" type="uinteger" minver="1" webm="0">Timecode
|
||||
of the end of Chapter (timecode excluded, not scaled).</element>
|
||||
<element name="ChapterFlagHidden" level="4" id="0x98" type="uinteger" mandatory="1" minver="1"
|
||||
webm="0" default="0" range="0-1">If a chapter is hidden (1), it should not be available to
|
||||
the user interface (but still to Control Tracks). (1 bit)</element>
|
||||
<element name="ChapterFlagEnabled" level="4" id="0x4598" type="uinteger" mandatory="1"
|
||||
minver="1" webm="0" default="1" range="0-1">Specify wether the chapter is enabled. It can be
|
||||
enabled/disabled by a Control Track. When disabled, the movie should skip all the content
|
||||
between the TimeStart and TimeEnd of this chapter. (1 bit)</element>
|
||||
<element name="ChapterSegmentUID" level="4" id="0x6E67" type="binary" minver="1" webm="0"
|
||||
range=">0" bytesize="16">A segment to play in place of this chapter. Edition
|
||||
ChapterSegmentEditionUID should be used for this segment, otherwise no edition is used.</element>
|
||||
<element name="ChapterSegmentEditionUID" level="4" id="0x6EBC" type="uinteger" minver="1"
|
||||
webm="0" range="not 0">The EditionUID to play from the segment linked in ChapterSegmentUID.</element>
|
||||
<element name="ChapterPhysicalEquiv" level="4" id="0x63C3" type="uinteger" minver="1" webm="0">Specify
|
||||
the physical equivalent of this ChapterAtom like "DVD" (60) or "SIDE" (50), see <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#physical">complete list of
|
||||
values</a>.</element>
|
||||
<element name="ChapterTrack" level="4" id="0x8F" type="master" minver="1" webm="0">List of
|
||||
tracks on which the chapter applies. If this element is not present, all tracks apply</element>
|
||||
<element name="ChapterTrackNumber" level="5" id="0x89" type="uinteger" mandatory="1"
|
||||
multiple="1" minver="1" webm="0" range="not 0">UID of the Track to apply this chapter too.
|
||||
In the absense of a control track, choosing this chapter will select the listed Tracks and
|
||||
deselect unlisted tracks. Absense of this element indicates that the Chapter should be
|
||||
applied to any currently used Tracks.</element>
|
||||
<element name="ChapterDisplay" level="4" id="0x80" type="master" multiple="1" minver="1"
|
||||
webm="1">Contains all possible strings to use for the chapter display.</element>
|
||||
<element name="ChapString" cppname="ChapterString" level="5" id="0x85" type="utf-8"
|
||||
mandatory="1" minver="1" webm="1">Contains the string to use as the chapter atom.</element>
|
||||
<element name="ChapLanguage" cppname="ChapterLanguage" level="5" id="0x437C" type="string"
|
||||
mandatory="1" multiple="1" minver="1" webm="1" default="eng">The languages corresponding to
|
||||
the string, in the <a href="http://lcweb.loc.gov/standards/iso639-2/englangn.html#two">bibliographic
|
||||
ISO-639-2 form</a>.</element>
|
||||
<element name="ChapCountry" cppname="ChapterCountry" level="5" id="0x437E" type="string"
|
||||
multiple="1" minver="1" webm="0">The countries corresponding to the string, same 2 octets as
|
||||
in <a href="http://www.iana.org/cctld/cctld-whois.htm">Internet domains</a>.</element>
|
||||
<element name="ChapProcess" cppname="ChapterProcess" level="4" id="0x6944" type="master"
|
||||
multiple="1" minver="1" webm="0">Contains all the commands associated to the Atom.</element>
|
||||
<element name="ChapProcessCodecID" cppname="ChapterProcessCodecID" level="5" id="0x6955"
|
||||
type="uinteger" mandatory="1" minver="1" webm="0" default="0">Contains the type of the codec
|
||||
used for the processing. A value of 0 means native Matroska processing (to be defined), a
|
||||
value of 1 means the <a
|
||||
href="http://www.matroska.org/technical/specs/chapters/index.html#dvd">DVD</a> command
|
||||
set is used. More codec IDs can be added later.</element>
|
||||
<element name="ChapProcessPrivate" cppname="ChapterProcessPrivate" level="5" id="0x450D"
|
||||
type="binary" minver="1" webm="0">Some optional data attached to the ChapProcessCodecID
|
||||
information. <a href="http://www.matroska.org/technical/specs/chapters/index.html#dvd">For
|
||||
ChapProcessCodecID = 1</a>, it is the "DVD level" equivalent.</element>
|
||||
<element name="ChapProcessCommand" cppname="ChapterProcessCommand" level="5" id="0x6911"
|
||||
type="master" multiple="1" minver="1" webm="0">Contains all the commands associated to the
|
||||
Atom.</element>
|
||||
<element name="ChapProcessTime" cppname="ChapterProcessTime" level="6" id="0x6922"
|
||||
type="uinteger" mandatory="1" minver="1" webm="0">Defines when the process command should be
|
||||
handled (0: during the whole chapter, 1: before starting playback, 2: after playback of the
|
||||
chapter).</element>
|
||||
<element name="ChapProcessData" cppname="ChapterProcessData" level="6" id="0x6933" type="binary"
|
||||
mandatory="1" minver="1" webm="0">Contains the command information. The data should be
|
||||
interpreted depending on the ChapProcessCodecID value. <a
|
||||
href="http://www.matroska.org/technical/specs/chapters/index.html#dvd">For
|
||||
ChapProcessCodecID = 1</a>, the data correspond to the binary DVD cell pre/post commands.</element>
|
||||
<element name="Tags" level="1" id="0x1254C367" type="master" multiple="1" minver="1" webm="0">Element
|
||||
containing elements specific to Tracks/Chapters. A list of valid tags can be found <a
|
||||
href="http://www.matroska.org/technical/specs/tagging/index.html">here.</a></element>
|
||||
<element name="Tag" level="2" id="0x7373" type="master" mandatory="1" multiple="1" minver="1"
|
||||
webm="0">Element containing elements specific to Tracks/Chapters.</element>
|
||||
<element name="Targets" cppname="TagTargets" level="3" id="0x63C0" type="master" mandatory="1"
|
||||
minver="1" webm="0">Contain all UIDs where the specified meta data apply. It is empty to
|
||||
describe everything in the segment.</element>
|
||||
<element name="TargetTypeValue" cppname="TagTargetTypeValue" level="4" id="0x68CA"
|
||||
type="uinteger" minver="1" webm="0" default="50">A number to indicate the logical level of
|
||||
the target (see <a
|
||||
href="http://www.matroska.org/technical/specs/tagging/index.html#targettypes">TargetType</a>
|
||||
).</element>
|
||||
<element name="TargetType" cppname="TagTargetType" level="4" id="0x63CA" type="string"
|
||||
minver="1" webm="0">An <strong>informational</strong> string that can be used to display the
|
||||
logical level of the target like "ALBUM", "TRACK", "MOVIE", "CHAPTER", etc (see <a
|
||||
href="http://www.matroska.org/technical/specs/tagging/index.html#targettypes">TargetType</a>
|
||||
).</element>
|
||||
<element name="TagTrackUID" level="4" id="0x63C5" type="uinteger" multiple="1" minver="1"
|
||||
webm="0" default="0">A unique ID to identify the Track(s) the tags belong to. If the value
|
||||
is 0 at this level, the tags apply to all tracks in the Segment.</element>
|
||||
<element name="TagEditionUID" level="4" id="0x63C9" type="uinteger" multiple="1" minver="1"
|
||||
webm="0" default="0">A unique ID to identify the EditionEntry(s) the tags belong to. If the
|
||||
value is 0 at this level, the tags apply to all editions in the Segment.</element>
|
||||
<element name="TagChapterUID" level="4" id="0x63C4" type="uinteger" multiple="1" minver="1"
|
||||
webm="0" default="0">A unique ID to identify the Chapter(s) the tags belong to. If the value
|
||||
is 0 at this level, the tags apply to all chapters in the Segment.</element>
|
||||
<element name="TagAttachmentUID" level="4" id="0x63C6" type="uinteger" multiple="1" minver="1"
|
||||
webm="0" default="0">A unique ID to identify the Attachment(s) the tags belong to. If the
|
||||
value is 0 at this level, the tags apply to all the attachments in the Segment.</element>
|
||||
<element name="SimpleTag" cppname="TagSimple" level="3" recursive="1" id="0x67C8" type="master"
|
||||
mandatory="1" multiple="1" minver="1" webm="0">Contains general information about the
|
||||
target.</element>
|
||||
<element name="TagName" level="4" id="0x45A3" type="utf-8" mandatory="1" minver="1" webm="0">The
|
||||
name of the Tag that is going to be stored.</element>
|
||||
<element name="TagLanguage" level="4" id="0x447A" type="string" mandatory="1" minver="1"
|
||||
webm="0" default="und">Specifies the language of the tag specified, in the <a
|
||||
href="http://www.matroska.org/technical/specs/index.html#languages">Matroska languages
|
||||
form</a>.</element>
|
||||
<element name="TagDefault" level="4" id="0x4484" type="uinteger" mandatory="1" minver="1"
|
||||
webm="0" default="1" range="0-1">Indication to know if this is the default/original language
|
||||
to use for the given tag. (1 bit)</element>
|
||||
<element name="TagString" level="4" id="0x4487" type="utf-8" minver="1" webm="0">The value of
|
||||
the Tag.</element>
|
||||
<element name="TagBinary" level="4" id="0x4485" type="binary" minver="1" webm="0">The values of
|
||||
the Tag if it is binary. Note that this cannot be used in the same SimpleTag as TagString.</element>
|
||||
</table>
|
||||
99
biome.jsonc
Normal file
99
biome.jsonc
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"extends": [
|
||||
"ultracite"
|
||||
],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useSingleCaseStatement": "off",
|
||||
"noParameterProperties": "off",
|
||||
"noNonNullAssertion": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off"
|
||||
},
|
||||
"a11y": {
|
||||
"noSvgWithoutTitle": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noBannedTypes": "off",
|
||||
"noExcessiveCognitiveComplexity": {
|
||||
"level": "warn",
|
||||
"options": {
|
||||
"maxAllowedComplexity": 40
|
||||
}
|
||||
}
|
||||
},
|
||||
"nursery": {
|
||||
"noEnum": "off",
|
||||
"useConsistentMemberAccessibility": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"ignore": [
|
||||
".vscode/*.json"
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": [
|
||||
"apps/playground/**"
|
||||
],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"suspicious": {
|
||||
"noConsole": "off"
|
||||
},
|
||||
"performance": {
|
||||
"useTopLevelRegex": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"scripts/**"
|
||||
],
|
||||
"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"
|
||||
},
|
||||
"performance": {
|
||||
"useTopLevelRegex": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
11
justfile
Normal file
11
justfile
Normal file
@@ -0,0 +1,11 @@
|
||||
set windows-shell := ["pwsh.exe", "-c"]
|
||||
set dotenv-load := true
|
||||
|
||||
dev-playground:
|
||||
pnpm run --filter=@konoplayer/playground dev
|
||||
|
||||
dev-proxy:
|
||||
pnpm run --filter=@konoplayer/proxy --filter=@konoplayer/mock dev
|
||||
|
||||
download-samples:
|
||||
pnpm run download-samples
|
||||
36
package.json
Normal file
36
package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "konoplayer",
|
||||
"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"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "lonelyhentxi",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.6.1",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/node": "^22.13.11",
|
||||
"@webgpu/types": "^0.1.59",
|
||||
"change-case": "^5.4.4",
|
||||
"happy-dom": "^17.4.4",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.2",
|
||||
"ultracite": "^4.2.0"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
8
packages/core/package.json
Normal file
8
packages/core/package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "@konoplayer/core",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {},
|
||||
"dependencies": {}
|
||||
}
|
||||
40
packages/core/src/audition/index.ts
Normal file
40
packages/core/src/audition/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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$
|
||||
})));
|
||||
}
|
||||
32
packages/core/src/codecs/audio-codecs.ts
Normal file
32
packages/core/src/codecs/audio-codecs.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
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.
|
||||
}
|
||||
2
packages/core/src/codecs/index.ts
Normal file
2
packages/core/src/codecs/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { AudioCodec } from './audio-codecs';
|
||||
export { VideoCodec } from './video-codecs';
|
||||
97
packages/core/src/codecs/video-codecs.ts
Normal file
97
packages/core/src/codecs/video-codecs.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
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;
|
||||
};
|
||||
39
packages/core/src/data/bit.ts
Normal file
39
packages/core/src/data/bit.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
65
packages/core/src/data/fetch.ts
Normal file
65
packages/core/src/data/fetch.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
export interface RangedStream {
|
||||
controller: AbortController;
|
||||
response: Response;
|
||||
body: ReadableStream;
|
||||
totalSize?: number;
|
||||
}
|
||||
|
||||
export interface CreateRangedStreamOptions {
|
||||
url: string;
|
||||
byteStart?: number;
|
||||
byteEnd?: number;
|
||||
}
|
||||
|
||||
export async function createRangedStream({
|
||||
url,
|
||||
byteStart = 0,
|
||||
byteEnd,
|
||||
}: CreateRangedStreamOptions) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
6
packages/core/src/data/index.ts
Normal file
6
packages/core/src/data/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
type RangedStream,
|
||||
type CreateRangedStreamOptions,
|
||||
createRangedStream,
|
||||
} from './fetch';
|
||||
export { BitReader } from './bit';
|
||||
31
packages/core/src/errors.ts
Normal file
31
packages/core/src/errors.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
export class UnimplementedError extends Error {
|
||||
constructor(detail: string) {
|
||||
super(`unimplemented: ${detail}`);
|
||||
}
|
||||
}
|
||||
80
packages/core/src/graphics/index.ts
Normal file
80
packages/core/src/graphics/index.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
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$
|
||||
})));
|
||||
}
|
||||
10
packages/core/tsconfig.json
Normal file
10
packages/core/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
7
packages/ffmpeg/Cargo.toml
Normal file
7
packages/ffmpeg/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "konoplayer-ffmpeg"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
ffmpeg-sys-next = { version = "7.1.0", features = ["avcodec"] }
|
||||
85
packages/ffmpeg/src/lib.rs
Normal file
85
packages/ffmpeg/src/lib.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
11
packages/matroska/package.json
Normal file
11
packages/matroska/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@konoplayer/matroska",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"@konoplayer/core": "workspace:*",
|
||||
"konoebml": "^0.1.2"
|
||||
}
|
||||
}
|
||||
124
packages/matroska/src/codecs/aac.ts
Normal file
124
packages/matroska/src/codecs/aac.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
167
packages/matroska/src/codecs/av1.ts
Normal file
167
packages/matroska/src/codecs/av1.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
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('.');
|
||||
}
|
||||
148
packages/matroska/src/codecs/avc.ts
Normal file
148
packages/matroska/src/codecs/avc.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
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}`;
|
||||
}
|
||||
214
packages/matroska/src/codecs/hevc.ts
Normal file
214
packages/matroska/src/codecs/hevc.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
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('.');
|
||||
}
|
||||
436
packages/matroska/src/codecs/index.ts
Normal file
436
packages/matroska/src/codecs/index.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
232
packages/matroska/src/codecs/vp9.ts
Normal file
232
packages/matroska/src/codecs/vp9.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
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}`;
|
||||
}
|
||||
0
packages/matroska/src/index.ts
Normal file
0
packages/matroska/src/index.ts
Normal file
14
packages/matroska/src/model/cluster.ts
Normal file
14
packages/matroska/src/model/cluster.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
77
packages/matroska/src/model/index.ts
Normal file
77
packages/matroska/src/model/index.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { type EbmlEBMLTagType, EbmlTagIdEnum, EbmlTagPosition } from 'konoebml';
|
||||
import {
|
||||
switchMap,
|
||||
filter,
|
||||
take,
|
||||
shareReplay,
|
||||
map,
|
||||
combineLatest,
|
||||
of,
|
||||
type Observable,
|
||||
delayWhen,
|
||||
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)
|
||||
);
|
||||
}
|
||||
89
packages/matroska/src/model/resource.ts
Normal file
89
packages/matroska/src/model/resource.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
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$
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
448
packages/matroska/src/model/segment.ts
Normal file
448
packages/matroska/src/model/segment.ts
Normal file
@@ -0,0 +1,448 @@
|
||||
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,
|
||||
combineLatest,
|
||||
} 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 trackSystem = segment.track;
|
||||
const timestampScale = Number(infoSystem.info.TimestampScale) / 1000;
|
||||
|
||||
const frameProcessing = trackSystem.buildFrameEncodingProcessor(
|
||||
track.trackEntry
|
||||
);
|
||||
|
||||
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 (let frame of block.frames) {
|
||||
if (frameProcessing) {
|
||||
frame = frameProcessing(frame);
|
||||
}
|
||||
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 trackSystem = segment.track;
|
||||
const timestampScale = Number(infoSystem.info.TimestampScale) / 1000;
|
||||
|
||||
const frameProcessing = trackSystem.buildFrameEncodingProcessor(
|
||||
track.trackEntry
|
||||
);
|
||||
|
||||
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 (let frame of block.frames) {
|
||||
if (frameProcessing) {
|
||||
frame = frameProcessing(frame);
|
||||
}
|
||||
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$,
|
||||
};
|
||||
}
|
||||
1160
packages/matroska/src/schema.ts
Normal file
1160
packages/matroska/src/schema.ts
Normal file
File diff suppressed because it is too large
Load Diff
116
packages/matroska/src/systems/cluster.ts
Normal file
116
packages/matroska/src/systems/cluster.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
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.BlockGroup && cluster.SimpleBlock) {
|
||||
const blocks = [];
|
||||
for (const block of cluster.BlockGroup) {
|
||||
if (block.Block.track === track.TrackNumber) {
|
||||
blocks.push(new BlockGroupView(block));
|
||||
}
|
||||
}
|
||||
for (const block of cluster.SimpleBlock) {
|
||||
if (block.track === track.TrackNumber) {
|
||||
blocks.push(new SimpleBlockView(block));
|
||||
}
|
||||
}
|
||||
blocks.sort((a, b) => a.relTime - b.relTime);
|
||||
yield* blocks;
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
packages/matroska/src/systems/cue.ts
Normal file
85
packages/matroska/src/systems/cue.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
8
packages/matroska/src/systems/index.ts
Normal file
8
packages/matroska/src/systems/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
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";
|
||||
20
packages/matroska/src/systems/info.ts
Normal file
20
packages/matroska/src/systems/info.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
64
packages/matroska/src/systems/seek.ts
Normal file
64
packages/matroska/src/systems/seek.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
37
packages/matroska/src/systems/segment-component.ts
Normal file
37
packages/matroska/src/systems/segment-component.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
124
packages/matroska/src/systems/segment.ts
Normal file
124
packages/matroska/src/systems/segment.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
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_;
|
||||
}
|
||||
|
||||
27
packages/matroska/src/systems/tag.ts
Normal file
27
packages/matroska/src/systems/tag.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
276
packages/matroska/src/systems/track.ts
Normal file
276
packages/matroska/src/systems/track.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import {
|
||||
ParseCodecErrors,
|
||||
UnimplementedError,
|
||||
UnsupportedCodecError,
|
||||
} from '@konoplayer/core/errors';
|
||||
import {
|
||||
EbmlTagIdEnum,
|
||||
type EbmlTrackEntryTagType,
|
||||
type EbmlTracksTagType,
|
||||
} from 'konoebml';
|
||||
import {
|
||||
audioCodecIdToWebCodecs,
|
||||
videoCodecIdRequirePeekingKeyframe,
|
||||
videoCodecIdToWebCodecs,
|
||||
type AudioDecoderConfigExt,
|
||||
type VideoDecoderConfigExt,
|
||||
} from '../codecs';
|
||||
import {
|
||||
ContentCompAlgoRestrictionEnum,
|
||||
ContentEncodingTypeRestrictionEnum,
|
||||
TrackEntrySchema,
|
||||
type TrackEntryType,
|
||||
TrackTypeRestrictionEnum,
|
||||
} from '../schema';
|
||||
import type { SegmentComponent } from './segment';
|
||||
import { SegmentComponentSystemTrait } from './segment-component';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
buildFrameEncodingProcessor(
|
||||
track: TrackEntryType
|
||||
): undefined | ((source: Uint8Array) => Uint8Array) {
|
||||
let encodings = track.ContentEncodings?.ContentEncoding;
|
||||
if (!encodings?.length) {
|
||||
return undefined;
|
||||
}
|
||||
encodings = encodings.toSorted(
|
||||
(a, b) => Number(b.ContentEncodingOrder) - Number(a.ContentEncodingOrder)
|
||||
);
|
||||
const processors: Array<(source: Uint8Array) => Uint8Array> = [];
|
||||
for (const encoing of encodings) {
|
||||
if (
|
||||
encoing.ContentEncodingType ===
|
||||
ContentEncodingTypeRestrictionEnum.COMPRESSION
|
||||
) {
|
||||
const compression = encoing.ContentCompression;
|
||||
const algo = compression?.ContentCompAlgo;
|
||||
if (algo === ContentCompAlgoRestrictionEnum.HEADER_STRIPPING) {
|
||||
const settings = compression?.ContentCompSettings;
|
||||
if (settings?.length) {
|
||||
processors.push((source: Uint8Array) => {
|
||||
const dest = new Uint8Array(source.length + settings.length);
|
||||
dest.set(source);
|
||||
dest.set(settings, source.length);
|
||||
return dest;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// TODO: dynamic import packages to support more compression algos
|
||||
throw new UnimplementedError(
|
||||
`compression algo ${ContentCompAlgoRestrictionEnum[algo as ContentCompAlgoRestrictionEnum]}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return function processor(source: Uint8Array): Uint8Array<ArrayBufferLike> {
|
||||
let dest = source;
|
||||
for (const processor of processors) {
|
||||
dest = processor(dest);
|
||||
}
|
||||
return dest;
|
||||
};
|
||||
}
|
||||
}
|
||||
62
packages/matroska/src/util.ts
Normal file
62
packages/matroska/src/util.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { Type } from 'arktype';
|
||||
import { EbmlElementType, EbmlTagIdEnum, type EbmlTagType } from 'konoebml';
|
||||
import { IdMultiSet } from './schema';
|
||||
|
||||
export type InferType<T extends Type<any>> = T['infer'];
|
||||
|
||||
export type PredicateIdExtract<T, K> = Extract<T, { id: K }>;
|
||||
|
||||
export type PredicatePositionExtract<
|
||||
T extends { position: string },
|
||||
P,
|
||||
> = P extends T['position'] ? T : never;
|
||||
|
||||
export function isTagIdPos<
|
||||
I extends EbmlTagIdEnum,
|
||||
P extends PredicateIdExtract<EbmlTagType, I>['position'] | '*' = '*',
|
||||
>(id: I, pos?: P) {
|
||||
return (tag: EbmlTagType): tag is PredicateIdExtract<EbmlTagType, I> =>
|
||||
tag.id === id && (pos === '*' || pos === tag.position);
|
||||
}
|
||||
|
||||
export function isTagPos<
|
||||
T extends { position: string },
|
||||
P extends T['position'],
|
||||
>(pos: P | '*' = '*') {
|
||||
return (tag: T): tag is PredicatePositionExtract<T, P> =>
|
||||
pos === '*' || pos === tag.position;
|
||||
}
|
||||
|
||||
export function convertEbmlTagToComponent(tag: EbmlTagType) {
|
||||
if (tag.type === EbmlElementType.Master) {
|
||||
const obj: Record<string, any> = {};
|
||||
const children = tag.children;
|
||||
for (const c of children) {
|
||||
const name = EbmlTagIdEnum[c.id];
|
||||
const converted = convertEbmlTagToComponent(c);
|
||||
if (IdMultiSet.has(c.id)) {
|
||||
if (obj[name]) {
|
||||
obj[name].push(converted);
|
||||
} else {
|
||||
obj[name] = [converted];
|
||||
}
|
||||
} else {
|
||||
obj[name] = converted;
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
if (tag.id === EbmlTagIdEnum.SimpleBlock || tag.id === EbmlTagIdEnum.Block) {
|
||||
return tag;
|
||||
}
|
||||
return tag.data;
|
||||
}
|
||||
|
||||
export function waitTick() {
|
||||
return new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
resolve();
|
||||
timeout && clearTimeout(timeout);
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
20
packages/matroska/tsconfig.json
Normal file
20
packages/matroska/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"paths": {
|
||||
"@konoplayer/core/*": [
|
||||
"../core/src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
packages/symphonia/Cargo.toml
Normal file
7
packages/symphonia/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "konoplayer-symphonia"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
symphonia = "0.5.4"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user