diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc4e491 --- /dev/null +++ b/.gitignore @@ -0,0 +1,190 @@ +# Created by https://www.toptal.com/developers/gitignore/api/linux,node,rust,react +# Edit at https://www.toptal.com/developers/gitignore?templates=linux,node,rust,react + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# 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 +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://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/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### react ### +.DS_* +**/*.backup.* +**/*.back.* + +node_modules + +*.sublime* + +psd +thumb +sketch + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# End of https://www.toptal.com/developers/gitignore/api/linux,node,rust,react + +certs \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9de8790 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "repos/moq-rs/third_party/mp4-rust"] + path = repos/moq-rs/third_party/mp4-rust + url = https://github.com/streaming-university/mp4-rust.git diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4451b51 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +export CAROOT ?= $(shell cd scripts; go run filippo.io/mkcert -CAROOT) + +.PHONY: pub-moq pub-dash + +dev: certs/localhost.crt + @docker compose stop + @docker compose rm -fsv + @docker container prune -f + @docker compose --profile dev up --build --remove-orphans --renew-anon-volumes + +prod: certs/localhost.crt + @docker compose stop + @docker compose rm -fsv + @docker container prune -f + @docker compose --profile prod up --build --remove-orphans --renew-anon-volumes + +pub-moq: + @scripts/pub-moq.sh --docker 1 --testsrc 1 + +pub-dash: + @scripts/pub-dash.sh --testsrc 1 + +certs/localhost.crt: + @git submodule update --init --recursive + @scripts/cert + @mkdir -p certs + @mv scripts/localhost.* certs/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..b439d7f --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Media-over-QUIC vs DASH + +## Development (no Docker) +This section is under construction. + +## Development using Docker + +You need to have [Docker](https://docs.docker.com/get-docker/), [Go](https://go.dev/), and [FFmpeg](https://ffmpeg.org/) installed in your machine. + +> Go has to be installed because we use it to install CA certificates in the docker image and your browser will trust the certificates generated by the server. + +> [!IMPORTANT] +> Generated certificates are only valid for 10 days. If you ever face SSL errors, you can regenerate the certificates by running `make certs/localhost.crt`. + +### Running the server + +```bash +make dev +``` + +After running the command above, you should be able to access the server at [https://localhost:5173](https://localhost:5173). + +#### Remarks: + +- With this command, any changes you make in `repos/demo` and `repos/moq-rs` will be reflected in the server. +- No need to setup CA yourself, or modify `/etc/hosts` to trick the browser into trusting the certificates. +- When a file related to `moq-pub` changes, at the same time `moq-relay` will restart. So you have to wait until `moq-pub` fails for it to restart itself. Otherwise you can just send SIGINT and restart `moq-pub` + +### Starting a stream + +```bash +make pub-moq +make pub-dash +``` + +These will run FFmpeg and supply the generated stream to moq (via pipe) and dash (via http). Look at `scripts/pub-moq` and `scripts/pub-dash` to see how we use FFmpeg to publish the stream. + +## Production + +### Building the server + +```bash +make prod +``` + +The only difference between `dev` and `prod` is that no live-reloading is enabled in the `prod` version. + +### Starting the stream + +This is the same as in development. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4ac26e3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,136 @@ +services: + demo-dev: + build: + context: repos/demo + dockerfile: Dockerfile.dev + command: run dev + container_name: demo + depends_on: + - relay-dev + entrypoint: ["npm"] + ports: + - "5173:5173" + profiles: + - dev + restart: on-failure + volumes: + - ./repos/demo/:/app/ + - /app/node_modules + - ./certs/localhost.crt:/etc/tls/cert:ro + - ./certs/localhost.key:/etc/tls/key:ro + - certs:/etc/ssl/certs + working_dir: /app + + demo-prod: + build: + context: repos/demo + dockerfile: Dockerfile.prod + container_name: demo + depends_on: + - relay-prod + ports: + - "5173:5173" + profiles: + - prod + restart: on-failure + volumes: + - ./certs/localhost.crt:/etc/tls/cert:ro + - ./certs/localhost.key:/etc/tls/key:ro + - certs:/etc/ssl/certs + + install-certs: + command: go run filippo.io/mkcert -install + environment: + CAROOT: /work/caroot + image: golang:latest + volumes: + - ${CAROOT:-.}:/work/caroot + - certs:/etc/ssl/certs + - ./repos/moq-rs/dev/go.mod:/work/go.mod:ro + - ./repos/moq-rs/dev/go.sum:/work/go.sum:ro + working_dir: /work + + publish-moq: + build: + context: repos/moq-rs + container_name: publish-moq + network_mode: host + profiles: + - publish + restart: on-failure + + dash-origin: + build: + context: repos/dash-origin + container_name: dash-origin + environment: + - NODE_ENV=production + ports: + - "8079:8079" + - "8080:8080" + profiles: + - dev + - prod + restart: on-failure + + relay-dev: + build: + context: repos/moq-rs + command: moq-relay --listen [::]:4443 --tls-cert /etc/tls/cert --tls-key /etc/tls/key --dev + container_name: relay + depends_on: + install-certs: + condition: service_completed_successfully + environment: + DEVELOPMENT: true + RUST_LOG: ${RUST_LOG:-debug} + ports: + - "4443:4443" + - 4443:4443/udp + profiles: + - dev + restart: on-failure + volumes: + - ./repos/moq-rs:/project + - ./certs/localhost.crt:/etc/tls/cert:ro + - ./certs/localhost.key:/etc/tls/key:ro + - certs:/etc/ssl/certs + + relay-prod: + build: + context: repos/moq-rs + command: moq-relay --listen [::]:4443 --tls-cert /etc/tls/cert --tls-key /etc/tls/key + container_name: relay + environment: + RUST_LOG: ${RUST_LOG:-debug} + network_mode: host + ports: + - "4443:4443" + - 4443:4443/udp + profiles: + - prod + restart: on-failure + volumes: + - ./certs/localhost.crt:/etc/tls/cert:ro + - ./certs/localhost.key:/etc/tls/key:ro + - certs:/etc/ssl/certs + + cockpit-server: + build: + context: repos/cockpit-server + cap_add: + - NET_ADMIN # Required for tc + container_name: cockpit-server + ports: + - "8000:8000" + profiles: + - dev + - prod + restart: on-failure + volumes: + - ./certs/localhost.crt:/app/cert/cert.pem:ro + - ./certs/localhost.key:/app/cert/key.pem:ro + - certs:/etc/ssl/certs + +volumes: + certs: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..99d9842 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8127 @@ +{ + "name": "moq-vs-dash", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "moq-vs-dash", + "version": "1.0.0", + "license": "ISC", + "workspaces": [ + "repos/**" + ] + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", + "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.8.tgz", + "integrity": "sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", + "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", + "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", + "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kixelated/moq": { + "resolved": "repos/demo/src/lib/moq", + "link": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@observablehq/plot": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@observablehq/plot/-/plot-0.6.13.tgz", + "integrity": "sha512-ebQS4ENodOy+O3WUjhqv9jNPZENAZRQMIdO3ziOlAKfUzSf69+gaFAqqc04SGrQA6JwJjPYnbfeN3YIpNsCF/A==", + "dependencies": { + "d3": "^7.8.0", + "interval-tree-1d": "^1.0.0", + "isoformat": "^0.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@opencensus/core": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.9.tgz", + "integrity": "sha512-31Q4VWtbzXpVUd2m9JS6HEaPjlKvNMOiF7lWKNmXF84yUcgfAFL5re7/hjDmdyQbOp32oGc+RFV78jXIldVz6Q==", + "dev": true, + "dependencies": { + "continuation-local-storage": "^3.2.1", + "log-driver": "^1.2.7", + "semver": "^5.5.0", + "shimmer": "^1.2.0", + "uuid": "^3.2.1" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@opencensus/core/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@opencensus/propagation-b3": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@opencensus/propagation-b3/-/propagation-b3-0.0.8.tgz", + "integrity": "sha512-PffXX2AL8Sh0VHQ52jJC4u3T0H6wDK6N/4bg7xh4ngMYOIi13aR1kzVvX1sVDBgfGwDOkMbl4c54Xm3tlPx/+A==", + "dev": true, + "dependencies": { + "@opencensus/core": "^0.0.8", + "uuid": "^3.2.1" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@opencensus/propagation-b3/node_modules/@opencensus/core": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.8.tgz", + "integrity": "sha512-yUFT59SFhGMYQgX0PhoTR0LBff2BEhPrD9io1jWfF/VDbakRfs6Pq60rjv0Z7iaTav5gQlttJCX2+VPxFWCuoQ==", + "dev": true, + "dependencies": { + "continuation-local-storage": "^3.2.1", + "log-driver": "^1.2.7", + "semver": "^5.5.0", + "shimmer": "^1.2.0", + "uuid": "^3.2.1" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@opencensus/propagation-b3/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.0.tgz", + "integrity": "sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@pm2/agent": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.0.3.tgz", + "integrity": "sha512-xkqqCoTf5VsciMqN0vb9jthW7olVAi4KRFNddCc7ZkeJZ3i8QwZANr4NSH2H5DvseRFHq7MiPspRY/EWAFWWTg==", + "dev": true, + "dependencies": { + "async": "~3.2.0", + "chalk": "~3.0.0", + "dayjs": "~1.8.24", + "debug": "~4.3.1", + "eventemitter2": "~5.0.1", + "fast-json-patch": "^3.0.0-1", + "fclone": "~1.0.11", + "nssocket": "0.6.0", + "pm2-axon": "~4.0.1", + "pm2-axon-rpc": "~0.7.0", + "proxy-agent": "~6.3.0", + "semver": "~7.5.0", + "ws": "~7.4.0" + } + }, + "node_modules/@pm2/agent/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@pm2/agent/node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "node_modules/@pm2/agent/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@pm2/agent/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@pm2/agent/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@pm2/agent/node_modules/dayjs": { + "version": "1.8.36", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.36.tgz", + "integrity": "sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==", + "dev": true + }, + "node_modules/@pm2/agent/node_modules/eventemitter2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", + "integrity": "sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==", + "dev": true + }, + "node_modules/@pm2/agent/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@pm2/agent/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/agent/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/agent/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@pm2/agent/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@pm2/io": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@pm2/io/-/io-5.0.2.tgz", + "integrity": "sha512-XAvrNoQPKOyO/jJyCu8jPhLzlyp35MEf7w/carHXmWKddPzeNOFSEpSEqMzPDawsvpxbE+i918cNN+MwgVsStA==", + "dev": true, + "dependencies": { + "@opencensus/core": "0.0.9", + "@opencensus/propagation-b3": "0.0.8", + "async": "~2.6.1", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "require-in-the-middle": "^5.0.0", + "semver": "~7.5.4", + "shimmer": "^1.2.0", + "signal-exit": "^3.0.3", + "tslib": "1.9.3" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@pm2/io/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/@pm2/io/node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "dev": true + }, + "node_modules/@pm2/io/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/io/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/io/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/@pm2/io/node_modules/tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, + "node_modules/@pm2/io/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@pm2/js-api": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@pm2/js-api/-/js-api-0.6.7.tgz", + "integrity": "sha512-jiJUhbdsK+5C4zhPZNnyA3wRI01dEc6a2GhcQ9qI38DyIk+S+C8iC3fGjcjUbt/viLYKPjlAaE+hcT2/JMQPXw==", + "dev": true, + "dependencies": { + "async": "^2.6.3", + "axios": "^0.21.0", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "ws": "^7.0.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@pm2/js-api/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/@pm2/js-api/node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "dev": true + }, + "node_modules/@pm2/pm2-version-check": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", + "integrity": "sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==", + "dev": true, + "dependencies": { + "debug": "^4.3.1" + } + }, + "node_modules/@remix-run/router": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.2.tgz", + "integrity": "sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", + "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", + "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", + "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", + "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", + "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", + "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", + "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", + "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", + "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", + "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", + "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", + "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", + "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", + "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", + "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", + "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true + }, + "node_modules/@types/audioworklet": { + "version": "0.0.52", + "resolved": "https://registry.npmjs.org/@types/audioworklet/-/audioworklet-0.0.52.tgz", + "integrity": "sha512-+C0QA8HJS07NjSdLFUDsSfUGJiLs+FPa6K7Tu/e76dqHEnuTOjAjDyiFOnZTuf9j4x9P8Nmv0OOfcMNYnGzbAQ==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/dom-mediacapture-transform": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.9.tgz", + "integrity": "sha512-/K96dASG23bqF+VAftybbI5SUj9qSsdsSKZglm7Bq/sIaEve5z8I+GdClARcSQMAAVkH7bc83UI1jiH/qc5LMw==", + "dev": true, + "dependencies": { + "@types/dom-webcodecs": "*" + } + }, + "node_modules/@types/dom-webcodecs": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.11.tgz", + "integrity": "sha512-yPEZ3z7EohrmOxbk/QTAa0yonMFkNkjnVXqbGb7D4rMr+F1dGQ8ZUFxXkyLLJuiICPejZ0AZE9Rrk9wUCczx4A==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.48", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz", + "integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.18", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", + "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.20.0.tgz", + "integrity": "sha512-fTwGQUnjhoYHeSF6m5pWNkzmDDdsKELYrOBxhjMrofPqCkoC2k3B2wvGHFxa1CTIqkEn88nlW1HVMztjo2K8Hg==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.20.0", + "@typescript-eslint/type-utils": "6.20.0", + "@typescript-eslint/utils": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.20.0.tgz", + "integrity": "sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.20.0", + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/typescript-estree": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.20.0.tgz", + "integrity": "sha512-p4rvHQRDTI1tGGMDFQm+GtxP1ZHyAh64WANVoyEcNMpaTFn3ox/3CcgtIlELnRfKzSs/DwYlDccJEtr3O6qBvA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.20.0.tgz", + "integrity": "sha512-qnSobiJQb1F5JjN0YDRPHruQTrX7ICsmltXhkV536mp4idGAYrIyr47zF/JmkJtEcAVnIz4gUYJ7gOZa6SmN4g==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.20.0", + "@typescript-eslint/utils": "6.20.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.20.0.tgz", + "integrity": "sha512-MM9mfZMAhiN4cOEcUOEx+0HmuaW3WBfukBZPCfwSqFnQy0grXYtngKCqpQN339X3RrwtzspWJrpbrupKYUSBXQ==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.20.0.tgz", + "integrity": "sha512-RnRya9q5m6YYSpBN7IzKu9FmLcYtErkDkc8/dKv81I9QiLLtVBHrjz+Ev/crAqgMNW2FCsoZF4g2QUylMnJz+g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/visitor-keys": "6.20.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.20.0.tgz", + "integrity": "sha512-/EKuw+kRu2vAqCoDwDCBtDRU6CTKbUmwwI7SH7AashZ+W+7o8eiyy6V2cdOqN49KsTcASWsC5QeghYuRDTyOOg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.20.0", + "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/typescript-estree": "6.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.20.0.tgz", + "integrity": "sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.20.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript/lib-dom": { + "name": "@types/web", + "version": "0.0.123", + "resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.123.tgz", + "integrity": "sha512-MUERvYn3bHYc5YBufbUTW4JaAKFbCLu2/IUQRtI0LACtsM6rwgLRnngjocq41ljLPD7M4xg5aTxJJq2yGWsplg==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", + "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.5", + "@babel/plugin-transform-react-jsx-self": "^7.23.3", + "@babel/plugin-transform-react-jsx-source": "^7.23.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/amp": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", + "integrity": "sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw==", + "dev": true + }, + "node_modules/amp-message": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/amp-message/-/amp-message-0.1.2.tgz", + "integrity": "sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg==", + "dev": true, + "dependencies": { + "amp": "0.3.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async-listener": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz", + "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==", + "dev": true, + "dependencies": { + "semver": "^5.3.0", + "shimmer": "^1.1.0" + }, + "engines": { + "node": "<=0.11.8 || >0.11.10" + } + }, + "node_modules/async-listener/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.17", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", + "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.22.2", + "caniuse-lite": "^1.0.30001578", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/basic-ftp": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.4.tgz", + "integrity": "sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bcp-47": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-1.0.8.tgz", + "integrity": "sha512-Y9y1QNBBtYtv7hcmoX0tR+tUNSFZGZ6OL6vKPObq8BbOhkCoyayF6ogfLTgAli/KuAEbsYHYUNq2AQuY6IuLag==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-1.0.3.tgz", + "integrity": "sha512-LggQ4YTdjWQSKELZF5JwchnBa1u0pIQSZf5lSdOHEdbVP55h0qICA/FUp3+W99q0xqxYa1ZQizTUH87gecII5w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-normalize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-1.1.1.tgz", + "integrity": "sha512-jWZ1Jdu3cs0EZdfCkS0UE9Gg01PtxnChjEBySeB+Zo6nkqtFfnvtoQQgP1qU1Oo4qgJgxhTI6Sf9y/pZIhPs0A==", + "dependencies": { + "bcp-47": "^1.0.0", + "bcp-47-match": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/binary-search-bounds": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", + "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==" + }, + "node_modules/bl": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.10.tgz", + "integrity": "sha512-F14DFhDZfxtVm2FY0k9kG2lWAwzZkO9+jX3Ytuoy/V0E1/5LBuBzzQHXAjqpxXEDIpmTPZZf5GVIGPQcLxFpaA==", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/blessed": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz", + "integrity": "sha512-LoF5gae+hlmfORcG1M5+5XZi4LBmvlXTzwJWzUlPryN/SJdSflZvROM2TwkT0GMpq7oqT48NRd4GS7BiVBc5OQ==", + "dev": true, + "bin": { + "blessed": "bin/tput.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/bodec": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bodec/-/bodec-0.1.0.tgz", + "integrity": "sha512-Ylo+MAo5BDUq1KA3f3R/MFhh+g8cnHmo8bz3YPGhI1znrMaf77ol1sfvYJzsw3nTE+Y2GryfDxBaR+AqpAkEHQ==", + "dev": true + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001580", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001580.tgz", + "integrity": "sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/charm": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/charm/-/charm-0.1.2.tgz", + "integrity": "sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ==", + "dev": true + }, + "node_modules/child_process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", + "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==" + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cli-tableau": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cli-tableau/-/cli-tableau-2.0.1.tgz", + "integrity": "sha512-he+WTicka9cl0Fg/y+YyxcN6/bfQ/1O3QmgxRXDhABKqLzvoOSM4fMzp39uMyLBulAFuywD2N7UaoQE7WaADxQ==", + "dev": true, + "dependencies": { + "chalk": "3.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/cli-tableau/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-tableau/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-tableau/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cli-tableau/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cli-tableau/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-tableau/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/cockpit-server": { + "resolved": "repos/cockpit-server", + "link": true + }, + "node_modules/codem-isoboxer": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/codem-isoboxer/-/codem-isoboxer-0.3.9.tgz", + "integrity": "sha512-4XOTqEzBWrGOZaMd+sTED2hLpzfBbiQCf1W6OBGkIHqk1D8uwy8WFLazVbdQwfDpQ+vf39lqTGPa9IhWW0roTA==" + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/continuation-local-storage": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", + "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==", + "dev": true, + "dependencies": { + "async-listener": "^0.6.0", + "emitter-listener": "^1.1.1" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/croner": { + "version": "4.1.97", + "resolved": "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz", + "integrity": "sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/culvert": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/culvert/-/culvert-0.1.2.tgz", + "integrity": "sha512-yi1x3EAWKjQTreYWeSd98431AV+IEE0qoDyOoaHJ7KJ21gv6HtBXHVLX74opVSGqcR8/AbjJBHAHpcOy2bj5Gg==", + "dev": true + }, + "node_modules/d3": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dash-origin": { + "resolved": "repos/dash-origin", + "link": true + }, + "node_modules/dashjs": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/dashjs/-/dashjs-4.7.4.tgz", + "integrity": "sha512-+hldo25QPP3H/NOwqUrvt4uKdMse60/Gsz9AUAnoYfhga8qHWq4nWiojUosOiigbigkDTCAn9ORcvUaKCvmfCA==", + "dependencies": { + "bcp-47-match": "^1.0.3", + "bcp-47-normalize": "^1.1.1", + "codem-isoboxer": "0.3.9", + "es6-promise": "^4.2.8", + "fast-deep-equal": "2.0.1", + "html-entities": "^1.2.1", + "imsc": "^1.1.5", + "localforage": "^1.7.1", + "path-browserify": "^1.0.1", + "ua-parser-js": "^1.0.37" + } + }, + "node_modules/dashjs/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.1.tgz", + "integrity": "sha512-MZd3VlchQkp8rdend6vrx7MmVDJzSNTBvghvKjirLkD+WTChA3KUf0jkE68Q4UyctNqI11zZO9/x2Yx+ub5Cvg==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.628", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.628.tgz", + "integrity": "sha512-2k7t5PHvLsufpP6Zwk0nof62yLOsCf032wZx7/q0mv8gwlXjhcxI3lz6f0jBr0GrnWKcm3burXzI3t5IrcdUxw==", + "dev": true + }, + "node_modules/emitter-listener": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", + "dev": true, + "dependencies": { + "shimmer": "^1.2.0" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.5.tgz", + "integrity": "sha512-D53FYKJa+fDmZMtriODxvhwrO+IOqrxoEo21gMA0sjHdU6dPVH4OhyFip9ypl8HOF5RV5KdTo+rBQLvnY2cO8w==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-loops": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.4.tgz", + "integrity": "sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg==", + "license": "MIT" + }, + "node_modules/fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" + }, + "node_modules/fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==" + }, + "node_modules/fastq": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fclone": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz", + "integrity": "sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==", + "dev": true + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-uri": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.2.tgz", + "integrity": "sha512-5KLucCJobh8vBY1K07EFV4+cPZH3mrV9YeAruUseCQKHB58SGjjT2l9/eA9LD082IiuMjSlFJEcdJ27TXvbZNw==", + "dev": true, + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.0", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/get-uri/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/get-uri/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/git-node-fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/git-node-fs/-/git-node-fs-1.0.0.tgz", + "integrity": "sha512-bLQypt14llVXBg0S0u8q8HmU7g9p3ysH+NvVlae5vILuUvs759665HvmR5+wb04KjHyjFcDRxdYb4kyNnluMUQ==", + "dev": true + }, + "node_modules/git-sha1": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/git-sha1/-/git-sha1-0.1.2.tgz", + "integrity": "sha512-2e/nZezdVlyCopOCYHeW0onkbZg7xP1Ad6pndPy1rCygeRykefUS6r7oA5cJRGEFvseiaz5a/qUHFVX1dd6Isg==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-entities": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", + "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imsc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/imsc/-/imsc-1.1.5.tgz", + "integrity": "sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==", + "dependencies": { + "sax": "1.2.1" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/inline-style-prefixer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.0.tgz", + "integrity": "sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ==", + "dependencies": { + "css-in-js-utils": "^3.1.0", + "fast-loops": "^1.1.3" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/interval-tree-1d": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/interval-tree-1d/-/interval-tree-1d-1.0.4.tgz", + "integrity": "sha512-wY8QJH+6wNI0uh4pDQzMvl+478Qh7Rl4qLmqiluxALlNvl+I+o5x38Pw3/z7mDPTPS1dQalZJXsmbvxx5gclhQ==", + "dependencies": { + "binary-search-bounds": "^2.0.0" + } + }, + "node_modules/ip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", + "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", + "dev": true + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isoformat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/isoformat/-/isoformat-0.2.1.tgz", + "integrity": "sha512-tFLRAygk9NqrRPhJSnNGh7g7oaVWDwR0wKh/GM2LgmPa50Eg4UfyaCO4I8k6EqJHl1/uh2RAD6g06n5ygEnrjQ==" + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, + "node_modules/js-git": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/js-git/-/js-git-0.7.8.tgz", + "integrity": "sha512-+E5ZH/HeRnoc/LW0AmAyhU+mNcWBzAKE+30+IDMLSLbbK+Tdt02AdkOKq9u15rlJsDEGFqtgckc8ZM59LhhiUA==", + "dev": true, + "dependencies": { + "bodec": "^0.1.0", + "culvert": "^0.1.2", + "git-sha1": "^0.1.2", + "pako": "^0.2.5" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/lazy/-/lazy-1.0.11.tgz", + "integrity": "sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA==", + "dev": true, + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true, + "engines": { + "node": ">=0.8.6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mhv-2024-moqt-demo": { + "resolved": "repos/demo", + "link": true + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==", + "dev": true + }, + "node_modules/mp4box": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/mp4box/-/mp4box-0.5.2.tgz", + "integrity": "sha512-zRmGlvxy+YdW3Dmt+TR4xPHynbxwXtAQDTN/Fo9N3LMxaUlB2C5KmZpzYyGKy4c7k4Jf3RCR0A2pm9SZELOLXw==" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nano-css": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.1.tgz", + "integrity": "sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "css-tree": "^1.1.2", + "csstype": "^3.1.2", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^7.0.0", + "rtl-css-js": "^1.16.1", + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/needle": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "dev": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/needle/node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nssocket": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/nssocket/-/nssocket-0.6.0.tgz", + "integrity": "sha512-a9GSOIql5IqgWJR3F/JXG4KpJTA3Z53Cj0MeMvGpglytB1nxE4PdFNC0jINe27CS7cGivoynwc054EzCcT3M3w==", + "dev": true, + "dependencies": { + "eventemitter2": "~0.4.14", + "lazy": "~1.0.11" + }, + "engines": { + "node": ">= 0.10.x" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", + "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", + "dev": true, + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "pac-resolver": "^7.0.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.0.tgz", + "integrity": "sha512-Fd9lT9vJbHYRACT8OhCbZBbxr6KRSawSovFpy8nDGshaK99S/EBhVIHp9+crhxrsZOuvLpgL1n23iyPg6Rl2hg==", + "dev": true, + "dependencies": { + "degenerator": "^5.0.0", + "ip": "^1.1.8", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidusage": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-3.0.2.tgz", + "integrity": "sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pm2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pm2/-/pm2-5.3.0.tgz", + "integrity": "sha512-xscmQiAAf6ArVmKhjKTeeN8+Td7ZKnuZFFPw1DGkdFPR/0Iyx+m+1+OpCdf9+HQopX3VPc9/wqPQHqVOfHum9w==", + "dev": true, + "dependencies": { + "@pm2/agent": "~2.0.0", + "@pm2/io": "~5.0.0", + "@pm2/js-api": "~0.6.7", + "@pm2/pm2-version-check": "latest", + "async": "~3.2.0", + "blessed": "0.1.81", + "chalk": "3.0.0", + "chokidar": "^3.5.3", + "cli-tableau": "^2.0.0", + "commander": "2.15.1", + "croner": "~4.1.92", + "dayjs": "~1.11.5", + "debug": "^4.3.1", + "enquirer": "2.3.6", + "eventemitter2": "5.0.1", + "fclone": "1.0.11", + "mkdirp": "1.0.4", + "needle": "2.4.0", + "pidusage": "~3.0", + "pm2-axon": "~4.0.1", + "pm2-axon-rpc": "~0.7.1", + "pm2-deploy": "~1.0.2", + "pm2-multimeter": "^0.1.2", + "promptly": "^2", + "semver": "^7.2", + "source-map-support": "0.5.21", + "sprintf-js": "1.1.2", + "vizion": "~2.2.1", + "yamljs": "0.3.0" + }, + "bin": { + "pm2": "bin/pm2", + "pm2-dev": "bin/pm2-dev", + "pm2-docker": "bin/pm2-docker", + "pm2-runtime": "bin/pm2-runtime" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "pm2-sysmonit": "^1.2.8" + } + }, + "node_modules/pm2-axon": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pm2-axon/-/pm2-axon-4.0.1.tgz", + "integrity": "sha512-kES/PeSLS8orT8dR5jMlNl+Yu4Ty3nbvZRmaAtROuVm9nYYGiaoXqqKQqQYzWQzMYWUKHMQTvBlirjE5GIIxqg==", + "dev": true, + "dependencies": { + "amp": "~0.3.1", + "amp-message": "~0.1.1", + "debug": "^4.3.1", + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=5" + } + }, + "node_modules/pm2-axon-rpc": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/pm2-axon-rpc/-/pm2-axon-rpc-0.7.1.tgz", + "integrity": "sha512-FbLvW60w+vEyvMjP/xom2UPhUN/2bVpdtLfKJeYM3gwzYhoTEEChCOICfFzxkxuoEleOlnpjie+n1nue91bDQw==", + "dev": true, + "dependencies": { + "debug": "^4.3.1" + }, + "engines": { + "node": ">=5" + } + }, + "node_modules/pm2-axon/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pm2-deploy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pm2-deploy/-/pm2-deploy-1.0.2.tgz", + "integrity": "sha512-YJx6RXKrVrWaphEYf++EdOOx9EH18vM8RSZN/P1Y+NokTKqYAca/ejXwVLyiEpNju4HPZEk3Y2uZouwMqUlcgg==", + "dev": true, + "dependencies": { + "run-series": "^1.1.8", + "tv4": "^1.3.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pm2-multimeter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/pm2-multimeter/-/pm2-multimeter-0.1.2.tgz", + "integrity": "sha512-S+wT6XfyKfd7SJIBqRgOctGxaBzUOmVQzTAS+cg04TsEUObJVreha7lvCfX8zzGVr871XwCSnHUU7DQQ5xEsfA==", + "dev": true, + "dependencies": { + "charm": "~0.1.1" + } + }, + "node_modules/pm2-sysmonit": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/pm2-sysmonit/-/pm2-sysmonit-1.2.8.tgz", + "integrity": "sha512-ACOhlONEXdCTVwKieBIQLSi2tQZ8eKinhcr9JpZSUAL8Qy0ajIgRtsLxG/lwPOW3JEKqPyw/UaHmTWhUzpP4kA==", + "dev": true, + "optional": true, + "dependencies": { + "async": "^3.2.0", + "debug": "^4.3.1", + "pidusage": "^2.0.21", + "systeminformation": "^5.7", + "tx2": "~1.0.4" + } + }, + "node_modules/pm2-sysmonit/node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true, + "optional": true + }, + "node_modules/pm2-sysmonit/node_modules/pidusage": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.21.tgz", + "integrity": "sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA==", + "dev": true, + "optional": true, + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pm2/node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "node_modules/pm2/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/pm2/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/pm2/node_modules/commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "node_modules/pm2/node_modules/eventemitter2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", + "integrity": "sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==", + "dev": true + }, + "node_modules/pm2/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pm2/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pm2/node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + }, + "node_modules/pm2/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", + "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.11.tgz", + "integrity": "sha512-AvI/DNyMctyyxGOjyePgi/gqj5hJYClZ1avtQvLlqMT3uDZkRbi4HhGUpok3DRzv9z7Lti85Kdj3s3/1CeNI0w==", + "dev": true, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "prettier-plugin-twig-melody": { + "optional": true + } + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/promptly": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/promptly/-/promptly-2.2.0.tgz", + "integrity": "sha512-aC9j+BZsRSSzEsXBNBwDnAxujdx19HycZoKgRgzWnS8eOHg1asuf9heuLprfbe739zY3IdUQx+Egv6Jn135WHA==", + "dev": true, + "dependencies": { + "read": "^1.0.4" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.3.tgz", + "integrity": "sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg==", + "dependencies": { + "@remix-run/router": "1.14.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.3.tgz", + "integrity": "sha512-kNzubk7n4YHSrErzjLK72j0B5i969GsuCGazRl3G6j1zqZBLjuSlYBdVdkDOgzGdPIffUOc9nmgiadTEVoq91g==", + "dependencies": { + "@remix-run/router": "1.14.2", + "react-router": "6.21.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "peerDependencies": { + "react": "*", + "tslib": "*" + } + }, + "node_modules/react-use": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.5.0.tgz", + "integrity": "sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==", + "dependencies": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.6.1", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz", + "integrity": "sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, + "node_modules/rollup": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", + "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.17.2", + "@rollup/rollup-android-arm64": "4.17.2", + "@rollup/rollup-darwin-arm64": "4.17.2", + "@rollup/rollup-darwin-x64": "4.17.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", + "@rollup/rollup-linux-arm-musleabihf": "4.17.2", + "@rollup/rollup-linux-arm64-gnu": "4.17.2", + "@rollup/rollup-linux-arm64-musl": "4.17.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", + "@rollup/rollup-linux-riscv64-gnu": "4.17.2", + "@rollup/rollup-linux-s390x-gnu": "4.17.2", + "@rollup/rollup-linux-x64-gnu": "4.17.2", + "@rollup/rollup-linux-x64-musl": "4.17.2", + "@rollup/rollup-win32-arm64-msvc": "4.17.2", + "@rollup/rollup-win32-ia32-msvc": "4.17.2", + "@rollup/rollup-win32-x64-msvc": "4.17.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/run-series": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-series/-/run-series-1.1.9.tgz", + "integrity": "sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "dependencies": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==", + "engines": { + "node": ">=6.9" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "dev": true + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dev": true, + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", + "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks/node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "dev": true + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + }, + "node_modules/stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + } + }, + "node_modules/stacktrace-gps/node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", + "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/systeminformation": { + "version": "5.21.22", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.21.22.tgz", + "integrity": "sha512-gNHloAJSyS+sKWkwvmvozZ1eHrdVTEsynWMTY6lvLGBB70gflkBQFw8drXXr1oEXY84+Vr9tOOrN8xHZLJSycA==", + "dev": true, + "optional": true, + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", + "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/terser": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", + "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==" + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tv4": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", + "integrity": "sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/tx2": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tx2/-/tx2-1.0.5.tgz", + "integrity": "sha512-sJ24w0y03Md/bxzK4FU8J8JveYYUbSs2FViLJ2D/8bytSiyPRbuE3DyL/9UKYXTZlV3yXq0L8GLlhobTnekCVg==", + "dev": true, + "optional": true, + "dependencies": { + "json-stringify-safe": "^5.0.1" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", + "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vizion": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vizion/-/vizion-2.2.1.tgz", + "integrity": "sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==", + "dev": true, + "dependencies": { + "async": "^2.6.3", + "git-node-fs": "^1.0.0", + "ini": "^1.3.5", + "js-git": "^0.7.8" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/vizion/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "repos/cockpit-server": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "child_process": "^1.0.2", + "express": "^4.18.2", + "ws": "^8.16.0" + } + }, + "repos/cockpit-server/node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "repos/dash-origin": { + "dependencies": { + "bl": "^6.0.9", + "yargs": "^17.7.2" + }, + "devDependencies": { + "pm2": "^5.3.0" + } + }, + "repos/demo": { + "name": "mhv-2024-moqt-demo", + "version": "0.0.0", + "workspaces": [ + "src/lib/moq" + ], + "dependencies": { + "@observablehq/plot": "^0.6.13", + "clsx": "^2.1.0", + "dashjs": "^4.7.4", + "lodash": "^4.17.21", + "mp4box": "^0.5.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.3", + "react-use": "^17.5.0", + "tslib": "^2.6.2" + }, + "devDependencies": { + "@types/lodash": "^4.14.202", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@typescript-eslint/eslint-plugin": "^6.20.0", + "@typescript-eslint/parser": "^6.20.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.17", + "eslint": "^8.56.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.33", + "prettier": "^3.2.4", + "prettier-plugin-tailwindcss": "^0.5.11", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3", + "vite": "^5.0.12" + } + }, + "repos/demo/src/lib/moq": { + "name": "@kixelated/moq", + "version": "0.1.4", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "mp4box": "^0.5.2" + }, + "devDependencies": { + "@types/audioworklet": "^0.0.52", + "@types/dom-mediacapture-transform": "^0.1.9", + "@types/dom-webcodecs": "^0.1.11", + "@typescript-eslint/eslint-plugin": "^6.13.1", + "@typescript-eslint/parser": "^6.13.1", + "@typescript/lib-dom": "npm:@types/web@^0.0.123", + "eslint": "^8.54.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "prettier": "^3.1.0", + "typescript": "^4.9.5" + } + }, + "repos/demo/src/lib/moq/node_modules/typescript": { + "version": "4.9.5", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a04af4a --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "moq-vs-dash", + "version": "1.0.0", + "description": "", + "workspaces": [ + "repos/**" + ], + "scripts": {}, + "author": "", + "license": "ISC" +} diff --git a/repos/cockpit-server/.gitignore b/repos/cockpit-server/.gitignore new file mode 100644 index 0000000..44110b5 --- /dev/null +++ b/repos/cockpit-server/.gitignore @@ -0,0 +1,3 @@ +node_modules +.DS_Store +tc/.env diff --git a/repos/cockpit-server/Dockerfile b/repos/cockpit-server/Dockerfile new file mode 100644 index 0000000..cca9c37 --- /dev/null +++ b/repos/cockpit-server/Dockerfile @@ -0,0 +1,17 @@ +FROM node:21 + +WORKDIR /app + +# Install iptables and iproute2 +RUN apt-get update && \ + apt-get install -y --no-install-recommends iptables iproute2 bc && \ + rm -rf /var/lib/apt/lists/* + +COPY package.json . + +RUN npm install + +COPY . . + +# Run the server when the container launches +CMD ["node", "server.js", "--verbose"] diff --git a/repos/cockpit-server/package-lock.json b/repos/cockpit-server/package-lock.json new file mode 100644 index 0000000..e26c07b --- /dev/null +++ b/repos/cockpit-server/package-lock.json @@ -0,0 +1,685 @@ +{ + "name": "cockpit-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cockpit-server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "child_process": "^1.0.2", + "express": "^4.18.2", + "ws": "^8.16.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/child_process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", + "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "dependencies": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/repos/cockpit-server/package.json b/repos/cockpit-server/package.json new file mode 100644 index 0000000..504cfec --- /dev/null +++ b/repos/cockpit-server/package.json @@ -0,0 +1,17 @@ +{ + "name": "cockpit-server", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "child_process": "^1.0.2", + "express": "^4.18.2", + "ws": "^8.16.0" + } +} diff --git a/repos/cockpit-server/server.js b/repos/cockpit-server/server.js new file mode 100644 index 0000000..ac37e22 --- /dev/null +++ b/repos/cockpit-server/server.js @@ -0,0 +1,132 @@ +const https = require("https"); +const fs = require("fs"); +// http version +// const WebSocket = require("ws"); +const { WebSocketServer } = require("ws"); +const { join } = require("path"); +const { exec } = require("child_process"); + +const BASH_PATH = "/bin/bash"; + +const httpsServer = https.createServer({ + cert: fs.readFileSync("cert/cert.pem"), + key: fs.readFileSync("cert/key.pem"), +}); + +// http version +// const wss = new WebSocket.Server({ port: 8000 }); + +const currentBandwidthLimits = { + dash: [], + moq: [], +}; + +const wss = new WebSocketServer({ server: httpsServer }); + +wss.on("connection", function connection(ws, req) { + ws.on("message", function incoming(rawMessage) { + try { + // Convert message to string if it's not already + const message = rawMessage.toString(); + + let client_ip; + if (!req.headers["x-forwarded-for"]) { + // there is no reverse proxy + client_ip = req.socket.remoteAddress; + } else { + client_ip = req.headers["x-forwarded-for"].split(/\s*,\s*/)[0]; + } + // fix for ipv6 + // In order to run iptables scripts correctly, we need to remove the ipv6 prefix + // more info: https://stackoverflow.com/a/33790357/195124 (Express.js req.ip is returning ::ffff:127.0.0.1) + if (client_ip.startsWith('::ffff:')) { + client_ip = client_ip.substring(7) + } + + console.log("received:", message); + console.log("from:", client_ip); + const [command, serverType, rate] = message.split(" "); + ws.send(`Command: ${command}, Server Type: ${serverType}, Rate: ${rate}`); + + if (command === "set" && serverType && rate) { + handleBandwidthLimit(serverType, rate, ws, client_ip); + currentBandwidthLimits[serverType].push({ + limit: rate, + updatedAt: new Date().toLocaleTimeString(), + }); + } else if (command === "clear" && serverType) { + ws.send(`Clearing bandwidth limit on ${serverType}`); + clearBandwidthLimit(serverType, ws, client_ip); + currentBandwidthLimits[serverType].push({ + limit: Math.max, + updatedAt: new Date().toLocaleTimeString(), + }); + } else if (command === "get" && serverType) { + // server type can be "all" + ws.send(JSON.stringify(getBandwidthLimits(serverType))); + } else { + ws.send("Error: Invalid message format."); + } + } catch (error) { + console.error("Error handling message:", error); + ws.send(`Error: ${error.message}`); + } + }); + + ws.send("Connected to Cockpit server"); +}); + +function handleBandwidthLimit(serverType, rate, ws, client_ip) { + // serverType is either "dash" or "moq" + const scriptPath = join(__dirname, "tc", "set_bandwidth.sh"); + const command = `${BASH_PATH} ${scriptPath} ${serverType} ${rate} ${client_ip}`; + + exec(command, (error, stdout, stderr) => { + if (error) { + console.error(`Error executing script: ${error.message}`); + ws.send(`Error: ${error.message}`); + return; + } + if (stderr) { + console.error(`Stderr from script: ${stderr}`); + ws.send(`Error: ${stderr}`); + return; + } + + console.log(`Stdout from script: ${stdout}`); + ws.send(`Bandwidth set to ${rate}Mbps on ${serverType}`); + }); +} + +function clearBandwidthLimit(serverType, ws, client_ip) { + const scriptPath = join(__dirname, "tc", "set_bandwidth.sh"); + const command = `${BASH_PATH} ${scriptPath} ${serverType} 0 ${client_ip} del`; + + exec(command, (error, stdout, stderr) => { + if (error) { + console.error(`Error executing script: ${error.message}`); + ws.send(`Error: ${error.message}`); + return; + } + if (stderr) { + console.error(`Stderr from script: ${stderr}`); + ws.send(`Error: ${stderr}`); + return; + } + console.log(`Stdout from script: ${stdout}`); + ws.send(`Bandwidth cleared on ${serverType}`); + }); +} + +function getBandwidthLimits(serverType) { + if (serverType === "all") { + return currentBandwidthLimits; + } else { + return currentBandwidthLimits[serverType] || []; + } +} + +const port = process.env.PORT || 8000; +httpsServer.listen(port, function () { + console.log(`Cockpit WebSocket server listening on ${port}`); +}); diff --git a/repos/cockpit-server/tc/README.md b/repos/cockpit-server/tc/README.md new file mode 100644 index 0000000..ad940cf --- /dev/null +++ b/repos/cockpit-server/tc/README.md @@ -0,0 +1,45 @@ +# Traffic Control Scripts + +## Scripts used by cockpit-server + +- set_bandwidth.sh +- delete_iptable_rule.sh + +### Setting up bandwidth limit on an interface for moq mode + +The name of the interface is hard-coded in the script. Mode can be moq or dash. + +```bash +./set_bandwidth.sh moq 1000000 +``` + +### Deleting bandwidth limit for moq mode + +```bash +./set_bandwidth.sh moq 0 del +``` + +### Clear all rules on interface wlo1 + +```bash +./clear_all.sh wlo1 +``` + +## Some useful commands + +### Watching class traffic on interface wlo1 + +```bash +watch /sbin/tc -s -d class show dev wlo1 +``` + +### Checking qdisc, class, filter, and iptables rules + +```bash +tc filter show dev wlo1 +tc class show dev wlo1 +tc qdisc show dev wlo1 +iptables -L OUTPUT -t mangle -n -v +`````` + +To analyze the traffic on interfaces, `iptraf` can be used. diff --git a/repos/cockpit-server/tc/clear_all.sh b/repos/cockpit-server/tc/clear_all.sh new file mode 100755 index 0000000..ec1c91e --- /dev/null +++ b/repos/cockpit-server/tc/clear_all.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +TC='/sbin/tc' +IPTABLES="/usr/sbin/iptables" + +SCRIPT=$(realpath "$0") +CURRENT_DIR=$(dirname "$SCRIPT") + +# read interface from command line argument +INTERFACE_1=$1 + +if [ -z $INTERFACE_1 ]; then + # if no interface is provided, read from .env file + if [ -f "$CURRENT_DIR/.env" ]; then + source $CURRENT_DIR/.env + fi + INTERFACE_1=$INTERFACE + + if [ -z $INTERFACE_1 ]; then + echo "interface has to be specified" + exit 1 + fi +fi + +killall sleep 1>/dev/null 2>&1 +killall tc 1>/dev/null 2>&1 + +$TC qdisc del dev $INTERFACE_1 root 1:0 1>/dev/null 2>&1 +$TC qdisc del dev $INTERFACE_1 root 1>/dev/null 2>&1 + +$IPTABLES -F OUTPUT -t mangle 1>/dev/null 2>&1 diff --git a/repos/cockpit-server/tc/delete_iptable_rule.sh b/repos/cockpit-server/tc/delete_iptable_rule.sh new file mode 100755 index 0000000..d8d4a98 --- /dev/null +++ b/repos/cockpit-server/tc/delete_iptable_rule.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# This script deletes the iptables rule for given protocol, port, and mark. + +IPTABLES="/usr/sbin/iptables" + +DEST_ADDRESS="$1" +PORT="$2" +PROTO="$3" # udp or tcp +MARK="$4" # eg. 10 + +if [ -z $PORT ] || [ -z $PROTO ] || [ -z $MARK ]; then + echo "Usage: ./delete_iptable_rule.sh " + exit 1 +fi + +# just remove the iptables rule +# -n for numeric output, -L for list, -t for table, -p for protocol, +#-sport for source port, -j for jump, -A for append, -D for delete +if $IPTABLES -n -L OUTPUT -t mangle | grep -q -E "MARK.+$DEST_ADDRESS.+$PROTO.+$PORT"; then + $IPTABLES -D OUTPUT -t mangle -p $PROTO --sport $PORT -d $DEST_ADDRESS -j MARK --set-mark $MARK + echo "iptables rule deleted for $PROTO on destination $DEST_ADDRESS and port $PORT" +fi diff --git a/repos/cockpit-server/tc/set_bandwidth.sh b/repos/cockpit-server/tc/set_bandwidth.sh new file mode 100755 index 0000000..2f8fa7f --- /dev/null +++ b/repos/cockpit-server/tc/set_bandwidth.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +usage() { + echo "Usage: ./set_bandwidth.sh " + echo "mode can be dash or moq" + echo "op can be set or del" + exit 1 +} + +SCRIPT=$(realpath "$0") +CURRENT_DIR=$(dirname "$SCRIPT") + +echo "Current dir: $CURRENT_DIR" + +# load environment variables like INTERFACE +if [ -f "$CURRENT_DIR/.env" ]; then + source $CURRENT_DIR/.env +fi + +if [ -z "$INTERFACE" ]; then + echo "Please set the INTERFACE variable in .env file" + exit 1 +fi + +TC="/sbin/tc" +IPTABLES="/usr/sbin/iptables" + +MODE="$1" +RATE="$2" # Bps +DEST_ADDRESS="$3" +OP=${4:-"set"} + +if [ -z "$MODE" ] || [ -z "$RATE" ]; then + usage +fi + +if [[ $MODE == "dash" ]]; then + INTERFACE_1=$INTERFACE # change this according to your interface used for dash streaming + PORT="8080" + PROTO="tcp" + FLOW_ID="1:11" + MARK="11" +elif [[ $MODE == "moq" ]]; then + INTERFACE_1=$INTERFACE # change this according to your interface used for moq streaming + PORT="4443" + PROTO="udp" + FLOW_ID="1:10" + MARK="10" +else + usage +fi + +if [[ $OP == "set" ]]; then + echo "Setting bandwidth limit" + # call tc_qdisc.sh with the given rate and ceiling + $CURRENT_DIR/tc_qdisc.sh $RATE $INTERFACE_1 $DEST_ADDRESS $PORT $PROTO $FLOW_ID $MARK +elif [[ $OP == "del" ]]; then + echo "Deleting bandwidth limit" + $CURRENT_DIR/delete_iptable_rule.sh $DEST_ADDRESS $PORT $PROTO $MARK +else + usage +fi diff --git a/repos/cockpit-server/tc/tc_qdisc.sh b/repos/cockpit-server/tc/tc_qdisc.sh new file mode 100755 index 0000000..7ad8f4a --- /dev/null +++ b/repos/cockpit-server/tc/tc_qdisc.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +# This script uses tc to set up a qdisc for a given interface and port. +# It also sets up iptables rules to mark packets for the qdisc. +# The qdisc is set up with a rate and a ceiling. +# The rate is the guaranteed bandwidth for the qdisc. +# The ceiling is the maximum bandwidth for the qdisc. +# The qdisc is set up with a flow id. + +# To configure iptables to count incoming and outgoing traffic from/to given IP +# iptables -I INPUT 1 -s -j ACCEPT +# iptables -I OUTPUT 1 -d -j ACCEPT + +TC="/sbin/tc" +IPTABLES="/usr/sbin/iptables" + +RATE="$1" # bps +INTERFACE_1="$2" +DEST_ADDRESS="$3" +PORT="$4" +PROTO="$5" # udp or tcp +FLOW_ID="$6" # eg. 1:10 +MARK="$7" # eg. 10 + +CEIL=$(echo "$RATE*1.1" | bc) # bps + +if [ -z $INTERFACE_1 ] || [ -z $DEST_ADDRESS ] || [ -z $PORT ] || [ -z $RATE ] || [ -z $CEIL ]; then + echo "Usage: ./tc_qdisc.sh " + exit 1 +fi + +# create a new queuing discipline +if $TC qdisc show dev $INTERFACE_1 | grep -q "qdisc htb 1:"; then + echo "qdisc already exists" +else + echo "adding qdisc" + echo "$TC qdisc add dev $INTERFACE_1 root handle 1: htb" + $TC qdisc add dev $INTERFACE_1 root handle 1: htb +fi + +if $TC class show dev $INTERFACE_1 | grep -q "class htb $FLOW_ID"; then + echo "class already exists, updating rate and ceiling" + echo "$TC class change dev $INTERFACE_1 parent 1: classid $FLOW_ID htb rate $RATE ceil $CEIL prio 0 burst 15k quantum 1514" + $TC class change dev $INTERFACE_1 parent 1: classid $FLOW_ID htb rate $RATE ceil $CEIL prio 0 burst 15k quantum 1514 +else + echo "adding class" + echo "$TC class add dev $INTERFACE_1 parent 1: classid $FLOW_ID htb rate $RATE ceil $CEIL prio 0 burst 15k quantum 1514" + $TC class add dev $INTERFACE_1 parent 1: classid $FLOW_ID htb rate $RATE ceil $CEIL prio 0 burst 15k quantum 1514 + # for stochastic fair queueing the following can be created as well + # echo $TC qdisc add dev $INTERFACE_1 parent $FLOW_ID handle 10: sfq perturb 10 + # $TC qdisc add dev $INTERFACE_1 parent $FLOW_ID handle 10: sfq perturb 10 +fi +if $TC filter show dev $INTERFACE_1 | grep -q "classid $FLOW_ID"; then + echo "filter already exists" +else + echo "adding filter" + echo "$TC filter add dev $INTERFACE_1 parent 1: prio 0 protocol ip handle $MARK fw flowid $FLOW_ID" + $TC filter add dev $INTERFACE_1 parent 1: prio 0 protocol ip handle $MARK fw flowid $FLOW_ID + +fi + +# -n for numeric output, -L for list, -t for table, -p for protocol, +#-sport for source port, -j for jump, -A for append, -D for delete +if $IPTABLES -n -L OUTPUT -t mangle | grep -q -E "MARK.+$DEST_ADDRESS.+$PROTO.+$PORT"; then + echo "iptables rule already exists for $PROTO on destination $DEST_ADDRESS and port $PORT" +else + echo "adding iptables rule" + echo "$IPTABLES -A OUTPUT -t mangle -p $PROTO --sport $PORT -d $DEST_ADDRESS -j MARK --set-mark $MARK" + $IPTABLES -A OUTPUT -t mangle -p $PROTO --sport $PORT -d $DEST_ADDRESS -j MARK --set-mark $MARK +fi diff --git a/repos/dash-origin/.gitignore b/repos/dash-origin/.gitignore new file mode 100644 index 0000000..4bb5645 --- /dev/null +++ b/repos/dash-origin/.gitignore @@ -0,0 +1,3 @@ +node_modules +.DS_Store +data/live diff --git a/repos/dash-origin/Dockerfile b/repos/dash-origin/Dockerfile new file mode 100644 index 0000000..7d2fb6c --- /dev/null +++ b/repos/dash-origin/Dockerfile @@ -0,0 +1,15 @@ +FROM node:21 + +WORKDIR /app + +# Copy the current directory contents into the container at /app +COPY . . + +# Create the live output directory +RUN mkdir -p /app/data/live + +# Install any needed packages specified in package.json +RUN npm install + +# Run the server when the container launches +CMD ["node", "origin.js", "--data", "/app/data", "--verbose"] \ No newline at end of file diff --git a/repos/dash-origin/LICENSE b/repos/dash-origin/LICENSE new file mode 100644 index 0000000..4e9b16a --- /dev/null +++ b/repos/dash-origin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Thilo Borgmann < thilo _at_ fflabs.eu > + +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. diff --git a/repos/dash-origin/README.md b/repos/dash-origin/README.md new file mode 100644 index 0000000..18306f4 --- /dev/null +++ b/repos/dash-origin/README.md @@ -0,0 +1,47 @@ +Dash Low-Latency Origin Server (Demo) + +# Summary +Origin server to serve live ingests. + +Receives all ingests and serves from disk or memory/cache. +All data served is send in HTTP/1.1 chunked encoding mode. + +NOT SECURE, DO NOT USE FOR PRODUCTION. DO NOT LET THIS SERVER LEAVE YOUR VPN. +ALL MACHINES THIS SERVER TALKS TO HAVE TO BE TRUSTWORTHY. YOU HAVE BEEN WARNED. + +# License +MIT license. + +# Installation +## Requirements +### Origin Server (this software) +* node.js version >= 8 +* node.js modules "bl" and "yargs" +* dash.js player version >= 2.9.3 + * Can be found here: https://github.com/Dash-Industry-Forum/dash.js +### Ingest Server +* FFmpeg newer or equal to commit e7c04eaf + * Can be found here: https://git.ffmpeg.org/ffmpeg.git + +## Setup +### Origin Server (this software) +* Clone the repository into a directory (refered as ${ROOT}) +* Clone the dash.js repository into some directory (refered as ${DASHJS}) and build the debug version +* Choose/create a main data directory as the web root for the server (refered as ${DATAROOT}, e.g. ${ROOT}/data) +* Copy ${ROOT}/data/live.html into ${DATAROOT} +* Create a symlink to the dash.js player "dist" directory in ${DATAROOT} (${DATAROOT}/dist => ${DASHJS}/dist) +* Create an empty folder ${DATAROOT}/live for storing & serving ingested data +* Go to ${ROOT} and install the required modules via "npm install ${MODULE}" + * See [Requirements] for the modules to install + * This will create and populate a new directory in ${ROOT}/node_modules +* Start the origin server (node origin.js -h) to get a list of command line options and default values +* Run the origin server with the options you need + +### Ingest Server +* Clone the FFmpeg repository and build + * for libx264 support, install libx264 onto your system and run configure with + + ./configure --enable-gpl --enable-libx264 +* Once the server is up and running, start your ingest using FFmpeg + See ${ROOT}/extras/gen_live_ingest.sh for a simple example to stream a local webcam +* Open a browser and browse to http://${SERVER}/live.html to receive the live stream diff --git a/repos/dash-origin/data/.gitkeep b/repos/dash-origin/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/repos/dash-origin/origin.js b/repos/dash-origin/origin.js new file mode 100644 index 0000000..79ac49f --- /dev/null +++ b/repos/dash-origin/origin.js @@ -0,0 +1,422 @@ +/* +MIT License + +Copyright (c) 2019 Thilo Borgmann < thilo _at_ fflabs.eu > + +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. +*/ + +// NOT SECURE, DO NOT USE FOR PRODUCTION. DO NOT LET THIS SERVER LEAVE YOUR VPN. +// ALL MACHINES THIS SERVER TALKS TO HAVE TO BE TRUSTWORTHY. YOU HAVE BEEN WARNED. + +// Simple relay server for ingested streams to be served to clients +// with low latency and HTTP/1.1 chunked encoding + +/// required modules +const BufferList = require("bl"); // dependency usually to be installed via npm +const yargs = require("yargs"); // dependency usually to be installed via npm +const EventEmitter = require("events"); +const fs = require("fs"); +const http = require("http"); +const https = require("https"); +const util = require("util"); + +/// globals +var data_root; /// root directory for all data +var server_port_ingest; /// server listening port for ingest +var server_port_delivery; /// server listening port for delivery +var server_proto; /// set to serve HTTP or HTTPS connections +const stream_cache = new Map(); /// global data cache for incoming ingests + +/// command line options +const options = yargs + .usage("Usage: $0 [options]") + .option("clear_data", { + description: "Clears during startup", + type: "boolean", + default: false, + }) + .option("delete_chunks_timeout", { + description: + "Deletes old chunks after n seconds, any value below 10 keeps chunks forever", + type: "number", + default: 60, + }) + .option("data", { + description: "Set directory for the server", + type: "string", + default: "./data", + alias: "d", + }) + .option("port_in", { + description: "Set the server to listen for ingest on the given port", + type: "number", + default: 8079, + }) + .option("port_out", { + description: "Set the server to listen for delivery on the given port", + type: "number", + default: 8080, + }) + .option("key", { + description: "Server-side private key", + type: "string", + default: "", + alias: "k", + }) + .option("cert", { + description: "Server-side certificate", + type: "string", + default: "", + alias: "c", + }) + .option("ca", { + description: "Server-side CA certificate", + type: "string", + default: "", + }) + .option("verbose", { + description: "More logging", + type: "boolean", + default: false, + alias: "v", + }) + .version("version", "Shows the server version", "1.0") + .help().argv; + +/// process command line options +function parse_cmd() { + // set given or default values + if (options.data) { + data_root = options.data; + } + + if (options.port_in) { + server_port_ingest = options.port_in; + } + if (options.port_out) { + server_port_delivery = options.port_out; + } + + // choose connection type based on required options + if (options.key && options.cert && options.ca) { + server_proto = "https"; + } else { + server_proto = "http"; + } + + // clear data if wanted + if (options.clear_data) { + unlink_data_ingest(); + } + + if (options.verbose) { + console.log("Finished parsing cmd line:"); + console.log(`data_root: ${data_root}`); + console.log(`server_port_ingest: ${server_port_ingest}`); + console.log(`server_port_delivery: ${server_port_delivery}`); + console.log(`server_proto: ${server_proto}`); + } +} + +/// data object for caching ingest data +/// Writes all incoming data to storage and keeps +/// a copy in memory for live serving until the +/// file is completely received and written. +class CacheElem extends EventEmitter { + constructor() { + super(); + this.buffer_list = new BufferList(); + this.res = []; + this.ended = false; + } +} + +/// checks GET or POST request URLs for sanity to avoid spammers & crashes +function check_sanity(url) { + var begin = url.substr(0, 5); + return begin == "/live" || begin == "/dist" || begin == "/dash"; +} + +/// unlinks all files in data_ingest directory +/// Called during startup before the server starts listening. +function unlink_data_ingest() { + const data_ingest = data_root + "/live"; + console.log(`Unlinking all ingest data at: ${data_ingest}`); + files = fs.readdirSync(data_ingest); + + for (var i = 0; i < files.length; i++) { + fs.unlinkSync(data_ingest + "/" + files[i]); + } +} + +/// sends the no such file response (http 404) +function send_404(res) { + res.statusCode = 404; + res.statusMessage = "Not found"; + res.end(); +} + +/// sends the internal server error response (http 500) +function send_500(res) { + res.statusCode = 500; + res.statusMessage = "Internal error"; + res.end(); +} + +/// sends a complete file of known length from storage as a single response +/// @param res HttpResponse to write the data into +/// @param content_type String to write for the content type of the response +/// @param filename The file containing the data to be send +function send_fixed_length(res, content_type, filename) { + fs.readFile(filename, (err, data) => { + if (err) { + send_404(res); + throw err; + } else { + res.writeHead(200, { + "Content-Length": Buffer.byteLength(data), + "Content-Type": content_type, + "Access-Control-Allow-Origin": "*", + }); + res.write(data); + res.end(); + } + }); +} + +/// sends a complete file from storage as a chunked response +/// @param res HttpResponse to write the data into +/// @param content_type String to write for the content type of the response +/// @param filename The file containing the data to be send +function send_chunked(res, content_type, filename) { + var stream = fs.createReadStream(filename); + + stream.on("error", (err) => { + console.log(`404 bad file ${filename}`); + + send_404(res); + }); + + stream.once("readable", () => { + // implicitly set to chunked encoding if pipe'd to res, needs to be set for res.write() + // also set content-type correctly (even if pipe'd) + res.writeHead(200, { + "Content-Type": content_type, + "Transfer-Encoding": "chunked", + "Access-Control-Allow-Origin": "*", + }); + stream.pipe(res); + }); +} + +/// sends a complete file from cache (if found) or storage (fallback) as a chunked response +/// @param res HttpResponse to write the data into +/// @param content_type String to write for the content type of the response +/// @param filename The file containing the data to be send +function send_chunked_cached(res, content_type, filename) { + if (stream_cache.has(filename)) { + const cache_elem = stream_cache.get(filename); + + res.writeHead(200, { + "Content-Type": content_type, + "Transfer-Encoding": "chunked", + "Access-Control-Allow-Origin": "*", + }); + + const current = cache_elem.buffer_list.slice(); + res.write(current); + + if (cache_elem.ended) { + res.end(); + } else { + cache_elem.res.push(res); + cache_elem.on("data", function (chunk) { + //console.log(`data event for ${filename}`); + res.write(chunk); + }); + } + } else { + send_chunked(res, content_type, filename); + } +} + +function set_cors_headers(res) { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader( + "Access-Control-Allow-Methods", + "GET, POST, OPTIONS, PUT, DELETE" + ); + res.setHeader( + "Access-Control-Allow-Headers", + "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With" + ); +} + +/// create server and define handling of supported requests +function request_listener(req, res) { + set_cors_headers(res); + + if (req.method == "OPTIONS") { + res.statusCode = 204; + res.end(); + return; + } + + if (!check_sanity(req.url)) { + if (options.verbose) { + console.log(`ReJECT ${req.method} ${req.url}`); + } + } else if (req.method == "GET") { + const suffix_idx = req.url.lastIndexOf("."); + const suffix = req.url.slice(suffix_idx, req.url.length); + var filename = data_root + req.url; + + if (options.verbose) { + console.log(`GET ${req.url}`); + } + + switch (suffix) { + case ".html": + send_chunked(res, "text/html", filename); + break; + case ".js": + send_chunked(res, "application/javascript", filename); + break; + case ".mpd": + send_chunked(res, "application/dash+xml", filename); + break; + case ".m4s": + send_chunked_cached(res, "video/iso.segment", filename); + break; + default: + console.log(`404 bad suffix ${suffix}`); + send_404(res); + break; + } + } else if (req.method == "POST") { + // check for POST method, ignore others + const suffix_idx = req.url.lastIndexOf("."); + const suffix = req.url.slice(suffix_idx, req.url.length); + const filename = data_root + req.url; + + if (options.verbose) { + console.log(`POST ${req.url}`); + } + + const file_stream = fs.createWriteStream(filename); + const file_cache = new CacheElem(); + + file_stream.on("error", (err) => { + send_500(res); + throw err; + }); + + stream_cache.set(filename, file_cache); + + stream_cache.get(filename).on("end", function () { + this.ended = true; + const l = this.res.length; + for (var i = 0; i < l; i++) { + this.res[0].end(); // end transmission on first response + this.res.shift(); // delete response from array + } + stream_cache.delete(filename); + }); + + req.on("data", (chunk) => { + stream_cache.get(filename).buffer_list.append(chunk); + stream_cache.get(filename).emit("data", chunk); + file_stream.write(chunk); + }); + //req.on('close', () => { // not every stream emits 'close', so rely on 'end' event + //}); + req.on("end", () => { + stream_cache.get(filename).emit("end"); + file_stream.end(); + + // set timer to delete old segment files on disk after timeout + // only apply if options.delete_chunks_timeout has a sane value of more or equal to 10s + // only apply to files with "chunk-" in it + if (options.delete_chunks_timeout >= 10 && req.url.includes("chunk-")) { + const unlink_timer = util.promisify(setTimeout); + unlink_timer(options.delete_chunks_timeout * 1000, filename).then( + (fname) => { + fs.unlinkSync(fname); + } + ); + } + }); + } else if (req.method == "DELETE") { + // check for DELETE method + const suffix_idx = req.url.lastIndexOf("."); + const suffix = req.url.slice(suffix_idx, req.url.length); + const filename = data_root + req.url; + + if (options.verbose) { + console.log(`DELETE ${req.url}`); + } + + fs.unlink(filename, (err) => { + if (err) throw err; + }); + } else { + if (options.verbose) { + console.log(`Unhandled request method ${req.method}.`); + } + } +} + +// parse cmd line +parse_cmd(); + +// create the servers +var server_ingest; +var server_delivery; + +if (server_proto == "https") { + try { + var https_options = { + key: fs.readFileSync(options.key), + cert: fs.readFileSync(options.cert), + ca: fs.readFileSync(options.ca), + requestCert: false, + rejectUnauthorized: false, + }; + server_ingest = https.createServer(https_options, request_listener); + } catch (err) { + console.error( + `Error reading private key or certificate or CA certificate file!`, + err + ); + process.exit(1); + } +} else { + server_ingest = http.createServer(request_listener); +} + +server_delivery = http.createServer(request_listener); + +// start the servers +server_ingest.listen(server_port_ingest); +server_delivery.listen(server_port_delivery); + +// ready to receive ingests and client requests +console.log(`Listening for ingest on port: ${server_port_ingest}`); +console.log(`Listening for delivery on port: ${server_port_delivery}`); diff --git a/repos/dash-origin/package.json b/repos/dash-origin/package.json new file mode 100755 index 0000000..bebae92 --- /dev/null +++ b/repos/dash-origin/package.json @@ -0,0 +1,13 @@ +{ + "scripts": { + "start": "pm2 start origin.js -i 2", + "stop": "pm2 stop origin.js && pm2 delete origin.js" + }, + "dependencies": { + "bl": "^6.0.9", + "yargs": "^17.7.2" + }, + "devDependencies": { + "pm2": "^5.3.0" + } +} diff --git a/repos/demo/.eslintrc.cjs b/repos/demo/.eslintrc.cjs new file mode 100644 index 0000000..40a22ed --- /dev/null +++ b/repos/demo/.eslintrc.cjs @@ -0,0 +1,12 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"], + ignorePatterns: ["dist", ".eslintrc.cjs"], + parser: "@typescript-eslint/parser", + plugins: ["react-refresh"], + rules: { + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + "@typescript-eslint/no-unused-vars": "off", + } +} diff --git a/repos/demo/.gitignore b/repos/demo/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/repos/demo/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/repos/demo/.prettierrc b/repos/demo/.prettierrc new file mode 100644 index 0000000..6c0e61a --- /dev/null +++ b/repos/demo/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": false, + "tabWidth": 4, + "useTabs": false, + "printWidth": 200, + "singleQuote": false, + "trailingComma": "none", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/repos/demo/Dockerfile.dev b/repos/demo/Dockerfile.dev new file mode 100644 index 0000000..f3308a0 --- /dev/null +++ b/repos/demo/Dockerfile.dev @@ -0,0 +1,10 @@ +FROM node:21 + +# Set the working directory +WORKDIR /app + +# Copy package.json +COPY package.json . + +# Install the dependencies +RUN npm install \ No newline at end of file diff --git a/repos/demo/Dockerfile.prod b/repos/demo/Dockerfile.prod new file mode 100644 index 0000000..0bf27ad --- /dev/null +++ b/repos/demo/Dockerfile.prod @@ -0,0 +1,17 @@ +FROM node:21 as builder + +# Set the working directory +WORKDIR /build + +# Copy package.json +COPY package.json . +COPY src/lib/moq/package.json src/lib/moq/package.json + +# Install the dependencies +RUN npm install + +# Build the app +COPY . . +RUN npm run build + +CMD ["npm", "run", "preview"] \ No newline at end of file diff --git a/repos/demo/assets/ozu-logo.png b/repos/demo/assets/ozu-logo.png new file mode 100644 index 0000000..ad8dd20 Binary files /dev/null and b/repos/demo/assets/ozu-logo.png differ diff --git a/repos/demo/assets/perculus-logo.png b/repos/demo/assets/perculus-logo.png new file mode 100644 index 0000000..67a8a32 Binary files /dev/null and b/repos/demo/assets/perculus-logo.png differ diff --git a/repos/demo/assets/perculuslogo.webp b/repos/demo/assets/perculuslogo.webp new file mode 100644 index 0000000..c3bdce5 Binary files /dev/null and b/repos/demo/assets/perculuslogo.webp differ diff --git a/repos/demo/index.html b/repos/demo/index.html new file mode 100644 index 0000000..0f6203e --- /dev/null +++ b/repos/demo/index.html @@ -0,0 +1,15 @@ + + + + + + MOQ vs DASH + + + + + +
+ + + diff --git a/repos/demo/package.json b/repos/demo/package.json new file mode 100644 index 0000000..ed2f353 --- /dev/null +++ b/repos/demo/package.json @@ -0,0 +1,46 @@ +{ + "name": "mhv-2024-moqt-demo", + "private": true, + "version": "0.0.0", + "type": "module", + "workspaces": [ + "src/lib/moq" + ], + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "format": "prettier --write .", + "preview": "vite preview" + }, + "dependencies": { + "@observablehq/plot": "^0.6.13", + "clsx": "^2.1.0", + "dashjs": "^4.7.4", + "lodash": "^4.17.21", + "mp4box": "^0.5.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.3", + "react-use": "^17.5.0", + "tslib": "^2.6.2" + }, + "devDependencies": { + "@types/lodash": "^4.14.202", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@typescript-eslint/eslint-plugin": "^6.20.0", + "@typescript-eslint/parser": "^6.20.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.17", + "eslint": "^8.56.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.33", + "prettier": "^3.2.4", + "prettier-plugin-tailwindcss": "^0.5.11", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3", + "vite": "^5.0.12" + } +} diff --git a/repos/demo/postcss.config.js b/repos/demo/postcss.config.js new file mode 100644 index 0000000..9264204 --- /dev/null +++ b/repos/demo/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +} diff --git a/repos/demo/src/App.tsx b/repos/demo/src/App.tsx new file mode 100644 index 0000000..1492fb9 --- /dev/null +++ b/repos/demo/src/App.tsx @@ -0,0 +1,420 @@ +import Metrics from "./components/metrics" +import { DASHPlayer, MOQPlayer } from "./components/players" +import { DASHAggregator, MOQAggregator } from "./lib/internal/aggregator" +import type { PlayerType, Track } from "./lib/internal/types" +import type { ChangeEvent, Ref } from "react" +import { useCallback, useEffect, useRef, useState } from "react" +import { useMetricSynchroniser } from "./lib/internal/synchroniser" +import { useBandwidthSynchroniser } from "./lib/internal/bandwidthSynchroniser" +import clsx from "clsx" + +// bandwidth options from 1 Mbps to 10 Mbps and to unlimited +const BANDWIDTH_OPTIONS = [ + { label: "No limit ", value: "unlimited" }, + { label: "1 Mbps", value: "1000000" }, + { label: "1.1 Mbps", value: "1100000" }, + { label: "1.65 Mbps", value: "1650000" }, + { label: "2 Mbps", value: "2000000" }, + { label: "3 Mbps", value: "3000000" }, + { label: "4 Mbps", value: "4000000" }, + { label: "4.5 Mbps", value: "4500000" }, + { label: "5 Mbps", value: "5000000" }, + { label: "6 Mbps", value: "6000000" }, + { label: "6.75 Mbps", value: "6750000" }, + { label: "7 Mbps", value: "7000000" }, + { label: "8 Mbps", value: "8000000" }, + { label: "9 Mbps", value: "9000000" }, + { label: "10 Mbps", value: "10000000" }, + { label: "12 Mbps", value: "12000000" }, + { label: "14 Mbps", value: "14000000" }, + { label: "16 Mbps", value: "16000000" }, + { label: "18 Mbps", value: "18000000" }, + { label: "20 Mbps", value: "20000000" }, + { label: "100 Mbps", value: "100000000" } +] + +const LATENCY_TARGETS = [ + { label: "Live edge", value: "0" }, + { label: "1s", value: "1" }, + { label: "2s", value: "2" }, + { label: "3s", value: "3" }, + { label: "4s", value: "4" }, + { label: "5s", value: "5" } +] + +const WARMUP_ON_START = false +const ABR_ENABLED_ON_START = true + +const INITIAL_BANDWIDTH = localStorage.getItem("tc_bandwidth") || "unlimited" + +const DEFAULT_LATENCY_TARGET_DASH = 1.5 // seconds +const DEFAULT_LATENCY_TARGET_MOQ = 0 + +export default function App({ mode }: { mode: PlayerType }) { + const ms = useMetricSynchroniser() + const bandwidthSync = useBandwidthSynchroniser() + + const ref = useRef(null) + + // Catalog tracks + const [tracks, setTracks] = useState([]) + const [currentTrack, setCurrentTrack] = useState("") + + const [bandwidth, setBandwidth] = useState("") + const [statusMessage, setStatusMessage] = useState("") + const [showStatusMessage, setShowStatusMessage] = useState(false) + + const [abrEnabled, setAbrEnabled] = useState(ABR_ENABLED_ON_START && (!WARMUP_ON_START || mode === "dash")) + const [isSyncEnabled, setIsSyncEnabled] = useState(false) + + const [latencyTarget, setLatencyTarget] = useState(mode === "dash" ? DEFAULT_LATENCY_TARGET_DASH : DEFAULT_LATENCY_TARGET_MOQ) + + const [warmingUp, setWarmingUp] = useState(WARMUP_ON_START && mode === "moq") + + const ws = useRef(null) + + useEffect(() => { + const channel = bandwidthSync.channel + + const handleSyncChange = (event: MessageEvent) => { + const { isSyncEnabled: receivedSyncEnabled } = event.data + setIsSyncEnabled(receivedSyncEnabled) + } + + const handleBandwidthChange = (event: MessageEvent) => { + const { bandwidth: receivedBandwidth } = event.data + // if sync is enabled, update the bandwidth + if (bandwidthSync.settings.isSyncEnabled) { + setBandwidth(receivedBandwidth) + } + } + + channel.addEventListener("message", handleSyncChange) + channel.addEventListener("message", handleBandwidthChange) + + return () => { + channel.removeEventListener("message", handleSyncChange) + } + }, [bandwidthSync]) + + useEffect(() => { + setIsSyncEnabled(bandwidthSync.settings.isSyncEnabled) + + // Apply new bandwidth and update WebSocket server if sync is enabled + if (bandwidthSync.settings.isSyncEnabled) { + updateBandwidth(bandwidthSync.settings.bandwidth) + const newBandwidth = bandwidthSync.settings.bandwidth + if (newBandwidth === "unlimited") { + const message = `clear ${mode}` + ws.current?.send(message) + setStatusMessage("Bandwidth limit is not applied.") + } else if (newBandwidth !== "" && !isNaN(parseInt(newBandwidth)) && parseInt(newBandwidth) !== 0) { + const message = `set ${mode} ${newBandwidth}` + ws.current?.send(message) + setStatusMessage(`Bandwidth is limited to ${parseInt(newBandwidth) / 1000000} Mbps`) + } else { + console.warn("App | Invalid bandwidth setting:", newBandwidth) + } + } + }, [bandwidthSync.settings]) + + useEffect(() => { + // if status message is changed, set showStatusMessage to true + // after a timeout of 5 seconds, set showStatusMessage to false + if (statusMessage) { + setShowStatusMessage(true) + setTimeout(() => setShowStatusMessage(false), 5000) + } + }, [statusMessage]) + + // Read mode from URL + useEffect(() => { + console.log("App | initializing app", mode) + + // if there is an endpoint address (host:port) in the querystring + const m = /[?&]ws_server=([^&]+)/.exec(location.search) + let endpoint = m ? m[1] : `wss://${location.hostname}:8000` + + // Ensure the endpoint is a valid WebSocket URL + if (!endpoint.match(/^wss*:\/\//)) { + setStatusMessage("App | Error: Invalid WebSocket server address.") + return + } + + // Upgrade endpoint to secure WebSocket if necessary + if (location.protocol === "https:" && endpoint.startsWith("ws:")) { + console.log("App | Upgrading WebSocket server address to secure connection") + endpoint = endpoint.replace(/^ws:/, "wss:") + } + + console.log("App | Connecting to WebSocket server:", endpoint) + ws.current = new WebSocket(endpoint) + + ws.current.onopen = () => { + console.log("App | WebSocket connection established") + // update bandwidth to no limit + setBandwidth(INITIAL_BANDWIDTH) + updateBandwidth(INITIAL_BANDWIDTH) + } + + ws.current.onmessage = (event) => { + console.log("App | WebSocket message received:", event.data) + } + + ws.current.onerror = (error) => { + console.error("WebSocket error:", error) + setStatusMessage("App | Error: Could not connect to WebSocket server.") + } + + return () => { + if (ws.current) { + ws.current.close() + ws.current = null + } + } + }, [mode]) + + // Register synchroniser + useEffect(() => { + if (!ref.current) return + + if (mode === "dash") { + document.title = "LL-DASH | Streaming University" + } else if (mode === "moq") { + document.title = "Media-over-QUIC | Streaming University" + } + + const init = async (view: HTMLVideoElement | HTMLCanvasElement) => { + switch (mode) { + case "moq": { + const moqAggregator = new MOQAggregator(view as HTMLCanvasElement) + moqAggregator.setLatencyTarget(DEFAULT_LATENCY_TARGET_MOQ) + await ms.register(moqAggregator) + // setting tracks + setTracks(moqAggregator.getTracks() || []) + moqAggregator.registerABREvents(setCurrentTrack) + ms.aggregator?.toggleABR(abrEnabled) + + // warm-up + if (WARMUP_ON_START) relayWarmUp() + break + } + case "dash": { + const dashAggregator = new DASHAggregator(view as HTMLVideoElement) + dashAggregator.setLatencyTarget(DEFAULT_LATENCY_TARGET_DASH) + await ms.register(dashAggregator) + break + } + } + } + + init(ref.current) + .then(() => console.log("Registered")) + .catch((err) => console.error("Failed to register", err)) + + return () => ms.unregister() + }, [mode]) + + const onTrackChange = (e: ChangeEvent) => { + if ((e.target! as HTMLSelectElement).value === "") return + ms.aggregator?.setTrack((e.target! as HTMLSelectElement).value) + } + const onBandwidthChange = (e: React.ChangeEvent) => { + const newBandwidth = e.target.value + if (newBandwidth !== "") { + // Update the bandwidth without changing the sync state + setBandwidth(newBandwidth) + bandwidthSync.updateSettings(newBandwidth, bandwidthSync.settings.isSyncEnabled) + updateBandwidth(newBandwidth) + } + } + + const updateBandwidth = useCallback( + (newBandwidth: string) => { + bandwidthSync.updateSettings(newBandwidth, bandwidthSync.settings.isSyncEnabled) + + if (!ws.current) return + + if (ws.current.readyState === WebSocket.OPEN) { + if (newBandwidth === "unlimited") { + const message = `clear ${mode}` + ws.current.send(message) + setStatusMessage("Bandwidth limit is not applied.") + localStorage.removeItem("tc_bandwidth") + } else { + const message = `set ${mode} ${newBandwidth}` + ws.current.send(message) + const readableBandwidth = BANDWIDTH_OPTIONS.find((option) => option.value === newBandwidth)?.label || "unlimited" + // set this bandwidth to local storage + localStorage.setItem("tc_bandwidth", newBandwidth) + setStatusMessage(`Bandwidth is limited to ${readableBandwidth}`) + } + } + }, + [bandwidthSync, mode] + ) + + const onSyncChange = (e: React.ChangeEvent) => { + const newIsSyncEnabled = e.target.checked + setIsSyncEnabled(newIsSyncEnabled) + bandwidthSync.updateSettings(bandwidthSync.settings.bandwidth, newIsSyncEnabled) + } + + const toggleAbr = () => { + setAbrEnabled(!abrEnabled) + ms.aggregator!.toggleABR(!abrEnabled) + } + + const relayWarmUp = () => { + console.log("App | Starting warm-up") + const warmUpInterval = 1000 + // change tracks one by one with 5 seconds interval + const tracks = ms.aggregator?.getTracks() || [] + const trackIds = tracks.map((track) => track.id) + let i = 0 + const interval = setInterval(() => { + if (i >= trackIds.length) { + clearInterval(interval) + return + } + console.log("App | Setting track:", trackIds[i]) + ms.aggregator?.setTrack(trackIds[i]!) + i++ + }, warmUpInterval) + // after warm-up is complete, set ABR to true + if (warmingUp) { + setTimeout( + () => { + console.log("App | Setting ABR to true") + setAbrEnabled(true) + setWarmingUp(false) + ms.aggregator?.toggleABR(true) + }, + warmUpInterval * trackIds.length + 2000 + ) + } + setTimeout( + () => { + console.log("App | Setting ABR to true") + setAbrEnabled(true) + setWarmingUp(false) + ms.aggregator?.toggleABR(true) + }, + warmUpInterval * trackIds.length + 2000 + ) + } + + const onLatencyTargetChange = (e: React.ChangeEvent) => { + if (e.target.value === "") return + const latency = parseInt(e.target.value) + console.log("App | Setting latency target:", latency) + setLatencyTarget(latency) + ms.aggregator?.setLatencyTarget(latency) + } + + // For testing purposes, change the bandwidth every 10 seconds + const bandwidthChanger = (newBandwidth: number, direction: "up" | "down" = "down") => { + console.log("App | Changing bandwidth to", newBandwidth, direction) + const ddl = document.getElementById("ddlBandwidth") as HTMLSelectElement + ddl.value = newBandwidth.toString() + updateBandwidth(newBandwidth.toString()) + + if (newBandwidth === 20000000 && direction === "up") { + return + } + + if (newBandwidth === 2000000 && direction === "down") { + // direction = "up" + return + } + + if (direction === "up") { + newBandwidth += 2000000 + } else { + newBandwidth -= 2000000 + } + setTimeout(() => { + bandwidthChanger(newBandwidth, direction) + }, 10000); + + } + + return ( +
+
+

{mode === "moq" ? "Media-over-QUIC" : "LL-DASH"}

+
+ {mode === "moq" && + <> + This project is part of + + Streaming University + (Source code is coming soon...) + + } + {mode === "dash" && Powered by dash.js.} +
+
+
+ {mode === "dash" && } muted />} + {mode === "moq" && } />} +
+ {warmingUp && Initializing...} + {!warmingUp && ( + + + + + + + + + + + + + + + +
+ + + (?) + +
+ + + + +
+ +
+ )} +
+
{statusMessage}
+
+
{...Metrics}
+
+ ) +} diff --git a/repos/demo/src/Cockpit.tsx b/repos/demo/src/Cockpit.tsx new file mode 100644 index 0000000..d693cc9 --- /dev/null +++ b/repos/demo/src/Cockpit.tsx @@ -0,0 +1,146 @@ +import { createContext, useContext, useEffect, useRef, useState } from "react" +import { BaseSynchroniserData, useMetricSynchroniser } from "./lib/internal/synchroniser" +import { AggregatorData, SynchroniserData } from "./lib/internal/types" +import { useRafLoop, useWindowSize } from "react-use" +import * as Plot from "@observablehq/plot" +import { convertCamelCase } from "./utils/text" +import { merge } from "lodash" +import { humanizeValue } from "./utils/number" + +// We might want to use metric specific data in the future +const metricSpecificOptions: Partial>> = { + latency: { + marginLeft: 50, + y: { + tickFormat: (d: number) => humanizeValue(d, "latency", true) + } + }, + bitrate: { + marginLeft: 60, + y: { + tickFormat: (d: number) => humanizeValue(d, "bitrate", true) + } + }, + stallDuration: { + y: { + tickFormat: (d: number) => humanizeValue(d, "stallDuration", true) + } + }, + measuredBandwidth: { + marginLeft: 60, + y: { + tickFormat: (d: number) => humanizeValue(d, "measuredBandwidth", true) + } + } +} + +/** + * latency: moq ve dash line chart (ayri ayri) + * measuredBandwidth: moq, tc ve dash line + * stallDuration: moq ve dash line + * bitrate: moq ve dash line + * skippedDuration: moq ve dash line + * + * zoom in/out + * scroll + */ + +const MetricContext = createContext(BaseSynchroniserData) +const useMetric = () => useContext(MetricContext) + +const MetricChart = () => { + const ref = useRef(null) + const [metricKey, setMetricKey] = useState("latency") + const metric = useMetric() + const { width, height } = useWindowSize() + + useEffect(() => { + // Get the data + const { moq, dash } = metric + + // use only last 10 seconds + const dashInterpolated = dash[metricKey].history.filter((d) => d.time > performance.now() + performance.timeOrigin - 10000) + const moqInterpolated = moq[metricKey].history.filter((d) => d.time > performance.now() + performance.timeOrigin - 10000) + + // flatten both + const data = [...dashInterpolated.map((d) => ({ ...d, symbol: "dash" })), ...moqInterpolated.map((d) => ({ ...d, symbol: "moq" }))] + + // Form the chart + const chart = Plot.plot( + merge(metricSpecificOptions[metricKey], { + width: width / 2, + height: height / 2 - 24 - 32, // hacky, removes padding and select height + inset: 20, + x: { + label: "Time", + grid: true, + tickFormat: (d: number) => { + const date = new Date(d) + return `${date.getHours()}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}` + } + }, + y: { + grid: true + }, + color: { + domain: ["moq", "dash"], + range: ["rgb(214, 102, 41)", "rgb(40, 95, 233)"] + }, + marks: [ + Plot.lineY(data, { + x: "time", + y: "value", + stroke: "symbol" + }), + Plot.text( + data, + Plot.selectLast({ + x: "time", + y: "value", + text: "symbol", + dx: 5, + dy: -5, + fill: "symbol" + }) + ) + ] + } as Plot.PlotOptions) + ) + + // Attach the chart to the DOM + ref.current?.append(chart) + return () => chart.remove() + }, [metric, metricKey, width, height]) + + return ( +
+ +
+
+ ) +} + +export default function Cockpit() { + const metrics = useMetricSynchroniser() + + // Render loop + const [data, setData] = useState(BaseSynchroniserData) + useRafLoop(() => setData(Object.assign({}, metrics.data)), true) + + return ( + +
+ + + + +
+
+ ) +} diff --git a/repos/demo/src/components/metrics/BaseMetric.module.css b/repos/demo/src/components/metrics/BaseMetric.module.css new file mode 100644 index 0000000..574acb2 --- /dev/null +++ b/repos/demo/src/components/metrics/BaseMetric.module.css @@ -0,0 +1,42 @@ +.bar { + --shadow-color: rgba(74, 222, 128, 1); + position: relative; + z-index: 0; + /* box-shadow: 0px 0px 2px 2px var(--shadow-color); */ + color: white; +} + +.bar::after { + position: absolute; + inset: 0; + content: ''; + width: 100%; + height: 100%; + /* box-shadow: 0px 0px 6px 6px var(--shadow-color); */ + opacity: 0; + animation: bar 1s infinite; +} + +.bar.green { + --shadow-color: rgba(74, 222, 128, 1); +} + +.bar.yellow { + --shadow-color: rgba(250, 204, 21, 1); +} + +.bar.orange { + --shadow-color: rgba(255, 146, 41, 1); +} + +@keyframes bar { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + } + 100% { + opacity: 1; + } +} diff --git a/repos/demo/src/components/metrics/BaseMetric.tsx b/repos/demo/src/components/metrics/BaseMetric.tsx new file mode 100644 index 0000000..ff998b3 --- /dev/null +++ b/repos/demo/src/components/metrics/BaseMetric.tsx @@ -0,0 +1,50 @@ +import { useMetrics } from "../../lib/internal/synchroniser" +import type { AggregatorData } from "../../lib/internal/types" +import { convertCamelCase } from "../../utils/text" +import { humanizeValue } from "../../utils/number" + +export default function BaseMetric({ metric }: { metric: keyof AggregatorData }) { + // Metric related state + const metrics = useMetrics() + const { snapshot, data } = metrics + const { current } = snapshot[metric] + const recentHistory = data[metric].history.slice(-3) + const invert = data[metric].invert + + // Decide on diff color + let diffColor = "rgb(245, 245, 245)" + if (recentHistory.length > 1) { + let upTrend = null + // If last values have decending trend, color red + if (recentHistory.slice(1).every(({ value }, index) => value < recentHistory[index].value)) { + upTrend = false + } else if (recentHistory.slice(1).every(({ value }, index) => value > recentHistory[index].value)) { + upTrend = true + } + + if (upTrend !== null) { + if (invert) upTrend = !upTrend + diffColor = upTrend ? "rgb(0, 255, 0)" : "rgb(255, 0, 0)" + } + } + + // Humanize value + const humanizedValue = humanizeValue(current, metric).split(" ") + let value = humanizedValue[0] + const unit = humanizedValue[1] + + // Special handling for latency + if (metric === "latency" && Number(value) < 30 && unit === "ms") value = "<30" + + return ( +
+ {convertCamelCase(metric)} +
+
+                    {value.padStart(3, " ")}
+                
+ {unit} +
+
+ ) +} diff --git a/repos/demo/src/components/metrics/index.tsx b/repos/demo/src/components/metrics/index.tsx new file mode 100644 index 0000000..a4343fc --- /dev/null +++ b/repos/demo/src/components/metrics/index.tsx @@ -0,0 +1,7 @@ +import BaseMetric from "./BaseMetric" +import { BaseAggregatorData } from "../../lib/internal/aggregator/base" +import type { AggregatorData } from "../../lib/internal/types" + +const Metrics = Object.keys(BaseAggregatorData).map((key) => ) + +export default Metrics diff --git a/repos/demo/src/components/metrics/useWindowProps.ts b/repos/demo/src/components/metrics/useWindowProps.ts new file mode 100644 index 0000000..f83e255 --- /dev/null +++ b/repos/demo/src/components/metrics/useWindowProps.ts @@ -0,0 +1,31 @@ +import { useEffect, useMemo, useState } from "react" + +const WINDOW_ID = Math.random().toString(36).slice(2) + +export default function useWindowProps(metric: string) { + const bc = useMemo(() => new BroadcastChannel("window-props"), []) + const [leftAligned, setLeftAligned] = useState(true) + const [metricOpen, setMetricOpen] = useState(false) + + useEffect(() => { + const onMouseOut = () => bc.postMessage({ id: WINDOW_ID, event: "window", screenLeft: window.screenLeft }) + window.addEventListener("mouseout", onMouseOut) + return () => window.removeEventListener("mouseout", onMouseOut) + }, []) + + useEffect(() => { + const onMessage = ({ data }: MessageEvent) => { + if (data.id !== WINDOW_ID && data.event === "window") setLeftAligned(data.screenLeft >= window.screenLeft) + if (data.event === "metric") setMetricOpen(data.metric === metric && data.open) + } + bc.addEventListener("message", onMessage) + return () => bc.removeEventListener("message", onMessage) + }, []) + + const setOpen = (open: boolean) => { + setMetricOpen(open) + bc.postMessage({ id: WINDOW_ID, event: "metric", metric, open }) + } + + return { leftAligned, metricOpen, setOpen } +} diff --git a/repos/demo/src/components/players/index.tsx b/repos/demo/src/components/players/index.tsx new file mode 100644 index 0000000..aa7145d --- /dev/null +++ b/repos/demo/src/components/players/index.tsx @@ -0,0 +1,37 @@ +import { forwardRef, useEffect } from "react" +import type { Ref } from "react" +import useBackground from "./useBackground" + +const DASHPlayer = forwardRef(function (props: React.VideoHTMLAttributes, ref: Ref) { + const canvasRef = useBackground(ref) + + return ( +
+
+ ) +}) + +const MOQPlayer = forwardRef(function (props: React.CanvasHTMLAttributes, ref: Ref) { + const canvasRef = useBackground(ref) + + return ( +
+ + +
+ ) +}) + +export { DASHPlayer, MOQPlayer } diff --git a/repos/demo/src/components/players/useBackground.ts b/repos/demo/src/components/players/useBackground.ts new file mode 100644 index 0000000..4f56ef3 --- /dev/null +++ b/repos/demo/src/components/players/useBackground.ts @@ -0,0 +1,26 @@ +import type { Ref, RefObject } from "react" +import { useRef } from "react" +import { useMedia, useRafLoop } from "react-use" + +export default function useBackground(src: Ref | Ref) { + const reducedMotion = useMedia("(prefers-reduced-motion: reduce)") + const ref = useRef(null) + + useRafLoop(() => { + const video = (src as RefObject).current + if (!video || reducedMotion || !ref.current) return + + // Get render context + const ctx = ref.current.getContext("2d")! + ctx.filter = "blur(1px)" + + // Update canvas size + ref.current.width = 16 + ref.current.height = 9 + + // Draw video on canvas + ctx.drawImage(video as any, 0, 0, ref.current.width, ref.current.height) + }, true) + + return ref +} diff --git a/repos/demo/src/index.css b/repos/demo/src/index.css new file mode 100644 index 0000000..4787a9f --- /dev/null +++ b/repos/demo/src/index.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + @apply bg-black; +} diff --git a/repos/demo/src/lib/internal/aggregator/base.ts b/repos/demo/src/lib/internal/aggregator/base.ts new file mode 100644 index 0000000..39da83c --- /dev/null +++ b/repos/demo/src/lib/internal/aggregator/base.ts @@ -0,0 +1,100 @@ +import type { AggregatorData, AggregatorType, Track } from "../types" +import mergeWith from "lodash/mergeWith" +import { ArrayMerger } from "../../../utils/array" + +export const BaseAggregatorData: AggregatorData = { + latency: { + history: [], + invert: true + }, + measuredBandwidth: { + history: [], + invert: false + }, + stallDuration: { + history: [], + invert: true + }, + bitrate: { + history: [], + invert: false + }, + skippedDuration: { + history: [], + invert: true + } +} + +export abstract class Aggregator { + abstract identifier: AggregatorType + abstract publicState: Record + + abstract getTracks(): Track[] + abstract setTrack(id: string): void + abstract toggleABR(state?: boolean): void + abstract registerABREvents(callback: (newTrack: string) => void): void + abstract setLatencyTarget(val: number): void + + protected snapshot: AggregatorData = structuredClone(BaseAggregatorData) + private lastValues: AggregatorData = structuredClone(BaseAggregatorData) + + private callback: (data: AggregatorData) => void = () => {} + + private raf: number | null = null + private sampleTime = 100 // ms + private lastTime = 0 // ms + + async init(callback: (data: AggregatorData) => void): Promise { + this.callback = callback + this.raf = window.requestAnimationFrame(this.relay.bind(this)) + return Promise.resolve() + } + + async destroy(): Promise { + if (this.raf) window.cancelAnimationFrame(this.raf) + return Promise.resolve() + } + + protected updateSnapshot(data: AggregatorData): void { + mergeWith(this.snapshot, data, ArrayMerger) + } + + protected relay() { + // This is the relay function, where we ensure we send a complete snapshot + // to the callback function. Because some data might not be updated every + // frame, we need to fill in the gaps with the last known value. + + // Throttle the relay to 100ms + if (performance.now() - this.lastTime < this.sampleTime) { + this.raf = window.requestAnimationFrame(this.relay.bind(this)) + return + } + + // If we have data for a metric, send it as is. But save the last known to lastValues + // If we don't have data for a metric, send the last known value from lastValues + const completeSnapshot: AggregatorData = Object.keys(this.snapshot).reduce((acc, key) => { + const metric = key as keyof AggregatorData + + if (this.snapshot[metric].history.length > 0) { + acc[metric] = this.snapshot[metric] + this.lastValues[metric].history = [this.snapshot[metric].history[this.snapshot[metric].history.length - 1]] + } else { + const lastValue = this.lastValues[metric].history.length > 0 ? this.lastValues[metric].history[this.lastValues[metric].history.length - 1].value : 0 + acc[metric].history = [ + { + time: performance.now() + performance.timeOrigin, + value: lastValue + } + ] + } + return acc + }, structuredClone(BaseAggregatorData)) + + // Send the complete snapshot to the callback + this.callback(completeSnapshot) + this.snapshot = structuredClone(BaseAggregatorData) + + this.lastTime = performance.now() + this.raf = window.requestAnimationFrame(this.relay.bind(this)) + } +} diff --git a/repos/demo/src/lib/internal/aggregator/dash.ts b/repos/demo/src/lib/internal/aggregator/dash.ts new file mode 100644 index 0000000..5af3b59 --- /dev/null +++ b/repos/demo/src/lib/internal/aggregator/dash.ts @@ -0,0 +1,235 @@ +import { Aggregator, BaseAggregatorData } from "./base" +import dashjs, { MediaPlayer, LogLevel } from "dashjs" +import type { AggregatorData, AggregatorType, Track } from "../types" +import type { MediaPlayerClass, BufferStateChangedEvent, Event, QualityChangeRenderedEvent } from "dashjs" +import { SWMA } from "../../moq/common/utils" + +class TPHistory { + #parts: { + [partName: string]: PerformanceResourceTiming[] + } + #swma = new SWMA(7, "dash-tput") + + constructor() { + this.#parts = {} + } + + addPart(part: PerformanceResourceTiming): void { + const name = part.name.split("/").pop() as string + if (!this.#parts[name]) this.#parts[name] = [] + this.#parts[name].push(part) + + if (this.#hasCompletedParts()) this.#calc() + } + + getMeasuredBandwidth(): number { + return this.#swma.next() + } + + #hasCompletedParts(): boolean { + return Object.values(this.#parts).some((part) => part.length === 2) + } + + #calc() { + // Get completed parts + const completedParts = Object.fromEntries(Object.entries(this.#parts).filter(([, part]) => part.length === 2)) + this.#parts = Object.fromEntries(Object.entries(this.#parts).filter(([, part]) => part.length < 2)) + + for (const bundle of Object.values(completedParts)) { + // Calculate total size and time + const totalSize = bundle.reduce((acc, part) => acc + part.transferSize, 0) / 1000 // kbit + let totalTime = Math.max(bundle[0].responseEnd, bundle[1].responseEnd) - Math.min(bundle[0].responseStart, bundle[1].responseStart) + totalTime /= 1000 // s + + if (totalSize < 1000) continue + this.#swma.next(totalSize / totalTime) + } + } +} + +export class DASHAggregator extends Aggregator { + identifier: AggregatorType = "dash" + publicState: Record = {} + + #data: AggregatorData = structuredClone(BaseAggregatorData) + #player: MediaPlayerClass + + #state = { + raf: -1, + lastBufferStalled: 0, // ms + lastBufferStallDuration: 0, // ms + bufferState: "bufferLoaded" as BufferStateChangedEvent["state"], + lastSeekTime: -1, // ms + lastSkippedDuration: 0, // ms + bwHistory: new TPHistory() + } + + constructor(view: HTMLVideoElement) { + super() + + // Create a new player instance + this.#player = MediaPlayer().create() + this.#player.initialize() + + // Apply settings + this.#player.updateSettings({ + streaming: { + buffer: { + stallThreshold: 0.05 + }, + delay: { + liveDelay: 1.5 + }, + liveCatchup: { + maxDrift: 0.1, + enabled: true + } + }, + debug: { + logLevel: dashjs.LogLevel.LOG_LEVEL_DEBUG + } + }) + + // Attach player to video element + this.#player.attachView(view) + } + + async init(callback: (data: AggregatorData) => void): Promise { + // Add event listeners + this.#player.on("bufferStateChanged", this.#onBufferStateChanged.bind(this)) + this.#player.on("qualityChangeRendered", this.#onQualityChangeRendered.bind(this)) + this.#player.on("playbackSeeking", this.#onPlaybackSeek.bind(this)) + this.#player.on("playbackSeeked", this.#onPlaybackSeek.bind(this)) + + // Attach source + // if there is an endpoint address (host:port) in the querystring, pass it to the player + const m = /[?&]server=([^&]+)/.exec(location.search) + let endpoint = m ? m[1] : `${location.hostname}:8080` + if (!/^https?:\/\//.test(endpoint)) { + endpoint = location.protocol + "//" + endpoint + } + this.#player.attachSource(endpoint + "/live/live.mpd") + + // Start observing + this.#state.raf = window.requestAnimationFrame(this.#onAnimationFrame.bind(this)) + + // Register data relay in super class + return super.init(callback) + } + + async destroy(): Promise { + // Remove event listeners + this.#player.off("bufferStateChanged", this.#onBufferStateChanged) + this.#player.off("qualityChangeRendered", this.#onQualityChangeRendered) + this.#player.off("playbackSeeking", this.#onPlaybackSeek) + this.#player.off("playbackSeeked", this.#onPlaybackSeek) + + // Stop observing + window.cancelAnimationFrame(this.#state.raf) + + // Destroy player instance + this.#player.reset() + + // Reset data + this.#data = structuredClone(BaseAggregatorData) + + // Continue with super class + return super.destroy() + } + + getTracks(): Track[] { + console.error("Track selection not implemented for DASH") + return [] + } + + setTrack(_id: string): void { + console.error("Track selection not implemented for DASH") + } + + toggleABR(_state?: boolean): void { + console.error("ABR not implemented for DASH") + } + + registerABREvents(_callback: (newTrack: string) => void): void { + console.error("ABR not implemented for DASH") + } + + setLatencyTarget(val: number): void { + const currentSettings = { ...this.#player.getSettings() } + if (!currentSettings.streaming) currentSettings.streaming = {} + if (!currentSettings.streaming.delay) currentSettings.streaming.delay = {} + currentSettings.streaming.delay.liveDelay = val + this.#player.updateSettings(currentSettings) + } + + #onAnimationFrame(): void { + this.#data.latency.history.push({ + time: performance.now() + performance.timeOrigin, + value: this.#player.getCurrentLiveLatency() + }) + + if (this.#state.bufferState === "bufferStalled") { + this.#state.lastBufferStallDuration += performance.now() - this.#state.lastBufferStalled + this.#state.lastBufferStalled = performance.now() + } + + this.#data.stallDuration.history.push({ + time: performance.now() + performance.timeOrigin, + value: this.#state.lastBufferStallDuration + }) + + const bw = this.#player.getAverageThroughput("video") + if (bw > 0) { + this.#data.measuredBandwidth.history.push({ + time: performance.now() + performance.timeOrigin, + value: bw + }) + } + + // Relay data + this.updateSnapshot(this.#data) + this.#data = structuredClone(BaseAggregatorData) + + // Request next frame + this.#state.raf = window.requestAnimationFrame(this.#onAnimationFrame.bind(this)) + } + + #onBufferStateChanged(e: Event): void { + const event = e as BufferStateChangedEvent + if (event.state === "bufferStalled") this.#state.lastBufferStalled = performance.now() + this.#state.bufferState = event.state + } + + #onQualityChangeRendered(e: Event): void { + const event = e as QualityChangeRenderedEvent + const bitrates = this.#player.getBitrateInfoListFor("video") + + const oldQuality = event.oldQuality + const newQuality = this.#player.getQualityFor("video") + + if (!isNaN(oldQuality)) + this.#data.bitrate.history.push({ + time: performance.now() + performance.timeOrigin, + value: bitrates[oldQuality].bitrate + }) + + if (!isNaN(newQuality)) + this.#data.bitrate.history.push({ + time: performance.now() + performance.timeOrigin, + value: bitrates[newQuality].bitrate + }) + } + + #onPlaybackSeek(event: Event) { + const time = this.#player.timeAsUTC() + if (event.type === "playbackSeeking") this.#state.lastSeekTime = time + else if (event.type === "playbackSeeked") { + if (this.#state.lastSeekTime === -1) return + this.#state.lastSkippedDuration += time - this.#state.lastSeekTime + this.#data.skippedDuration.history.push({ + time: performance.now() + performance.timeOrigin, + value: this.#state.lastSkippedDuration + }) + } + } +} diff --git a/repos/demo/src/lib/internal/aggregator/index.ts b/repos/demo/src/lib/internal/aggregator/index.ts new file mode 100644 index 0000000..f9af2c9 --- /dev/null +++ b/repos/demo/src/lib/internal/aggregator/index.ts @@ -0,0 +1,4 @@ +import { DASHAggregator } from "./dash" +import { MOQAggregator } from "./moq" + +export { DASHAggregator, MOQAggregator } diff --git a/repos/demo/src/lib/internal/aggregator/moq.ts b/repos/demo/src/lib/internal/aggregator/moq.ts new file mode 100644 index 0000000..57bc9a5 --- /dev/null +++ b/repos/demo/src/lib/internal/aggregator/moq.ts @@ -0,0 +1,449 @@ +import { SWMA } from "../../moq/common/utils" +import { Catalog } from "../../moq/media/catalog" +import { Player } from "../../moq/playback" +import { SkipEvent } from "../../moq/playback/webcodecs/message" +import type { AggregatorData, AggregatorType, Track } from "../types" +import { Aggregator, BaseAggregatorData } from "./base" + +export class MOQAggregator extends Aggregator { + identifier: AggregatorType = "moq" + publicState: Record = { + abrEnabled: true + } + + #data: AggregatorData = structuredClone(BaseAggregatorData) + #player: Player | null = null + #provisionalPlayer: Promise + + #measuredBandwidth = new SWMA(5, "moq-bw") + #measuredBandwidthHistory = [] as { + timestamp: string; + tc_limit: string; + mb_tc_ratio: string; + measured_bandwidth: string; + bit_rate: string, + }[] + + #abrConfig = { + // startup delay is the time we wait before starting ABR using after the first measuredBandwidth event + startupDelay: 5000, // ms - wait for 5 seconds before starting ABR + // maxSkipSegmentCountForCongestion is the number of skip events we wait for before switching down + maxSkipSegmentCountForCongestion: 5, + // congestionWindow is the time window in which we look for congestion events + congestionWindow: 5000, // ms + // coolOffTime is the time we wait after a congestion event before switching up/down + coolOffTime: 15000, // ms + // switchUpMultiplier is the multiplier for switching up + switchUpMultiplier: 2.3, + // switchDownMultiplier is the multiplier for switching down + switchDownMultiplier: 1.5, + // coolOffTimeAfterSwitch is the time we wait after a switch event before switching up/down + coolOffTimeAfterSwitch: 15000, // ms + // bwSignalWindow is the time window in which we look for bandwidth change events + bwSignalWindow: 5000, // ms + // minBWHighEventsForSwitchUp is the number of bandwidth change events we wait for before switching up + minBWHighEventsForSwitchUp: 3, + // minBWLownEventsForSwitchDown is the number of bandwidth change events we wait for before switching down + minBWLownEventsForSwitchDown: 3 + } + + // initialization time is set when the first measuredBandwidth event is received + #initializationTime = 0 // ms + // lastCongestionTime is the time of the last congestion event + #lastCongestionTime = 0 // ms + // lastSwitchTime is the time of the last switch event + #lastSwitchTime = 0 // ms + // a list of recent events to detect congestion, track bandwidth changes, etc. + #eventHistory = [] as { time: number; event: string }[] + // timerEventHistoryCleanup is the timer for cleaning up the event history + #timerEventHistoryCleanup: number | null = null + + #abrEventCallback: ((newTrack: string) => void) | null = null + + #state: { + stallDuration: number + provisionalStallDuration: number + lastDisplayTime: number + totalSkipDuration: number + tracks: Track[] + latencyTarget: number + serverTimeOffset: number + } = { + stallDuration: 0, + provisionalStallDuration: 0, + lastDisplayTime: 0, + totalSkipDuration: 0, + tracks: [], + latencyTarget: 0, + serverTimeOffset: 0 + } + + constructor(view: HTMLCanvasElement) { + super() + + // Create a new player instance + // if there is an endpoint address (host:port) in the querystring, pass it to the player + // we don't set fingerprint for other domains (we assume, a proper TLS certificate is used) + let endpoint = /[?&]server=([^&]+)/.exec(location.search) + if (endpoint) { + console.log("moq | endpoint", endpoint[1]) + this.#provisionalPlayer = Player.create({ + url: `https://${endpoint[1]}/dev`, + element: view + }) + } else { + this.#provisionalPlayer = Player.create({ + url: `https://${location.hostname}:4443/dev`, + fingerprint: import.meta.env.DEV ? `https://${location.hostname}:4443/fingerprint` : undefined, + element: view + }) + } + + // get timestamp from the time server if available + endpoint = /[?&]ts_url=([^&]+)/.exec(location.search) + const ts_url = endpoint ? endpoint[1] : "/ts" + if (ts_url) { + const start = performance.now() + fetch(ts_url).then((res) => res.text()).then((res) => { + // returns seconds with granularity of milliseconds + const time = parseFloat(res) * 1000 + if (isNaN(time)) { + if (ts_url !== "/ts") + throw new Error("Invalid time server response from " + ts_url) + else + console.log("moq | No time server response") + return + } + const rtt = performance.now() - start + const server_time = time + rtt / 2 + const offset = server_time - Date.now() + console.log("moq | Server timestamp and rtt", time, rtt, offset, res) + this.#state.serverTimeOffset = offset + }).catch ((err) => { + console.error("moq | Error fetching time server", err) + }); + } + } + + async init(callback: (data: AggregatorData) => void): Promise { + // Resolve provisional player + this.#player = await this.#provisionalPlayer + console.log("moq | init", this.#player) + + // Add event listeners + // Custom event support is not yet implemented in TS + // so we have to cast to EventListener :( + this.#player.addEventListener("stat", this.#onStat as EventListener) + this.#player.addEventListener("track_change", this.#onTrackChange as EventListener) + this.#player.addEventListener("skip", this.#onSkip as EventListener) + + // Set latency target + this.#player.setLatencyTarget(this.#state.latencyTarget) + if (!isNaN(this.#state.serverTimeOffset)) { + this.#player.setServerTimeOffset(this.#state.serverTimeOffset) + } + + // Start playback + this.#player.start() + + // Set tracks + this.#state.tracks = this.getTracks() + + this.#timerEventHistoryCleanup = setInterval( + () => { + console.log("event history cleanup", this.#eventHistory.length) + this.#eventHistory = this.#eventHistory.filter((e) => e.time > Date.now() - Math.max(this.#abrConfig.bwSignalWindow, this.#abrConfig.congestionWindow) * 2) + console.log("event history cleanup done", this.#eventHistory.length) + }, + Math.max(this.#abrConfig.bwSignalWindow, this.#abrConfig.congestionWindow) * 2 + ) + + // Register data relay in super class + return super.init(callback) + } + + async destroy(): Promise { + if (!this.#player) throw new Error("Player not initialised") + + // Remove event listeners + this.#player.removeEventListener("stat", this.#onStat as EventListener) + this.#player.removeEventListener("track_change", this.#onTrackChange as EventListener) + this.#player.removeEventListener("skip", this.#onSkip as EventListener) + + // Destroy player instance + await this.#player.close() + + // Reset data + this.#data = structuredClone(BaseAggregatorData) + + // clear event history cleanup timer + if (this.#timerEventHistoryCleanup) clearInterval(this.#timerEventHistoryCleanup) + + // Continue with super class + return super.destroy() + } + + getTracks(): Track[] { + if (!this.#player) throw new Error("Player not initialised") + return this.#player + .getCatalog() + .getVideoTracks() + .map((track) => ({ + id: Catalog.getUniqueTrackId(track), + sid: track.data_track, + bitrate: track.bit_rate, + size: { + width: track.width, + height: track.height + } + })) + } + + setTrack(id: string): void { + if (!this.#player) throw new Error("Player not initialised") + const track = this.#player + .getCatalog() + .getVideoTracks() + .find((track) => Catalog.getUniqueTrackId(track) === id) + if (!track) throw new Error("Track not found") + this.#player.selectVideoTrack(track) + } + + toggleABR(state?: boolean): void { + if (state === undefined) state = !this.publicState.abrEnabled + this.publicState.abrEnabled = state + console.log("moq | ABR", state) + } + + registerABREvents(callback: (newTrack: string) => void): void { + if (!this.#player) throw new Error("Player not initialised") + this.#abrEventCallback = callback + this.#abrEventCallback(this.getTracks().find((track) => track.bitrate === this.#player?.getCurrentVideoTrack()?.bit_rate)?.id ?? "") + } + + setLatencyTarget(val: number): void { + this.#state.latencyTarget = val + + // Set latency target if player is initialised + if (this.#player) { + this.#player.setLatencyTarget(val) + } + } + + #isCoolOffTimeFinished = () => { + let isCoolOffTimeFinished = true + if (this.#lastSwitchTime > this.#lastCongestionTime) { + // seems that we switched up/down recently + isCoolOffTimeFinished = this.#lastSwitchTime + this.#abrConfig.coolOffTimeAfterSwitch < Date.now() + } else { + // a recent congestion event happened + isCoolOffTimeFinished = this.#lastCongestionTime + this.#abrConfig.coolOffTime < Date.now() + } + return isCoolOffTimeFinished + } + + #onSkip = (e: CustomEvent) => { + console.log("moq | Skip event", e.type, e.detail) + const skipData = e.detail as SkipEvent + + const currentTrack = this.#player?.getCurrentVideoTrack() + const isABREnabled = this.publicState.abrEnabled && this.#initializationTime > 0 && this.#initializationTime + this.#abrConfig.startupDelay < performance.now() + const skipEventInTheCurrentTrack = + ["too_old", "too_slow"].includes(skipData?.type) && currentTrack?.data_track === skipData.skippedGroup.track && skipData.skippedGroup.track === skipData.currentGroup.track + + if (isABREnabled && skipEventInTheCurrentTrack && this.#isCoolOffTimeFinished()) { + // if we have a skip event in the current track, add it to the list of skip events + this.#eventHistory.push({ time: Date.now(), event: "skip" }) + + // if we have "maxSkipSegmentCountForCongestion" congestion events in a window of N seconds, switch down + const lastSkipEvents = this.#eventHistory.filter((e) => e.event === "skip" && Date.now() - e.time < this.#abrConfig.congestionWindow) + const congestionIsLikely = lastSkipEvents.length >= this.#abrConfig.maxSkipSegmentCountForCongestion + if (congestionIsLikely) { + console.log("moq | switch down (congestion detected)", lastSkipEvents) + this.#lastCongestionTime = Date.now() + + document.dispatchEvent(new CustomEvent("congestion", { detail: { time: Date.now() } })) + + const tracks = this.getTracks().filter((track) => track.id !== skipData.skippedGroup.track) + // sort tracks by bitrate and select the lowest bitrate + const track = tracks.length > 0 ? tracks.sort((a, b) => (a.bitrate! < b.bitrate! ? -1 : 1))[0] : null + + if (track?.id) { + this.setTrack(track.id) + } else { + console.log("moq | no track to switch down to") + } + + // we reset bandwidth measurement because the last value is not valid anymore + this.#player?.resetBandwidthMeasurement() + } + } + + this.#state.totalSkipDuration += skipData.duration + this.#data.skippedDuration.history.push({ + time: performance.now() + performance.timeOrigin, + value: this.#state.totalSkipDuration + }) + } + + #downloadBWStats = () => { + const link = document.createElement("a") + document.body.appendChild(link) + + // download logs + if (this.#measuredBandwidthHistory.length > 0) { + const headers = [...this.#data.measuredBandwidth.history.keys()] + const csvContent = "data:application/vnd.ms-excel;charset=utf-8," + headers.join("\t") + "\n" + this.#measuredBandwidthHistory.map((e) => Object.values(e).join("\t")).join("\n") + const encodedUri = encodeURI(csvContent) + link.setAttribute("href", encodedUri) + link.setAttribute("download", "logs_" + Date.now() + ".xls") + link.click() + } else { + console.log("no logs") + } + + link.remove() + } + + + #onStat = (e: CustomEvent) => { + if (e.detail.type === "latency") { + this.#data.latency.history.push({ + time: performance.now() + performance.timeOrigin, + value: e.detail.value + }) + this.#player?.setCurrentLatency(parseFloat(e.detail.value) * 1000) + } + + if (e.detail.type === "measuredBandwidth") { + + const measuredBandwidth = e.detail.value + // console.log("measuredBandwidth", measuredBandwidth) + const measuredBandwidth_smoothed = this.#measuredBandwidth.next(measuredBandwidth) + // console.log("measuredBandwidth smoothed", measuredBandwidth) + const tc_limit = (parseFloat(localStorage.getItem("tc_bandwidth") || "0") || 0) / 1000000 // Mbps + + const historyItem = { + time: performance.now() + performance.timeOrigin, + value: measuredBandwidth_smoothed + } + this.#data.measuredBandwidth.history.push(historyItem) + + const currentBitrate = (this.#player?.getCurrentVideoTrack().bit_rate || 0) / 1000000 // Mbps + + this.#measuredBandwidthHistory.push({ + timestamp: performance.now().toString(), + tc_limit: tc_limit.toFixed(2), + mb_tc_ratio: (measuredBandwidth / tc_limit / 1000).toFixed(2), + measured_bandwidth: (measuredBandwidth / 1000).toFixed(2), + bit_rate: currentBitrate.toString()} + ) + + // TODO: Uncomment the following code to download bandwidth stats + /* + const downloadLogsAfterNMeasurementForSameTCLimit = 5 + if (this.#measuredBandwidthHistory.filter(s => s.tc_limit === tc_limit.toFixed(2)).length === downloadLogsAfterNMeasurementForSameTCLimit) { + console.log("moq | onStat | downloading logs", tc_limit) + this.#downloadBWStats() + } + */ + + // initialization time for the ABR algorithm + if (this.#initializationTime === 0) this.#initializationTime = performance.now() + + console.log( + "measuredBandwidth event", + measuredBandwidth, + this.#lastCongestionTime + this.#abrConfig.coolOffTime, + Date.now(), + this.#lastCongestionTime + this.#abrConfig.coolOffTime > Date.now() + ) + + // if the last congestion event happened more than 5 seconds ago + // then we do not want to switch up/down + const isABREnabled = this.publicState.abrEnabled && this.#initializationTime > 0 && this.#initializationTime + this.#abrConfig.startupDelay < performance.now() + + if (this.#isCoolOffTimeFinished() && isABREnabled) { + const currentTrack = this.#player?.getCurrentVideoTrack() + const currentTrackBitrate = currentTrack?.bit_rate ?? 0 + + if (currentTrackBitrate > 0) { + const tracks = this.getTracks() + if (measuredBandwidth * 1000 > currentTrackBitrate * this.#abrConfig.switchUpMultiplier) { + this.#eventHistory.push({ time: Date.now(), event: "bw-high" }) + const lastBWHighEvents = this.#eventHistory.filter((e) => e.event === "bw-high" && Date.now() - e.time < this.#abrConfig.bwSignalWindow) + + const canSwitchUp = lastBWHighEvents.length >= this.#abrConfig.minBWHighEventsForSwitchUp + console.log("moq | switch up", canSwitchUp, lastBWHighEvents) + if (canSwitchUp) { + // switch up + // sort by bitrate and select the highest bitrate among the candidates + const candidates = tracks.filter( + (track) => track.bitrate && track.bitrate * this.#abrConfig.switchUpMultiplier <= measuredBandwidth * 1000 && track.bitrate > currentTrackBitrate + ) + candidates?.length && candidates.sort((a, b) => (a.bitrate! > b.bitrate! ? -1 : 1)) + const track = candidates.length > 0 ? candidates[0] : null + if (track?.id) { + console.log("moq | switch up: track bitrate: %d bps | tput: %d Kbps", track.bitrate, measuredBandwidth) + this.setTrack(track.id) + this.#lastSwitchTime = Date.now() + } + } + } else if (measuredBandwidth * 1000 < currentTrackBitrate * this.#abrConfig.switchDownMultiplier) { + this.#eventHistory.push({ time: Date.now(), event: "bw-low" }) + const lastBWLowEvents = this.#eventHistory.filter((e) => e.event === "bw-low" && Date.now() - e.time < this.#abrConfig.bwSignalWindow) + + const canSwitchDown = lastBWLowEvents.length >= this.#abrConfig.minBWLownEventsForSwitchDown + + if (canSwitchDown) { + // switch down + // sort by bitrate and select the highest bitrate among the candidates + const candidates = tracks.filter( + (track) => track.bitrate && track.bitrate * this.#abrConfig.switchDownMultiplier <= measuredBandwidth * 1000 && track.bitrate < currentTrackBitrate + ) + candidates?.length && candidates.sort((a, b) => (a.bitrate! > b.bitrate! ? -1 : 1)) + + const track = candidates.length > 0 ? candidates[0] : null + if (track?.id) { + console.log("moq | switch down: track bitrate: %d bps | measuredBandwidth: %d Kbps", track.bitrate, measuredBandwidth, candidates) + this.setTrack(track.id) + this.#lastSwitchTime = Date.now() + } + } + } + } + } + } + + if (e.detail.type === "stall") { + const stall = e.detail.value + if (this.#state.lastDisplayTime !== stall.since) { + this.#state.lastDisplayTime = stall.since + this.#state.stallDuration += this.#state.provisionalStallDuration + this.#state.provisionalStallDuration = 0 + } + this.#state.provisionalStallDuration = stall.duration + + this.#data.stallDuration.history.push({ + time: performance.now() + performance.timeOrigin, + value: this.#state.stallDuration + this.#state.provisionalStallDuration + }) + } + + // Relay data + this.updateSnapshot(this.#data) + this.#data = structuredClone(BaseAggregatorData) + } + + #onTrackChange = (e: CustomEvent) => { + console.log("track change event", e.detail.value) + const trackId = e.detail + const track: Track | undefined = this.#state.tracks.find((track) => track.sid === trackId) + if (!track) return + this.#data.bitrate.history.push({ + time: performance.now() + performance.timeOrigin, + value: track.bitrate ?? 0 + }) + if (this.#abrEventCallback) this.#abrEventCallback(track.id ?? "") + } +} diff --git a/repos/demo/src/lib/internal/bandwidthSynchroniser.ts b/repos/demo/src/lib/internal/bandwidthSynchroniser.ts new file mode 100644 index 0000000..3fc944e --- /dev/null +++ b/repos/demo/src/lib/internal/bandwidthSynchroniser.ts @@ -0,0 +1,48 @@ +import { createContext, useContext } from "react"; + +export type BandwidthSettings = { + bandwidth: string; + isSyncEnabled: boolean; +}; + +export const BaseBandwidthSettings: BandwidthSettings = { + bandwidth: "", // default bandwidth value + isSyncEnabled: false, +}; + +// Context for bandwidth synchronizer +export const BandwidthContext = createContext(BaseBandwidthSettings); +export const useBandwidth = () => useContext(BandwidthContext); + +export class BandwidthSynchroniser { + #settings: BandwidthSettings = BaseBandwidthSettings; + #bc: BroadcastChannel; + + constructor() { + this.#bc = new BroadcastChannel("bandwidth_sync"); + this.#bc.onmessage = this.#synchronise.bind(this); + } + + get settings(): BandwidthSettings { + return this.#settings; + } + + get channel(): BroadcastChannel { + return this.#bc; + } + + #synchronise({ data }: MessageEvent) { + console.log("Received sync data", data); + this.#settings = { ...data }; + console.log("Updated sync data", this.#settings) + } + + updateSettings(bandwidth: string, isSyncEnabled: boolean) { + this.#settings = { ...this.#settings, bandwidth, isSyncEnabled }; + this.#bc.postMessage(this.#settings); + } +} + +// BandwidthSynchroniser context +export const BandwidthSynchroniserContext = createContext(new BandwidthSynchroniser()); +export const useBandwidthSynchroniser = () => useContext(BandwidthSynchroniserContext); diff --git a/repos/demo/src/lib/internal/synchroniser.ts b/repos/demo/src/lib/internal/synchroniser.ts new file mode 100644 index 0000000..b98bfc4 --- /dev/null +++ b/repos/demo/src/lib/internal/synchroniser.ts @@ -0,0 +1,125 @@ +import { BaseAggregatorData, type Aggregator } from "./aggregator/base" +import type { AggregatorSnapshot, AggregatorType, SynchroniserData, SynchroniserEventData, SynchroniserSnapshot } from "./types" +import mergeWith from "lodash/mergeWith" +import { createContext } from "react" +import { useContext } from "react" +import { ArrayMerger } from "../../utils/array" + +export const BaseSynchroniserData: SynchroniserData = { + moq: structuredClone(BaseAggregatorData), + dash: structuredClone(BaseAggregatorData) +} + +export const BaseSynchroniserSnapshot: SynchroniserSnapshot = { + snapshot: Object.keys(BaseAggregatorData).reduce((acc, key) => { + const metric = key as keyof AggregatorSnapshot + acc[metric] = { + current: 0, + ratio: 0 + } + return acc + }, {} as AggregatorSnapshot), + data: structuredClone(BaseAggregatorData) +} + +// Metric data context +export const MetricsContext = createContext(structuredClone(BaseSynchroniserSnapshot)) +export const useMetrics = () => useContext(MetricsContext) + +export class MetricSynchroniser { + #data: SynchroniserData = structuredClone(BaseSynchroniserData) + #aggregator: Aggregator | null = null + #aggregatorInitialised = false + #bc: BroadcastChannel + #comparisonIdentifier: AggregatorType | null = null + + constructor() { + // Open a BroadcastChannel to receive data from other tabs + this.#bc = new BroadcastChannel("metrics") + this.#bc.onmessage = this.#synchronise.bind(this) + } + + /** + * Get the current metrics. If a comparison is set, the ratio is calculated. + * The ratio is the comparison value divided by the current value. Which gives the magnitude of greatness. + */ + get metrics(): SynchroniserSnapshot { + if (!this.#aggregator) throw new Error("No aggregator registered") + + const snapshot = Object.entries(this.#data[this.#aggregator.identifier]).reduce((acc, [key, value]) => { + const metric = key as keyof AggregatorSnapshot + const currentValue = value.history[value.history.length - 1]?.value ?? 0 + + const comparison = this.#comparisonIdentifier ? this.#data[this.#comparisonIdentifier][metric] : null + const comparisonValue = comparison?.history[comparison.history.length - 1]?.value ?? 0 + + let ratio = comparisonValue ? currentValue / comparisonValue : 0 + + // Clamp and adjust ratio + if (comparison) { + ratio -= 1 + ratio = Math.min(Math.max(ratio, -1), 1) + } + + // Invert ratio if needed + if (value.invert) ratio *= -1 + + acc[metric] = { + current: currentValue, + ratio + } + return acc + }, {} as AggregatorSnapshot) + + // Compare data + return { + snapshot, + data: this.#data[this.#aggregator.identifier] + } + } + + get aggregator(): Aggregator | null { + if (!this.#aggregatorInitialised) { + console.warn("aggregator is not initialised") + return null + } + return this.#aggregator + } + + get data(): SynchroniserData { + return this.#data + } + + #synchronise({ data }: MessageEvent) { + // Save comparison data + this.#comparisonIdentifier = data.identifier + if (this.#aggregator?.identifier === data.identifier) return + mergeWith(this.#data[data.identifier], data.data, ArrayMerger) + } + + async register(aggregator: Aggregator) { + this.#aggregator = aggregator + await aggregator.init((data) => { + // Save current data + mergeWith(this.#data[aggregator.identifier], data, ArrayMerger) + + // Send data to comparison + this.#bc.postMessage({ + identifier: aggregator.identifier, + data + }) + }) + this.#aggregatorInitialised = true + } + + unregister() { + if (!this.#aggregator) throw new Error("No aggregator registered") + this.#aggregator.destroy() + this.#aggregator = null + this.#data = structuredClone(BaseSynchroniserData) + } +} + +// MetricSyncroniser context +export const MetricSynchroniserContext = createContext(new MetricSynchroniser()) +export const useMetricSynchroniser = () => useContext(MetricSynchroniserContext) diff --git a/repos/demo/src/lib/internal/types.ts b/repos/demo/src/lib/internal/types.ts new file mode 100644 index 0000000..e66365b --- /dev/null +++ b/repos/demo/src/lib/internal/types.ts @@ -0,0 +1,64 @@ +type HistoryItem = { + time: number + value: number +} + +type Snapshot = { + current: number + ratio: number +} + +export type AggregatorType = "moq" | "dash" +export type PlayerType = AggregatorType + +export type AggregatorSnapshot = { + [key in keyof AggregatorData]: Snapshot +} + +export interface AggregatorData { + latency: { + history: HistoryItem[] + invert: boolean + } + measuredBandwidth: { + history: HistoryItem[] + invert: boolean + } + stallDuration: { + history: HistoryItem[] + invert: boolean + } + bitrate: { + history: HistoryItem[] + invert: boolean + } + skippedDuration: { + history: HistoryItem[] + invert: boolean + } +} + +export interface SynchroniserSnapshot { + snapshot: AggregatorSnapshot + data: AggregatorData +} + +export interface SynchroniserData { + moq: AggregatorData + dash: AggregatorData +} + +export interface SynchroniserEventData { + identifier: AggregatorType + data: AggregatorData +} + +export interface Track { + id: string | undefined, + sid: string | undefined + bitrate: number | undefined + size: { + width: number + height: number + } +} diff --git a/repos/demo/src/lib/moq/.eslintrc.cjs b/repos/demo/src/lib/moq/.eslintrc.cjs new file mode 100644 index 0000000..8d6b5bb --- /dev/null +++ b/repos/demo/src/lib/moq/.eslintrc.cjs @@ -0,0 +1,50 @@ +/* eslint-env node */ +module.exports = { + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking", "plugin:@typescript-eslint/strict", "prettier"], + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint", "prettier"], + root: true, + env: { + browser: true, + es2022: true, + worker: true + }, + ignorePatterns: ["dist", "node_modules", ".eslintrc.cjs"], + rules: { + // Allow the ! operator because typescript can't always figure out when something is not undefined + "@typescript-eslint/no-non-null-assertion": "off", + + // Allow `any` because Javascript was not designed to be type safe. + "@typescript-eslint/no-explicit-any": "off", + + // Requring a comment in empty function is silly + "@typescript-eslint/no-empty-function": "off", + + // Warn when an unused variable doesn't start with an underscore + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_" + } + ], + + // The no-unsafe-* rules are a pain an introduce a lot of false-positives. + // Typescript will make sure things are properly typed. + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-return": "off", + + // Make formatting errors into warnings + "prettier/prettier": 1 + }, + + parserOptions: { + project: true, + tsconfigRootDir: __dirname + } +} diff --git a/repos/demo/src/lib/moq/README.md b/repos/demo/src/lib/moq/README.md new file mode 100644 index 0000000..4aa2e40 --- /dev/null +++ b/repos/demo/src/lib/moq/README.md @@ -0,0 +1,20 @@ +# Media over QUIC + +Media over QUIC (MoQ) is a live media delivery protocol utilizing QUIC streams. +See the [Warp draft](https://datatracker.ietf.org/doc/draft-lcurley-warp/). + +This is a Typescript library that supports both contribution (ingest) and distribution (playback). +It requires a server, such as [moq-rs](https://github.com/kixelated/moq-rs). + +## Usage + +``` +npm install @kixelated/moq +``` + +## License + +Licensed under either: + +- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) diff --git a/repos/demo/src/lib/moq/common/async.ts b/repos/demo/src/lib/moq/common/async.ts new file mode 100644 index 0000000..21596a3 --- /dev/null +++ b/repos/demo/src/lib/moq/common/async.ts @@ -0,0 +1,120 @@ +export class Deferred { + promise: Promise + resolve!: (value: T | PromiseLike) => void + reject!: (reason: any) => void + pending = true + + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = (value) => { + this.pending = false + resolve(value) + } + this.reject = (reason) => { + this.pending = false + reject(reason) + } + }) + } +} + +export type WatchNext = [T, Promise> | undefined] + +export class Watch { + #current: WatchNext + #next = new Deferred>() + + constructor(init: T) { + this.#next = new Deferred>() + this.#current = [init, this.#next.promise] + } + + value(): WatchNext { + return this.#current + } + + update(v: T | ((v: T) => T)) { + if (!this.#next.pending) { + throw new Error("already closed") + } + + // If we're given a function, call it with the current value + if (v instanceof Function) { + v = v(this.#current[0]) + } + + const next = new Deferred>() + this.#current = [v, next.promise] + this.#next.resolve(this.#current) + this.#next = next + } + + close() { + this.#current[1] = undefined + this.#next.resolve(this.#current) + } +} + +// Wakes up a multiple consumers. +export class Notify { + #next = new Deferred() + + async wait() { + return this.#next.promise + } + + wake() { + if (!this.#next.pending) { + throw new Error("closed") + } + + this.#next.resolve() + this.#next = new Deferred() + } + + close() { + this.#next.resolve() + } +} + +// Allows queuing N values, like a Channel. +export class Queue { + #stream: TransformStream + #closed = false + + constructor(capacity = 1) { + const queue = new CountQueuingStrategy({ highWaterMark: capacity }) + this.#stream = new TransformStream({}, undefined, queue) + } + + async push(v: T) { + const w = this.#stream.writable.getWriter() + await w.write(v) + w.releaseLock() + } + + async next(): Promise { + const r = this.#stream.readable.getReader() + const { value, done } = await r.read() + r.releaseLock() + + if (done) return + return value + } + + async abort(err: Error) { + if (this.#closed) return + await this.#stream.writable.abort(err) + this.#closed = true + } + + async close() { + if (this.#closed) return + await this.#stream.writable.close() + this.#closed = true + } + + closed() { + return this.#closed + } +} diff --git a/repos/demo/src/lib/moq/common/download.ts b/repos/demo/src/lib/moq/common/download.ts new file mode 100644 index 0000000..978c5e9 --- /dev/null +++ b/repos/demo/src/lib/moq/common/download.ts @@ -0,0 +1,18 @@ +// Utility function to download a Uint8Array for debugging. +export function download(data: Uint8Array, name: string) { + const blob = new Blob([data], { + type: "application/octet-stream" + }) + + const url = window.URL.createObjectURL(blob) + + const a = document.createElement("a") + a.href = url + a.download = name + document.body.appendChild(a) + a.style.display = "none" + a.click() + a.remove() + + setTimeout(() => window.URL.revokeObjectURL(url), 1000) +} diff --git a/repos/demo/src/lib/moq/common/error.ts b/repos/demo/src/lib/moq/common/error.ts new file mode 100644 index 0000000..6e9096a --- /dev/null +++ b/repos/demo/src/lib/moq/common/error.ts @@ -0,0 +1,14 @@ +// I hate javascript +export function asError(e: any): Error { + if (e instanceof Error) { + return e + } else if (typeof e === "string") { + return new Error(e) + } else { + return new Error(String(e)) + } +} + +export function isError(e: any): e is Error { + return e instanceof Error +} diff --git a/repos/demo/src/lib/moq/common/index.ts b/repos/demo/src/lib/moq/common/index.ts new file mode 100644 index 0000000..6d519d3 --- /dev/null +++ b/repos/demo/src/lib/moq/common/index.ts @@ -0,0 +1 @@ +export { asError } from "./error" diff --git a/repos/demo/src/lib/moq/common/ring.ts b/repos/demo/src/lib/moq/common/ring.ts new file mode 100644 index 0000000..9486b45 --- /dev/null +++ b/repos/demo/src/lib/moq/common/ring.ts @@ -0,0 +1,176 @@ +// Ring buffer with audio samples. + +enum STATE { + READ_POS = 0, // The current read position + WRITE_POS, // The current write position + LENGTH // Clever way of saving the total number of enums values. +} + +interface FrameCopyToOptions { + frameCount?: number + frameOffset?: number + planeIndex: number +} + +// This is implemented by AudioData in WebCodecs, but we don't import it because it's a DOM type. +interface Frame { + numberOfFrames: number + numberOfChannels: number + copyTo(dst: Float32Array, options: FrameCopyToOptions): void +} + +// No prototype to make this easier to send via postMessage +export class RingShared { + state: SharedArrayBuffer + + channels: SharedArrayBuffer[] + capacity: number + + constructor(channels: number, capacity: number) { + // Store the current state in a separate ring buffer. + this.state = new SharedArrayBuffer(STATE.LENGTH * Int32Array.BYTES_PER_ELEMENT) + + // Create a buffer for each audio channel + this.channels = [] + for (let i = 0; i < channels; i += 1) { + const buffer = new SharedArrayBuffer(capacity * Float32Array.BYTES_PER_ELEMENT) + this.channels.push(buffer) + } + + this.capacity = capacity + } +} + +export class Ring { + state: Int32Array + channels: Float32Array[] + capacity: number + + constructor(shared: RingShared) { + this.state = new Int32Array(shared.state) + + this.channels = [] + for (const channel of shared.channels) { + this.channels.push(new Float32Array(channel)) + } + + this.capacity = shared.capacity + } + + // Write samples for single audio frame, returning the total number written. + write(frame: Frame): number { + const readPos = Atomics.load(this.state, STATE.READ_POS) + const writePos = Atomics.load(this.state, STATE.WRITE_POS) + + const startPos = writePos + let endPos = writePos + frame.numberOfFrames + + if (endPos > readPos + this.capacity) { + endPos = readPos + this.capacity + if (endPos <= startPos) { + // No space to write + return 0 + } + } + + const startIndex = startPos % this.capacity + const endIndex = endPos % this.capacity + + // Loop over each channel + for (let i = 0; i < this.channels.length; i += 1) { + const channel = this.channels[i] + + // If the AudioData doesn't have enough channels, duplicate it. + const planeIndex = Math.min(i, frame.numberOfChannels - 1) + + if (startIndex < endIndex) { + // One continuous range to copy. + const full = channel.subarray(startIndex, endIndex) + + frame.copyTo(full, { + planeIndex, + frameCount: endIndex - startIndex + }) + } else { + const first = channel.subarray(startIndex) + const second = channel.subarray(0, endIndex) + + frame.copyTo(first, { + planeIndex, + frameCount: first.length + }) + + // We need this conditional when startIndex == 0 and endIndex == 0 + // When capacity=4410 and frameCount=1024, this was happening 52s into the audio. + if (second.length) { + frame.copyTo(second, { + planeIndex, + frameOffset: first.length, + frameCount: second.length + }) + } + } + } + + Atomics.store(this.state, STATE.WRITE_POS, endPos) + + return endPos - startPos + } + + read(dst: Float32Array[]): number { + const readPos = Atomics.load(this.state, STATE.READ_POS) + const writePos = Atomics.load(this.state, STATE.WRITE_POS) + + const startPos = readPos + let endPos = startPos + dst[0].length + + if (endPos > writePos) { + endPos = writePos + if (endPos <= startPos) { + // Nothing to read + return 0 + } + } + + const startIndex = startPos % this.capacity + const endIndex = endPos % this.capacity + + // Loop over each channel + for (let i = 0; i < dst.length; i += 1) { + if (i >= this.channels.length) { + // ignore excess channels + } + + const input = this.channels[i] + const output = dst[i] + + if (startIndex < endIndex) { + const full = input.subarray(startIndex, endIndex) + output.set(full) + } else { + const first = input.subarray(startIndex) + const second = input.subarray(0, endIndex) + + output.set(first) + output.set(second, first.length) + } + } + + Atomics.store(this.state, STATE.READ_POS, endPos) + + return endPos - startPos + } + + clear() { + const pos = Atomics.load(this.state, STATE.WRITE_POS) + Atomics.store(this.state, STATE.READ_POS, pos) + } + + size() { + // TODO is this thread safe? + const readPos = Atomics.load(this.state, STATE.READ_POS) + const writePos = Atomics.load(this.state, STATE.WRITE_POS) + + return writePos - readPos + } +} diff --git a/repos/demo/src/lib/moq/common/settings.ts b/repos/demo/src/lib/moq/common/settings.ts new file mode 100644 index 0000000..06016be --- /dev/null +++ b/repos/demo/src/lib/moq/common/settings.ts @@ -0,0 +1,33 @@ +// MediaTrackSettings can represent both audio and video, which means a LOT of possibly undefined properties. +// This is a fork of the MediaTrackSettings interface with properties required for audio or vidfeo. +export interface AudioTrackSettings { + deviceId: string + groupId: string + + autoGainControl: boolean + channelCount: number + echoCancellation: boolean + noiseSuppression: boolean + sampleRate: number + sampleSize: number +} + +export interface VideoTrackSettings { + deviceId: string + groupId: string + + aspectRatio: number + facingMode: "user" | "environment" | "left" | "right" + frameRate: number + height: number + resizeMode: "none" | "crop-and-scale" + width: number +} + +export function isAudioTrackSettings(settings: MediaTrackSettings): settings is AudioTrackSettings { + return "sampleRate" in settings +} + +export function isVideoTrackSettings(settings: MediaTrackSettings): settings is VideoTrackSettings { + return "width" in settings +} diff --git a/repos/demo/src/lib/moq/common/tsconfig.json b/repos/demo/src/lib/moq/common/tsconfig.json new file mode 100644 index 0000000..3ae2a24 --- /dev/null +++ b/repos/demo/src/lib/moq/common/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.json", + "include": ["."] +} diff --git a/repos/demo/src/lib/moq/common/utils.ts b/repos/demo/src/lib/moq/common/utils.ts new file mode 100644 index 0000000..9ee3ba5 --- /dev/null +++ b/repos/demo/src/lib/moq/common/utils.ts @@ -0,0 +1,331 @@ +interface ObjectCompleteness { + done: boolean + diff?: number + parsedObjectCount?: number +} +export class SWMA { + /* + Sliding Window Moving Average + Given size N, calculate the average of the last N values. + Also filter out outliers by z-score. + */ + + #size: number + #values: number[] + #method: "std" | "p90" = "std" + #lastCalculation: number = 0 + #label: string = "SWMA" + + constructor(size: number, label: string = "SWMA") { + this.#size = size + this.#values = [] + this.#label = label + } + + reset() { + this.#values = [] + } + + next(value?: number) { + let val = this.#lastCalculation + if (value && !isNaN(value)) { + this.#values.push(value) + if (this.#values.length > this.#size) { + // remove as many values as the calculation window + this.#values.shift() + } + val = this.#calc() ?? 0 + this.#lastCalculation = val + } + return val + } + + #calc() { + // If we don't have enough values, return only the last value + /*if (this.#values.length < this.#size) { + console.warn(`SWMA ${this.#label} | Not enough values for SWMA, returning last value: ${this.#values[this.#values.length - 1]}`) + return this.#values[this.#values.length - 1] + }*/ + + if (this.#method === "p90") { + // Sort the values + const sorted = this.#values.sort((a, b) => a - b) + + // Get the 90th percentile + const p95 = sorted[Math.floor(sorted.length * 0.95)] + const p5 = sorted[Math.floor(sorted.length * 0.05)] + + // Filter out values that are more than 2 standard deviations from the mean + const filtered = this.#values.filter((x) => x > p5 && x < p95) + + // Calculate the average of the filtered values + const avg = filtered.reduce((a, b) => a + b, 0) / filtered.length + console.log(`SWMA: ${this.#values.length} values, ${filtered.length} filtered, ${p5} p5, ${p95} p95 => ${avg}`, this.#values) + return avg + } else if (this.#method === "std") { + // Calculate the average + const mean = this.#values.reduce((a, b) => a + b, 0) / this.#values.length + + // Calculate the sample standard deviation + const std = Math.sqrt(this.#values.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b, 0) / (this.#values.length - 1)) + + // Filter out values that are more than 2 standard deviations from the mean + const filtered = this.#values.filter((x) => Math.abs(x - mean) < 2 * std && x > 0) + + // Calculate the average of the filtered values + const avg = filtered.reduce((a, b) => a + b, 0) / filtered.length + // console.log(`SWMA: ${this.#values.length} values, ${filtered.length} filtered, ${mean} mean, ${std} std => ${avg}`, this.#values) + return avg + } + } +} + +enum ObjectReceiveStatus { + None, + Waiting, + Done +} +export class TPEstimator { + #measuredBandwidths: SWMA + #passthrough: TransformStream + #lastTPutAnnounce: number = 0 + #announceInterval: number = 1000 // ms + + #lastObjectStart = 0 + #lastReceivedOffset = 0 + #previousChunkBuffer: Uint8Array = new Uint8Array() + + #currentObjectStatus: ObjectReceiveStatus = ObjectReceiveStatus.None + + #chunkCounter = 0 + + #segmentId: string + + #resultCallback: (measurement: number) => void + + constructor(measuredBandwidths: SWMA, resultCallback: (measurement: number) => void) { + this.#resultCallback = resultCallback + this.#measuredBandwidths = measuredBandwidths + + this.#passthrough = new TransformStream({ + transform: this.#transform.bind(this), + flush: () => {} + }) + } + + get stream() { + return this.#passthrough + } + + set segmentId (id: string) { + this.#segmentId = id + } + + get segmentId () { + return this.#segmentId + } + + isObjectComplete = (data: Uint8Array, parsedObjectCount = 0): ObjectCompleteness => { + if (parsedObjectCount > 100) { + throw new Error("TPEstimator | isObjectComplete | parsedObjectCount is too high. Something is wrong.") + } + + if (data.length < 8) { + return { done: false } + } + + const prftLength = new DataView(data.buffer, 0, 4).getUint32(0) + + // do we have moof? + if (prftLength && prftLength + 4 > data.length) { + return { done: false } + } + + const moofLength = new DataView(data.buffer, prftLength, 4).getUint32(0) + + // do we have mdat + if (moofLength && prftLength + moofLength + 4 > data.length) { + return { done: false } + } + + const mdatLength = new DataView(data.buffer, prftLength + moofLength, 4).getUint32(0) + + const diff = data.length - (prftLength + moofLength + mdatLength) + + if (diff >= 0) { + parsedObjectCount++ + } + + if (diff < 0 && parsedObjectCount === 0) { + // we don't have enough data to parse the first object + return { done: false } + } else if (diff < 0 && parsedObjectCount > 0) { + // there are more than one objects in the buffer but + // the last one is incomplete + // console.log("TPEstimator | isObjectComplete 1 | parsedObjectCount: %d, diff: %d", parsedObjectCount, diff) + return { done: true, diff: diff, parsedObjectCount } + } else if (diff > 0) { + // we have more than one objects in the buffer + // continue parsing the next object + // console.log("TPEstimator | isObjectComplete 2 | parsedObjectCount: %d, data.length: %d diff: %d p/m/md:%d/%d/%d", parsedObjectCount, data.length, diff, prftLength, moofLength, mdatLength) + return this.isObjectComplete(data.slice(data.length - diff), parsedObjectCount) + } else { + // objects are perfectly aligned in the buffer + // we have one or more objects + // console.log("TPEstimator | isObjectComplete 3 | parsedObjectCount: %d", parsedObjectCount, data.length, diff, prftLength, moofLength, mdatLength) + return { done: true, diff: 0, parsedObjectCount } + } + } + + #transform(chunk: Uint8Array, controller: TransformStreamDefaultController) { + const buffer = this.#previousChunkBuffer.length > 0 ? new Uint8Array([...this.#previousChunkBuffer, ...chunk]) : new Uint8Array(chunk) + + // GoP = CMAF segment = QUIC stream = MoQ Group + // Object = CMAF chunk = QUIC stream frame = [prft][moof][mdat] + + // console.debug("TPEstimator | transform | chunk length: %d, previous chunk length: %d", chunk.length, this.#previousChunkBuffer.length) + // const size = new DataView(copy.buffer, copy.byteOffset, copy.byteLength).getUint32(0) + // const atom = await stream.bytes(size) + + // boxes: [prft][moof][mdat]......[prft][moof][mdat] + // first 4 bytes => size + // following 4 bytes => box type + /* + --------------------------------- + t0 [prft][md at][moof][prft] + --------------------------------- + isObjectComplete: false + waiting=true + + --------------------------------- + t1 [prft][md at][moof][prft] + --------------------------------- + + isObjectComplete: true + bw = size([md) / (t1-t0) + + --------------------------------- + t2 [mdat][moof] [prft] + --------------------------------- + isObjectComplete: false + + o o o o o o o o (B bps) + o o o o o ooo + o o o o o o + + C > B -> Source limited + C < B -> Network limited + R: congestion control rate + R <= C + */ + + // if boxes come in one shot, we can measure the bandwidth by using the latency + let latency = 0 + + if (this.#currentObjectStatus === ObjectReceiveStatus.None) { + if (buffer.length >= 32) { + // get ntp_timestamp from the prft box + const track_id = new DataView(buffer.buffer, 12, 4).getUint32(0) + const ntp_timestamp = new Number(new DataView(buffer.buffer, 16, 8).getBigUint64(0)) + const recv_ts = ntptoms(ntp_timestamp.valueOf()) + latency = performance.now() + performance.timeOrigin - recv_ts + } + } + // do we have all the boxes? + let result: ObjectCompleteness = { done: false } + + try { + result = this.isObjectComplete(buffer) + } catch (e: any) { + console.warn("TPEstimator | isObjectComplete | error: %s", e.message) + } + + if (!result.done) { + // we don't have all of the boxes. So we wait for the next chunk + // we assume that the next chunk is on the link layer + // so when we have it, we can measure the bandwidth + this.#previousChunkBuffer = buffer + if (this.#currentObjectStatus === ObjectReceiveStatus.None) { + this.#currentObjectStatus = ObjectReceiveStatus.Waiting + this.#lastObjectStart = performance.now() + performance.timeOrigin // recv_ts + this.#lastReceivedOffset = buffer.length + } + } else { + // if object status is none (not waiting), it means that + // we had all the boxes in one chunk + this.#chunkCounter++ + const isOneShot = this.#currentObjectStatus === ObjectReceiveStatus.None + this.#currentObjectStatus = ObjectReceiveStatus.Done + + let downloadDuration = 0 + let tput = 0 + + if (this.#lastObjectStart > 0) { + downloadDuration = (performance.now() + performance.timeOrigin - this.#lastObjectStart) / 1000 + this.#lastObjectStart = 0 + } + + if (downloadDuration > 0.001 && this.#chunkCounter === 1) { + tput = ((buffer.length - (result.diff ?? 0) - this.#lastReceivedOffset) * 8) / 1000 / downloadDuration + // tput = ((buffer.length - this.#lastReceivedOffset) * 8) / 1000 / downloadDuration + + const measurement = this.#measuredBandwidths.next(tput) + + if (performance.now() - this.#lastTPutAnnounce > this.#announceInterval) { + console.log("TPEstimator | announcing bw measurement: %d segmentId: %s", measurement, this.#segmentId) + if (this.#segmentId) { + this.#lastTPutAnnounce = performance.now() + if (this.#resultCallback !== undefined) { + this.#resultCallback(measurement) + } + } + } + // console.log(`TPEstimator | dur: ${downloadDuration} buflen:${buffer.length} lROffset:${this.#lastReceivedOffset} diff:${result.diff} prevChunkLen: ${this.#previousChunkBuffer.length} meas:${measurement} tput:${tput} lOStart: ${this.#lastObjectStart} oneShot:${isOneShot} chunk: ${this.#chunkCounter}`) + } else { + // console.log(`TPEstimator | dur: ${downloadDuration} buflen:${buffer.length} lROffset:${this.#lastReceivedOffset} diff:${result.diff} prevChunkLen: ${this.#previousChunkBuffer.length} meas:0 tput:0 lOStart: ${this.#lastObjectStart} oneShot:${isOneShot} chunk: ${this.#chunkCounter}`) + } + + /* + // TODO: this is not working. ZG. For one shot, I tried to measure the bandwidth by using the latency + // but it did not work. So, I commented out this part. We discard one shot bandwidth measurement for now. + if (tput === 0 && isOneShot && latency > 0) { + const measurement = (buffer.length * 8) / 1000.0 / latency + console.log("TPEstimator | one shot | latency: %d, bw measurement: %f", latency, measurement, buffer.length * 8) + } + */ + + // pass overflowing data + if (result.diff || 0 < 0) { + this.#previousChunkBuffer = buffer.slice(buffer.length + result.diff!) + this.#lastObjectStart = performance.now() + performance.timeOrigin + this.#lastReceivedOffset = -1 * result.diff! + this.#currentObjectStatus = ObjectReceiveStatus.Waiting + } else { + this.#previousChunkBuffer = new Uint8Array(0) + this.#lastObjectStart = 0 + this.#lastReceivedOffset = 0 + } + + this.#currentObjectStatus = ObjectReceiveStatus.None + } + + controller.enqueue(chunk) + } +} + +function ntptoms(ntpTimestamp?: number) { + if (!ntpTimestamp) return NaN + + const ntpEpochOffset = 2208988800000 // milliseconds between 1970 and 1900 + + // Split the 64-bit NTP timestamp into upper and lower 32-bit parts + const upperPart = Math.floor(ntpTimestamp / Math.pow(2, 32)) + const lowerPart = ntpTimestamp % Math.pow(2, 32) + + // Calculate milliseconds for upper and lower parts + const upperMilliseconds = upperPart * 1000 + const lowerMilliseconds = (lowerPart / Math.pow(2, 32)) * 1000 + + // Combine both parts and adjust for the NTP epoch offset + return upperMilliseconds + lowerMilliseconds - ntpEpochOffset +} diff --git a/repos/demo/src/lib/moq/contribute/audio.ts b/repos/demo/src/lib/moq/contribute/audio.ts new file mode 100644 index 0000000..a61cd8c --- /dev/null +++ b/repos/demo/src/lib/moq/contribute/audio.ts @@ -0,0 +1,71 @@ +const SUPPORTED = [ + // TODO support AAC + // "mp4a" + "Opus" +] + +export class Encoder { + #encoder!: AudioEncoder + #encoderConfig: AudioEncoderConfig + #decoderConfig?: AudioDecoderConfig + + frames: TransformStream + + constructor(config: AudioEncoderConfig) { + this.#encoderConfig = config + + this.frames = new TransformStream({ + start: this.#start.bind(this), + transform: this.#transform.bind(this), + flush: this.#flush.bind(this) + }) + } + + #start(controller: TransformStreamDefaultController) { + this.#encoder = new AudioEncoder({ + output: (frame, metadata) => { + this.#enqueue(controller, frame, metadata) + }, + error: (err) => { + throw err + } + }) + + this.#encoder.configure(this.#encoderConfig) + } + + #transform(frame: AudioData) { + this.#encoder.encode(frame) + frame.close() + } + + #enqueue(controller: TransformStreamDefaultController, frame: EncodedAudioChunk, metadata?: EncodedAudioChunkMetadata) { + const config = metadata?.decoderConfig + if (config && !this.#decoderConfig) { + const config = metadata.decoderConfig + if (!config) throw new Error("missing decoder config") + + controller.enqueue(config) + this.#decoderConfig = config + } + + controller.enqueue(frame) + } + + #flush() { + this.#encoder.close() + } + + static async isSupported(config: AudioEncoderConfig) { + // Check if we support a specific codec family + const short = config.codec.substring(0, 4) + if (!SUPPORTED.includes(short)) return false + + const res = await AudioEncoder.isConfigSupported(config) + return !!res.supported + } + + get config() { + return this.#encoderConfig + } +} diff --git a/repos/demo/src/lib/moq/contribute/broadcast.ts b/repos/demo/src/lib/moq/contribute/broadcast.ts new file mode 100644 index 0000000..7ba9c29 --- /dev/null +++ b/repos/demo/src/lib/moq/contribute/broadcast.ts @@ -0,0 +1,247 @@ +import { Connection, SubscribeRecv } from "../transport" +import { asError } from "../common/error" +import { Segment } from "./segment" +import { Track } from "./track" +import { Catalog, Mp4Track, VideoTrack, Track as CatalogTrack, AudioTrack } from "../media/catalog" + +import { isAudioTrackSettings, isVideoTrackSettings } from "../common/settings" + +export interface BroadcastConfig { + connection: Connection + media: MediaStream + + audio?: AudioEncoderConfig + video?: VideoEncoderConfig +} + +export interface BroadcastConfigTrack { + codec: string + bitrate: number +} + +export class Broadcast { + #tracks = new Map() + + readonly config: BroadcastConfig + readonly catalog: Catalog + readonly connection: Connection + + #running: Promise + + constructor(config: BroadcastConfig) { + this.connection = config.connection + this.config = config + this.catalog = new Catalog() + + for (const media of this.config.media.getTracks()) { + const track = new Track(media, config) + this.#tracks.set(track.name, track) + + const settings = media.getSettings() + + let catalog: CatalogTrack + + const mp4Catalog: Mp4Track = { + container: "mp4", + kind: media.kind, + init_track: `${track.name}.mp4`, + data_track: `${track.name}.m4s` + } + + if (isVideoTrackSettings(settings)) { + if (!config.video) { + throw new Error("no video configuration provided") + } + + const videoCatalog: VideoTrack = { + ...mp4Catalog, + kind: "video", + codec: config.video.codec, + width: settings.width, + height: settings.height, + frame_rate: settings.frameRate, + bit_rate: config.video.bitrate + } + + catalog = videoCatalog + } else if (isAudioTrackSettings(settings)) { + if (!config.audio) { + throw new Error("no audio configuration provided") + } + + const audioCatalog: AudioTrack = { + ...mp4Catalog, + kind: "audio", + codec: config.audio.codec, + sample_rate: settings.sampleRate, + sample_size: settings.sampleSize, + channel_count: settings.channelCount, + bit_rate: config.audio.bitrate + } + + catalog = audioCatalog + } else { + throw new Error(`unknown track type: ${media.kind}`) + } + + this.catalog.tracks.push(catalog) + } + + this.#running = this.#run() + } + + async #run() { + for (;;) { + const subscriber = await this.connection.subscribed() + if (!subscriber) break + + // Run an async task to serve each subscription. + this.#serveSubscribe(subscriber).catch((e) => { + const err = asError(e) + console.warn("failed to serve subscribe", err) + }) + } + } + + async #serveSubscribe(subscriber: SubscribeRecv) { + try { + const [base, ext] = splitExt(subscriber.track) + if (ext === "catalog") { + await this.#serveCatalog(subscriber, base) + } else if (ext === "mp4") { + await this.#serveInit(subscriber, base) + } else if (ext === "m4s") { + await this.#serveTrack(subscriber, base) + } else { + throw new Error(`unknown subscription: ${subscriber.track}`) + } + } catch (e) { + const err = asError(e) + await subscriber.close(1n, `failed to process subscribe: ${err.message}`) + } finally { + // TODO we can't close subscribers because there's no support for clean termination + // await subscriber.close() + } + } + + async #serveCatalog(subscriber: SubscribeRecv, name: string) { + // We only support ".catalog" + if (name !== "") throw new Error(`unknown catalog: ${name}`) + + const bytes = this.catalog.encode() + + // Send a SUBSCRIBE_OK + await subscriber.ack() + + const stream = await subscriber.data({ + group: 0, + object: 0, + priority: 0 + }) + + const writer = stream.getWriter() + + try { + await writer.write(bytes) + await writer.close() + } catch (e) { + const err = asError(e) + await writer.abort(err.message) + throw err + } finally { + writer.releaseLock() + } + } + + async #serveInit(subscriber: SubscribeRecv, name: string) { + const track = this.#tracks.get(name) + if (!track) throw new Error(`no track with name ${subscriber.track}`) + + // Send a SUBSCRIBE_OK + await subscriber.ack() + + const init = await track.init() + + // Create a new stream for each segment. + const stream = await subscriber.data({ + group: 0, + object: 0, + priority: 0, // TODO + expires: 0 // Never expires + }) + + const writer = stream.getWriter() + + // TODO make a helper to pipe a Uint8Array to a stream + try { + // Write the init segment to the stream. + await writer.write(init) + await writer.close() + } catch (e) { + const err = asError(e) + await writer.abort(err.message) + throw err + } finally { + writer.releaseLock() + } + } + + async #serveTrack(subscriber: SubscribeRecv, name: string) { + const track = this.#tracks.get(name) + if (!track) throw new Error(`no track with name ${subscriber.track}`) + + // Send a SUBSCRIBE_OK + await subscriber.ack() + + const segments = track.segments().getReader() + + for (;;) { + const { value: segment, done } = await segments.read() + if (done) break + + // Serve the segment and log any errors that occur. + this.#serveSegment(subscriber, segment).catch((e) => { + const err = asError(e) + console.warn("failed to serve segment", err) + }) + } + } + + async #serveSegment(subscriber: SubscribeRecv, segment: Segment) { + // Create a new stream for each segment. + const stream = await subscriber.data({ + group: segment.id, + object: 0, + priority: 0, // TODO + expires: 30 // TODO configurable + }) + + // Pipe the segment to the stream. + await segment.chunks().pipeTo(stream) + } + + // Attach the captured video stream to the given video element. + attach(video: HTMLVideoElement) { + video.srcObject = this.config.media + } + + close() { + // TODO implement publish close + } + + // Returns the error message when the connection is closed + async closed(): Promise { + try { + await this.#running + return new Error("closed") // clean termination + } catch (e) { + return asError(e) + } + } +} + +function splitExt(s: string): [string, string] { + const i = s.lastIndexOf(".") + if (i < 0) throw new Error(`no extension found`) + return [s.substring(0, i), s.substring(i + 1)] +} diff --git a/repos/demo/src/lib/moq/contribute/chunk.ts b/repos/demo/src/lib/moq/contribute/chunk.ts new file mode 100644 index 0000000..e2b115b --- /dev/null +++ b/repos/demo/src/lib/moq/contribute/chunk.ts @@ -0,0 +1,7 @@ +// Extends EncodedVideoChunk, allowing a new "init" type +export interface Chunk { + type: "init" | "key" | "delta" + timestamp: number // microseconds + duration: number // microseconds + data: Uint8Array +} diff --git a/repos/demo/src/lib/moq/contribute/container.ts b/repos/demo/src/lib/moq/contribute/container.ts new file mode 100644 index 0000000..9f91d4b --- /dev/null +++ b/repos/demo/src/lib/moq/contribute/container.ts @@ -0,0 +1,164 @@ +import * as MP4 from "../media/mp4" +import { Chunk } from "./chunk" + +type DecoderConfig = AudioDecoderConfig | VideoDecoderConfig +type EncodedChunk = EncodedAudioChunk | EncodedVideoChunk + +export class Container { + #mp4: MP4.ISOFile + #frame?: EncodedAudioChunk | EncodedVideoChunk // 1 frame buffer + #track?: number + #segment = 0 + + encode: TransformStream + + constructor() { + this.#mp4 = new MP4.ISOFile() + this.#mp4.init() + + this.encode = new TransformStream({ + transform: (frame, controller) => { + if (isDecoderConfig(frame)) { + return this.#init(frame, controller) + } else { + return this.#enqueue(frame, controller) + } + } + }) + } + + #init(frame: DecoderConfig, controller: TransformStreamDefaultController) { + console.log("container | init", frame) + if (this.#track) throw new Error("duplicate decoder config") + + let codec = frame.codec.substring(0, 4) + if (codec == "opus") { + codec = "Opus" + } + + const options: MP4.TrackOptions = { + type: codec, + timescale: 1_000_000 + } + + if (isVideoConfig(frame)) { + options.width = frame.codedWidth + options.height = frame.codedHeight + } else { + options.channel_count = frame.numberOfChannels + options.samplerate = frame.sampleRate + } + + if (!frame.description) throw new Error("missing frame description") + const desc = frame.description as ArrayBufferLike + + if (codec === "avc1") { + options.avcDecoderConfigRecord = desc + } else if (codec === "hev1") { + options.hevcDecoderConfigRecord = desc + } else if (codec === "Opus") { + // description is an identification header: https://datatracker.ietf.org/doc/html/rfc7845#section-5.1 + // The first 8 bytes are the magic string "OpusHead", followed by what we actually want. + const dops = new MP4.BoxParser.dOpsBox(undefined) + + // Annoyingly, the header is little endian while MP4 is big endian, so we have to parse. + const data = new MP4.Stream(desc, 8, MP4.Stream.LITTLE_ENDIAN) + dops.parse(data) + + options.description = dops + } else { + throw new Error(`unsupported codec: ${codec}`) + } + + this.#track = this.#mp4.addTrack(options) + if (!this.#track) throw new Error("failed to initialize MP4 track") + + const buffer = MP4.ISOFile.writeInitializationSegment(this.#mp4.ftyp!, this.#mp4.moov!, 0, 0) + const data = new Uint8Array(buffer) + + controller.enqueue({ + type: "init", + timestamp: 0, + duration: 0, + data + }) + } + + #enqueue(frame: EncodedChunk, controller: TransformStreamDefaultController) { + // Check if we should create a new segment + if (frame.type == "key") { + this.#segment += 1 + } else if (this.#segment == 0) { + throw new Error("must start with keyframe") + } + + // We need a one frame buffer to compute the duration + if (!this.#frame) { + this.#frame = frame + return + } + + const duration = frame.timestamp - this.#frame.timestamp + + // TODO avoid this extra copy by writing to the mdat directly + // ...which means changing mp4box.js to take an offset instead of ArrayBuffer + const buffer = new Uint8Array(this.#frame.byteLength) + this.#frame.copyTo(buffer) + + if (!this.#track) throw new Error("missing decoder config") + + // Add the sample to the container + this.#mp4.addSample(this.#track, buffer, { + duration, + dts: this.#frame.timestamp, + cts: this.#frame.timestamp, + is_sync: this.#frame.type == "key" + }) + + const stream = new MP4.Stream(undefined, 0, MP4.Stream.BIG_ENDIAN) + + // Moof and mdat atoms are written in pairs. + // TODO remove the moof/mdat from the Box to reclaim memory once everything works + for (;;) { + const moof = this.#mp4.moofs.shift() + const mdat = this.#mp4.mdats.shift() + + if (!moof && !mdat) break + if (!moof) throw new Error("moof missing") + if (!mdat) throw new Error("mdat missing") + + moof.write(stream) + mdat.write(stream) + } + + // TODO avoid this extra copy by writing to the buffer provided in copyTo + const data = new Uint8Array(stream.buffer) + + controller.enqueue({ + type: this.#frame.type, + timestamp: this.#frame.timestamp, + duration: this.#frame.duration ?? 0, + data + }) + + this.#frame = frame + } + + /* TODO flush the last frame + #flush(controller: TransformStreamDefaultController) { + if (this.#frame) { + // TODO guess the duration + this.#enqueue(this.#frame, 0, controller) + } + } + */ +} + +function isDecoderConfig(frame: DecoderConfig | EncodedChunk): frame is DecoderConfig { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return (frame as DecoderConfig).codec !== undefined +} + +function isVideoConfig(frame: DecoderConfig): frame is VideoDecoderConfig { + return (frame as VideoDecoderConfig).codedWidth !== undefined +} diff --git a/repos/demo/src/lib/moq/contribute/index.ts b/repos/demo/src/lib/moq/contribute/index.ts new file mode 100644 index 0000000..704f676 --- /dev/null +++ b/repos/demo/src/lib/moq/contribute/index.ts @@ -0,0 +1,5 @@ +export { Broadcast } from "./broadcast" +export type { BroadcastConfig, BroadcastConfigTrack } from "./broadcast" + +export { Encoder as VideoEncoder } from "./video" +export { Encoder as AudioEncoder } from "./audio" diff --git a/repos/demo/src/lib/moq/contribute/segment.ts b/repos/demo/src/lib/moq/contribute/segment.ts new file mode 100644 index 0000000..9c83e94 --- /dev/null +++ b/repos/demo/src/lib/moq/contribute/segment.ts @@ -0,0 +1,47 @@ +import { Chunk } from "./chunk" + +export class Segment { + id: number + + // Take in a stream of chunks + input: WritableStream + + // Output a stream of bytes, which we fork for each new subscriber. + #cache: ReadableStream + + timestamp = 0 + + track?: string + + constructor(id: number) { + this.id = id + + // Set a max size for each segment, dropping the tail if it gets too long. + // We tee the reader, so this limit applies to the FASTEST reader. + const backpressure = new ByteLengthQueuingStrategy({ highWaterMark: 8_000_000 }) + + const transport = new TransformStream( + { + transform: (chunk: Chunk, controller) => { + // Compute the max timestamp of the segment + this.timestamp = Math.max(chunk.timestamp + chunk.duration) + + // Push the chunk to any listeners. + controller.enqueue(chunk.data) + } + }, + undefined, + backpressure + ) + + this.input = transport.writable + this.#cache = transport.readable + } + + // Split the output reader into two parts. + chunks(): ReadableStream { + const [tee, cache] = this.#cache.tee() + this.#cache = cache + return tee + } +} diff --git a/repos/demo/src/lib/moq/contribute/track.ts b/repos/demo/src/lib/moq/contribute/track.ts new file mode 100644 index 0000000..ea06678 --- /dev/null +++ b/repos/demo/src/lib/moq/contribute/track.ts @@ -0,0 +1,171 @@ +import { Segment } from "./segment" +import { Notify } from "../common/async" +import { Chunk } from "./chunk" +import { Container } from "./container" +import { BroadcastConfig } from "./broadcast" + +import * as Audio from "./audio" +import * as Video from "./video" + +export class Track { + name: string + + #init?: Uint8Array + #segments: Segment[] = [] + + #offset = 0 // number of segments removed from the front of the queue + #closed = false + #error?: Error + #notify = new Notify() + + constructor(media: MediaStreamTrack, config: BroadcastConfig) { + // TODO allow multiple tracks of the same kind + this.name = media.kind + + // We need to split based on type because Typescript is hard + if (isAudioTrack(media)) { + if (!config.audio) throw new Error("no audio config") + this.#runAudio(media, config.audio).catch((err) => this.#close(err)) + } else if (isVideoTrack(media)) { + if (!config.video) throw new Error("no video config") + this.#runVideo(media, config.video).catch((err) => this.#close(err)) + } else { + throw new Error(`unknown track type: ${media.kind}`) + } + } + + async #runAudio(track: MediaStreamAudioTrack, config: AudioEncoderConfig) { + const source = new MediaStreamTrackProcessor({ track }) + const encoder = new Audio.Encoder(config) + const container = new Container() + + // Split the container at keyframe boundaries + const segments = new WritableStream({ + write: (chunk) => this.#write(chunk), + close: () => this.#close(), + abort: (e) => this.#close(e) + }) + + return source.readable.pipeThrough(encoder.frames).pipeThrough(container.encode).pipeTo(segments) + } + + async #runVideo(track: MediaStreamVideoTrack, config: VideoEncoderConfig) { + const source = new MediaStreamTrackProcessor({ track }) + const encoder = new Video.Encoder(config) + const container = new Container() + + // Split the container at keyframe boundaries + const segments = new WritableStream({ + write: (chunk) => this.#write(chunk), + close: () => this.#close(), + abort: (e) => this.#close(e) + }) + + return source.readable.pipeThrough(encoder.frames).pipeThrough(container.encode).pipeTo(segments) + } + + async #write(chunk: Chunk) { + if (chunk.type === "init") { + this.#init = chunk.data + this.#notify.wake() + return + } + + let current = this.#segments.at(-1) + if (!current || chunk.type === "key") { + if (current) { + await current.input.close() + } + + const segment = new Segment(this.#offset + this.#segments.length) + this.#segments.push(segment) + + this.#notify.wake() + + current = segment + + // Clear old segments + while (this.#segments.length > 1) { + const first = this.#segments[0] + + // Expire after 10s + if (chunk.timestamp - first.timestamp < 10_000_000) break + this.#segments.shift() + this.#offset += 1 + + await first.input.abort("expired") + } + } + + const writer = current.input.getWriter() + + if ((writer.desiredSize || 0) > 0) { + await writer.write(chunk) + } else { + console.warn("dropping chunk", writer.desiredSize) + } + + writer.releaseLock() + } + + async #close(e?: Error) { + this.#error = e + + const current = this.#segments.at(-1) + if (current) { + await current.input.close() + } + + this.#closed = true + this.#notify.wake() + } + + async init(): Promise { + while (!this.#init) { + if (this.#closed) throw new Error("track closed") + await this.#notify.wait() + } + + return this.#init + } + + // TODO generize this + segments(): ReadableStream { + let pos = this.#offset + + return new ReadableStream({ + pull: async (controller) => { + for (;;) { + let index = pos - this.#offset + if (index < 0) index = 0 + + if (index < this.#segments.length) { + controller.enqueue(this.#segments[index]) + pos += 1 + return // Called again when more data is requested + } + + if (this.#error) { + controller.error(this.#error) + return + } else if (this.#closed) { + controller.close() + return + } + + // Pull again on wakeup + // NOTE: We can't return until we enqueue at least one segment. + await this.#notify.wait() + } + } + }) + } +} + +function isAudioTrack(track: MediaStreamTrack): track is MediaStreamAudioTrack { + return track.kind === "audio" +} + +function isVideoTrack(track: MediaStreamTrack): track is MediaStreamVideoTrack { + return track.kind === "video" +} diff --git a/repos/demo/src/lib/moq/contribute/tsconfig.json b/repos/demo/src/lib/moq/contribute/tsconfig.json new file mode 100644 index 0000000..d132eac --- /dev/null +++ b/repos/demo/src/lib/moq/contribute/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "include": ["."], + "compilerOptions": { + "types": ["dom-mediacapture-transform", "dom-webcodecs"] + }, + "references": [ + { + "path": "../common" + }, + { + "path": "../transport" + }, + { + "path": "../media" + } + ] +} diff --git a/repos/demo/src/lib/moq/contribute/video.ts b/repos/demo/src/lib/moq/contribute/video.ts new file mode 100644 index 0000000..73f8b10 --- /dev/null +++ b/repos/demo/src/lib/moq/contribute/video.ts @@ -0,0 +1,107 @@ +const SUPPORTED = [ + "avc1", // H.264 + "hev1" // HEVC (aka h.265) + // "av01", // TDOO support AV1 +] + +export interface EncoderSupported { + codecs: string[] +} + +export class Encoder { + #encoder!: VideoEncoder + #encoderConfig: VideoEncoderConfig + #decoderConfig?: VideoDecoderConfig + + // true if we should insert a keyframe, undefined when the encoder should decide + #keyframeNext: true | undefined = true + + // Count the number of frames without a keyframe. + #keyframeCounter = 0 + + // Converts raw rames to encoded frames. + frames: TransformStream + + constructor(config: VideoEncoderConfig) { + config.bitrateMode ??= "constant" + config.latencyMode ??= "realtime" + + this.#encoderConfig = config + + this.frames = new TransformStream({ + start: this.#start.bind(this), + transform: this.#transform.bind(this), + flush: this.#flush.bind(this) + }) + } + + static async isSupported(config: VideoEncoderConfig) { + // Check if we support a specific codec family + const short = config.codec.substring(0, 4) + if (!SUPPORTED.includes(short)) return false + + // Default to hardware encoding + config.hardwareAcceleration ??= "prefer-hardware" + + // Default to CBR + config.bitrateMode ??= "constant" + + // Default to realtime encoding + config.latencyMode ??= "realtime" + + const res = await VideoEncoder.isConfigSupported(config) + return !!res.supported + } + + #start(controller: TransformStreamDefaultController) { + this.#encoder = new VideoEncoder({ + output: (frame, metadata) => { + this.#enqueue(controller, frame, metadata) + }, + error: (err) => { + throw err + } + }) + + this.#encoder.configure(this.#encoderConfig) + } + + #transform(frame: VideoFrame) { + const encoder = this.#encoder + + // Set keyFrame to undefined when we're not sure so the encoder can decide. + encoder.encode(frame, { keyFrame: this.#keyframeNext }) + this.#keyframeNext = undefined + + frame.close() + } + + #enqueue(controller: TransformStreamDefaultController, frame: EncodedVideoChunk, metadata?: EncodedVideoChunkMetadata) { + if (!this.#decoderConfig) { + const config = metadata?.decoderConfig + if (!config) throw new Error("missing decoder config") + + controller.enqueue(config) + this.#decoderConfig = config + } + + if (frame.type === "key") { + this.#keyframeCounter = 0 + } else { + this.#keyframeCounter += 1 + if (this.#keyframeCounter + this.#encoder.encodeQueueSize >= 2 * this.#encoderConfig.framerate!) { + this.#keyframeNext = true + } + } + + controller.enqueue(frame) + } + + #flush() { + this.#encoder.close() + } + + get config() { + return this.#encoderConfig + } +} diff --git a/repos/demo/src/lib/moq/media/catalog/index.ts b/repos/demo/src/lib/moq/media/catalog/index.ts new file mode 100644 index 0000000..78ca0fb --- /dev/null +++ b/repos/demo/src/lib/moq/media/catalog/index.ts @@ -0,0 +1,163 @@ +import { Connection } from "../../transport" +import { Reader } from "../../transport/stream" +import { asError } from "../../common/error" + +// JSON encoded catalog +export class Catalog { + tracks = new Array() + + public getTracks(): Track[] { + return this.tracks + } + + public getVideoTracks(): VideoTrack[] { + // console.log("getVideoTracks", this.tracks) + return this.tracks.filter(isVideoTrack) + } + + getAudioTracks(): AudioTrack[] { + return this.tracks.filter(isAudioTrack) + } + + encode(): Uint8Array { + const encoder = new TextEncoder() + const str = JSON.stringify(this) + return encoder.encode(str) + } + + static decode(raw: Uint8Array): Catalog { + const decoder = new TextDecoder() + const str = decoder.decode(raw) + + try { + const catalog = new Catalog() + catalog.tracks = JSON.parse(str).tracks + + if (!isCatalog(catalog)) { + throw new Error("invalid catalog") + } + + return catalog + } catch (e) { + throw new Error("invalid catalog") + } + } + + static async fetch(connection: Connection): Promise { + let raw: Uint8Array + + const subscribe = await connection.subscribe("", ".catalog") + console.debug("catalog fetch subscribe", subscribe) + try { + const segment = await subscribe.data() + if (!segment) throw new Error("no catalog data") + + console.log("catalog fetch segment", segment) + + const { header, stream } = segment + + if (header.group !== 0) { + throw new Error("TODO updates not supported") + } + + if (header.object !== 0) { + throw new Error("TODO delta updates not supported") + } + + const reader = new Reader(stream) + raw = await reader.readAll() + console.log("catalog fetch raw", raw) + + await subscribe.close() // we done + } catch (e) { + console.debug("catalog fetch error", e) + const err = asError(e) + + // Close the subscription after we're done. + await subscribe.close(1n, err.message) + + throw err + } + + return Catalog.decode(raw) + } + + static getUniqueTrackId(mp4Track: Mp4Track) { + if (mp4Track.kind === "audio") { + const track = mp4Track as AudioTrack + return `${track.kind}:${track.codec}:${track.channel_count}:${track.sample_rate}:${track.sample_size}` + } else if (mp4Track.kind === "video") { + const track = mp4Track as VideoTrack + return `${track.kind}:${track.codec}:${track.width}:${track.height}:${track.frame_rate}` + } + } +} + +export function isCatalog(catalog: any): catalog is Catalog { + if (!Array.isArray(catalog.tracks)) return false + return catalog.tracks.every((track: any) => isTrack(track)) +} + +export interface Track { + kind: string + container: string +} + +export interface Mp4Track extends Track { + container: "mp4" + init_track: string + data_track: string +} + +export interface AudioTrack extends Mp4Track { + kind: "audio" + codec: string + channel_count: number + sample_rate: number + sample_size: number + bit_rate?: number +} + +export interface VideoTrack extends Mp4Track { + kind: "video" + codec: string + width: number + height: number + frame_rate: number + bit_rate?: number +} + +export function isTrack(track: any): track is Track { + if (typeof track.kind !== "string") return false + if (typeof track.container !== "string") return false + return true +} + +export function isMp4Track(track: any): track is Mp4Track { + if (track.container !== "mp4") return false + if (typeof track.init_track !== "string") return false + if (typeof track.data_track !== "string") return false + if (!isTrack(track)) return false + return true +} + +export function isVideoTrack(track: any): track is VideoTrack { + if (track.kind !== "video") return false + if (typeof track.codec !== "string") return false + if (typeof track.width !== "number") return false + if (typeof track.height !== "number") return false + // frame rate is not required + // if (typeof track.frame_rate !== "number") return false + if (!isTrack(track)) return false + return true +} + +export function isAudioTrack(track: any): track is AudioTrack { + if (track.kind !== "audio") return false + if (typeof track.codec !== "string") return false + if (typeof track.channel_count !== "number") return false + if (typeof track.sample_rate !== "number") return false + if (typeof track.sample_size !== "number") return false + if (!isTrack(track)) return false + return true +} diff --git a/repos/demo/src/lib/moq/media/mp4/index.ts b/repos/demo/src/lib/moq/media/mp4/index.ts new file mode 100644 index 0000000..128974e --- /dev/null +++ b/repos/demo/src/lib/moq/media/mp4/index.ts @@ -0,0 +1,37 @@ +// Rename some stuff so it's on brand. +// We need a separate file so this file can use the rename too. +import * as MP4 from "./rename" +export * from "./rename" + +export * from "./parser" + +export function isAudioTrack(track: MP4.Track): track is MP4.AudioTrack { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return (track as MP4.AudioTrack).audio !== undefined +} + +export function isVideoTrack(track: MP4.Track): track is MP4.VideoTrack { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return (track as MP4.VideoTrack).video !== undefined +} + +// TODO contribute to mp4box +MP4.BoxParser.dOpsBox.prototype.write = function (stream: MP4.Stream) { + this.size = this.ChannelMappingFamily === 0 ? 11 : 13 + this.ChannelMapping!.length + this.writeHeader(stream) + + stream.writeUint8(this.Version) + stream.writeUint8(this.OutputChannelCount) + stream.writeUint16(this.PreSkip) + stream.writeUint32(this.InputSampleRate) + stream.writeInt16(this.OutputGain) + stream.writeUint8(this.ChannelMappingFamily) + + if (this.ChannelMappingFamily !== 0) { + stream.writeUint8(this.StreamCount!) + stream.writeUint8(this.CoupledCount!) + for (const mapping of this.ChannelMapping!) { + stream.writeUint8(mapping) + } + } +} diff --git a/repos/demo/src/lib/moq/media/mp4/parser.ts b/repos/demo/src/lib/moq/media/mp4/parser.ts new file mode 100644 index 0000000..bcac754 --- /dev/null +++ b/repos/demo/src/lib/moq/media/mp4/parser.ts @@ -0,0 +1,86 @@ +import * as MP4 from "./index" + +export interface Frame { + track: MP4.Track // The track this frame belongs to + sample: MP4.Sample // The actual sample contain the frame data + prfts: MP4.BoxParser.prftBox[] // Recent prft boxes +} + +// Decode a MP4 container into individual samples. +export class Parser { + #mp4 = MP4.New() + #offset = 0 + #last_boxes_length = 0 + + // TODO Parser should extend TransformStream + decode: TransformStream + + constructor() { + this.decode = new TransformStream( + { + start: this.#start.bind(this), + transform: this.#transform.bind(this), + flush: this.#flush.bind(this) + }, + // Buffer a single sample on either end + { highWaterMark: 1 }, + { highWaterMark: 1 } + ) + } + + #start(controller: TransformStreamDefaultController) { + this.#mp4.onError = (err) => { + controller.error(err) + } + + this.#mp4.onReady = (info: MP4.Info) => { + // Extract all of the tracks, because we don't know if it's audio or video. + for (const track of info.tracks) { + this.#mp4.setExtractionOptions(track.id, track, { nbSamples: 1 }) + } + } + + this.#mp4.onSamples = (_track_id: number, track: MP4.Track, samples: MP4.Sample[]) => { + for (const sample of samples) { + controller.enqueue({ track, sample, prfts: this.#getLastPRFTs() as MP4.BoxParser.prftBox[] }) + } + } + + this.#mp4.start() + } + + #transform(chunk: Uint8Array) { + const copy = new Uint8Array(chunk) + + // For some reason we need to modify the underlying ArrayBuffer with offset + const buffer = copy.buffer as MP4.ArrayBuffer + buffer.fileStart = this.#offset + + // Parse the data + this.#mp4.appendBuffer(buffer) + this.#mp4.flush() + + this.#offset += buffer.byteLength + } + + #flush() { + this.#mp4.flush() + } + + #getLastPRFTs(): MP4.BoxParser.Box[] { + const length = this.#mp4.boxes.length + const delta = length - this.#last_boxes_length + this.#last_boxes_length = length + + // FIXME: Filter only prfts that are related to the current track + + // In reverse order check last delta chunks for PRFT boxes + const prfts = [] + for (let i = 1; i <= delta; i++) { + const box = this.#mp4.boxes[length - i] + if (box.type === "prft") prfts.push(box) + } + + return prfts + } +} diff --git a/repos/demo/src/lib/moq/media/mp4/rename.ts b/repos/demo/src/lib/moq/media/mp4/rename.ts new file mode 100644 index 0000000..77bee42 --- /dev/null +++ b/repos/demo/src/lib/moq/media/mp4/rename.ts @@ -0,0 +1,4 @@ +// Rename some stuff so it's on brand. +export { createFile as New, DataStream as Stream, ISOFile, BoxParser, Log } from "mp4box" + +export type { MP4ArrayBuffer as ArrayBuffer, MP4Info as Info, MP4Track as Track, MP4AudioTrack as AudioTrack, MP4VideoTrack as VideoTrack, Sample, TrackOptions, SampleOptions } from "mp4box" diff --git a/repos/demo/src/lib/moq/media/tsconfig.json b/repos/demo/src/lib/moq/media/tsconfig.json new file mode 100644 index 0000000..f7d77ec --- /dev/null +++ b/repos/demo/src/lib/moq/media/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "include": ["."], + "compilerOptions": { + "types": ["mp4box"] + }, + "references": [ + { + "path": "../transport" + }, + { + "path": "../common" + } + ] +} diff --git a/repos/demo/src/lib/moq/package.json b/repos/demo/src/lib/moq/package.json new file mode 100644 index 0000000..90de178 --- /dev/null +++ b/repos/demo/src/lib/moq/package.json @@ -0,0 +1,29 @@ +{ + "name": "@kixelated/moq", + "type": "module", + "version": "0.1.4", + "description": "Media over QUIC library", + "license": "(MIT OR Apache-2.0)", + "repository": "github:kixelated/moq-js", + "scripts": { + "build": "tsc -b && cp ../LICENSE* ./dist && cp ./README.md ./dist && cp ./package.json ./dist", + "lint": "eslint .", + "fmt": "prettier --write ." + }, + "devDependencies": { + "@types/audioworklet": "^0.0.52", + "@types/dom-mediacapture-transform": "^0.1.9", + "@types/dom-webcodecs": "^0.1.11", + "@typescript-eslint/eslint-plugin": "^6.13.1", + "@typescript-eslint/parser": "^6.13.1", + "@typescript/lib-dom": "npm:@types/web@^0.0.123", + "eslint": "^8.54.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "prettier": "^3.1.0", + "typescript": "^4.9.5" + }, + "dependencies": { + "mp4box": "^0.5.2" + } +} diff --git a/repos/demo/src/lib/moq/playback/backend.ts b/repos/demo/src/lib/moq/playback/backend.ts new file mode 100644 index 0000000..1bbd630 --- /dev/null +++ b/repos/demo/src/lib/moq/playback/backend.ts @@ -0,0 +1,21 @@ +import { Catalog } from "../media/catalog" +import { Header } from "../transport/object" + +// TODO make an interface for backends + +export interface Config { + catalog: Catalog +} + +export interface Init { + name: string // name of the init track + stream: ReadableStream +} + +export interface Segment { + init: string // name of the init track + data: string // name of the data track + kind: "audio" | "video" + header: Header + stream: ReadableStream +} diff --git a/repos/demo/src/lib/moq/playback/index.ts b/repos/demo/src/lib/moq/playback/index.ts new file mode 100644 index 0000000..243389e --- /dev/null +++ b/repos/demo/src/lib/moq/playback/index.ts @@ -0,0 +1,765 @@ +import * as Message from "./webcodecs/message" + +import { Connection } from "../transport/connection" +import { Catalog, isAudioTrack, isMp4Track, Mp4Track, VideoTrack, AudioTrack } from "../media/catalog" +import { asError } from "../common/error" + +// We support two different playback implementations: +import Webcodecs from "./webcodecs" +import MSE from "./mse" +import { Client } from "../transport/client" +import { SubscribeSend } from "../transport" +import { TrackBuffer } from "./trackBuffer" +import { SWMA, TPEstimator } from "../common/utils" + +export type Range = Message.Range +export type Timeline = Message.Timeline + +async function readStream(stream: ReadableStream) { + const reader = stream.getReader() + const chunks: Uint8Array[] = [] + for (;;) { + const { done, value } = await reader.read() + if (done) { + break + } + if (value) { + chunks.push(value) + } + } + const res = chunks.flatMap((a) => Array.from(a)) + return new Uint8Array(res) +} + +function arrayToReadableStream(array: Uint8Array) { + return new ReadableStream({ + start(controller) { + controller.enqueue(array) + controller.close() + } + }) +} + +export interface PlayerConfig { + url: string + fingerprint?: string // URL to fetch TLS certificate fingerprint + element: HTMLCanvasElement +} + +// This class must be created on the main thread due to AudioContext. +export class Player extends EventTarget { + #backend: Webcodecs + + // A periodically updated timeline + //#timeline = new Watch(undefined) + + #connection: Connection + #catalog: Catalog + + #currentTracks: Map = new Map() + + // For a single track, there are two subscriptions (init_track and data_track) + #currentSubscriptions: Map = new Map() + + #runningTrackThreads: Map> = new Map>() + #nextVideoTrack: Mp4Track | undefined + + // probing settings + #useProbing: boolean = false + #probeInterval: number = 2000 + #probeSize: number = 40000 + #probePriority: number = 0 // 0 is lowest priority, 1 is highest + #probeTimer: number = 0 + #useProbeTestData = false + #probeTestResults: any[] = [] + #probeTestData = { + start: 10000, + stop: 300000, + increment: 10000, + iteration: 3, + lastIteration: 0 + } + + #trackBuffers = new Map() + #latencyTarget = 3000 // default is 3000 ms + + #measuredBandwidth = new SWMA(2, "moq-bw") + + #currentLatency: number | undefined + #bufferInitialized = false + + // if this is true, the switchTrackId is set as 0 + #enableSwitchTrackIdFeature = this.getFromQueryString("enableSwitchTrackIdFeature", "false") === "true" + + private static GROUP_DURATION: number = 1000 // milliseconds + + // Running is a promise that resolves when the player is closed. + // #close is called with no error, while #abort is called with an error. + #running: Promise = Promise.resolve() + #close!: () => void + #abort!: (err: Error) => void + + private constructor(connection: Connection, catalog: Catalog, backend: Webcodecs) { + super() + this.#connection = connection + this.#catalog = catalog + this.#backend = backend + + if (this.#backend instanceof Webcodecs) { + this.#backend.on(this.#onMessage) + } + } + + getFromQueryString(key: string, defaultValue: string = ""): string { + const re = new RegExp("[?&]" + key + "=([^&]+)") + const m = re.exec(location.search) + console.log("playback | getFromQueryString", re, m) + if (m && m[1]) { + return m[1] + } + return defaultValue + } + + parseProbeParametersAndRun() { + try { + if (!location.search) return + + const probeSize = parseInt(this.getFromQueryString("probeSize", "0")) + const probePriority = parseInt(this.getFromQueryString("probePriority", "-1")) + const probeInterval = parseInt(this.getFromQueryString("probeInterval", "0")) + + let useProbing = false + if (probeSize > 0) { + useProbing = true + this.#probeSize = probeSize + } + if (probePriority > -1) { + useProbing = true + this.#probePriority = probePriority + } + // set probeInterval and start probeTimer + if (probeInterval > 0 && probeInterval !== this.#probeInterval) { + useProbing = true + if (this.#probeTimer) { + clearInterval(this.#probeTimer) + } + this.#probeInterval = probeInterval + this.#probeTimer = setInterval(this.runProbe, this.#probeInterval) + } + + if (useProbing) { + this.#useProbing = true + console.log("playback | parseProbeParameters | probeSize: %d probePriority: %d probeInterval: %d", this.#probeSize, this.#probePriority, this.#probeInterval) + } + } catch (e) { + console.error("playback | parseProbeParameters | error", e) + } + } + + start() { + const abort = new Promise((resolve, reject) => { + this.#close = resolve + this.#abort = reject + }) + + // Async work + this.#running = Promise.race([this.#run(), abort]).catch(this.#close) + + // Wait for the player to start before probing + this.parseProbeParametersAndRun() + + window.onhashchange = () => { + this.parseProbeParametersAndRun() + } + + // if probing didn't start, start it + if (this.#useProbing && !this.#probeTimer) { + this.#probeTimer = setInterval(this.runProbe, this.#probeInterval) + } + } + + static async create(config: PlayerConfig): Promise { + const client = new Client({ + url: config.url, + fingerprint: config.fingerprint, + role: "subscriber" + }) + const connection = await client.connect() + + console.log("playback | connected") + + const catalog = await Catalog.fetch(connection) + + console.log("Plaplaybackyer | fetched the catalog", catalog) + + const element = config.element.transferControlToOffscreen() + const backend = new Webcodecs({ element, catalog }) + + return new Player(connection, catalog, backend) + } + + async #run() { + const inits = new Set() + const tracks = new Array() + + // to get low res first use the following: + // for (const track of (this.#catalog.tracks as unknown as Mp4Track[]).sort((a, b) => -1 * a.data_track.localeCompare(b.data_track))) { + for (const track of this.#catalog.tracks) { + if (!isMp4Track(track)) { + throw new Error(`expected CMAF track`) + } + + if (isAudioTrack(track) && this.#backend instanceof MSE) { + // TODO temporary hack to disable audio in MSE + continue + } + + // just one video and audio may be active at a time + if (!this.#currentTracks.has(track.kind)) { + this.#currentTracks.set(track.kind, track) + // TODO: put this back + if (!tracks.some((t) => t.init_track === track.init_track)) { + console.log("playback | run | adding init track", track.init_track) + inits.add(track.init_track) + } + if (!tracks.some((t) => t.data_track === track.data_track)) { + console.log("playback | run | adding data track", track.data_track) + tracks.push(track) + } + + // Initialize the track buffer + if (this.#latencyTarget > 0 && !this.#trackBuffers.has(track.data_track)) { + this.#trackBuffers.set(track.data_track, new TrackBuffer()) + } + } + } + + // Call #runInit on each unique init track + // TODO do this in parallel with #runTrack to remove a round trip + Array.from(tracks).forEach((track) => { + // the following is a hack to prevent multiple init tracks from being run + // actually, we prevent this by only adding the init track once by checking above + if (track.kind === "video" && this.#backend instanceof Webcodecs) { + this.#backend.setVideoTrack(track as VideoTrack) + } + if (!this.#runningTrackThreads.has(Catalog.getUniqueTrackId(track) + "_init")) { + this.#runningTrackThreads.set(Catalog.getUniqueTrackId(track) + "_init", this.#runInit(track)) + } + this.#runningTrackThreads.set(Catalog.getUniqueTrackId(track) + "_data", this.#runTrack(track)) + }) + + this.#runBuffer().catch((e) => console.error(e)) + // Wait for all tracks to finish + await this.runners() + } + + async runners() { + while (this.#runningTrackThreads.size > 0) { + await new Promise((resolve) => setTimeout(resolve, 100)) + } + console.log("playback | runners | done") + } + + async #runBuffer() { + while (this.#runningTrackThreads.size > 0) { + if (this.#latencyTarget < 0) { + await new Promise((resolve) => setTimeout(resolve, 100)) + continue + } + const currentTrack = this.getCurrentVideoTrack() + if (!currentTrack) { + console.error("runBuffer | No current track") + await new Promise((resolve) => setTimeout(resolve, 100)) + continue + } + + let buffer = this.#trackBuffers.get(currentTrack.data_track) + if (!buffer) { + console.warn("runBuffer | No buffer for track", currentTrack.data_track) + buffer = new TrackBuffer() + this.#trackBuffers.set(currentTrack.data_track, buffer) + } + + const bufferLength = buffer?.getBufferLength() + if ((bufferLength || 0) === 0 && this.#latencyTarget > 0) { + // console.log("playback | runBuffer | Nothing in buffer", currentTrack.data_track) + await new Promise((resolve) => setTimeout(resolve, 100)) + continue + } + + if (this.#latencyTarget > 0) { + let waitMS = Player.GROUP_DURATION + if (this.#latencyTarget === 0) { + waitMS = 0 + } //else if (this.#currentLatency === undefined) { + // waitMS = Math.max(Player.GROUP_DURATION, this.#latencyTarget - (bufferLength - 1) * Player.GROUP_DURATION) + // } + else if (this.#latencyTarget >= (this.#currentLatency || 0)) { + // latency target: 5000 + // current latency: 4200 + if (!this.#bufferInitialized) { + // console.log("playback | runBuffer | buffer not initialized") + waitMS = Math.max(Player.GROUP_DURATION, this.#latencyTarget - bufferLength * Player.GROUP_DURATION) + this.#bufferInitialized = true + } else if (bufferLength > 0) { + // console.log("playback | runBuffer | don't wait") + // starve buffer, don't let stalling happen + waitMS = Player.GROUP_DURATION + } + } else if ((this.#currentLatency || 0) >= this.#latencyTarget + Player.GROUP_DURATION) { + // latency target: 5000 + // current latency: 6000 + // TODO: magic number is 0.7 which makes the speed-up rate to 1/0.7. + // console.log("playback | runBuffer | speedup") + waitMS = Player.GROUP_DURATION * 0.7 + } + + console.log("playback | runBuffer | bufferLength:%d currentLatency:%d waitMS:%d", bufferLength, this.#currentLatency, waitMS) + + if (waitMS > 0) { + await new Promise((resolve) => setTimeout(resolve, waitMS)) + } + } + + const nextSegmentToPlay = buffer.nextSegment + if (!nextSegmentToPlay) { + if (this.#latencyTarget > 0) { + console.warn("runBuffer | no nextSegment") + } + await new Promise((resolve) => setTimeout(resolve, 100)) + continue + } + this.#backend.segment(nextSegmentToPlay) + // console.log("playback | runBuffer | played from buffer", nextSegmentToPlay.header.ntp_timestamp) + } + } + + async #runInit(track: Mp4Track) { + const name = track.init_track + console.log("playback | runInit", name) + const sub = await this.#connection.subscribe("", name) + this.#currentSubscriptions.set(name, sub) + + const uniqueTrackId = Catalog.getUniqueTrackId(track) + + try { + const init = await Promise.race([sub.data(), this.#running]) + if (!init) throw new Error("no init data") + + this.#backend.init({ stream: init.stream, name }) + } finally { + if (this.#currentSubscriptions.has(track.init_track)) { + this.#currentSubscriptions.delete(track.init_track) + await sub.close() + } + // remove the init track from the running track threads + if (this.#runningTrackThreads.has(uniqueTrackId + "_init")) { + this.#runningTrackThreads.delete(uniqueTrackId + "_init") + } + + if (this.#trackBuffers.has(track.data_track)) { + const buffer = this.#trackBuffers.get(track.data_track) + if (buffer) { + buffer.clear() + } + } + } + } + + async #runTrack(track: Mp4Track) { + // console.log("playback | runTrack", track) + if (track.kind !== "audio" && track.kind !== "video") { + throw new Error(`unknown track kind: ${track.kind}`) + } + + const uniqueTrackId = Catalog.getUniqueTrackId(track) + + if (!uniqueTrackId) { + throw new Error("missing unique track id") + } + + // When this is false, the transition is smoother + // but when there's congestion, the video stalls because + // two subscriptions are competing for bandwidth. + const dontSubBeforePrevTrackIsDone = false + let subInited = false + let sub: SubscribeSend + let failedSegmentRead = 0 + try { + for (;;) { + const doSubscribe = !subInited && (!dontSubBeforePrevTrackIsDone || this.isPlayingTrack(track)) + if (doSubscribe) { + console.log("playback | runTrack | subscribing", track.data_track) + subInited = true + let switchTrackId: bigint = BigInt(0) + if (this.#enableSwitchTrackIdFeature) { + const currentSub = this.#currentSubscriptions.get(this.getCurrentVideoTrack()?.data_track) + switchTrackId = (currentSub?.id || 0) as bigint + } + sub = await this.#connection.subscribe("", track.data_track, switchTrackId) + this.#currentSubscriptions.set(track.data_track, sub) + } + + // wait for the previous video track to be done + if (!subInited && !this.isPlayingTrack(track)) { + // delay 50ms + console.log("playback | runTrack | delaying 50ms", track.kind, track.data_track) + await new Promise((resolve) => setTimeout(resolve, 50)) + continue + } + + if (!this.getCurrentVideoTrack()) { + this.setCurrentVideoTrack(track as VideoTrack) + } + + /* TODO: uncomment + if (!this.#currentTracks.has(track.kind)) { + console.log("playback | runTrack | track no longer active", track.kind) + break + } + */ + + let segmentDone = false + + // wait for the next video track to be set + // when it's set, the race will be won by the nextVideoTrackSet promise + const nextVideoTrackSet = new Promise((resolve) => { + const check = () => { + if (segmentDone) { + // segment is done, so we can resolve + resolve() + return + } else if (this.#nextVideoTrack) { + if (Catalog.getUniqueTrackId(this.#nextVideoTrack) !== uniqueTrackId) { + // console.log("playback | nextVideoTrackSet", this.#nextVideoTrack) + // let the current segment finish + // setTimeout(resolve, 1000) + resolve() + return + } + } + // segment is not done and next video track is not set, so check again + setTimeout(check, 10) + } + + check() + }) + + const setNextVideoTrackIfExists = () => { + if (this.#nextVideoTrack) { + const nextTrackId = Catalog.getUniqueTrackId(this.#nextVideoTrack) + if (nextTrackId !== uniqueTrackId) { + if (this.#latencyTarget > 0) { + const minBufferLengthToSwitch = Math.ceil(this.#latencyTarget / Player.GROUP_DURATION) + const currentBufferItemCount = this.#trackBuffers?.get(this.#nextVideoTrack.data_track)?.getBufferLength() || 0 + if (currentBufferItemCount < minBufferLengthToSwitch) { + // console.log("playback | runTrack | not enough buffer to switch", this.#nextVideoTrack.data_track, uniqueTrackId, currentBufferItemCount, minBufferLengthToSwitch) + return false + } + } + + console.log("playback | runTrack | switching to next video track", uniqueTrackId, nextTrackId) + this.setCurrentVideoTrack(this.#nextVideoTrack) + return true + } + } + return false + } + + console.log("playback | runTrack | fetch segment", track.kind, uniqueTrackId) + const segment = await Promise.race([sub!.data(), this.#running, nextVideoTrackSet]) + if (!segment) { + segmentDone = false + failedSegmentRead++ + if (failedSegmentRead > 10) { + console.error("playback | runTrack | failed to read segment", track.kind, uniqueTrackId) + break + } + } else { + segmentDone = true + let buffer = this.#trackBuffers.get(track.data_track) + if (!buffer) { + this.#trackBuffers.set(track.data_track, new TrackBuffer()) + buffer = this.#trackBuffers.get(track.data_track) + } + + if (!buffer) throw new Error("missing buffer") + + // add incoming to buffer + // before adding the segment to the buffer, pipe it through the throughput estimator + const callback = (value: number) => this.dispatchEvent(new CustomEvent("stat", { detail: { type: "measuredBandwidth", value } })) + const tpEstimator = new TPEstimator(this.#measuredBandwidth, callback) + // const segmentStream = segment.stream.pipeThrough(tpEstimator.stream) + const segmentId = segment.header.group + " " + segment.header.object + " " + segment.header.ntp_timestamp + tpEstimator.segmentId = segmentId + + if (this.#latencyTarget > 0) { + // force stream to be fetched in order for the tpEstimator to work + setTimeout(async () => { + if (!this.#useProbing) { + segment.stream = arrayToReadableStream(await readStream(segment.stream.pipeThrough(tpEstimator.stream))) + } + buffer.addSegment({ + init: track.init_track, + data: track.data_track, + kind: track.kind as "audio" | "video", + header: segment.header, + stream: segment.stream + }) + }, 0) + console.log("playback | runTrack | segment buffered", track.data_track, uniqueTrackId, segment.header.ntp_timestamp) + } else { + if (!this.#useProbing) { + segment.stream = segment.stream.pipeThrough(tpEstimator.stream) + } + this.#backend.segment({ + init: track.init_track, + data: track.data_track, + kind: track.kind, + header: segment.header, + stream: segment.stream + }) + console.log("playback | runTrack | segment played out", track.data_track, uniqueTrackId, segment.header.ntp_timestamp) + } + } + if (setNextVideoTrackIfExists()) { + break + } + } + } finally { + console.log("playback | runTrack | closing subscription", track, uniqueTrackId) + if (this.#currentSubscriptions.has(track.data_track)) { + this.#currentSubscriptions.delete(track.data_track) + } + // remove the data track from the running track threads + if (this.#runningTrackThreads.has(uniqueTrackId + "_data")) { + this.#runningTrackThreads.delete(uniqueTrackId + "_data") + } + // clear the buffer + this.#trackBuffers.set(track.data_track, new TrackBuffer()) + + if (this.#currentTracks.get("video") === track) { + this.#currentTracks.delete("video") + } + + sub!.close() + } + } + + downloadProbeStats = () => { + const link = document.createElement("a") + document.body.appendChild(link) + + // download logs + if (this.#probeTestResults.length > 0) { + const headers = ["duration", "size", "bandwidth"] + const csvContent = "data:application/vnd.ms-excel;charset=utf-8," + headers.join("\t") + "\n" + this.#probeTestResults.map((e) => Object.values(e).join("\t")).join("\n") + const encodedUri = encodeURI(csvContent) + link.setAttribute("href", encodedUri) + link.setAttribute("download", "logs_" + Date.now() + ".xls") + link.click() + } else { + console.warn("playback | downloadProbeStats | no logs") + } + + link.remove() + } + + runProbe = async () => { + console.log("playback | runProbe") + + let totalIteration = 0 + if (this.#useProbeTestData && this.#probeTestData) { + const totalProbeSizes = (this.#probeTestData.stop - this.#probeTestData.start) / this.#probeTestData.increment + 1 + totalIteration = totalProbeSizes * this.#probeTestData.iteration + console.log("playback | probe | totalIteration", totalIteration) + this.#probeSize = this.#probeTestData.start + Math.floor(this.#probeTestData.lastIteration / this.#probeTestData.iteration) * this.#probeTestData.increment + ++this.#probeTestData.lastIteration + } + + let sub: SubscribeSend + try { + const start = performance.now() + // .probe:20000:0 + const probeTrackName = ".probe:" + this.#probeSize + ":" + this.#probePriority + sub = await this.#connection.subscribe("", probeTrackName) + // a delay + console.log("playback | probe sub", sub, probeTrackName) + const result = await Promise.race([sub.data(), this.#running]) + console.log("playback | probe subSend", result) + if (result) { + const reader = result.stream.getReader() + let done = false + let totalBufferSize = 0 + let rtt = 0 + while (!done) { + const { value, done: d } = await reader.read() + totalBufferSize += value?.byteLength ?? 0 + done = d + if (rtt === 0) { + rtt = performance.now() - start + } + if (done) { + console.log("playback | probe | buffer: %d", totalBufferSize) + const end = performance.now() + const duration = end - start + const measuredBandwidth = (totalBufferSize * 8) / (duration / 1000) / 1000 + const tc_bandwidth = parseFloat(localStorage.getItem("tc_bandwidth") || "0") || 0 + console.log("playback | probe | duration: %d size: %d measured: %f tc_w: %f", duration, totalBufferSize, measuredBandwidth.toFixed(2), tc_bandwidth.toFixed(2)) + this.dispatchEvent(new CustomEvent("stat", { detail: { type: "measuredBandwidth", value: measuredBandwidth } })) + this.#probeTestResults.push([duration, totalBufferSize, measuredBandwidth.toFixed(2), tc_bandwidth.toFixed(2)]) + } + } + } + } catch (e) { + console.error("playback | probe error", e) + } finally { + console.log("playback | probe done") + sub!.close() + } + + if (this.#useProbeTestData && this.#probeTestData.lastIteration === totalIteration) { + this.downloadProbeStats() + this.#probeTestData.lastIteration = 0 + // stop the probe + clearInterval(this.#probeTimer) + } + } + + #onMessage = (msg: Message.FromWorker) => { + if (msg.timeline) { + //this.#timeline.update(msg.timeline) + } else if (msg.latency) { + this.dispatchEvent(new CustomEvent("stat", { detail: { type: "latency", value: msg.latency } })) + } else if (!this.#useProbing && msg.measuredBandwidth) { + // If the bandwidth measurement is done in the worker, we can catch this message here + // but if it's done in the main thread, the following line won't be reached + // this.dispatchEvent(new CustomEvent("stat", { detail: { type: "measuredBandwidth", value: msg.measuredBandwidth } })) + } else if (msg.stall) { + this.dispatchEvent(new CustomEvent("stat", { detail: { type: "stall", value: msg.stall } })) + } else if (msg.skip) { + this.dispatchEvent(new CustomEvent("skip", { detail: msg.skip })) + } else if (msg.trackId) { + this.dispatchEvent(new CustomEvent("track_change", { detail: msg.trackId })) + } + } + + async close(err?: Error) { + if (err) this.#abort(err) + else this.#close() + + if (this.#connection) this.#connection.close() + if (this.#backend) await this.#backend.close() + if (this.#backend instanceof Webcodecs) this.#backend.off() + } + + async closed(): Promise { + try { + await this.#running + } catch (e) { + return asError(e) + } + } + + /* + play() { + this.#backend.play({ minBuffer: 0.5 }) // TODO configurable + } + + seek(timestamp: number) { + this.#backend.seek({ timestamp }) + } + */ + + async play() { + // await this.#backend.play() + } + + /* + async *timeline() { + for (;;) { + const [timeline, next] = this.#timeline.value() + if (timeline) yield timeline + if (!next) break + + await next + } + } + */ + + getCatalog() { + return this.#catalog + } + + // Only one audio and video track may be active at a time + isPlayingTrack(track: Mp4Track) { + return this.#currentTracks.get(track.kind) === track + } + + selectVideoTrack(track: VideoTrack) { + console.log("playback | selectVideoTrack", track) + if (track.kind !== "video") { + throw new Error(`expected video track`) + } + + if (this.#nextVideoTrack) { + console.warn("playback | selectVideoTrack | next video track already set", this.#nextVideoTrack) + return + } + + if (this.#currentTracks.get("video") === track) { + console.warn("playback | selectVideoTrack | already playing video track", track) + return + } + + this.#nextVideoTrack = track + this.#runningTrackThreads.set(Catalog.getUniqueTrackId(track) + "_data", this.#runTrack(track)) + } + + getCurrentVideoTrack() { + return this.#currentTracks.get("video") as VideoTrack + } + + setCurrentVideoTrack(track: Mp4Track) { + const currentTrack = this.#currentTracks.get("video") + + if (currentTrack) { + if (this.#currentSubscriptions.has(currentTrack.init_track)) { + this.#currentSubscriptions.delete(currentTrack.init_track) + } + + if (this.#currentSubscriptions.has(currentTrack.data_track)) { + this.#currentSubscriptions.delete(currentTrack.data_track) + } + } + + this.#currentTracks.set("video", track) + + const backend = this.#backend + backend.setVideoTrack(track as VideoTrack) + this.#nextVideoTrack = undefined + } + + setServerTimeOffset(serverTimeOffset: number) { + this.#backend.setServerTimeOffset(serverTimeOffset) + } + + resetBandwidthMeasurement() { + const backend = this.#backend + backend.resetSWMA() + } + + setLatencyTarget(seconds: number) { + if (seconds < 0) throw new Error("latency target must be greater than or equal to 0") + const latencyTarget = Math.max(0, Math.ceil((seconds * 1000) / Player.GROUP_DURATION)) * 1000 + if (latencyTarget > this.#latencyTarget) { + // build-up buffer again + this.#bufferInitialized = false + } + this.#latencyTarget = latencyTarget + } + + setCurrentLatency(milliseconds: number) { + this.#currentLatency = milliseconds + } +} diff --git a/repos/demo/src/lib/moq/playback/mse/index.ts b/repos/demo/src/lib/moq/playback/mse/index.ts new file mode 100644 index 0000000..ec2ff50 --- /dev/null +++ b/repos/demo/src/lib/moq/playback/mse/index.ts @@ -0,0 +1,173 @@ +import { Source } from "./source" +import { InitParser } from "./init" +import { Segment } from "./segment" +import { Track } from "./track" +import * as MP4 from "../../media/mp4" +import * as Message from "../backend" + +export interface PlayerConfig { + element: HTMLVideoElement +} + +export default class Player { + #source: MediaSource + + #init: Map + #audio: Track + #video: Track + + #element: HTMLVideoElement + #interval: number + + constructor(config: PlayerConfig) { + this.#element = config.element + + this.#source = new MediaSource() + this.#element.src = URL.createObjectURL(this.#source as unknown as Blob) + this.#element.addEventListener("play", () => { + this.play().catch(console.warn) + }) + + this.#init = new Map() + this.#audio = new Track(new Source(this.#source)) + this.#video = new Track(new Source(this.#source)) + + this.#interval = window.setInterval(this.#tick.bind(this), 100) + this.#element.addEventListener("waiting", this.#tick.bind(this)) + } + + #tick() { + // Try skipping ahead if there's no data in the current buffer. + this.#trySeek() + + // Try skipping video if it would fix any desync. + this.#trySkip() + } + + // Seek to the end and then play + async play() { + const ranges = this.#element.buffered + if (!ranges.length) { + return + } + + this.#element.currentTime = ranges.end(ranges.length - 1) + await this.#element.play() + } + + // Try seeking ahead to the next buffered range if there's a gap + #trySeek() { + if (this.#element.readyState > 2) { + // HAVE_CURRENT_DATA + // No need to seek + return + } + + const ranges = this.#element.buffered + if (!ranges.length) { + // Video has not started yet + return + } + + for (let i = 0; i < ranges.length; i += 1) { + const pos = ranges.start(i) + + if (this.#element.currentTime >= pos) { + // This would involve seeking backwards + continue + } + + console.warn("seeking forward", pos - this.#element.currentTime) + + this.#element.currentTime = pos + return + } + } + + // Try dropping video frames if there is future data available. + #trySkip() { + let playhead: number | undefined + + if (this.#element.readyState > 2) { + // If we're not buffering, only skip video if it's before the current playhead + playhead = this.#element.currentTime + } + + this.#video.advance(playhead) + } + + init(msg: Message.Init) { + this.#runInit(msg).catch((e) => console.warn("failed to run init", e)) + } + + async #runInit(msg: Message.Init) { + let init = this.#init.get(msg.name) + if (!init) { + init = new InitParser() + this.#init.set(msg.name, init) + } + + const reader = msg.stream.getReader() + + for (;;) { + const { value, done } = await reader.read() + if (done) break + + init.push(value) + } + } + + segment(msg: Message.Segment) { + this.#runSegment(msg).catch((e) => console.warn("failed to run segment", e)) + } + + async #runSegment(msg: Message.Segment) { + let pending = this.#init.get(msg.init) + if (!pending) { + pending = new InitParser() + this.#init.set(msg.init, pending) + } + + // Wait for the init segment to be fully received and parsed + const init = await pending.ready + + let track: Track + if (init.info.videoTracks.length) { + track = this.#video + } else { + track = this.#audio + } + + if (msg.header.object !== 0) { + throw new Error("multiple objects per group not supported") + } + + const segment = new Segment(track.source, init, msg.header.group) + track.add(segment) + + const container = new MP4.Parser() + + // We need to reparse the init segment to work with mp4box + const writer = container.decode.writable.getWriter() + for (const raw of init.raw) { + // I hate this + await writer.write(new Uint8Array(raw)) + } + writer.releaseLock() + + const reader = msg.stream.pipeThrough(container.decode).getReader() + for (;;) { + const { value, done } = await reader.read() + if (done) break + + segment.push(value.sample) + track.flush() + } + + segment.finish() + } + + close() { + clearInterval(this.#interval) + } +} diff --git a/repos/demo/src/lib/moq/playback/mse/init.ts b/repos/demo/src/lib/moq/playback/mse/init.ts new file mode 100644 index 0000000..80385a9 --- /dev/null +++ b/repos/demo/src/lib/moq/playback/mse/init.ts @@ -0,0 +1,59 @@ +import * as MP4 from "../../media/mp4" + +export class InitParser { + mp4box: MP4.ISOFile + offset: number + + raw: MP4.ArrayBuffer[] + ready: Promise + + constructor() { + this.mp4box = MP4.New() + + this.raw = [] + this.offset = 0 + + // Create a promise that gets resolved once the init segment has been parsed. + this.ready = new Promise((resolve, reject) => { + this.mp4box.onError = reject + + // https://github.com/gpac/mp4box.js#onreadyinfo + this.mp4box.onReady = (info: MP4.Info) => { + if (!info.isFragmented) { + reject("expected a fragmented mp4") + } + + if (info.tracks.length != 1) { + reject("expected a single track") + } + + resolve({ + info: info, + raw: this.raw + }) + } + }) + } + + push(data: Uint8Array) { + // Make a copy of the atom because mp4box only accepts an ArrayBuffer unfortunately + const box = new Uint8Array(data.byteLength) + box.set(data) + + // and for some reason we need to modify the underlying ArrayBuffer with fileStart + const buffer = box.buffer as MP4.ArrayBuffer + buffer.fileStart = this.offset + + // Parse the data + this.offset = this.mp4box.appendBuffer(buffer) + this.mp4box.flush() + + // Add the box to our queue of chunks + this.raw.push(buffer) + } +} + +export interface Init { + raw: MP4.ArrayBuffer[] + info: MP4.Info +} diff --git a/repos/demo/src/lib/moq/playback/mse/segment.ts b/repos/demo/src/lib/moq/playback/mse/segment.ts new file mode 100644 index 0000000..d826da7 --- /dev/null +++ b/repos/demo/src/lib/moq/playback/mse/segment.ts @@ -0,0 +1,126 @@ +import { Source } from "./source" +import { Init } from "./init" +import * as MP4 from "../../media/mp4" + +// Manage a segment download, keeping a buffer of a single sample to potentially rewrite the duration. +export class Segment { + source: Source // The SourceBuffer used to decode media. + offset: number // The byte offset in the received file so far + samples: MP4.Sample[] // The samples ready to be flushed to the source. + init: Init + + sequence: number // The order within the track + dts?: number // The parsed DTS of the first sample + timescale?: number // The parsed timescale of the segment + + output: MP4.ISOFile // MP4Box file used to write the outgoing atoms after modification. + + done: boolean // The segment has been completed + + constructor(source: Source, init: Init, sequence: number) { + this.source = source + this.offset = 0 + this.done = false + this.init = init + this.sequence = sequence + + this.output = MP4.New() + this.samples = [] + + // We have to reparse the init segment to work with mp4box + for (let i = 0; i < init.raw.length; i += 1) { + // Populate the output with our init segment so it knows about tracks + this.output.appendBuffer(init.raw[i]) + } + + this.output.flush() + } + + push(sample: MP4.Sample) { + if (this.dts === undefined) { + this.dts = sample.dts + this.timescale = sample.timescale + } + + // Add the samples to a queue + this.samples.push(sample) + } + + // Flushes any pending samples, returning true if the stream has finished. + flush(): boolean { + const stream = new MP4.Stream(new ArrayBuffer(0), 0, false) // big-endian + + while (this.samples.length) { + // Keep a single sample if we're not done yet + if (!this.done && this.samples.length < 2) break + + const sample = this.samples.shift() + if (!sample) break + + const moof = this.output.createSingleSampleMoof(sample) + moof.write(stream) + + // adjusting the data_offset now that the moof size is known + // TODO find a better way to do this or remove it? + const trun = moof.trafs[0].truns[0] + if (trun.data_offset_position && moof.size) { + trun.data_offset = moof.size + 8 // 8 is mdat header + stream.adjustUint32(trun.data_offset_position, trun.data_offset) + } + + const mdat = new MP4.BoxParser.mdatBox() + mdat.data = sample.data + mdat.write(stream) + } + + if (stream.buffer.byteLength == 0) { + return this.done + } + + this.source.initialize(this.init) + this.source.append(stream.buffer) + + return this.done + } + + // The segment has completed + finish() { + this.done = true + this.flush() + + // Trim the buffer to 30s long after each segment. + this.source.trim(30) + } + + // Extend the last sample so it reaches the provided timestamp + skipTo(pts: number) { + if (this.samples.length == 0) return + const last = this.samples[this.samples.length - 1] + + const skip = pts - (last.dts + last.duration) + + if (skip == 0) return + if (skip < 0) throw "can't skip backwards" + + last.duration += skip + + if (this.timescale) { + console.warn("skipping video", skip / this.timescale) + } + } + + buffered() { + // Ignore if we have a single sample + if (this.samples.length <= 1) return undefined + if (!this.timescale) return undefined + + const first = this.samples[0] + const last = this.samples[this.samples.length - 1] + + return { + length: 1, + start: first.dts / this.timescale, + end: (last.dts + last.duration) / this.timescale + } + } +} diff --git a/repos/demo/src/lib/moq/playback/mse/source.ts b/repos/demo/src/lib/moq/playback/mse/source.ts new file mode 100644 index 0000000..378eab1 --- /dev/null +++ b/repos/demo/src/lib/moq/playback/mse/source.ts @@ -0,0 +1,148 @@ +import { Init } from "./init" + +// Create a SourceBuffer with convenience methods +export class Source { + sourceBuffer?: SourceBuffer + mediaSource: MediaSource + queue: Array + init?: Init + + constructor(mediaSource: MediaSource) { + this.mediaSource = mediaSource + this.queue = [] + } + + // (re)initialize the source using the provided init segment. + initialize(init: Init) { + // Check if the init segment is already in the queue. + for (let i = this.queue.length - 1; i >= 0; i--) { + if ((this.queue[i] as SourceInit).init == init) { + // Already queued up. + return + } + } + + // Check if the init segment has already been applied. + if (this.init == init) { + return + } + + // Add the init segment to the queue so we call addSourceBuffer or changeType + this.queue.push({ + kind: "init", + init: init + }) + + for (let i = 0; i < init.raw.length; i += 1) { + this.queue.push({ + kind: "data", + data: init.raw[i] + }) + } + + this.flush() + } + + // Append the segment data to the buffer. + append(data: Uint8Array | ArrayBuffer) { + if (data.byteLength == 0) { + throw new Error("empty append") + } + + this.queue.push({ + kind: "data", + data: data + }) + + this.flush() + } + + // Return the buffered range. + buffered() { + if (!this.sourceBuffer) { + return { length: 0 } + } + + return this.sourceBuffer.buffered + } + + // Delete any media older than x seconds from the buffer. + trim(duration: number) { + this.queue.push({ + kind: "trim", + trim: duration + }) + + this.flush() + } + + // Flush any queued instructions + flush() { + for (;;) { + // Check if the buffer is currently busy. + if (this.sourceBuffer && this.sourceBuffer.updating) { + break + } + + // Process the next item in the queue. + const next = this.queue.shift() + if (!next) { + break + } + + if (next.kind == "init") { + this.init = next.init + + if (!this.sourceBuffer) { + // Create a new source buffer. + this.sourceBuffer = this.mediaSource.addSourceBuffer(this.init.info.mime) + + // Call flush automatically after each update finishes. + this.sourceBuffer.addEventListener("updateend", this.flush.bind(this)) + } else { + this.sourceBuffer.changeType(next.init.info.mime) + } + } else if (next.kind == "data") { + if (!this.sourceBuffer) { + throw "failed to call initailize before append" + } + + this.sourceBuffer.appendBuffer(next.data) + } else if (next.kind == "trim") { + if (!this.sourceBuffer) { + throw "failed to call initailize before trim" + } + + if (this.sourceBuffer.buffered.length == 0) { + break + } + + const end = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length - 1) - next.trim + const start = this.sourceBuffer.buffered.start(0) + + if (end > start) { + this.sourceBuffer.remove(start, end) + } + } else { + throw "impossible; unknown SourceItem" + } + } + } +} + +interface SourceItem {} + +class SourceInit implements SourceItem { + kind!: "init" + init!: Init +} + +class SourceData implements SourceItem { + kind!: "data" + data!: Uint8Array | ArrayBuffer +} + +class SourceTrim implements SourceItem { + kind!: "trim" + trim!: number +} diff --git a/repos/demo/src/lib/moq/playback/mse/track.ts b/repos/demo/src/lib/moq/playback/mse/track.ts new file mode 100644 index 0000000..13e99ff --- /dev/null +++ b/repos/demo/src/lib/moq/playback/mse/track.ts @@ -0,0 +1,80 @@ +import { Source } from "./source" +import { Segment } from "./segment" + +// An audio or video track that consists of multiple sequential segments. +// +// Instead of buffering, we want to drop video while audio plays uninterupted. +// Chrome actually plays up to 3s of audio without video before buffering when in low latency mode. +// Unforuntately, this does not recover correctly when there are gaps (pls fix). +// Our solution is to flush segments in decode order, buffering a single additional frame. +// We extend the duration of the buffered frame and flush it to cover any gaps. +export class Track { + source: Source + segments: Segment[] + + constructor(source: Source) { + this.source = source + this.segments = [] + } + + add(segment: Segment) { + // TODO don't add if the segment is out of date already + this.segments.push(segment) + + // Sort by timestamp ascending + // NOTE: The timestamp is in milliseconds, and we need to parse the media to get the accurate PTS/DTS. + this.segments.sort((a: Segment, b: Segment): number => { + return a.sequence - b.sequence + }) + } + + flush() { + for (;;) { + if (!this.segments.length) break + + const first = this.segments[0] + const done = first.flush() + if (!done) break + + this.segments.shift() + } + } + + // Given the current playhead, determine if we should drop any segments + // If playhead is undefined, it means we're buffering so skip to anything now. + advance(playhead: number | undefined) { + if (this.segments.length < 2) return + + while (this.segments.length > 1) { + const current = this.segments[0] + const next = this.segments[1] + + if (next.dts === undefined || next.timescale == undefined) { + // No samples have been parsed for the next segment yet. + break + } + + if (current.dts === undefined) { + // No samples have been parsed for the current segment yet. + // We can't cover the gap by extending the sample so we have to seek. + // TODO I don't think this can happen, but I guess we have to seek past the gap. + break + } + + if (playhead !== undefined) { + // Check if the next segment has playable media now. + // Otherwise give the current segment more time to catch up. + if (next.dts / next.timescale > playhead) { + return + } + } + + current.skipTo(next.dts || 0) // tell typescript that it's not undefined; we already checked + current.finish() + + // TODO cancel the QUIC stream to save bandwidth + + this.segments.shift() + } + } +} diff --git a/repos/demo/src/lib/moq/playback/trackBuffer.ts b/repos/demo/src/lib/moq/playback/trackBuffer.ts new file mode 100644 index 0000000..81ddfdb --- /dev/null +++ b/repos/demo/src/lib/moq/playback/trackBuffer.ts @@ -0,0 +1,54 @@ +import { TPEstimator } from "../common/utils" +import * as Message from "./webcodecs/message" + +export class TrackBuffer { + #playbackBuffer: Message.Segment[] = [] + #groupByTimestamp: Map = new Map() // by 10ms precision + + constructor() { + this.#playbackBuffer = [] + } + + get nextSegment(): Message.Segment | undefined { + const next = this.#playbackBuffer.shift() + return next + } + + get isBufferEmpty(): boolean { + return this.#playbackBuffer.length === 0 + } + + addSegment(segment: Message.Segment) { + // TODO: This is a hack. We remove the same timestamped segments out of + // the buffer. This needs to be fixed on the server-side, I think (ZG) + if (segment.header.ntp_timestamp === undefined) { + return + } + let maxGroupByTimestamp = 0 + if (this.#groupByTimestamp.has(Math.floor(segment.header.ntp_timestamp / 10))) { + maxGroupByTimestamp = this.#groupByTimestamp.get(Math.floor(segment.header.ntp_timestamp / 10))! + } + if (segment.header.group! < maxGroupByTimestamp) { + console.log("Lower group number, possible burst, discard", segment.header.group, maxGroupByTimestamp) + return + } + + this.#groupByTimestamp.set(Math.floor(segment.header.ntp_timestamp / 10), segment.header.group! || 0) + + if (maxGroupByTimestamp > 0) { + // remove the old segment with the same timestamp (with 10ms precision) + console.log("trackBuffer | remove old segment with the same timestamp", maxGroupByTimestamp) + this.#playbackBuffer = this.#playbackBuffer.filter((s) => s.header.group! !== maxGroupByTimestamp) + } + this.#playbackBuffer.push(segment) + this.#playbackBuffer = this.#playbackBuffer.sort((a, b) => a.header.group! - b.header.group!) + } + + clear() { + this.#playbackBuffer = [] + } + + getBufferLength(): number { + return this.#playbackBuffer.length + } +} diff --git a/repos/demo/src/lib/moq/playback/tsconfig.json b/repos/demo/src/lib/moq/playback/tsconfig.json new file mode 100644 index 0000000..3dd498f --- /dev/null +++ b/repos/demo/src/lib/moq/playback/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../tsconfig.json", + "include": ["."], + "compilerOptions": { + "types": ["dom-mediacapture-transform", "dom-webcodecs"] + }, + "references": [ + { + "path": "../common" + }, + { + "path": "../transport" + }, + { + "path": "../media" + } + ], + "paths": { + "@/*": ["*"] + } +} diff --git a/repos/demo/src/lib/moq/playback/webcodecs/audio.ts b/repos/demo/src/lib/moq/playback/webcodecs/audio.ts new file mode 100644 index 0000000..aafd2c7 --- /dev/null +++ b/repos/demo/src/lib/moq/playback/webcodecs/audio.ts @@ -0,0 +1,73 @@ +import * as Message from "./message" +import { Ring } from "../../common/ring" +import { Component, Frame } from "./timeline" +import * as MP4 from "../../media/mp4" + +// This is run in a worker. +export class Renderer { + #ring: Ring + #timeline: Component + + #decoder!: AudioDecoder + #stream: TransformStream + + constructor(config: Message.ConfigAudio, timeline: Component) { + this.#timeline = timeline + this.#ring = new Ring(config.ring) + + this.#stream = new TransformStream({ + start: this.#start.bind(this), + transform: this.#transform.bind(this) + }) + + this.#run().catch(console.error) + } + + #start(controller: TransformStreamDefaultController) { + this.#decoder = new AudioDecoder({ + output: (frame: AudioData) => { + controller.enqueue(frame) + }, + error: console.warn + }) + } + + #transform(frame: Frame) { + if (this.#decoder.state !== "configured") { + const track = frame.track + if (!MP4.isAudioTrack(track)) throw new Error("expected audio track") + + // We only support OPUS right now which doesn't need a description. + this.#decoder.configure({ + codec: track.codec, + sampleRate: track.audio.sample_rate, + numberOfChannels: track.audio.channel_count + }) + } + + const chunk = new EncodedAudioChunk({ + type: frame.sample.is_sync ? "key" : "delta", + timestamp: frame.sample.cts, //frame.sample.dts / frame.track.timescale,, + duration: frame.sample.duration, + data: frame.sample.data + }) + + this.#decoder.decode(chunk) + } + + async #run() { + const reader = this.#timeline.frames.pipeThrough(this.#stream).getReader() + + for (;;) { + const { value: frame, done } = await reader.read() + if (done) break + + // Write audio samples to the ring buffer, dropping when there's no space. + const written = this.#ring.write(frame) + + if (written < frame.numberOfFrames) { + console.warn(`droppped ${frame.numberOfFrames - written} audio samples`) + } + } + } +} diff --git a/repos/demo/src/lib/moq/playback/webcodecs/context.ts b/repos/demo/src/lib/moq/playback/webcodecs/context.ts new file mode 100644 index 0000000..0e8ddd3 --- /dev/null +++ b/repos/demo/src/lib/moq/playback/webcodecs/context.ts @@ -0,0 +1,56 @@ +import * as Message from "./message" + +// This is a non-standard way of importing worklet/workers. +// Unfortunately, it's the only option because of a Vite bug: https://github.com/vitejs/vite/issues/11823 +import workletURL from "./worklet" + +// NOTE: This must be on the main thread +export class Context { + context: AudioContext + worklet: Promise + + constructor(config: Message.ConfigAudio) { + this.context = new AudioContext({ + latencyHint: "interactive", + sampleRate: config.sampleRate + }) + + this.worklet = this.load(config) + } + + private async load(config: Message.ConfigAudio): Promise { + // Load the worklet source code. + await this.context.audioWorklet.addModule("renderer") // workletURL) + + const volume = this.context.createGain() + volume.gain.value = 2.0 + + // Create the worklet + const worklet = new AudioWorkletNode(this.context, "renderer") + + worklet.port.addEventListener("message", this.on.bind(this)) + worklet.onprocessorerror = (e: Event) => { + console.error("Audio worklet error:", e) + } + + // Connect the worklet to the volume node and then to the speakers + worklet.connect(volume) + volume.connect(this.context.destination) + + worklet.port.postMessage({ config }) + + return worklet + } + + private on(_event: MessageEvent) { + // TODO + } + + async resume() { + await this.context.resume() + } + + async close() { + await this.context.close() + } +} diff --git a/repos/demo/src/lib/moq/playback/webcodecs/index.ts b/repos/demo/src/lib/moq/playback/webcodecs/index.ts new file mode 100644 index 0000000..16e9189 --- /dev/null +++ b/repos/demo/src/lib/moq/playback/webcodecs/index.ts @@ -0,0 +1,117 @@ +import { RingShared } from "../../common/ring" +import { AudioTrack, Catalog, Mp4Track, VideoTrack, isAudioTrack } from "../../media/catalog" +import { Segment, Init } from "../backend" +import { Context } from "./context" +import * as Message from "./message" + +export interface PlayerConfig { + element: OffscreenCanvas + catalog: Catalog +} + +// Responsible for sending messages to the worker and worklet. +export default class WebcodecsPlayerBackend { + // General worker + #worker: Worker + + // The audio context, which must be created on the main thread. + #context?: Context + + #registeredListeners: Array<(e: MessageEvent) => void> = [] + + constructor(config: PlayerConfig) { + // TODO does this block the main thread? If so, make this async + // this.#worker = new MediaWorker({ format: "es" }) + this.#worker = new Worker(new URL("./worker.ts", import.meta.url), { + type: "module" + }) + + let sampleRate: number | undefined + let channels: number | undefined + + for (const track of config.catalog.tracks) { + if (isAudioTrack(track)) { + if (sampleRate && track.sample_rate !== sampleRate) { + throw new Error(`TODO multiple audio tracks with different sample rates`) + } + + sampleRate = track.sample_rate + channels = Math.max(track.channel_count, channels ?? 0) + } + } + + const msg: Message.Config = {} + + // Only configure audio is we have an audio track + if (sampleRate && channels) { + msg.audio = { + channels: channels, + sampleRate: sampleRate, + ring: new RingShared(2, sampleRate / 20) // 50ms + } + + this.#context = new Context(msg.audio) + } + + // TODO only send the canvas if we have a video track + msg.video = { + canvas: config.element + } + + this.send({ config: msg }, msg.video.canvas) + } + + // TODO initialize context now since the user clicked + play() {} + + init(init: Init) { + this.send({ init }, init.stream) + } + + segment(segment: Segment) { + // console.log("webcodecs | segment", segment) + this.send({ segment }, segment.stream) + } + + async close() { + this.#worker.terminate() + await this.#context?.close() + } + + setVideoTrack(videoTrack: VideoTrack) { + console.log("webcodecs | setVideoTrack", videoTrack) + this.send({ currentVideoTrack: videoTrack }) + } + + setTargetLatency(targetLatency: number) { + this.send({ targetLatency }) + } + + setServerTimeOffset(serverTimeOffset: number) { + this.send({ serverTimeOffset }) + } + + resetSWMA() { + this.send({ resetSWMA: true }) + } + + // Enforce we're sending valid types to the worker + private send(msg: Message.ToWorker, ...transfer: Transferable[]) { + //console.log("sent message from main to worker", msg) + this.#worker.postMessage(msg, transfer) + } + + on(callback: (msg: Message.FromWorker) => void) { + const listener = (e: MessageEvent) => callback(e.data) + this.#registeredListeners.push(listener) + this.#worker.addEventListener("message", listener) + } + + off() { + for (;;) { + const listener = this.#registeredListeners.pop() + if (!listener) break + this.#worker.removeEventListener("message", listener) + } + } +} diff --git a/repos/demo/src/lib/moq/playback/webcodecs/message.ts b/repos/demo/src/lib/moq/playback/webcodecs/message.ts new file mode 100644 index 0000000..5f10905 --- /dev/null +++ b/repos/demo/src/lib/moq/playback/webcodecs/message.ts @@ -0,0 +1,118 @@ +import { Header } from "../../transport/object" +import { RingShared } from "../../common/ring" +import { VideoTrack } from "../../media/catalog" + +export interface Config { + audio?: ConfigAudio + video?: ConfigVideo +} + +export interface ConfigAudio { + channels: number + sampleRate: number + + ring: RingShared +} + +export interface ConfigVideo { + canvas: OffscreenCanvas +} + +export interface Init { + name: string // name of the init object + stream: ReadableStream +} + +export interface Segment { + init: string // name of the init object + data: string // name of the data object + kind: "audio" | "video" + header: Header + stream: ReadableStream +} + +/* +export interface Play { + // Start playback once the minimum buffer size has been reached. + minBuffer: number +} + +export interface Seek { + timestamp: number +} +*/ + +// Sent periodically with the current timeline info. +export interface Timeline { + // The current playback position + timestamp?: number + + // Audio specific information + audio: TimelineAudio + + // Video specific information + video: TimelineVideo +} + +export interface TimelineAudio { + buffer: Range[] +} + +export interface TimelineVideo { + buffer: Range[] +} + +export interface Range { + start: number + end: number +} + +// Used to validate that only the correct messages can be sent. + +// Any top level messages that can be sent to the worker. +export interface ToWorker { + // Sent to configure on startup. + config?: Config + + // Sent on each init/data stream + init?: Init + segment?: Segment + + /* + // Sent to control playback + play?: Play + seek?: Seek + */ + // Sent to change the current video track + currentVideoTrack?: VideoTrack + resetSWMA?: boolean + serverTimeOffset?: number // in ms +} + +// Any top-level messages that can be sent from the worker. +export interface FromWorker { + // Sent back to the main thread regularly to update the UI + timeline?: Timeline + skip?: SkipEvent + latency?: number + measuredBandwidth?: number + trackId?: string + stall?: { + since: number + duration: number + } +} + +export interface SkipEvent { + type: "too_slow" | "too_old" + skippedGroup: { sequence: number; track: string } + currentGroup: { sequence: number; track: string } + duration: number +} + +/* +interface ToWorklet { + config?: Audio.Config +} + +*/ diff --git a/repos/demo/src/lib/moq/playback/webcodecs/timeline.ts b/repos/demo/src/lib/moq/playback/webcodecs/timeline.ts new file mode 100644 index 0000000..19c652f --- /dev/null +++ b/repos/demo/src/lib/moq/playback/webcodecs/timeline.ts @@ -0,0 +1,189 @@ +import type { Frame } from "../../media/mp4" +export type { Frame } + +export interface Range { + start: number + end: number +} + +export class Timeline { + // Maintain audio and video seprarately + audio: Component + video: Component + + // Construct a timeline + constructor() { + this.audio = new Component() + this.video = new Component() + } +} + +interface Segment { + track: string + sequence: number + frames: ReadableStream +} + +export class Component { + #current?: Segment + + frames: ReadableStream + #segments: TransformStream + #sequenceDurationMap = new Map() + + #framesCompleted = false + + constructor() { + this.frames = new ReadableStream({ + pull: this.#pull.bind(this), + cancel: this.#cancel.bind(this) + }) + + // This is a hack to have an async channel with 100 items. + this.#segments = new TransformStream({}, { highWaterMark: 100 }) + } + + get segments() { + return this.#segments.writable + } + + async #pull(controller: ReadableStreamDefaultController) { + for (;;) { + // Get the next segment to render. + let segments + let res: ReadableStreamReadResult | ReadableStreamReadResult + + try { + segments = this.#segments.readable.getReader() + + if (this.#current) { + let frames + try { + // Get the next frame to render. + frames = this.#current.frames.getReader() + + // Wait for either the frames or segments to be ready. + // NOTE: This assume that the first promise gets priority. + res = await Promise.race([frames.read(), segments.read()]) + } catch (e) { + // TODO handle errors + console.error("error in timeline pull 1", e) + continue + } finally { + if (frames) frames.releaseLock() + } + } else { + res = await segments.read() + } + } catch (e) { + console.error("error in timeline pull 2", e) + continue + } finally { + if (segments) segments.releaseLock() + } + + const { value, done } = res + + if (done) { + // We assume the current segment has been closed + // TODO support the segments stream closing + // console.log("timeline stream done", value) + this.#current = undefined + continue + } + + if (!isSegment(value)) { + if (!this.#current) throw new Error("impossible. a frame without segment") + + // We got a frame, so we need to update the sequence duration + const id = this.#current.track + this.#current.sequence + const duration = this.#sequenceDurationMap.get(id)! + + // Update the sequence duration + // console.log("updating sequence duration", id, duration + value.sample.duration / value.sample.timescale) + this.#sequenceDurationMap.set(id, duration + value.sample.duration / value.sample.timescale) + + // Return so the reader can decide when to get the next frame. + controller.enqueue(value) + + // assuming the GOP duration is 1 second + // in order to avoid excession skip messages, we check if all frames are received + // and if so, we set the framesCompleted flag to true + if (Math.floor(duration + value.sample.duration / value.sample.timescale + 0.0001) === 1) { + this.#framesCompleted = true + } + return + } + + // We didn't get any frames, and instead got a new segment. + if (this.#current && !this.#framesCompleted) { + let skipDuration = 0 + const maxDuration = [...this.#sequenceDurationMap.values()].reduce((acc, duration) => Math.max(acc, duration), 0) + if (this.#sequenceDurationMap.has(this.#current.track + this.#current.sequence)) { + const duration = this.#sequenceDurationMap.get(this.#current.track + this.#current.sequence)! + skipDuration = maxDuration - duration + } + + if (value.sequence < this.#current.sequence) { + // The incoming segment is older than the current, abandon the incoming one. + try { + await value.frames.cancel( + "skipping incoming segment; too old | sequence (incoming): " + value.sequence + " | current sequence: " + this.#current.sequence + " track: " + this.#current.track + ) + } finally { + postMessage({ + skip: { + type: "too_old", + skippedGroup: { sequence: value.sequence, track: value.track }, + currentGroup: { sequence: this.#current.sequence, track: this.#current.track }, + duration: skipDuration + } + }) + } + continue + } else { + // The incoming segment is newer than the current, cancel the current one. + try { + // Our segment is newer than the current, cancel the old one. + await this.#current.frames.cancel( + "skipping current segment; too slow | sequence (incoming): " + value.sequence + " | current sequence: " + this.#current.sequence + " track: " + this.#current.track + ) + } finally { + postMessage({ + skip: { + type: "too_slow", + skippedGroup: { sequence: this.#current.sequence, track: this.#current.track }, + currentGroup: { sequence: value.sequence, track: value.track }, + duration: skipDuration + } + }) + } + } + } + this.#framesCompleted = false + this.#current = value + this.#sequenceDurationMap.set(this.#current.track + this.#current.sequence, 0) + } + } + + async #cancel(reason: any) { + if (this.#current) { + await this.#current.frames.cancel(reason) + } + + const segments = this.#segments.readable.getReader() + for (;;) { + const { value: segment, done } = await segments.read() + if (done) break + + await segment.frames.cancel(reason) + } + } +} + +// Return if a type is a segment or frame +// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents +function isSegment(value: Segment | Frame): value is Segment { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return (value as Segment).frames !== undefined +} diff --git a/repos/demo/src/lib/moq/playback/webcodecs/video.ts b/repos/demo/src/lib/moq/playback/webcodecs/video.ts new file mode 100644 index 0000000..d858aaf --- /dev/null +++ b/repos/demo/src/lib/moq/playback/webcodecs/video.ts @@ -0,0 +1,227 @@ +import { SWMA } from "../../common/utils" +import { VideoTrack } from "../../media/catalog" +import * as MP4 from "../../media/mp4" +import * as Message from "./message" +import { Frame, Component } from "./timeline" + +export class Renderer { + #canvas: OffscreenCanvas + #timeline: Component + + #decoder!: VideoDecoder + #queue: TransformStream + #prftMap = new Map() // PTS -> NTP + #currentVideoTrack?: VideoTrack + #serverTimeOffset: number = 0 + + #state = { + lastDisplayTime: 0, + tickHandlerStarted: false, + differences: new SWMA(20, "stall-diff"), + lastTrackId: "", + lastFrameTimestamp: 0 + } + + constructor(config: Message.ConfigVideo, timeline: Component) { + this.#canvas = config.canvas + this.#timeline = timeline + + this.#queue = new TransformStream({ + start: this.#start.bind(this), + transform: this.#transform.bind(this) + }) + + this.#run().catch(console.error) + } + + async #run() { + let frameWaiting = Promise.resolve() + let previousFrame: VideoFrame | undefined + + const reader = this.#timeline.frames.pipeThrough(this.#queue).getReader() + let lastFrameReceipt = -1 + for (;;) { + await frameWaiting + if (lastFrameReceipt > -1) { + const diff = performance.now() - lastFrameReceipt + // TODO: this is a hack to keep the frame rate at 30fps (ZG). Make this parameterized. + const frameTime = 1000 / 30 + if (diff < frameTime) { + const wait = frameTime - diff + await new Promise((resolve) => setTimeout(resolve, wait)) + } + } + lastFrameReceipt = performance.now() + const { value: frame, done } = await reader.read() + if (done) break + + const prft = this.#prftMap.get(frame.timestamp) + let ntp = ntptoms(prft) + + // See if the track has changed + if (this.#currentVideoTrack && this.#currentVideoTrack.data_track !== this.#state.lastTrackId) { + this.#state.lastTrackId = this.#currentVideoTrack.data_track + postMessage({ trackId: this.#state.lastTrackId }) + } + + // Post the latency to the main thread + if (!isNaN(this.#serverTimeOffset)) { + ntp -= this.#serverTimeOffset + } + postMessage({ + latency: (performance.now() + performance.timeOrigin - ntp) / 1000 + }) + + // Setup frame wait promise + frameWaiting = new Promise((resolve) => { + this.#displayFrame(frame, previousFrame) + .then(() => { + if (previousFrame) previousFrame.close() + previousFrame = frame + }) + .then(resolve) + .catch(console.error) + }) + + if (!this.#state.tickHandlerStarted) { + self.requestAnimationFrame(this.#tickHandler.bind(this)) + this.#state.tickHandlerStarted = true + } + } + } + + async #displayFrame(frame: VideoFrame, previousFrame: VideoFrame | undefined): Promise { + const pts = frame.timestamp + const previousPts = previousFrame?.timestamp || frame.timestamp + let ptsDiff = (pts - previousPts) / 1000 + let lastRAF: number | undefined = undefined + + return new Promise((resolve) => { + const fn = (now: number) => { + if (ptsDiff > 0) { + if (!lastRAF) lastRAF = now + const localDiff = now - lastRAF + ptsDiff -= localDiff + if (ptsDiff > 0) { + return self.requestAnimationFrame(fn) + } + } + + this.#canvas.width = frame.displayWidth + this.#canvas.height = frame.displayHeight + + const ctx = this.#canvas.getContext("2d") + if (!ctx) throw new Error("failed to get canvas context") + + // Difference calculation for stall + const diff = now - this.#state.lastDisplayTime + this.#state.differences.next(diff) + this.#state.lastDisplayTime = now + + ctx.drawImage(frame, 0, 0, frame.displayWidth, frame.displayHeight) // TODO respect aspect ratio + + // assuming that resolutions don't exceed this... + const scale = frame.displayWidth / 1920 + ctx.fillStyle = "rgba(0, 0, 0, 0.5)" + ctx.fillRect(20 * scale, 20 * scale, 950 * scale, 70 * scale) + ctx.font = 60 * scale + "px courier" + ctx.fillStyle = "white" + ctx.fillText(`w:${frame.displayWidth} h:${frame.displayHeight} b: ${((this.#currentVideoTrack?.bit_rate || 0) / 1000).toFixed(0)} Kbps`, 50 * scale, 70 * scale) + // ctx.fillText(`pts:${(frame.timestamp / 15360).toFixed(2)}`, 50 * scale, 120 * scale) // tn = 15360 + resolve() + } + + self.requestAnimationFrame(fn) + }) + } + + #tickHandler(now: number) { + const diff = now - this.#state.lastDisplayTime + const swma = this.#state.differences.next() + // TODO: This is needed because I don't want to mess with syncronisation. But it's not ideal. + if (diff > swma * 2) { + postMessage({ stall: { since: this.#state.lastDisplayTime, duration: diff } }) + } + self.requestAnimationFrame(this.#tickHandler.bind(this)) + } + + #start(controller: TransformStreamDefaultController) { + this.#decoder = new VideoDecoder({ + output: (frame: VideoFrame) => { + controller.enqueue(frame) + }, + error: console.error + }) + } + + #transform(frame: Frame) { + // Configure the decoder with the first frame + //if (this.#decoder.state !== "configured") { + + // TODO: when resolution changes, reset decoder ZG + const { sample, track } = frame + + if (!MP4.isVideoTrack(track)) throw new Error("expected video track") + + // console.log("renderer | transform | frame", frame.track.id) + if (this.#canvas.width !== frame.track.track_width) { + const desc = sample.description + const box = desc.avcC ?? desc.hvcC ?? desc.vpcC ?? desc.av1C + if (!box) throw new Error(`unsupported codec: ${track.codec}`) + + const buffer = new MP4.Stream(undefined, 0, MP4.Stream.BIG_ENDIAN) + box.write(buffer) + const description = new Uint8Array(buffer.buffer, 8) // Remove the box header. + + // if the frame is not a keyframe, wait for it + if (frame.sample.is_sync) { + this.#decoder.configure({ + codec: track.codec, + codedHeight: track.video.height, + codedWidth: track.video.width, + description + // optimizeForLatency: true + }) + } + } + + const chunk = new EncodedVideoChunk({ + type: frame.sample.is_sync ? "key" : "delta", + data: frame.sample.data, + timestamp: frame.sample.cts //frame.sample.dts / frame.track.timescale, + }) + + for (const prft of frame.prfts) { + this.#prftMap.set(chunk.timestamp, prft.ntp_timestamp) + } + + this.#decoder.decode(chunk) + } + + setVideoTrack(videoTrack: VideoTrack) { + // console.log("renderer | setVideoTrack", videoTrack) + this.#currentVideoTrack = videoTrack + } + + setServerTimeOffset(serverTimeOffset: number) { + // console.log("renderer | setVideoTrack", videoTrack) + this.#serverTimeOffset = serverTimeOffset + } +} + +function ntptoms(ntpTimestamp?: number) { + if (!ntpTimestamp) return NaN + + const ntpEpochOffset = 2208988800000 // milliseconds between 1970 and 1900 + + // Split the 64-bit NTP timestamp into upper and lower 32-bit parts + const upperPart = Math.floor(ntpTimestamp / Math.pow(2, 32)) + const lowerPart = ntpTimestamp % Math.pow(2, 32) + + // Calculate milliseconds for upper and lower parts + const upperMilliseconds = upperPart * 1000 + const lowerMilliseconds = (lowerPart / Math.pow(2, 32)) * 1000 + + // Combine both parts and adjust for the NTP epoch offset + return upperMilliseconds + lowerMilliseconds - ntpEpochOffset +} diff --git a/repos/demo/src/lib/moq/playback/webcodecs/worker.ts b/repos/demo/src/lib/moq/playback/webcodecs/worker.ts new file mode 100644 index 0000000..fba26ec --- /dev/null +++ b/repos/demo/src/lib/moq/playback/webcodecs/worker.ts @@ -0,0 +1,139 @@ +import { Timeline } from "./timeline" + +import * as Audio from "./audio" +import * as Video from "./video" + +import * as MP4 from "../../media/mp4" +import * as Message from "./message" +import { asError } from "../../common/error" +import { TPEstimator, SWMA } from "../../common/utils" +import { VideoTrack } from "../../media/catalog" + +class Worker { + // Timeline receives samples, buffering them and choosing the timestamp to render. + #timeline = new Timeline() + + // A map of init tracks. + #inits = new Map>() + + // SWMA for bandwidth measurement + #measuredBandwidth = new SWMA(2, "moq-bw") + + // Renderer requests samples, rendering video frames and emitting audio frames. + #audio?: Audio.Renderer + #video?: Video.Renderer + + #currentVideoTrack?: VideoTrack + + on(e: MessageEvent) { + const msg = e.data as Message.ToWorker + + if (msg.config) { + this.#onConfig(msg.config) + } else if (msg.init) { + // TODO buffer the init segmnet so we don't hold the stream open. + this.#onInit(msg.init) + } else if (msg.segment) { + const segment = msg.segment + this.#onSegment(segment).catch(async (e) => { + // Cancel the stream so we don't hold it open. + const err = asError(e) + await segment.stream.cancel(err) + + throw e + }) + } else if (msg.currentVideoTrack) { + console.log("worker | currentVideoTrack", msg.currentVideoTrack) + this.#currentVideoTrack = msg.currentVideoTrack + this.#video?.setVideoTrack(msg.currentVideoTrack) + } else if (msg.resetSWMA) { + this.#measuredBandwidth.reset() + } else if (msg.serverTimeOffset !== undefined) { + this.#video?.setServerTimeOffset(msg.serverTimeOffset) + } else { + throw new Error(`unknown message: + ${JSON.stringify(msg)}`) + } + } + + #onConfig(msg: Message.Config) { + if (msg.audio) { + this.#audio = new Audio.Renderer(msg.audio, this.#timeline.audio) + } + + if (msg.video) { + this.#video = new Video.Renderer(msg.video, this.#timeline.video) + } + } + + #onInit(msg: Message.Init) { + // NOTE: We don't buffer the init segments because I'm lazy. + // Instead, we fork the reader on each segment so it gets a copy of the data. + // This is mostly done because I'm lazy and don't want to create a promise. + this.#inits.set(msg.name, msg.stream) + } + + async #onSegment(msg: Message.Segment) { + // console.log("worker | onSegment", msg) ZG + const init = this.#inits.get(msg.init) + if (!init) throw new Error(`unknown init track: ${msg.init}`) + + // Make a copy of the init stream + // TODO: This could have performance ramifications? + const [initFork, initClone] = init.tee() + this.#inits.set(msg.init, initFork) + + // Create a new stream that we will use to decode. + const container = new MP4.Parser() + + const timeline = msg.kind === "audio" ? this.#timeline.audio : this.#timeline.video + + if (msg.header.object !== 0) { + throw new Error("multiple objects per group not supported") + } + + // Add the segment to the timeline + let segments + try { + // if there is no current track id, set it to the first track id we see + if (msg.kind === "video" && (!this.#currentVideoTrack || this.#currentVideoTrack.data_track === msg.data)) { + segments = timeline.segments.getWriter() + await segments.write({ + track: msg.data, + sequence: msg.header.group, + frames: container.decode.readable + }) + } else { + console.log("worker | onSegment | skipping segment", msg.kind, this.#currentVideoTrack, msg.data) + } + } finally { + if (segments) segments.releaseLock() + } + + // Decode the init and then the segment itself + // TODO avoid decoding the init every time. + await initClone.pipeTo(container.decode.writable, { preventClose: true }) + + await msg.stream.pipeTo(container.decode.writable) + + // Capture the time the segment was finished + if (msg.header.timings) msg.header.timings.finished = performance.now() + } +} + +// Pass all events to the worker +const worker = new Worker() +self.addEventListener("message", (msg) => { + try { + worker.on(msg as unknown as MessageEvent) + } catch (e) { + const err = asError(e) + console.warn("worker error:", err) + } +}) + +// Validates this is an expected message +function _send(msg: Message.FromWorker) { + postMessage(msg) +} + +export default worker diff --git a/repos/demo/src/lib/moq/playback/webcodecs/worklet/index.ts b/repos/demo/src/lib/moq/playback/webcodecs/worklet/index.ts new file mode 100644 index 0000000..f594ac5 --- /dev/null +++ b/repos/demo/src/lib/moq/playback/webcodecs/worklet/index.ts @@ -0,0 +1,58 @@ +// TODO add support for @/ to avoid relative imports +import { Ring } from "../../../common/ring" +import * as Message from "./message" + +export default class Renderer extends AudioWorkletProcessor { + ring?: Ring + base: number + + constructor() { + // The super constructor call is required. + super() + + this.base = 0 + this.port.onmessage = this.onMessage.bind(this) + } + + onMessage(e: MessageEvent) { + const msg = e.data as Message.From + if (msg.config) { + this.onConfig(msg.config) + } + } + + onConfig(config: Message.Config) { + this.ring = new Ring(config.ring) + } + + // Inputs and outputs in groups of 128 samples. + process(inputs: Float32Array[][], outputs: Float32Array[][], _parameters: Record): boolean { + if (!this.ring) { + // Paused + return true + } + + if (inputs.length != 1 && outputs.length != 1) { + throw new Error("only a single track is supported") + } + + if (this.ring.size() == this.ring.capacity) { + // This is a hack to clear any latency in the ring buffer. + // The proper solution is to play back slightly faster? + console.warn("resyncing ring buffer") + this.ring.clear() + return true + } + + const output = outputs[0] + + const size = this.ring.read(output) + if (size < output.length) { + // TODO trigger rebuffering event + } + + return true + } +} + +// registerProcessor("renderer", Renderer) diff --git a/repos/demo/src/lib/moq/playback/webcodecs/worklet/message.ts b/repos/demo/src/lib/moq/playback/webcodecs/worklet/message.ts new file mode 100644 index 0000000..d151fd8 --- /dev/null +++ b/repos/demo/src/lib/moq/playback/webcodecs/worklet/message.ts @@ -0,0 +1,12 @@ +import { RingShared } from "../../../common/ring" + +export interface From { + config?: Config +} + +export interface Config { + channels: number + sampleRate: number + + ring: RingShared +} diff --git a/repos/demo/src/lib/moq/playback/webcodecs/worklet/tsconfig.json b/repos/demo/src/lib/moq/playback/webcodecs/worklet/tsconfig.json new file mode 100644 index 0000000..ea7a8c8 --- /dev/null +++ b/repos/demo/src/lib/moq/playback/webcodecs/worklet/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["."], + "exclude": ["./index"], + "compilerOptions": { + "lib": ["es2022"], + "types": ["audioworklet"] + }, + "references": [ + { + "path": "../../../common" + } + ] +} diff --git a/repos/demo/src/lib/moq/transport/client.ts b/repos/demo/src/lib/moq/transport/client.ts new file mode 100644 index 0000000..500551a --- /dev/null +++ b/repos/demo/src/lib/moq/transport/client.ts @@ -0,0 +1,79 @@ +import * as Stream from "./stream" +import * as Setup from "./setup" +import * as Control from "./control" +import { Objects } from "./object" +import { Connection } from "./connection" + +export interface ClientConfig { + url: string + + // Parameters used to create the MoQ session + role: Setup.Role + + // If set, the server fingerprint will be fetched from this URL. + // This is required to use self-signed certificates with Chrome (May 2023) + fingerprint?: string +} + +export class Client { + #fingerprint: Promise + + readonly config: ClientConfig + + constructor(config: ClientConfig) { + this.config = config + + this.#fingerprint = this.#fetchFingerprint(config.fingerprint).catch((e) => { + console.warn("failed to fetch fingerprint: ", e) + return undefined + }) + } + + async connect(): Promise { + // Helper function to make creating a promise easier + const options: WebTransportOptions = {} + + const fingerprint = await this.#fingerprint + if (fingerprint) options.serverCertificateHashes = [fingerprint] + + const quic = new WebTransport(this.config.url, options) + await quic.ready + + const stream = await quic.createBidirectionalStream() + + const writer = new Stream.Writer(stream.writable) + const reader = new Stream.Reader(stream.readable) + + const setup = new Setup.Stream(reader, writer) + + // Send the setup message. + await setup.send.client({ versions: [Setup.Version.KIXEL_01], role: this.config.role }) + + // Receive the setup message. + // TODO verify the SETUP response. + const _server = await setup.recv.server() + + const control = new Control.Stream(reader, writer) + const objects = new Objects(quic) + + return new Connection(quic, control, objects) + } + + async #fetchFingerprint(url?: string): Promise { + if (!url) return + + // TODO remove this fingerprint when Chrome WebTransport accepts the system CA + const response = await fetch(url) + const hexString = await response.text() + + const hexBytes = new Uint8Array(hexString.length / 2) + for (let i = 0; i < hexBytes.length; i += 1) { + hexBytes[i] = parseInt(hexString.slice(2 * i, 2 * i + 2), 16) + } + + return { + algorithm: "sha-256", + value: hexBytes + } + } +} diff --git a/repos/demo/src/lib/moq/transport/connection.ts b/repos/demo/src/lib/moq/transport/connection.ts new file mode 100644 index 0000000..082b088 --- /dev/null +++ b/repos/demo/src/lib/moq/transport/connection.ts @@ -0,0 +1,95 @@ +import * as Control from "./control" +import { Objects } from "./object" +import { asError } from "../common/error" + +import { Publisher } from "./publisher" +import { Subscriber } from "./subscriber" + +export class Connection { + // The established WebTransport session. + #quic: WebTransport + + // Use to receive/send control messages. + #control: Control.Stream + + // Use to receive/send objects. + #objects: Objects + + // Module for contributing tracks. + #publisher: Publisher + + // Module for distributing tracks. + #subscriber: Subscriber + + // Async work running in the background + #running: Promise + + constructor(quic: WebTransport, control: Control.Stream, objects: Objects) { + this.#quic = quic + this.#control = control + this.#objects = objects + + this.#publisher = new Publisher(this.#control, this.#objects) + this.#subscriber = new Subscriber(this.#control, this.#objects) + + this.#running = this.#run() + } + + close(code = 0, reason = "") { + this.#quic.close({ closeCode: code, reason }) + } + + async #run(): Promise { + await Promise.all([this.#runControl(), this.#runObjects()]) + } + + announce(namespace: string) { + return this.#publisher.announce(namespace) + } + + announced() { + return this.#subscriber.announced() + } + + subscribe(namespace: string, track: string, switchTrackId?: bigint) { + return this.#subscriber.subscribe(namespace, track, switchTrackId) + } + + subscribed() { + return this.#publisher.subscribed() + } + + async #runControl() { + // Receive messages until the connection is closed. + for (;;) { + const msg = await this.#control.recv() + await this.#recv(msg) + } + } + + async #runObjects() { + for (;;) { + const obj = await this.#objects.recv() + if (!obj) break + + await this.#subscriber.recvObject(obj.header, obj.stream) + } + } + + async #recv(msg: Control.Message) { + if (Control.isPublisher(msg)) { + await this.#subscriber.recv(msg) + } else { + await this.#publisher.recv(msg) + } + } + + async closed(): Promise { + try { + await this.#running + return new Error("closed") + } catch (e) { + return asError(e) + } + } +} diff --git a/repos/demo/src/lib/moq/transport/control.ts b/repos/demo/src/lib/moq/transport/control.ts new file mode 100644 index 0000000..9f1e725 --- /dev/null +++ b/repos/demo/src/lib/moq/transport/control.ts @@ -0,0 +1,502 @@ +import { Reader, Writer } from "./stream" + +export type Message = Subscriber | Publisher + +// Sent by subscriber +export type Subscriber = Subscribe | Unsubscribe | AnnounceOk | AnnounceError + +export function isSubscriber(m: Message): m is Subscriber { + return m.kind == Msg.Subscribe || m.kind == Msg.Unsubscribe || m.kind == Msg.AnnounceOk || m.kind == Msg.AnnounceError +} + +// Sent by publisher +export type Publisher = SubscribeOk | SubscribeReset | SubscribeError | SubscribeFin | Announce | Unannounce + +export function isPublisher(m: Message): m is Publisher { + return m.kind == Msg.SubscribeOk || m.kind == Msg.SubscribeReset || m.kind == Msg.SubscribeError || m.kind == Msg.SubscribeFin || m.kind == Msg.Announce || m.kind == Msg.Unannounce +} + +// I wish we didn't have to split Msg and Id into separate enums. +// However using the string in the message makes it easier to debug. +// We'll take the tiny performance hit until I'm better at Typescript. +export enum Msg { + // NOTE: object and setup are in other modules + Subscribe = "subscribe", + SubscribeOk = "subscribe_ok", + SubscribeError = "subscribe_error", + SubscribeReset = "subscribe_reset", + SubscribeFin = "subscribe_fin", + Unsubscribe = "unsubscribe", + Announce = "announce", + AnnounceOk = "announce_ok", + AnnounceError = "announce_error", + Unannounce = "unannounce", + GoAway = "go_away" +} + +enum Id { + // NOTE: object and setup are in other modules + // Object = 0, + // Setup = 1, + + Subscribe = 0x3, + SubscribeOk = 0x4, + SubscribeError = 0x5, + SubscribeReset = 0xc, + SubscribeFin = 0xb, + Unsubscribe = 0xa, + Announce = 0x6, + AnnounceOk = 0x7, + AnnounceError = 0x8, + Unannounce = 0x9, + GoAway = 0x10 +} + +export interface Subscribe { + kind: Msg.Subscribe + + id: bigint + namespace: string + name: string + + start_group: Location + start_object: Location + end_group: Location + end_object: Location + + switch_track_id?: bigint // track alias that will be switched + + params?: Parameters +} + +export interface Location { + mode: "none" | "absolute" | "latest" | "future" + value?: number // ignored for type=none, otherwise defaults to 0 +} + +export type Parameters = Map + +export interface SubscribeOk { + kind: Msg.SubscribeOk + id: bigint + expires?: bigint +} + +export interface SubscribeReset { + kind: Msg.SubscribeReset + id: bigint + code: bigint + reason: string + final_group: number + final_object: number +} + +export interface SubscribeFin { + kind: Msg.SubscribeFin + id: bigint + final_group: number + final_object: number +} + +export interface SubscribeError { + kind: Msg.SubscribeError + id: bigint + code: bigint + reason: string +} + +export interface Unsubscribe { + kind: Msg.Unsubscribe + id: bigint +} + +export interface Announce { + kind: Msg.Announce + namespace: string + params?: Parameters +} + +export interface AnnounceOk { + kind: Msg.AnnounceOk + namespace: string +} + +export interface AnnounceError { + kind: Msg.AnnounceError + namespace: string + code: bigint + reason: string +} + +export interface Unannounce { + kind: Msg.Unannounce + namespace: string +} + +export class Stream { + private decoder: Decoder + private encoder: Encoder + + #mutex = Promise.resolve() + + constructor(r: Reader, w: Writer) { + this.decoder = new Decoder(r) + this.encoder = new Encoder(w) + } + + // Will error if two messages are read at once. + async recv(): Promise { + const msg = await this.decoder.message() + console.log("Stream | recv", msg) + return msg + } + + async send(msg: Message) { + const unlock = await this.#lock() + try { + console.log("Stream | send", msg) + await this.encoder.message(msg) + } finally { + unlock() + } + } + + async #lock() { + // Make a new promise that we can resolve later. + let done: () => void + const p = new Promise((resolve) => { + done = () => resolve() + }) + + // Wait until the previous lock is done, then resolve our our lock. + const lock = this.#mutex.then(() => done) + + // Save our lock as the next lock. + this.#mutex = p + + // Return the lock. + return lock + } +} + +export class Decoder { + r: Reader + + constructor(r: Reader) { + this.r = r + } + + private async msg(): Promise { + const t = (await this.r.u53()) as Id + switch (t) { + case Id.Subscribe: + return Msg.Subscribe + case Id.SubscribeOk: + return Msg.SubscribeOk + case Id.SubscribeReset: + return Msg.SubscribeReset + case Id.SubscribeFin: + return Msg.SubscribeFin + case Id.SubscribeError: + return Msg.SubscribeError + case Id.Unsubscribe: + return Msg.Unsubscribe + case Id.Announce: + return Msg.Announce + case Id.AnnounceOk: + return Msg.AnnounceOk + case Id.AnnounceError: + return Msg.AnnounceError + case Id.Unannounce: + return Msg.Unannounce + case Id.GoAway: + return Msg.GoAway + } + + throw new Error(`unknown control message type: ${t as number}`) + } + + async message(): Promise { + const t = await this.msg() + switch (t) { + case Msg.Subscribe: + return this.subscribe() + case Msg.SubscribeOk: + return this.subscribe_ok() + case Msg.SubscribeReset: + return this.subscribe_reset() + case Msg.SubscribeError: + return this.subscribe_error() + case Msg.SubscribeFin: + return this.subscribe_fin() + case Msg.Unsubscribe: + return this.unsubscribe() + case Msg.Announce: + return this.announce() + case Msg.AnnounceOk: + return this.announce_ok() + case Msg.Unannounce: + return this.unannounce() + case Msg.AnnounceError: + return this.announce_error() + case Msg.GoAway: + throw new Error("TODO: implement go away") + } + } + + private async subscribe(): Promise { + return { + kind: Msg.Subscribe, + id: await this.r.u62(), + namespace: await this.r.string(), + name: await this.r.string(), + start_group: await this.location(), + start_object: await this.location(), + end_group: await this.location(), + end_object: await this.location(), + params: await this.parameters() + } + } + + private async location(): Promise { + const mode = await this.r.u62() + if (mode == 0n) { + return { mode: "none", value: 0 } + } else if (mode == 1n) { + return { mode: "absolute", value: await this.r.u53() } + } else if (mode == 2n) { + return { mode: "latest", value: await this.r.u53() } + } else if (mode == 3n) { + return { mode: "future", value: await this.r.u53() } + } else { + throw new Error(`invalid location mode: ${mode}`) + } + } + + private async parameters(): Promise { + const count = await this.r.u53() + if (count == 0) return undefined + + const params = new Map() + + for (let i = 0; i < count; i++) { + const id = await this.r.u62() + const size = await this.r.u53() + const value = await this.r.readExact(size) + + if (params.has(id)) { + throw new Error(`duplicate parameter id: ${id}`) + } + + params.set(id, value) + } + + return params + } + + private async subscribe_ok(): Promise { + return { + kind: Msg.SubscribeOk, + id: await this.r.u62(), + expires: (await this.r.u62()) || undefined + } + } + + private async subscribe_reset(): Promise { + return { + kind: Msg.SubscribeReset, + id: await this.r.u62(), + code: await this.r.u62(), + reason: await this.r.string(), + final_group: await this.r.u53(), + final_object: await this.r.u53() + } + } + + private async subscribe_fin(): Promise { + return { + kind: Msg.SubscribeFin, + id: await this.r.u62(), + final_group: await this.r.u53(), + final_object: await this.r.u53() + } + } + + private async subscribe_error(): Promise { + return { + kind: Msg.SubscribeError, + id: await this.r.u62(), + code: await this.r.u62(), + reason: await this.r.string() + } + } + + private async unsubscribe(): Promise { + return { + kind: Msg.Unsubscribe, + id: await this.r.u62() + } + } + + private async announce(): Promise { + const namespace = await this.r.string() + + return { + kind: Msg.Announce, + namespace, + params: await this.parameters() + } + } + + private async announce_ok(): Promise { + return { + kind: Msg.AnnounceOk, + namespace: await this.r.string() + } + } + + private async announce_error(): Promise { + return { + kind: Msg.AnnounceError, + namespace: await this.r.string(), + code: await this.r.u62(), + reason: await this.r.string() + } + } + + private async unannounce(): Promise { + return { + kind: Msg.Unannounce, + namespace: await this.r.string() + } + } +} + +export class Encoder { + w: Writer + + constructor(w: Writer) { + this.w = w + } + + async message(m: Message) { + switch (m.kind) { + case Msg.Subscribe: + return this.subscribe(m) + case Msg.SubscribeOk: + return this.subscribe_ok(m) + case Msg.SubscribeReset: + return this.subscribe_reset(m) + case Msg.SubscribeError: + return this.subscribe_error(m) + case Msg.SubscribeFin: + return this.subscribe_fin(m) + case Msg.Unsubscribe: + return this.unsubscribe(m) + case Msg.Announce: + return this.announce(m) + case Msg.AnnounceOk: + return this.announce_ok(m) + case Msg.AnnounceError: + return this.announce_error(m) + case Msg.Unannounce: + return this.unannounce(m) + } + } + + async subscribe(s: Subscribe) { + await this.w.u53(Id.Subscribe) + await this.w.u62(s.id) + await this.w.string(s.namespace) + await this.w.string(s.name) + await this.location(s.start_group) + await this.location(s.start_object) + await this.location(s.end_group) + await this.location(s.end_object) + await this.w.u62(s.switch_track_id || BigInt(0)) + await this.parameters(s.params) + } + + private async location(l: Location) { + if (l.mode == "none") { + await this.w.u8(0) + } else if (l.mode == "absolute") { + await this.w.u8(1) + await this.w.u53(l.value ?? 0) + } else if (l.mode == "latest") { + await this.w.u8(2) + await this.w.u53(l.value ?? 0) + } else if (l.mode == "future") { + await this.w.u8(3) + await this.w.u53(l.value ?? 0) + } + } + + private async parameters(p: Parameters | undefined) { + if (!p) { + await this.w.u8(0) + return + } + + await this.w.u53(p.size) + for (const [id, value] of p) { + await this.w.u62(id) + await this.w.u53(value.length) + await this.w.write(value) + } + } + + async subscribe_ok(s: SubscribeOk) { + await this.w.u53(Id.SubscribeOk) + await this.w.u62(s.id) + await this.w.u62(s.expires ?? 0n) + } + + async subscribe_reset(s: SubscribeReset) { + await this.w.u53(Id.SubscribeReset) + await this.w.u62(s.id) + await this.w.u62(s.code) + await this.w.string(s.reason) + await this.w.u53(s.final_group) + await this.w.u53(s.final_object) + } + + async subscribe_fin(s: SubscribeFin) { + await this.w.u53(Id.SubscribeFin) + await this.w.u62(s.id) + await this.w.u53(s.final_group) + await this.w.u53(s.final_object) + } + + async subscribe_error(s: SubscribeError) { + await this.w.u53(Id.SubscribeError) + await this.w.u62(s.id) + } + + async unsubscribe(s: Unsubscribe) { + await this.w.u53(Id.Unsubscribe) + await this.w.u62(s.id) + } + + async announce(a: Announce) { + await this.w.u53(Id.Announce) + await this.w.string(a.namespace) + } + + async announce_ok(a: AnnounceOk) { + await this.w.u53(Id.AnnounceOk) + await this.w.string(a.namespace) + } + + async announce_error(a: AnnounceError) { + await this.w.u53(Id.AnnounceError) + await this.w.string(a.namespace) + await this.w.u62(a.code) + await this.w.string(a.reason) + } + + async unannounce(a: Unannounce) { + await this.w.u53(Id.Unannounce) + await this.w.string(a.namespace) + } +} diff --git a/repos/demo/src/lib/moq/transport/index.ts b/repos/demo/src/lib/moq/transport/index.ts new file mode 100644 index 0000000..ca86a6c --- /dev/null +++ b/repos/demo/src/lib/moq/transport/index.ts @@ -0,0 +1,7 @@ +export { Client } from "./client" +export type { ClientConfig } from "./client" + +export { Connection } from "./connection" + +export { SubscribeRecv, AnnounceSend } from "./publisher" +export { AnnounceRecv, SubscribeSend } from "./subscriber" diff --git a/repos/demo/src/lib/moq/transport/object.ts b/repos/demo/src/lib/moq/transport/object.ts new file mode 100644 index 0000000..809ba22 --- /dev/null +++ b/repos/demo/src/lib/moq/transport/object.ts @@ -0,0 +1,89 @@ +import { Reader, Writer } from "./stream" +export { Reader, Writer } + +// This is OBJECT but we can't use that name because it's a reserved word. + +export interface Header { + track: bigint + group: number // The group sequence, as a number because 2^53 is enough. + object: number // The object sequence within a group, as a number because 2^53 is enough. + priority: number // VarInt with a u32 maximum value + ntp_timestamp?: number // optional: NTP timestamp + expires?: number // optional: expiration in seconds + size?: number // optional: size of payload, otherwise it continues until end of stream + timings?: { + recv: number // when the object was received + finished: number // when the object was finished + } +} + +export class Objects { + private quic: WebTransport + + constructor(quic: WebTransport) { + this.quic = quic + } + + async send(header: Header): Promise> { + //console.debug("sending object: ", header) + const stream = await this.quic.createUnidirectionalStream() + await this.#encode(stream, header) + return stream + } + + async recv(): Promise<{ stream: ReadableStream; header: Header } | undefined> { + const streams = this.quic.incomingUnidirectionalStreams.getReader() + + const { value, done } = await streams.read() + streams.releaseLock() + + if (done) return + const stream = value + + const header = await this.#decode(stream) + + return { header, stream } + } + + async #decode(s: ReadableStream) { + const r = new Reader(s) + + const type = await r.u8() + if (type !== 0 && type !== 2) { + throw new Error(`invalid OBJECT type, got ${type}`) + } + + const has_size = type === 2 + + const object = { + track: await r.u62(), + group: await r.u53(), + object: await r.u53(), + priority: await r.u53(), + ntp_timestamp: (await r.u53()) || undefined, + expires: (await r.u53()) || undefined, + size: has_size ? await r.u53() : undefined, + timings: { + recv: performance.now() + performance.timeOrigin, + finished: 0 + } + } + + console.log("Objects | #decode | object", object) + + return object + } + + async #encode(s: WritableStream, h: Header) { + console.log("Objects | #encode", h) + const w = new Writer(s) + await w.u8(h.size ? 2 : 0) + await w.u62(h.track) + await w.u53(h.group) + await w.u53(h.object) + await w.u53(h.priority) + await w.u53(h.ntp_timestamp ?? 0) // TODO: use ntp timestamp + await w.u53(h.expires ?? 0) + if (h.size) await w.u53(h.size) + } +} diff --git a/repos/demo/src/lib/moq/transport/publisher.ts b/repos/demo/src/lib/moq/transport/publisher.ts new file mode 100644 index 0000000..d6b59d7 --- /dev/null +++ b/repos/demo/src/lib/moq/transport/publisher.ts @@ -0,0 +1,201 @@ +import * as Control from "./control" +import { Queue, Watch } from "../common/async" +import { Objects } from "./object" + +export class Publisher { + // Used to send control messages + #control: Control.Stream + + // Use to send objects. + #objects: Objects + + // Our announced tracks. + #announce = new Map() + + // Their subscribed tracks. + #subscribe = new Map() + #subscribeQueue = new Queue(Number.MAX_SAFE_INTEGER) // Unbounded queue in case there's no receiver + + constructor(control: Control.Stream, objects: Objects) { + this.#control = control + this.#objects = objects + } + + // Announce a track namespace. + async announce(namespace: string): Promise { + if (this.#announce.has(namespace)) { + throw new Error(`already announce: ${namespace}`) + } + + const announce = new AnnounceSend(this.#control, namespace) + this.#announce.set(namespace, announce) + + await this.#control.send({ + kind: Control.Msg.Announce, + namespace + }) + + return announce + } + + // Receive the next new subscription + async subscribed() { + return await this.#subscribeQueue.next() + } + + async recv(msg: Control.Subscriber) { + if (msg.kind == Control.Msg.Subscribe) { + await this.recvSubscribe(msg) + } else if (msg.kind == Control.Msg.Unsubscribe) { + this.recvUnsubscribe(msg) + } else if (msg.kind == Control.Msg.AnnounceOk) { + this.recvAnnounceOk(msg) + } else if (msg.kind == Control.Msg.AnnounceError) { + this.recvAnnounceError(msg) + } else { + throw new Error(`unknown control message`) // impossible + } + } + + recvAnnounceOk(msg: Control.AnnounceOk) { + const announce = this.#announce.get(msg.namespace) + if (!announce) { + throw new Error(`announce OK for unknown announce: ${msg.namespace}`) + } + + announce.onOk() + } + + recvAnnounceError(msg: Control.AnnounceError) { + const announce = this.#announce.get(msg.namespace) + if (!announce) { + // TODO debug this + console.warn(`announce error for unknown announce: ${msg.namespace}`) + return + } + + announce.onError(msg.code, msg.reason) + } + + async recvSubscribe(msg: Control.Subscribe) { + if (this.#subscribe.has(msg.id)) { + throw new Error(`duplicate subscribe for id: ${msg.id}`) + } + + const subscribe = new SubscribeRecv(this.#control, this.#objects, msg.id, msg.namespace, msg.name) + this.#subscribe.set(msg.id, subscribe) + await this.#subscribeQueue.push(subscribe) + + await this.#control.send({ kind: Control.Msg.SubscribeOk, id: msg.id }) + } + + recvUnsubscribe(_msg: Control.Unsubscribe) { + throw new Error("TODO unsubscribe") + } +} + +export class AnnounceSend { + #control: Control.Stream + + readonly namespace: string + + // The current state, updated by control messages. + #state = new Watch<"init" | "ack" | Error>("init") + + constructor(control: Control.Stream, namespace: string) { + this.#control = control + this.namespace = namespace + } + + async ok() { + for (;;) { + const [state, next] = this.#state.value() + if (state === "ack") return + if (state instanceof Error) throw state + if (!next) throw new Error("closed") + + await next + } + } + + async active() { + for (;;) { + const [state, next] = this.#state.value() + if (state instanceof Error) throw state + if (!next) return + + await next + } + } + + async close() { + // TODO implement unsubscribe + // await this.#inner.sendUnsubscribe() + } + + closed() { + const [state, next] = this.#state.value() + return state instanceof Error || next == undefined + } + + onOk() { + if (this.closed()) return + this.#state.update("ack") + } + + onError(code: bigint, reason: string) { + if (this.closed()) return + + const err = new Error(`ANNOUNCE_ERROR (${code})` + reason ? `: ${reason}` : "") + this.#state.update(err) + } +} + +export class SubscribeRecv { + #control: Control.Stream + #objects: Objects + #id: bigint + + readonly namespace: string + readonly track: string + + // The current state of the subscription. + #state: "init" | "ack" | "closed" = "init" + + constructor(control: Control.Stream, objects: Objects, id: bigint, namespace: string, track: string) { + this.#control = control // so we can send messages + this.#objects = objects // so we can send objects + this.#id = id + this.namespace = namespace + this.track = track + } + + // Acknowledge the subscription as valid. + async ack() { + if (this.#state !== "init") return + this.#state = "ack" + + // Send the control message. + return this.#control.send({ kind: Control.Msg.SubscribeOk, id: this.#id }) + } + + // Close the subscription with an error. + async close(code = 0n, reason = "") { + if (this.#state === "closed") return + this.#state = "closed" + + return this.#control.send({ + kind: Control.Msg.SubscribeReset, + id: this.#id, + code, + reason, + final_group: 0, // TODO + final_object: 0 // TODO + }) + } + + // Create a writable data stream + async data(header: { group: number; object: number; priority: number; expires?: number; ntp_timestamp?: number }) { + return this.#objects.send({ track: this.#id, ...header }) + } +} diff --git a/repos/demo/src/lib/moq/transport/setup.ts b/repos/demo/src/lib/moq/transport/setup.ts new file mode 100644 index 0000000..f009484 --- /dev/null +++ b/repos/demo/src/lib/moq/transport/setup.ts @@ -0,0 +1,161 @@ +import { Reader, Writer } from "./stream" + +export type Message = Client | Server +export type Role = "publisher" | "subscriber" | "both" + +export enum Version { + DRAFT_00 = 0xff00, + KIXEL_00 = 0xbad00, + KIXEL_01 = 0xbad01 +} + +// NOTE: These are forked from moq-transport-00. +// 1. messages lack a sized length +// 2. parameters are not optional and written in order (role + path) +// 3. role indicates local support only, not remote support + +export interface Client { + versions: Version[] + role: Role + params?: Parameters +} + +export interface Server { + version: Version + params?: Parameters +} + +export class Stream { + recv: Decoder + send: Encoder + + constructor(r: Reader, w: Writer) { + this.recv = new Decoder(r) + this.send = new Encoder(w) + } +} + +export type Parameters = Map + +export class Decoder { + r: Reader + + constructor(r: Reader) { + this.r = r + } + + async client(): Promise { + const type = await this.r.u53() + if (type !== 0x40) throw new Error(`client SETUP type must be 0x40, got ${type}`) + + const count = await this.r.u53() + + const versions = [] + for (let i = 0; i < count; i++) { + const version = await this.r.u53() + versions.push(version) + } + + const params = await this.parameters() + const role = this.role(params?.get(0n)) + + return { + versions, + role, + params + } + } + + async server(): Promise { + const type = await this.r.u53() + if (type !== 0x41) throw new Error(`server SETUP type must be 0x41, got ${type}`) + + const version = await this.r.u53() + const params = await this.parameters() + + return { + version, + params + } + } + + private async parameters(): Promise { + const count = await this.r.u53() + if (count == 0) return undefined + + const params = new Map() + + for (let i = 0; i < count; i++) { + const id = await this.r.u62() + const size = await this.r.u53() + const value = await this.r.readExact(size) + + if (params.has(id)) { + throw new Error(`duplicate parameter id: ${id}`) + } + + params.set(id, value) + } + + console.log("parameters", params) + + return params + } + + role(raw: Uint8Array | undefined): Role { + if (!raw) throw new Error("missing role parameter") + if (raw.length != 1) throw new Error("multi-byte varint not supported") + + switch (raw[0]) { + case 1: + return "publisher" + case 2: + return "subscriber" + case 3: + return "both" + default: + throw new Error(`invalid role: ${raw[0]}`) + } + } +} + +export class Encoder { + w: Writer + + constructor(w: Writer) { + this.w = w + } + + async client(c: Client) { + await this.w.u53(0x40) + await this.w.u53(c.versions.length) + for (const v of c.versions) { + await this.w.u53(v) + } + + // I hate it + const params = c.params ?? new Map() + params.set(0n, new Uint8Array([c.role == "publisher" ? 1 : c.role == "subscriber" ? 2 : 3])) + await this.parameters(params) + } + + async server(s: Server) { + await this.w.u53(0x41) + await this.w.u53(s.version) + await this.parameters(s.params) + } + + private async parameters(p: Parameters | undefined) { + if (!p) { + await this.w.u8(0) + return + } + + await this.w.u53(p.size) + for (const [id, value] of p) { + await this.w.u62(id) + await this.w.u53(value.length) + await this.w.write(value) + } + } +} diff --git a/repos/demo/src/lib/moq/transport/stream.ts b/repos/demo/src/lib/moq/transport/stream.ts new file mode 100644 index 0000000..bc7982b --- /dev/null +++ b/repos/demo/src/lib/moq/transport/stream.ts @@ -0,0 +1,239 @@ +const MAX_U6 = Math.pow(2, 6) - 1 +const MAX_U14 = Math.pow(2, 14) - 1 +const MAX_U30 = Math.pow(2, 30) - 1 +const MAX_U31 = Math.pow(2, 31) - 1 +const MAX_U53 = Number.MAX_SAFE_INTEGER +const MAX_U62: bigint = 2n ** 62n - 1n + +// Reader wraps a stream and provides convience methods for reading pieces from a stream +export class Reader { + #reader: ReadableStream + #scratch: Uint8Array + + constructor(reader: ReadableStream) { + this.#reader = reader + this.#scratch = new Uint8Array(8) + } + + async readAll(): Promise { + const reader = this.#reader.getReader() + let buf = new Uint8Array(0) + + for (;;) { + const { value, done } = await reader.read() + if (done) break + + if (buf.byteLength > 0) { + const append = new Uint8Array(buf.byteLength + value.byteLength) + append.set(buf) + append.set(value, buf.byteLength) + buf = append + } else { + buf = value + } + } + + reader.releaseLock() + + return buf + } + + async readExact(size: number): Promise { + const dst = new Uint8Array(size) + return this.read(dst, 0, size) + } + + async read(dst: Uint8Array, offset: number, size: number): Promise { + const reader = this.#reader.getReader({ mode: "byob" }) + + while (offset < size) { + const empty = new Uint8Array(dst.buffer, dst.byteOffset + offset, size - offset) + const { value, done } = await reader.read(empty) + if (done) { + throw new Error(`short buffer`) + } + + dst = new Uint8Array(value.buffer, value.byteOffset - offset) + offset += value.byteLength + } + + reader.releaseLock() + + return dst + } + + async string(maxLength?: number): Promise { + const length = await this.u53() + if (maxLength !== undefined && length > maxLength) { + throw new Error(`string length ${length} exceeds max length ${maxLength}`) + } + + const buffer = await this.readExact(length) + return new TextDecoder().decode(buffer) + } + + async u8(): Promise { + this.#scratch = await this.read(this.#scratch, 0, 1) + return this.#scratch[0] + } + + // Returns a Number using 53-bits, the max Javascript can use for integer math + async u53(): Promise { + const v = await this.u62() + if (v > MAX_U53) { + throw new Error("value larger than 53-bits; use v62 instead") + } + + return Number(v) + } + + // NOTE: Returns a bigint instead of a number since it may be larger than 53-bits + async u62(): Promise { + this.#scratch = await this.read(this.#scratch, 0, 1) + const first = this.#scratch[0] + + const size = (first & 0xc0) >> 6 + + if (size == 0) { + return BigInt(first) & 0x3fn + } else if (size == 1) { + this.#scratch = await this.read(this.#scratch, 1, 2) + const view = new DataView(this.#scratch.buffer, 0, 2) + + return BigInt(view.getInt16(0)) & 0x3fffn + } else if (size == 2) { + this.#scratch = await this.read(this.#scratch, 1, 4) + const view = new DataView(this.#scratch.buffer, 0, 4) + + return BigInt(view.getUint32(0)) & 0x3fffffffn + } else if (size == 3) { + this.#scratch = await this.read(this.#scratch, 1, 8) + const view = new DataView(this.#scratch.buffer, 0, 8) + + return view.getBigUint64(0) & 0x3fffffffffffffffn + } else { + throw new Error("impossible") + } + } +} + +// Writer wraps a stream and writes chunks of data +export class Writer { + #writer: WritableStream + #scratch: Uint8Array + + constructor(writer: WritableStream) { + this.#scratch = new Uint8Array(8) + this.#writer = writer + } + + async u8(v: number) { + await this.write(setUint8(this.#scratch, v)) + } + + async i32(v: number) { + if (Math.abs(v) > MAX_U31) { + throw new Error(`overflow, value larger than 32-bits: ${v}`) + } + + // We don't use a VarInt, so it always takes 4 bytes. + // This could be improved but nothing is standardized yet. + await this.write(setInt32(this.#scratch, v)) + } + + async u53(v: number) { + if (v < 0) { + throw new Error(`underflow, value is negative: ${v}`) + } else if (v > MAX_U53) { + throw new Error(`overflow, value larger than 53-bits: ${v}`) + } + + await this.write(setVint53(this.#scratch, v)) + } + + async u62(v: bigint) { + if (v < 0) { + throw new Error(`underflow, value is negative: ${v}`) + } else if (v >= MAX_U62) { + throw new Error(`overflow, value larger than 62-bits: ${v}`) + } + + await this.write(setVint62(this.#scratch, v)) + } + + async write(v: Uint8Array) { + const writer = this.#writer.getWriter() + try { + await writer.write(v) + } finally { + writer.releaseLock() + } + } + + async string(str: string) { + const data = new TextEncoder().encode(str) + await this.u53(data.byteLength) + await this.write(data) + } +} + +function setUint8(dst: Uint8Array, v: number): Uint8Array { + dst[0] = v + return dst.slice(0, 1) +} + +function setUint16(dst: Uint8Array, v: number): Uint8Array { + const view = new DataView(dst.buffer, dst.byteOffset, 2) + view.setUint16(0, v) + + return new Uint8Array(view.buffer, view.byteOffset, view.byteLength) +} + +function setInt32(dst: Uint8Array, v: number): Uint8Array { + const view = new DataView(dst.buffer, dst.byteOffset, 4) + view.setInt32(0, v) + + return new Uint8Array(view.buffer, view.byteOffset, view.byteLength) +} + +function setUint32(dst: Uint8Array, v: number): Uint8Array { + const view = new DataView(dst.buffer, dst.byteOffset, 4) + view.setUint32(0, v) + + return new Uint8Array(view.buffer, view.byteOffset, view.byteLength) +} + +function setVint53(dst: Uint8Array, v: number): Uint8Array { + if (v <= MAX_U6) { + return setUint8(dst, v) + } else if (v <= MAX_U14) { + return setUint16(dst, v | 0x4000) + } else if (v <= MAX_U30) { + return setUint32(dst, v | 0x80000000) + } else if (v <= MAX_U53) { + return setUint64(dst, BigInt(v) | 0xc000000000000000n) + } else { + throw new Error(`overflow, value larger than 53-bits: ${v}`) + } +} + +function setVint62(dst: Uint8Array, v: bigint): Uint8Array { + if (v < MAX_U6) { + return setUint8(dst, Number(v)) + } else if (v < MAX_U14) { + return setUint16(dst, Number(v) | 0x4000) + } else if (v <= MAX_U30) { + return setUint32(dst, Number(v) | 0x80000000) + } else if (v <= MAX_U62) { + return setUint64(dst, BigInt(v) | 0xc000000000000000n) + } else { + throw new Error(`overflow, value larger than 62-bits: ${v}`) + } +} + +function setUint64(dst: Uint8Array, v: bigint): Uint8Array { + const view = new DataView(dst.buffer, dst.byteOffset, 8) + view.setBigUint64(0, v) + + return new Uint8Array(view.buffer, view.byteOffset, view.byteLength) +} diff --git a/repos/demo/src/lib/moq/transport/subscriber.ts b/repos/demo/src/lib/moq/transport/subscriber.ts new file mode 100644 index 0000000..c8d3d08 --- /dev/null +++ b/repos/demo/src/lib/moq/transport/subscriber.ts @@ -0,0 +1,230 @@ +import * as Control from "./control" +import { Queue, Watch } from "../common/async" +import { Objects } from "./object" +import type { Header } from "./object" + +export class Subscriber { + // Use to send control messages. + #control: Control.Stream + + // Use to send objects. + #objects: Objects + + // Announced broadcasts. + #announce = new Map() + #announceQueue = new Watch([]) + + // Our subscribed tracks. + #subscribe = new Map() + #subscribeNext = 0n + + constructor(control: Control.Stream, objects: Objects) { + this.#control = control + this.#objects = objects + } + + announced(): Watch { + return this.#announceQueue + } + + async recv(msg: Control.Publisher) { + console.log("subscriber | recv", msg) + if (msg.kind == Control.Msg.Announce) { + await this.recvAnnounce(msg) + } else if (msg.kind == Control.Msg.Unannounce) { + this.recvUnannounce(msg) + } else if (msg.kind == Control.Msg.SubscribeOk) { + this.recvSubscribeOk(msg) + } else if (msg.kind == Control.Msg.SubscribeReset) { + await this.recvSubscribeReset(msg) + } else if (msg.kind == Control.Msg.SubscribeError) { + await this.recvSubscribeError(msg) + } else if (msg.kind == Control.Msg.SubscribeFin) { + await this.recvSubscribeFin(msg) + } else { + throw new Error(`unknown control message`) // impossible + } + } + + async recvAnnounce(msg: Control.Announce) { + if (this.#announce.has(msg.namespace)) { + throw new Error(`duplicate announce for namespace: ${msg.namespace}`) + } + + await this.#control.send({ kind: Control.Msg.AnnounceOk, namespace: msg.namespace }) + + const announce = new AnnounceRecv(this.#control, msg.namespace) + this.#announce.set(msg.namespace, announce) + + this.#announceQueue.update((queue) => [...queue, announce]) + } + + recvUnannounce(_msg: Control.Unannounce) { + throw new Error(`TODO Unannounce`) + } + + async subscribe(namespace: string, track: string, switchTrackId?: bigint) { + const id = this.#subscribeNext++ + + const subscribe = new SubscribeSend(this.#control, id, namespace, track) + this.#subscribe.set(id, subscribe) + subscribe.status = "SUBSCRIBING" + await this.#control.send({ + kind: Control.Msg.Subscribe, + id, + namespace, + name: track, + start_group: { mode: "latest", value: 0 }, + start_object: { mode: "absolute", value: 0 }, + end_group: { mode: "none" }, + end_object: { mode: "none" }, + switch_track_id: switchTrackId ?? BigInt(0) + }) + + return subscribe + } + + recvSubscribeOk(msg: Control.SubscribeOk) { + const subscribe = this.#subscribe.get(msg.id) + if (!subscribe) { + throw new Error(`subscribe ok for unknown id: ${msg.id}`) + } + + subscribe.onOk() + } + + async recvSubscribeReset(msg: Control.SubscribeReset) { + console.log("subscriber | recvSubscribeReset", msg) + const subscribe = this.#subscribe.get(msg.id) + if (!subscribe) { + throw new Error(`subscribe error for unknown id: ${msg.id}`) + } + subscribe.onReset() + // TODO: why is this an error? ZG + // await subscribe.onError(msg.code, msg.reason) + await new Promise(() => true) + } + + async recvSubscribeError(msg: Control.SubscribeError) { + const subscribe = this.#subscribe.get(msg.id) + if (!subscribe) { + throw new Error(`subscribe error for unknown id: ${msg.id}`) + } + + await subscribe.onError(msg.code, msg.reason) + } + + async recvSubscribeFin(msg: Control.SubscribeFin) { + const subscribe = this.#subscribe.get(msg.id) + if (!subscribe) { + throw new Error(`subscribe error for unknown id: ${msg.id}`) + } + + await subscribe.onError(0n, "fin") + } + + async recvObject(header: Header, stream: ReadableStream) { + console.log("subscriber | recvObject", header) + const subscribe = this.#subscribe.get(header.track) + if (!subscribe) { + throw new Error(`data for for unknown track: ${header.track}`) + } else { + await subscribe.onData(header, stream) + } + } +} + +export class AnnounceRecv { + #control: Control.Stream + + readonly namespace: string + + // The current state of the announce + #state: "init" | "ack" | "closed" = "init" + + constructor(control: Control.Stream, namespace: string) { + this.#control = control // so we can send messages + this.namespace = namespace + } + + // Acknowledge the subscription as valid. + async ok() { + if (this.#state !== "init") return + this.#state = "ack" + + // Send the control message. + return this.#control.send({ kind: Control.Msg.AnnounceOk, namespace: this.namespace }) + } + + async close(code = 0n, reason = "") { + if (this.#state === "closed") return + this.#state = "closed" + + return this.#control.send({ kind: Control.Msg.AnnounceError, namespace: this.namespace, code, reason }) + } +} + +export class SubscribeSend { + #control: Control.Stream + id: bigint + + readonly namespace: string + readonly track: string + + // A queue of received streams for this subscription. + #data = new Queue<{ header: Header; stream: ReadableStream }>() + + status: "NONE" | "SUBSCRIBING" | "SUBSCRIBED" | "UNSUBSCRIBING" | "UNSUBSCRIBED" = "NONE" + + constructor(control: Control.Stream, id: bigint, namespace: string, track: string) { + this.#control = control // so we can send messages + this.id = id + this.namespace = namespace + this.track = track + } + + async close(_code = 0n, _reason = "") { + // TODO implement unsubscribe + // await this.#inner.sendReset(code, reason) + if (this.status === "UNSUBSCRIBED" || this.status === "UNSUBSCRIBING") return + this.status = "UNSUBSCRIBING" + await this.#control.send({ + kind: Control.Msg.Unsubscribe, + id: this.id + }) + } + + onOk() { + // noop + this.status = "SUBSCRIBED" + } + + onReset() { + this.status = "UNSUBSCRIBED" + } + + async onError(code: bigint, reason: string) { + if (code == 0n) { + return await this.#data.close() + } + + if (reason !== "") { + reason = `: ${reason}` + } + + const err = new Error(`SUBSCRIBE_ERROR (${code})${reason}`) + this.status = "UNSUBSCRIBED" + return await this.#data.abort(err) + } + + async onData(header: Header, stream: ReadableStream) { + if (!this.#data.closed()) { + await this.#data.push({ header, stream }) + } + } + + // Receive the next a readable data stream + async data() { + return await this.#data.next() + } +} diff --git a/repos/demo/src/lib/moq/transport/tsconfig.json b/repos/demo/src/lib/moq/transport/tsconfig.json new file mode 100644 index 0000000..fb96e00 --- /dev/null +++ b/repos/demo/src/lib/moq/transport/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "include": ["."], + "references": [ + { + "path": "../common" + } + ] +} diff --git a/repos/demo/src/lib/moq/tsconfig.json b/repos/demo/src/lib/moq/tsconfig.json new file mode 100644 index 0000000..c536578 --- /dev/null +++ b/repos/demo/src/lib/moq/tsconfig.json @@ -0,0 +1,42 @@ +{ + "files": [], // don't build anything with these settings. + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "node", + "rootDir": ".", + "outDir": "./dist", + "declaration": true, + "strict": true, + "composite": true, + "declarationMap": true, + "sourceMap": true, + "isolatedModules": true, + "types": [], // Don't automatically import any @types modules. + "lib": ["es2022", "dom"], + "typeRoots": ["./types", "../node_modules/@types"] + }, + "references": [ + { + "path": "./common" + }, + { + "path": "./playback" + }, + { + "path": "./playback/webcodecs/worklet" + }, + { + "path": "./contribute" + }, + { + "path": "./transport" + }, + { + "path": "./media" + } + ], + "paths": { + "@/*": ["*"] + } +} diff --git a/repos/demo/src/lib/moq/types/mp4box.d.ts b/repos/demo/src/lib/moq/types/mp4box.d.ts new file mode 100644 index 0000000..e2958d2 --- /dev/null +++ b/repos/demo/src/lib/moq/types/mp4box.d.ts @@ -0,0 +1,1843 @@ +// https://github.com/gpac/mp4box.js/issues/233 + +declare module "mp4box" { + export interface MP4MediaTrack { + id: number + created: Date + modified: Date + movie_duration: number + layer: number + alternate_group: number + volume: number + track_width: number + track_height: number + timescale: number + duration: number + bitrate: number + codec: string + language: string + nb_samples: number + } + + export interface MP4VideoData { + width: number + height: number + } + + export interface MP4VideoTrack extends MP4MediaTrack { + video: MP4VideoData + } + + export interface MP4AudioData { + sample_rate: number + channel_count: number + sample_size: number + } + + export interface MP4AudioTrack extends MP4MediaTrack { + audio: MP4AudioData + } + + export type MP4Track = MP4VideoTrack | MP4AudioTrack + + export interface MP4Info { + duration: number + timescale: number + fragment_duration: number + isFragmented: boolean + isProgressive: boolean + hasIOD: boolean + brands: string[] + created: Date + modified: Date + tracks: MP4Track[] + mime: string + audioTracks: MP4AudioTrack[] + videoTracks: MP4VideoTrack[] + } + + export type MP4ArrayBuffer = ArrayBuffer & { fileStart: number } + + export function createFile(): ISOFile + + export interface Sample { + number: number + track_id: number + timescale: number + description_index: number + description: { + avcC?: BoxParser.avcCBox // h.264 + hvcC?: BoxParser.hvcCBox // hevc + vpcC?: BoxParser.vpcCBox // vp9 + av1C?: BoxParser.av1CBox // av1 + } + data: Uint8Array + size: number + alreadyRead?: number + duration: number + cts: number + dts: number + is_sync: boolean + is_leading?: number + depends_on?: number + is_depended_on?: number + has_redundancy?: number + degradation_priority?: number + offset?: number + subsamples?: any + } + + export interface ExtractionOptions { + nbSamples: number + } + + export class DataStream { + // WARNING, the default is little endian, which is not what MP4 uses. + constructor(buffer?: ArrayBuffer, byteOffset?: number, endianness?: boolean) + getPosition(): number + + get byteLength(): number + get buffer(): ArrayBuffer + set buffer(v: ArrayBuffer) + get byteOffset(): number + set byteOffset(v: number) + get dataView(): DataView + set dataView(v: DataView) + + seek(pos: number): void + isEof(): boolean + + mapFloat32Array(length: number, e?: boolean): any + mapFloat64Array(length: number, e?: boolean): any + mapInt16Array(length: number, e?: boolean): any + mapInt32Array(length: number, e?: boolean): any + mapInt8Array(length: number): any + mapUint16Array(length: number, e?: boolean): any + mapUint32Array(length: number, e?: boolean): any + mapUint8Array(length: number): any + + readInt32Array(length: number, endianness?: boolean): Int32Array + readInt16Array(length: number, endianness?: boolean): Int16Array + readInt8Array(length: number): Int8Array + readUint32Array(length: number, endianness?: boolean): Uint32Array + readUint16Array(length: number, endianness?: boolean): Uint16Array + readUint8Array(length: number): Uint8Array + readFloat64Array(length: number, endianness?: boolean): Float64Array + readFloat32Array(length: number, endianness?: boolean): Float32Array + + readInt32(endianness?: boolean): number + readInt16(endianness?: boolean): number + readInt8(): number + readUint32(endianness?: boolean): number + //readUint32Array(length: any, e: any): any + readUint24(): number + readUint16(endianness?: boolean): number + readUint8(): number + //readUint64(): any + readFloat32(endianness?: boolean): number + readFloat64(endianness?: boolean): number + //readCString(length: number): any + //readString(length: number, encoding: any): any + + static endianness: boolean + + memcpy(dst: ArrayBufferLike, dstOffset: number, src: ArrayBufferLike, srcOffset: number, byteLength: number): void + + // TODO I got bored porting all functions + + save(filename: string): void + shift(offset: number): void + + writeInt32Array(arr: Int32Array, endianness?: boolean): void + writeInt16Array(arr: Int16Array, endianness?: boolean): void + writeInt8Array(arr: Int8Array): void + writeUint32Array(arr: Uint32Array, endianness?: boolean): void + writeUint16Array(arr: Uint16Array, endianness?: boolean): void + writeUint8Array(arr: Uint8Array): void + writeFloat64Array(arr: Float64Array, endianness?: boolean): void + writeFloat32Array(arr: Float32Array, endianness?: boolean): void + writeInt32(v: number, endianness?: boolean): void + writeInt16(v: number, endianness?: boolean): void + writeInt8(v: number): void + writeUint32(v: number, endianness?: boolean): void + writeUint16(v: number, endianness?: boolean): void + writeUint8(v: number): void + writeFloat32(v: number, endianness?: boolean): void + writeFloat64(v: number, endianness?: boolean): void + writeUCS2String(s: string, endianness?: boolean, length?: number): void + writeString(s: string, encoding?: string, length?: number): void + writeCString(s: string, length?: number): void + writeUint64(v: number): void + writeUint24(v: number): void + adjustUint32(pos: number, v: number): void + + static LITTLE_ENDIAN: boolean + static BIG_ENDIAN: boolean + + // TODO add correct types; these are exported by dts-gen + readCString(length: any): any + readInt64(): any + readString(length: any, encoding: any): any + readUint64(): any + writeStruct(structDefinition: any, struct: any): void + writeType(t: any, v: any, struct: any): any + + static arrayToNative(array: any, arrayIsLittleEndian: any): any + static flipArrayEndianness(array: any): any + static memcpy(dst: any, dstOffset: any, src: any, srcOffset: any, byteLength: any): void + static nativeToEndian(array: any, littleEndian: any): any + } + + export interface TrackOptions { + id?: number + type?: string + width?: number + height?: number + duration?: number + layer?: number + timescale?: number + media_duration?: number + language?: string + hdlr?: string + + // video + avcDecoderConfigRecord?: any + hevcDecoderConfigRecord?: any + + // audio + balance?: number + channel_count?: number + samplesize?: number + samplerate?: number + + //captions + namespace?: string + schema_location?: string + auxiliary_mime_types?: string + + description?: BoxParser.Box + description_boxes?: BoxParser.Box[] + + default_sample_description_index_id?: number + default_sample_duration?: number + default_sample_size?: number + default_sample_flags?: number + } + + export interface FileOptions { + brands?: string[] + timescale?: number + rate?: number + duration?: number + width?: number + } + + export interface SampleOptions { + sample_description_index?: number + duration?: number + cts?: number + dts?: number + is_sync?: boolean + is_leading?: number + depends_on?: number + is_depended_on?: number + has_redundancy?: number + degradation_priority?: number + subsamples?: any + } + + // TODO add the remaining functions + // TODO move to another module + export class ISOFile { + constructor(stream?: DataStream) + + init(options?: FileOptions): ISOFile + addTrack(options?: TrackOptions): number + addSample(track: number, data: Uint8Array, options?: SampleOptions): Sample + + createSingleSampleMoof(sample: Sample): BoxParser.moofBox + + // helpers + getTrackById(id: number): BoxParser.trakBox | undefined + getTrexById(id: number): BoxParser.trexBox | undefined + + // boxes that are added to the root + boxes: BoxParser.Box[] + mdats: BoxParser.mdatBox[] + moofs: BoxParser.moofBox[] + + ftyp?: BoxParser.ftypBox + moov?: BoxParser.moovBox + + static writeInitializationSegment(ftyp: BoxParser.ftypBox, moov: BoxParser.moovBox, total_duration: number, sample_duration: number): ArrayBuffer + + // TODO add correct types; these are exported by dts-gen + add(name: any): any + addBox(box: any): any + appendBuffer(ab: any, last: any): any + buildSampleLists(): void + buildTrakSampleLists(trak: any): void + checkBuffer(ab: any): any + createFragment(track_id: any, sampleNumber: any, stream_: any): any + equal(b: any): any + flattenItemInfo(): void + flush(): void + getAllocatedSampleDataSize(): any + getBox(type: any): any + getBoxes(type: any, returnEarly: any): any + getBuffer(): any + getCodecs(): any + getInfo(): any + getItem(item_id: any): any + getMetaHandler(): any + getPrimaryItem(): any + getSample(trak: any, sampleNum: any): any + getTrackSample(track_id: any, number: any): any + getTrackSamplesInfo(track_id: any): any + hasIncompleteMdat(): any + hasItem(name: any): any + initializeSegmentation(): any + itemToFragmentedTrackFile(_options: any): any + parse(): void + print(output: any): void + processIncompleteBox(ret: any): any + processIncompleteMdat(): any + processItems(callback: any): void + processSamples(last: any): void + releaseItem(item_id: any): any + releaseSample(trak: any, sampleNum: any): any + releaseUsedSamples(id: any, sampleNum: any): void + resetTables(): void + restoreParsePosition(): any + save(name: any): void + saveParsePosition(): void + seek(time: any, useRap: any): any + seekTrack(time: any, useRap: any, trak: any): any + setExtractionOptions(id: any, user: any, options: any): void + setSegmentOptions(id: any, user: any, options: any): void + start(): void + stop(): void + unsetExtractionOptions(id: any): void + unsetSegmentOptions(id: any): void + updateSampleLists(): void + updateUsedBytes(box: any, ret: any): void + write(outstream: any): void + + static initSampleGroups(trak: any, traf: any, sbgps: any, trak_sgpds: any, traf_sgpds: any): void + static process_sdtp(sdtp: any, sample: any, number: any): void + static setSampleGroupProperties(trak: any, sample: any, sample_number: any, sample_groups_info: any): void + + // TODO Expand public API; it's difficult to tell what should be public + onMoovStart?: () => void + onReady?: (info: MP4Info) => void + onError?: (e: string) => void + onSamples?: (id: number, user: any, samples: Sample[]) => void + + appendBuffer(data: MP4ArrayBuffer): number + start(): void + stop(): void + flush(): void + + setExtractionOptions(id: number, user: any, options: ExtractionOptions): void + } + + export namespace BoxParser { + export class Box { + size?: number + flags?: number // Do these go here? + type?: string + data?: Uint8Array + + constructor(type?: string, size?: number) + + add(name: string): Box + addBox(box: Box): Box + set(name: string, value: any): void + addEntry(value: string, prop?: string): void + printHeader(output: any): void + write(stream: DataStream): void + writeHeader(stream: DataStream, msg?: string): void + computeSize(): void + + // TODO add types for these + parse(stream: any): void + parseDataAndRewind(stream: any): void + parseLanguage(stream: any): void + print(output: any): void + } + + // TODO finish add types for these classes + export class AudioSampleEntry extends SampleEntry { + constructor(type: any, size?: number) + + getChannelCount(): any + getSampleRate(): any + getSampleSize(): any + isAudio(): any + parse(stream: any): void + write(stream: any): void + } + + export class CoLLBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class ContainerBox extends Box { + constructor(type: any, size?: number, uuid?: any) + + parse(stream: any): void + print(output: any): void + write(stream: any): void + } + + export class FullBox extends Box { + constructor(type: any, size?: number, uuid?: any) + + parse(stream: any): void + parseDataAndRewind(stream: any): void + parseFullHeader(stream: any): void + printHeader(output: any): void + writeHeader(stream: any): void + } + + export class HintSampleEntry extends SampleEntry { + constructor(type: any, size?: number) + } + + export class MetadataSampleEntry extends SampleEntry { + constructor(type: any, size?: number) + + isMetadata(): any + } + + export class OpusSampleEntry extends SampleEntry { + constructor(size?: number) + } + + export class SampleEntry extends Box { + constructor(type: any, size?: number, hdr_size?: number, start?: number) + + getChannelCount(): any + getCodec(): any + getHeight(): any + getSampleRate(): any + getSampleSize(): any + getWidth(): any + isAudio(): any + isHint(): any + isMetadata(): any + isSubtitle(): any + isVideo(): any + parse(stream: any): void + parseDataAndRewind(stream: any): void + parseFooter(stream: any): void + parseHeader(stream: any): void + write(stream: any): void + writeFooter(stream: any): void + writeHeader(stream: any): void + } + + export class SampleGroupEntry { + constructor(type: any) + + parse(stream: any): void + write(stream: any): void + } + + export class SingleItemTypeReferenceBox extends ContainerBox { + constructor(type: any, size?: number, hdr_size?: number, start?: number) + + parse(stream: any): void + } + + export class SingleItemTypeReferenceBoxLarge { + constructor(type: any, size?: number, hdr_size?: number, start?: number) + + parse(stream: any): void + } + + export class SmDmBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class SubtitleSampleEntry extends SampleEntry { + constructor(type: any, size?: number) + + isSubtitle(): any + } + + export class SystemSampleEntry extends SampleEntry { + constructor(type: any, size?: number) + } + + export class TextSampleEntry extends SampleEntry { + constructor(type: any, size?: number) + } + + export class TrackGroupTypeBox extends FullBox { + constructor(type: any, size?: number) + + parse(stream: any): void + } + + export class TrackReferenceTypeBox extends ContainerBox { + constructor(type: any, size?: number, hdr_size?: number, start?: number) + + parse(stream: any): void + + write(stream: any): void + } + + export class VisualSampleEntry extends SampleEntry { + constructor(type: any, size?: number) + + getHeight(): any + getWidth(): any + isVideo(): any + parse(stream: any): void + write(stream: any): void + } + + export class a1lxBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class a1opBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class alstSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class auxCBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class av01SampleEntry extends SampleEntry { + constructor(size?: number) + + getCodec(): any + } + + export class av1CBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class avc1SampleEntry extends SampleEntry { + constructor(size?: number) + + getCodec(): any + } + + export class avc2SampleEntry extends SampleEntry { + constructor(size?: number) + + getCodec(): any + } + + export class avc3SampleEntry extends SampleEntry { + constructor(size?: number) + + getCodec(): any + } + + export class avc4SampleEntry extends SampleEntry { + constructor(size?: number) + + getCodec(): any + } + + export class avcCBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class avllSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class avssSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class btrtBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class bxmlBox extends FullBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class clapBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class clefBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class clliBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class co64Box extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class colrBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class cprtBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class cslgBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class cttsBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + unpack(samples: any): void + write(stream: any): void + } + + export class dOpsBox extends ContainerBox { + constructor(size?: number) + + parse(stream: DataStream): void + + Version: number + OutputChannelCount: number + PreSkip: number + InputSampleRate: number + OutputGain: number + ChannelMappingFamily: number + + // When channelMappingFamily != 0 + StreamCount?: number + CoupledCount?: number + ChannelMapping?: number[] + } + + export class dac3Box extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class dec3Box extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class dfLaBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class dimmBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class dinfBox extends ContainerBox { + constructor(size?: number) + } + + export class dmaxBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class dmedBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class drefBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class drepBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class dtrtSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class edtsBox extends ContainerBox { + constructor(size?: number) + } + + export class elngBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class elstBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class emsgBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class encaSampleEntry extends SampleEntry { + constructor(size?: number) + } + + export class encmSampleEntry extends SampleEntry { + constructor(size?: number) + } + + export class encsSampleEntry extends SampleEntry { + constructor(size?: number) + } + + export class enctSampleEntry extends SampleEntry { + constructor(size?: number) + } + + export class encuSampleEntry extends SampleEntry { + constructor(size?: number) + } + + export class encvSampleEntry extends SampleEntry { + constructor(size?: number) + } + + export class enofBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class esdsBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class fielBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class freeBox extends Box { + constructor(size?: number) + } + + export class frmaBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class ftypBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class hdlrBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class hev1SampleEntry extends SampleEntry { + constructor(size?: number) + + getCodec(): any + } + + export class hinfBox extends ContainerBox { + constructor(size?: number) + } + + export class hmhdBox extends FullBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class hntiBox extends ContainerBox { + constructor(size?: number) + } + + export class hvc1SampleEntry extends SampleEntry { + constructor(size?: number) + + getCodec(): any + } + + export class hvcCBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class idatBox extends Box { + constructor(size?: number) + } + + export class iinfBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class ilocBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class imirBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class infeBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class iodsBox extends FullBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class ipcoBox extends ContainerBox { + constructor(size?: number) + } + + export class ipmaBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class iproBox extends FullBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class iprpBox extends ContainerBox { + constructor(size?: number) + ipmas: ipmaBox[] + } + + export class irefBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class irotBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class ispeBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class kindBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + + write(stream: any): void + } + + export class levaBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class lselBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class maxrBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class mdatBox extends Box { + constructor(size?: number) + } + + export class mdcvBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class mdhdBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + + write(stream: any): void + } + + export class mdiaBox extends ContainerBox { + constructor(size?: number) + } + + export class mecoBox extends Box { + constructor(size?: number) + } + + export class mehdBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + + write(stream: any): void + } + + export class mereBox extends FullBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class metaBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class mettSampleEntry extends SampleEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class metxSampleEntry extends SampleEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class mfhdBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + + write(stream: any): void + } + + export class mfraBox extends ContainerBox { + constructor(size?: number) + tfras: tfraBox[] + } + + export class mfroBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class minfBox extends ContainerBox { + constructor(size?: number) + } + + export class moofBox extends ContainerBox { + constructor(size?: number) + trafs: trafBox[] + } + + export class moovBox extends ContainerBox { + constructor(size?: number) + traks: trakBox[] + psshs: psshBox[] + } + + export class mp4aSampleEntry extends SampleEntry { + constructor(size?: number) + + getCodec(): any + } + + export class msrcTrackGroupTypeBox extends ContainerBox { + constructor(size?: number) + } + + export class mvexBox extends ContainerBox { + constructor(size?: number) + + trexs: trexBox[] + } + + export class mvhdBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + print(output: any): void + write(stream: any): void + } + + export class mvifSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class nmhdBox extends FullBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class npckBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class numpBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class padbBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class paspBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class paylBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class paytBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class pdinBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class pitmBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class pixiBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class pmaxBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class prftBox extends ContainerBox { + version: number + ref_track_id: number + ntp_timestamp: number + media_time: number + + constructor(size?: number) + + parse(stream: any): void + } + + export class profBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class prolSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class psshBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class rashSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class rinfBox extends ContainerBox { + constructor(size?: number) + } + + export class rollSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class saioBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class saizBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class sbgpBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + + write(stream: any): void + } + + export class sbttSampleEntry extends SampleEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class schiBox extends ContainerBox { + constructor(size?: number) + } + + export class schmBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class scifSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class scnmSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class sdtpBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class seigSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class sencBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class sgpdBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class sidxBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class sinfBox extends ContainerBox { + constructor(size?: number) + } + + export class skipBox extends Box { + constructor(size?: number) + } + + export class smhdBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class ssixBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class stblBox extends ContainerBox { + constructor(size?: number) + + sgpds: sgpdBox[] + sbgps: sbgpBox[] + } + + export class stcoBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + unpack(samples: any): void + write(stream: any): void + } + + export class stdpBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class sthdBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class stppSampleEntry extends SampleEntry { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class strdBox extends ContainerBox { + constructor(size?: number) + } + + export class striBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class strkBox extends Box { + constructor(size?: number) + } + + export class stsaSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class stscBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + unpack(samples: any): void + write(stream: any): void + } + + export class stsdBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class stsgBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class stshBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class stssBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class stszBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + unpack(samples: any): void + write(stream: any): void + } + + export class sttsBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + unpack(samples: any): void + write(stream: any): void + } + + export class stviBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class stxtSampleEntry extends SampleEntry { + constructor(size?: number) + + getCodec(): any + parse(stream: any): void + } + + export class stypBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class stz2Box extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class subsBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class syncSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class taptBox extends ContainerBox { + constructor(size?: number) + } + + export class teleSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class tencBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class tfdtBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class tfhdBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class tfraBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class tkhdBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + print(output: any): void + write(stream: any): void + } + + export class tmaxBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class tminBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class totlBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class tpayBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class tpylBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class trafBox extends ContainerBox { + constructor(size?: number) + truns: trunBox[] + sgpd: sgpdBox[] + sbgp: sbgpBox[] + } + + export class trakBox extends ContainerBox { + constructor(size?: number) + } + + export class trefBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class trepBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class trexBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class trgrBox extends ContainerBox { + constructor(size?: number) + } + + export class trpyBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class trunBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + + sample_count: number + + sample_duration?: number[] + sample_size?: number[] + sample_flags?: number[] + sample_composition_time_offset?: number[] + + data_offset?: number + data_offset_position?: number + } + + export class tsasSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class tsclSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class tselBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class tx3gSampleEntry extends SampleEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class txtCBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class udtaBox extends ContainerBox { + constructor(size?: number) + kinds: kindBox[] + } + + export class viprSampleGroupEntry extends SampleGroupEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export class vmhdBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + write(stream: any): void + } + + export class vp08SampleEntry extends SampleEntry { + constructor(size?: number) + + getCodec(): any + } + + export class vp09SampleEntry extends SampleEntry { + constructor(size?: number) + + getCodec(): any + } + + export class vpcCBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class vttCBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class vttcBox extends ContainerBox { + constructor(size?: number) + } + + export class vvc1SampleEntry extends SampleEntry { + constructor(size?: number) + + getCodec(): any + } + + export class vvcCBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class vvcNSampleEntry extends SampleEntry { + constructor(size?: number) + } + + export class vvi1SampleEntry extends SampleEntry { + constructor(size?: number) + + getCodec(): any + } + + export class vvnCBox extends ContainerBox { + constructor(size?: number) + + parse(stream: any): void + } + + export class vvs1SampleEntry extends SampleEntry { + constructor(size?: number) + } + + export class wvttSampleEntry extends SampleEntry { + constructor(size?: number) + + parse(stream: any): void + } + + export const BASIC_BOXES: string[] + export const CONTAINER_BOXES: string[][] + export const DIFF_BOXES_PROP_NAMES: string[] + export const DIFF_PRIMITIVE_ARRAY_PROP_NAMES: string[] + export const ERR_INVALID_DATA: number + export const ERR_NOT_ENOUGH_DATA: number + export const FULL_BOXES: string[] + export const OK: number + export const SAMPLE_ENTRY_TYPE_AUDIO: string + export const SAMPLE_ENTRY_TYPE_HINT: string + export const SAMPLE_ENTRY_TYPE_METADATA: string + export const SAMPLE_ENTRY_TYPE_SUBTITLE: string + export const SAMPLE_ENTRY_TYPE_SYSTEM: string + export const SAMPLE_ENTRY_TYPE_TEXT: string + export const SAMPLE_ENTRY_TYPE_VISUAL: string + export const TFHD_FLAG_BASE_DATA_OFFSET: number + export const TFHD_FLAG_DEFAULT_BASE_IS_MOOF: number + export const TFHD_FLAG_DUR_EMPTY: number + export const TFHD_FLAG_SAMPLE_DESC: number + export const TFHD_FLAG_SAMPLE_DUR: number + export const TFHD_FLAG_SAMPLE_FLAGS: number + export const TFHD_FLAG_SAMPLE_SIZE: number + export const TKHD_FLAG_ENABLED: number + export const TKHD_FLAG_IN_MOVIE: number + export const TKHD_FLAG_IN_PREVIEW: number + export const TRUN_FLAGS_CTS_OFFSET: number + export const TRUN_FLAGS_DATA_OFFSET: number + export const TRUN_FLAGS_DURATION: number + export const TRUN_FLAGS_FIRST_FLAG: number + export const TRUN_FLAGS_FLAGS: number + export const TRUN_FLAGS_SIZE: number + export const UUIDs: string[] + export const boxCodes: string[] + export const containerBoxCodes: any[] + export const fullBoxCodes: any[] + + export const sampleEntryCodes: { + Audio: string[] + Hint: any[] + Metadata: string[] + Subtitle: string[] + System: string[] + Text: string[] + Visual: string[] + } + + export const sampleGroupEntryCodes: any[] + + export const trackGroupTypes: any[] + + export function addSubBoxArrays(subBoxNames: any): void + export function boxEqual(box_a: any, box_b: any): any + export function boxEqualFields(box_a: any, box_b: any): any + export function createBoxCtor(type: any, parseMethod: any): void + export function createContainerBoxCtor(type: any, parseMethod: any, subBoxNames: any): void + export function createEncryptedSampleEntryCtor(mediaType: any, type: any, parseMethod: any): void + export function createFullBoxCtor(type: any, parseMethod: any): void + export function createMediaSampleEntryCtor(mediaType: any, parseMethod: any, subBoxNames: any): void + export function createSampleEntryCtor(mediaType: any, type: any, parseMethod: any, subBoxNames: any): void + export function createSampleGroupCtor(type: any, parseMethod: any): void + export function createTrackGroupCtor(type: any, parseMethod: any): void + export function createUUIDBox(uuid: any, isFullBox: any, isContainerBox: any, parseMethod: any): void + export function decimalToHex(d: any, padding: any): any + export function initialize(): void + export function parseHex16(stream: any): any + export function parseOneBox(stream: any, headerOnly: any, parentsize?: number): any + export function parseUUID(stream: any): any + + /* ??? + namespace UUIDBoxes { + export class a2394f525a9b4f14a2446c427c648df4 { + constructor(size?: number) + } + + export class a5d40b30e81411ddba2f0800200c9a66 { + constructor(size?: number) + + parse(stream: any): void + } + + export class d08a4f1810f34a82b6c832d8aba183d3 { + constructor(size?: number) + + parse(stream: any): void + } + + export class d4807ef2ca3946958e5426cb9e46a79f { + constructor(size?: number) + + parse(stream: any): void + } + } + */ + } + + // TODO Add types for the remaining classes found via dts-gen + export class MP4BoxStream { + constructor(arrayBuffer: any) + + getEndPosition(): any + getLength(): any + getPosition(): any + isEos(): any + readAnyInt(size?: number, signed?: boolean): any + readCString(): any + readInt16(): any + readInt16Array(length: any): any + readInt32(): any + readInt32Array(length: any): any + readInt64(): any + readInt8(): any + readString(length: any): any + readUint16(): any + readUint16Array(length: any): any + readUint24(): any + readUint32(): any + readUint32Array(length: any): any + readUint64(): any + readUint8(): any + readUint8Array(length: any): any + seek(pos: any): any + } + + export class MultiBufferStream { + constructor(buffer: any) + + addUsedBytes(nbBytes: any): void + cleanBuffers(): void + findEndContiguousBuf(inputindex: any): any + findPosition(fromStart: any, filePosition: any, markAsUsed: any): any + getEndFilePositionAfter(pos: any): any + getEndPosition(): any + getLength(): any + getPosition(): any + initialized(): any + insertBuffer(ab: any): void + logBufferLevel(info: any): void + mergeNextBuffer(): any + reduceBuffer(buffer: any, offset: any, newLength: any): any + seek(filePosition: any, fromStart: any, markAsUsed: any): any + setAllUsedBytes(): void + } + + export class Textin4Parser { + constructor() + + parseConfig(data: any): any + parseSample(sample: any): any + } + + export class XMLSubtitlein4Parser { + constructor() + + parseSample(sample: any): any + } + + export function MPEG4DescriptorParser(): any + + export namespace BoxParser {} + + export namespace Log { + export const LOG_LEVEL_ERROR = 4 + export const LOG_LEVEL_WARNING = 3 + export const LOG_LEVEL_INFO = 2 + export const LOG_LEVEL_DEBUG = 1 + + export function debug(module: any, msg: any): void + export function error(module: any, msg: any): void + export function getDurationString(duration: any, _timescale: any): any + export function info(module: any, msg: any): void + export function log(module: any, msg: any): void + export function printRanges(ranges: any): any + export function setLogLevel(level: any): void + export function warn(module: any, msg: any): void + } +} diff --git a/repos/demo/src/lib/moq/types/tsconfig.json b/repos/demo/src/lib/moq/types/tsconfig.json new file mode 100644 index 0000000..3ae2a24 --- /dev/null +++ b/repos/demo/src/lib/moq/types/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.json", + "include": ["."] +} diff --git a/repos/demo/src/main.tsx b/repos/demo/src/main.tsx new file mode 100644 index 0000000..421adcb --- /dev/null +++ b/repos/demo/src/main.tsx @@ -0,0 +1,65 @@ +import React, { useMemo, useState } from "react" +import ReactDOM from "react-dom/client" +import App from "./App.tsx" +import { RouterProvider, createBrowserRouter } from "react-router-dom" +import { BaseSynchroniserSnapshot, MetricSynchroniser, MetricSynchroniserContext, MetricsContext } from "./lib/internal/synchroniser.ts" +import { useRafLoop } from "react-use" +import { SynchroniserSnapshot } from "./lib/internal/types.ts" +import Cockpit from "./Cockpit.tsx" +import "./index.css" + +export const Links = () => ( + +) + +export const MetricContainer = ({ children }: { children: React.ReactNode }) => { + // Metrics synchroniser + const ms = useMemo(() => new MetricSynchroniser(), []) + + // Render loop + const [data, setData] = useState(BaseSynchroniserSnapshot) + useRafLoop(() => setData(Object.assign({}, ms.metrics)), true) + + return ( + + {children} + + ) +} + +const router = createBrowserRouter([ + { + path: "/moq", + element: + }, + { + path: "/dash", + element: + }, + { + path: "/cockpit", + element: + }, + { + path: "/", + element: + } +]) + +ReactDOM.createRoot(document.getElementById("root")!).render( + // + + + + // +) diff --git a/repos/demo/src/utils/array.ts b/repos/demo/src/utils/array.ts new file mode 100644 index 0000000..03e3aea --- /dev/null +++ b/repos/demo/src/utils/array.ts @@ -0,0 +1,3 @@ +export const ArrayMerger = (objValue: unknown, srcValue: unknown) => { + if (Array.isArray(objValue)) return objValue.concat(srcValue) +} diff --git a/repos/demo/src/utils/number.ts b/repos/demo/src/utils/number.ts new file mode 100644 index 0000000..c23659d --- /dev/null +++ b/repos/demo/src/utils/number.ts @@ -0,0 +1,35 @@ +export const humanizeValue = (value: number, metric: string, noDecimal = false) => { + switch (metric) { + case "latency": { + if (value < 0.03) return "<30 ms" + return value > 1 ? `${value.toFixed(noDecimal ? 1 : 3)} s` : `${(value * 1000).toFixed(0)} ms` + } + case "measuredBandwidth": + return `${(value / 1000).toFixed(noDecimal ? 0 : 2)} Mbps` + case "stallDuration": + return value < 1000 ? `${value.toFixed(0)} ms` : `${(value / 1000).toFixed(noDecimal ? 0 : 2)} s` + case "bitrate": + return `${(value / 1000).toFixed(0)} Kbps` + case "skippedDuration": + return `${value.toFixed(noDecimal ? 0 : 2)} s` + default: + return String(value) + } +} + +export const normalizeValue = (value: number, metric: string) => { + switch (metric) { + case "latency": + return value + case "measuredBandwidth": + return value * 1000 + case "stallDuration": + return value + case "bitrate": + return value * 1000 + case "skippedDuration": + return value + default: + return value + } +} diff --git a/repos/demo/src/utils/text.ts b/repos/demo/src/utils/text.ts new file mode 100644 index 0000000..86a38a3 --- /dev/null +++ b/repos/demo/src/utils/text.ts @@ -0,0 +1 @@ +export const convertCamelCase = (str: string) => str.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()) diff --git a/repos/demo/src/vite-env.d.ts b/repos/demo/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/repos/demo/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/repos/demo/tailwind.config.js b/repos/demo/tailwind.config.js new file mode 100644 index 0000000..fb9ab13 --- /dev/null +++ b/repos/demo/tailwind.config.js @@ -0,0 +1,18 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: { + fontFamily: { + sans: ["Nunito", "sans-serif"], + anton: ["Anton", "sans-serif"], + display: ["Rubik Doodle Shadow", "sans-serif"] + }, + colors: { + dash: "#1B60F8", + moq: "#d66629" + } + } + }, + plugins: [] +} diff --git a/repos/demo/tsconfig.json b/repos/demo/tsconfig.json new file mode 100644 index 0000000..f88d752 --- /dev/null +++ b/repos/demo/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "rootDir": ".", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "importHelpers": true, + "target": "es2022", + "module": "esNext", + "lib": ["es2022", "dom", "webworker", "dom.Iterable"], + "skipLibCheck": true, + "skipDefaultLibCheck": true, + + /* Bundler mode */ + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "allowImportingTsExtensions": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/repos/demo/tsconfig.node.json b/repos/demo/tsconfig.node.json new file mode 100644 index 0000000..9aca5be --- /dev/null +++ b/repos/demo/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": true, + "moduleResolution": "NodeNext", + "module": "NodeNext" + }, + "include": ["vite.config.ts"] +} diff --git a/repos/demo/vite.config.ts b/repos/demo/vite.config.ts new file mode 100644 index 0000000..b530904 --- /dev/null +++ b/repos/demo/vite.config.ts @@ -0,0 +1,24 @@ +import react from "@vitejs/plugin-react" +import { defineConfig } from "vite" + +export default defineConfig({ + plugins: [react()], + + // Uncomment the following lines to configure the dev server and the preview server to use HTTPS + server: { + host: true, + port: 5173 /*, + https: { + cert: "/etc/tls/cert", + key: "/etc/tls/key" + }*/ + }, + preview: { + host: true, + port: 5173 /*, + https: { + cert: "/etc/tls/cert", + key: "/etc/tls/key" + }*/ + } +}) diff --git a/repos/moq-rs/.dockerignore b/repos/moq-rs/.dockerignore new file mode 100644 index 0000000..a7ed94c --- /dev/null +++ b/repos/moq-rs/.dockerignore @@ -0,0 +1,2 @@ +target +dev diff --git a/repos/moq-rs/.editorconfig b/repos/moq-rs/.editorconfig new file mode 100644 index 0000000..5e16f8e --- /dev/null +++ b/repos/moq-rs/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = tab +indent_size = 4 +max_line_length = 120 + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/repos/moq-rs/.github/logo.svg b/repos/moq-rs/.github/logo.svg new file mode 100644 index 0000000..109b070 --- /dev/null +++ b/repos/moq-rs/.github/logo.svg @@ -0,0 +1,348 @@ + + + + + + + + + + + + + + + + + + + diff --git a/repos/moq-rs/.github/workflows/check.yml b/repos/moq-rs/.github/workflows/check.yml new file mode 100644 index 0000000..b39df93 --- /dev/null +++ b/repos/moq-rs/.github/workflows/check.yml @@ -0,0 +1,29 @@ +name: moq.rs + +on: + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + components: clippy, rustfmt + + - name: test + run: cargo test --verbose + + - name: clippy + run: cargo clippy + + - name: fmt + run: cargo fmt --check diff --git a/repos/moq-rs/.github/workflows/main.yml b/repos/moq-rs/.github/workflows/main.yml new file mode 100644 index 0000000..0f82e5d --- /dev/null +++ b/repos/moq-rs/.github/workflows/main.yml @@ -0,0 +1,54 @@ +name: main + +on: + push: + branches: ["main"] + +env: + REGISTRY: docker.io + IMAGE: ${{ github.repository }} + SERVICE: api # Restart the API service TODO and relays + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + # Only one release at a time and cancel prior releases + concurrency: + group: release + cancel-in-progress: true + + steps: + - uses: actions/checkout@v3 + + # I'm paying for Depot for faster ARM builds. + - uses: depot/setup-action@v1 + + - uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + # Build and push Docker image with Depot + - uses: depot/build-push-action@v1 + with: + project: r257ctfqm6 + context: . + push: true + tags: ${{env.REGISTRY}}/${{env.IMAGE}} + platforms: linux/amd64,linux/arm64 + + # Log in to GCP + - uses: google-github-actions/auth@v1 + with: + credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT_KEY }} + + # Deploy to cloud run + - uses: google-github-actions/deploy-cloudrun@v1 + with: + service: ${{env.SERVICE}} + image: ${{env.REGISTRY}}/${{env.IMAGE}} diff --git a/repos/moq-rs/.github/workflows/pr.yml b/repos/moq-rs/.github/workflows/pr.yml new file mode 100644 index 0000000..8325078 --- /dev/null +++ b/repos/moq-rs/.github/workflows/pr.yml @@ -0,0 +1,28 @@ +name: pr + +on: + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + # Install Rust with clippy/rustfmt + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + components: clippy, rustfmt + + # Make sure u guys don't write bad code + - run: cargo test --verbose + - run: cargo clippy --no-deps + - run: cargo fmt --check + + # Check for unused dependencies + - uses: bnjbvr/cargo-machete@main diff --git a/repos/moq-rs/.gitignore b/repos/moq-rs/.gitignore new file mode 100644 index 0000000..f6e1016 --- /dev/null +++ b/repos/moq-rs/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +target/ +logs/ +out/ diff --git a/repos/moq-rs/.rustfmt.toml b/repos/moq-rs/.rustfmt.toml new file mode 100644 index 0000000..b3c42a2 --- /dev/null +++ b/repos/moq-rs/.rustfmt.toml @@ -0,0 +1,4 @@ +# i die on this hill +hard_tabs = true + +max_width = 120 diff --git a/repos/moq-rs/Cargo.toml b/repos/moq-rs/Cargo.toml new file mode 100644 index 0000000..3e49423 --- /dev/null +++ b/repos/moq-rs/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +members = ["moq-transport", "moq-relay", "moq-pub", "moq-api", "moq-clock"] +resolver = "2" + +[patch.crates-io] +mp4 = { path = "./third_party/mp4-rust" } diff --git a/repos/moq-rs/Dockerfile b/repos/moq-rs/Dockerfile new file mode 100644 index 0000000..20af005 --- /dev/null +++ b/repos/moq-rs/Dockerfile @@ -0,0 +1,34 @@ +FROM rust:bookworm as builder + +# Create a build directory and copy over all of the files +WORKDIR /build +COPY . ./ + +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/build/target \ + cargo build --release && cp /build/target/release/moq-* /usr/local/cargo/bin + +FROM rust:bookworm as cargo-watch-builder + +# Build cargo-watch +RUN git clone https://github.com/watchexec/cargo-watch.git && \ + cd cargo-watch && \ + cargo build --release && \ + cp target/release/cargo-watch /usr/local/cargo/bin + +# moq-rs image with just the binaries +FROM rust:bookworm + +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates curl libssl3 && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=builder /usr/local/cargo/bin/moq-* /usr/local/bin + +# Install cargo-watch +COPY --from=cargo-watch-builder /usr/local/cargo/bin/cargo-watch /usr/local/bin + +# Copy the entrypoint script +COPY entrypoint.sh /entrypoint.sh + +ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/repos/moq-rs/LICENSE-APACHE b/repos/moq-rs/LICENSE-APACHE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/repos/moq-rs/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/repos/moq-rs/LICENSE-MIT b/repos/moq-rs/LICENSE-MIT new file mode 100644 index 0000000..fbd437c --- /dev/null +++ b/repos/moq-rs/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Luke Curley + +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. diff --git a/repos/moq-rs/README.md b/repos/moq-rs/README.md new file mode 100644 index 0000000..5a4599d --- /dev/null +++ b/repos/moq-rs/README.md @@ -0,0 +1,69 @@ +

+ Media over QUIC +

+ +Media over QUIC (MoQ) is a live media delivery protocol utilizing QUIC streams. +See [quic.video](https://quic.video) for more information. + +This repository contains a few crates: + +- **moq-relay**: A relay server, accepting content from publishers and fanning it out to subscribers. +- **moq-pub**: A publish client, accepting media from stdin (ex. via ffmpeg) and sending it to a remote server. +- **moq-transport**: An async implementation of the underlying MoQ protocol. +- **moq-api**: A HTTP API server that stores the origin for each broadcast, backed by redis. +- **moq-clock**: A dumb clock client/server just to prove MoQ is more than media. + +There's currently no way to view media with this repo; you'll need to use [moq-js](https://github.com/kixelated/moq-js) for that. + +## Development + +Use the [dev helper scripts](dev/README.md) for local development. + +## Usage + +### moq-relay + +**moq-relay** is a server that forwards subscriptions from publishers to subscribers, caching and deduplicating along the way. +It's designed to be run in a datacenter, relaying media across multiple hops to deduplicate and improve QoS. +The relays register themselves via the [moq-api](moq-api) endpoints, which is used to discover other relays and share broadcasts. + +Notable arguments: + +- `--listen ` Listen on this address, default: `[::]:4443` +- `--tls-cert ` Use the certificate file at this path +- `--tls-key ` Use the private key at this path +- `--dev` Listen via HTTPS as well, serving the `/fingerprint` of the self-signed certificate. (dev only) + +This listens for WebTransport connections on `UDP https://localhost:4443` by default. +You need a client to connect to that address, to both publish and consume media. + +### moq-pub + +This is a client that publishes a fMP4 stream from stdin over MoQ. +This can be combined with ffmpeg (and other tools) to produce a live stream. + +Notable arguments: + +- `` connect to the given address, which must start with `https://` for WebTransport. + +**NOTE**: We're very particular about the fMP4 ingested. See [this script](dev/pub) for the required ffmpeg flags. + +### moq-transport + +A media-agnostic library used by [moq-relay](moq-relay) and [moq-pub](moq-pub) to serve the underlying subscriptions. +It has caching/deduplication built-in, so your application is oblivious to the number of connections under the hood. + +See the published [crate](https://crates.io/crates/moq-transport) and [documentation](https://docs.rs/moq-transport/latest/moq_transport/). + +### moq-api + +This is a API server that exposes a REST API. +It's used by relays to inserts themselves as origins when publishing, and to find the origin when subscribing. +It's basically just a thin wrapper around redis that is only needed to run multiple relays in a (simple) cluster. + +## License + +Licensed under either: + +- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) diff --git a/repos/moq-rs/deploy/fly-relay.sh b/repos/moq-rs/deploy/fly-relay.sh new file mode 100755 index 0000000..12613f9 --- /dev/null +++ b/repos/moq-rs/deploy/fly-relay.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +mkdir cert +# Nothing to see here... +echo "$MOQ_CRT" | base64 -d > dev/moq-demo.crt +echo "$MOQ_KEY" | base64 -d > dev/moq-demo.key + +RUST_LOG=info /usr/local/cargo/bin/moq-relay --tls-cert dev/moq-demo.crt --tls-key dev/moq-demo.key diff --git a/repos/moq-rs/deploy/fly.toml b/repos/moq-rs/deploy/fly.toml new file mode 100644 index 0000000..18f6558 --- /dev/null +++ b/repos/moq-rs/deploy/fly.toml @@ -0,0 +1,20 @@ +app = "englishm-moq-relay" +kill_signal = "SIGINT" +kill_timeout = 5 + +[env] +PORT = "4443" + +[experimental] +cmd = "./fly-relay.sh" + +[[services]] +internal_port = 4443 +protocol = "udp" + +[services.concurrency] +hard_limit = 25 +soft_limit = 20 + +[[services.ports]] +port = "4443" diff --git a/repos/moq-rs/deploy/publish.sh b/repos/moq-rs/deploy/publish.sh new file mode 100755 index 0000000..b905daa --- /dev/null +++ b/repos/moq-rs/deploy/publish.sh @@ -0,0 +1,41 @@ +#!/bin/bash +set -euo pipefail + +ADDR=${ADDR:-"https://relay.quic.video"} +NAME=${NAME:-"bbb"} +URL=${URL:-"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"} + +# Download the funny bunny +wget -nv "${URL}" -O "${NAME}.mp4" + +# ffmpeg +# -hide_banner: Hide the banner +# -v quiet: and any other output +# -stats: But we still want some stats on stderr +# -stream_loop -1: Loop the broadcast an infinite number of times +# -re: Output in real-time +# -i "${INPUT}": Read from a file on disk +# -vf "drawtext": Render the current time in the corner of the video +# -an: Disable audio for now +# -b:v 3M: Output video at 3Mbps +# -preset ultrafast: Don't use much CPU at the cost of quality +# -tune zerolatency: Optimize for latency at the cost of quality +# -f mp4: Output to mp4 format +# -movflags: Build a fMP4 file with a frame per fragment +# - | moq-pub: Output to stdout and moq-pub to publish + +# Run ffmpeg +ffmpeg \ + -stream_loop -1 \ + -hide_banner \ + -v quiet \ + -re \ + -i "${NAME}.mp4" \ + -vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf:text='%{gmtime\: %H\\\\\:%M\\\\\:%S.%3N}':x=(W-tw)-24:y=24:fontsize=48:fontcolor=white:box=1:boxcolor=black@0.5" \ + -an \ + -b:v 3M \ + -preset ultrafast \ + -tune zerolatency \ + -f mp4 \ + -movflags empty_moov+frag_every_frame+separate_moof+omit_tfhd_offset \ + - | moq-pub "${ADDR}/${NAME}" diff --git a/repos/moq-rs/dev/.gitignore b/repos/moq-rs/dev/.gitignore new file mode 100644 index 0000000..773d450 --- /dev/null +++ b/repos/moq-rs/dev/.gitignore @@ -0,0 +1,4 @@ +*.crt +*.key +*.hex +*.mp4 diff --git a/repos/moq-rs/dev/README.md b/repos/moq-rs/dev/README.md new file mode 100644 index 0000000..c161f9c --- /dev/null +++ b/repos/moq-rs/dev/README.md @@ -0,0 +1,129 @@ +# Local Development + +## Quickstart with Docker + +Launch a basic cluster, including provisioning certs and deploying root certificates: + +``` +# From repo root: +make run +``` + +Then, visit https://quic.video/publish/?server=localhost:4443. + +## Manual setup + +This is a collection of helpful scripts for local development. + +### moq-relay + +Unfortunately, QUIC mandates TLS and makes local development difficult. +If you have a valid certificate you can use it instead of self-signing. + +Use [mkcert](https://github.com/FiloSottile/mkcert) to generate a self-signed certificate. +Unfortunately, this currently requires [Go](https://golang.org/) to be installed in order to [fork](https://github.com/FiloSottile/mkcert/pull/513) the tool. +Somebody should get that merged or make something similar in Rust... + +```bash +./dev/cert +``` + +Unfortunately, WebTransport in Chrome currently (May 2023) doesn't verify certificates using the root CA. +The workaround is to use the `serverFingerprints` options, which requires the certificate MUST be only valid for at most **14 days**. +This is also why we're using a fork of mkcert, because it generates certificates valid for years by default. +This limitation will be removed once Chrome uses the system CA for WebTransport. + +### moq-pub + +You'll want some test footage to broadcast. +Anything works, but make sure the codec is supported by the player since `moq-pub` does not re-encode. + +Here's a criticially acclaimed short film: + +```bash +mkdir media +wget http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 -O dev/source.mp4 +``` + +`moq-pub` uses [ffmpeg](https://ffmpeg.org/) to convert the media to fMP4. +You should have it installed already if you're a video nerd, otherwise: + +```bash +brew install ffmpeg +``` + +### moq-api + +`moq-api` uses a redis instance to store active origins for clustering. +This is not relevant for most local development and the code path is skipped by default. + +However, if you want to test the clustering, you'll need either either [Docker](https://www.docker.com/) or [Podman](https://podman.io/) installed. +We run the redis instance via a container automatically as part of `dev/api`. + +## Development + +**tl;dr** run these commands in seperate terminals: + +```bash +./dev/cert +./dev/relay +./dev/pub +``` + +They will each print out a URL you can use to publish/watch broadcasts. + +### moq-relay + +You can run the relay with the following command, automatically using the self-signed certificates generated earlier. +This listens for WebTransport connections on WebTransport `https://localhost:4443` by default. + +```bash +./dev/relay +``` + +It will print out a URL when you can use to publish. Alternatively, you can use `dev/pub` instead. + +> Publish URL: https://quic.video/publish/?server=localhost:4443 + +### moq-pub + +The following command runs a development instance, broadcasing `dev/source.mp4` to WebTransport `https://localhost:4443`: + +```bash +./dev/pub +``` + +It will print out a URL when you can use to watch. +By default, the broadcast name is `dev` but you can overwrite it with the `NAME` env. + +> Watch URL: https://quic.video/watch/dev?server=localhost:4443 + +If you're debugging encoding issues, you can use this script to dump the file to disk instead, defaulting to +`dev/output.mp4`. + +```bash +./dev/pub-file +``` + +### moq-api + +The following commands runs an API server, listening for HTTP requests on `http://localhost:4442` by default. + +```bash +./dev/api +``` + +Nodes can now register themselves via the API, which means you can run multiple interconnected relays. +There's two separate `dev/relay-0` and `dev/relay-1` scripts to test clustering locally: + +```bash +./dev/relay-0 +./dev/relay-1 +``` + +These listen on `:4443` and `:4444` respectively, inserting themselves into the origin database as `localhost:$PORT`. + +There's also a separate `dev/pub-1` script to publish to the `:4444` instance. +You can use the exisitng `dev/pub` script to publish to the `:4443` instance. + +If all goes well, you would be able to publish to one relay and watch from the other. diff --git a/repos/moq-rs/dev/api b/repos/moq-rs/dev/api new file mode 100755 index 0000000..8a74d7d --- /dev/null +++ b/repos/moq-rs/dev/api @@ -0,0 +1,45 @@ +#!/bin/bash +set -euo pipefail + +# Change directory to the root of the project +cd "$(dirname "$0")/.." + +# Use debug logging by default +export RUST_LOG="${RUST_LOG:-debug}" + +# Run the API server on port 4442 by default +HOST="${HOST:-[::]}" +PORT="${PORT:-4442}" +LISTEN="${LISTEN:-$HOST:$PORT}" + +# Check for Podman/Docker and set runtime accordingly +if command -v podman &> /dev/null; then + RUNTIME=podman +elif command -v docker &> /dev/null; then + RUNTIME=docker +else + echo "Neither podman or docker found in PATH. Exiting." + exit 1 +fi + +REDIS_PORT=${REDIS_PORT:-6400} # The default is 6379, but we'll use 6400 to avoid conflicts + +# Cleanup function to stop Redis when script exits +cleanup() { + $RUNTIME rm -f moq-redis || true +} + +# Stop the redis instance if it's still running +cleanup + +# Run a Redis instance +REDIS_CONTAINER=$($RUNTIME run --rm --name moq-redis -d -p "$REDIS_PORT:6379" redis:latest) + +# Cleanup function to stop Redis when script exits +trap cleanup EXIT + +# Default to a sqlite database in memory +DATABASE="${DATABASE-sqlite::memory:}" + +# Run the relay and forward any arguments +cargo run --bin moq-api -- --listen "$LISTEN" --redis "redis://localhost:$REDIS_PORT" "$@" diff --git a/repos/moq-rs/dev/cert b/repos/moq-rs/dev/cert new file mode 100755 index 0000000..e2e9243 --- /dev/null +++ b/repos/moq-rs/dev/cert @@ -0,0 +1,18 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" + +# Generate a new RSA key/cert for local development +HOST="localhost" +CRT="$HOST.crt" +KEY="$HOST.key" + +# Install the system certificate if it's not already +# NOTE: The ecdsa flag does nothing but I wish it did +go run filippo.io/mkcert -ecdsa -install + +# Generate a new certificate for localhost +# This fork of mkcert supports the -days flag. +# TODO remove the -days flag when Chrome accepts self-signed certs. +go run filippo.io/mkcert -ecdsa -days 10 -cert-file "$CRT" -key-file "$KEY" localhost 127.0.0.1 ::1 relay1 relay2 diff --git a/repos/moq-rs/dev/clock b/repos/moq-rs/dev/clock new file mode 100755 index 0000000..ee2f91c --- /dev/null +++ b/repos/moq-rs/dev/clock @@ -0,0 +1,19 @@ +#!/bin/bash +set -euo pipefail + +# Change directory to the root of the project +cd "$(dirname "$0")/.." + +# Use debug logging by default +export RUST_LOG="${RUST_LOG:-debug}" + +# Connect to localhost by default. +HOST="${HOST:-localhost}" +PORT="${PORT:-4443}" +ADDR="${ADDR:-$HOST:$PORT}" +NAME="${NAME:-clock}" + +# Combine the host and name into a URL. +URL="${URL:-"https://$ADDR/$NAME"}" + +cargo run --bin moq-clock -- "$URL" "$@" diff --git a/repos/moq-rs/dev/go.mod b/repos/moq-rs/dev/go.mod new file mode 100644 index 0000000..ac3c3d0 --- /dev/null +++ b/repos/moq-rs/dev/go.mod @@ -0,0 +1,14 @@ +module github.com/kixelated/warp/cert + +go 1.18 + +require ( + filippo.io/mkcert v1.4.4 // indirect + golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 // indirect + golang.org/x/text v0.3.7 // indirect + howett.net/plist v1.0.0 // indirect + software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect +) + +replace filippo.io/mkcert => github.com/kixelated/mkcert v1.4.4-days diff --git a/repos/moq-rs/dev/go.sum b/repos/moq-rs/dev/go.sum new file mode 100644 index 0000000..94fb636 --- /dev/null +++ b/repos/moq-rs/dev/go.sum @@ -0,0 +1,22 @@ +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/kixelated/mkcert v1.4.4-days h1:T2P9W4ruEfgLHOl5UljPwh0d79FbFWkSe2IONcUBxG8= +github.com/kixelated/mkcert v1.4.4-days/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 h1:yssD99+7tqHWO5Gwh81phT+67hg+KttniBr6UnEXOY8= +golang.org/x/net v0.0.0-20220421235706-1d1ef9303861/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= +software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= diff --git a/repos/moq-rs/dev/pub b/repos/moq-rs/dev/pub new file mode 100755 index 0000000..f6b3695 --- /dev/null +++ b/repos/moq-rs/dev/pub @@ -0,0 +1,40 @@ +#!/bin/bash +set -euo pipefail + +# Change directory to the root of the project +cd "$(dirname "$0")/.." + +# Use debug logging by default +export RUST_LOG="${RUST_LOG:-debug}" + +# Connect to localhost by default. +HOST="${HOST:-localhost}" +PORT="${PORT:-4443}" +ADDR="${ADDR:-$HOST:$PORT}" + +# Generate a random 16 character name by default. +#NAME="${NAME:-$(head /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' | head -c 16)}" + +# JK use the name "bbb" instead, matching the Big Buck Bunny demo. +# TODO use that random name if the host is not localhost +NAME="${NAME:-bbb}" + +# Combine the host and name into a URL. +URL="${URL:-"https://$ADDR/$NAME"}" + +# Default to a source video +INPUT="${INPUT:-dev/source.mp4}" + +# Print out the watch URL +echo "Watch URL: https://quic.video/watch/$NAME?server=$ADDR" + +# Run ffmpeg and pipe the output to moq-pub +# TODO enable audio again once fixed. +ffmpeg -hide_banner -v quiet \ + -stream_loop -1 -re \ + -i "$INPUT" \ + -c copy \ + -an \ + -f mp4 -movflags cmaf+separate_moof+delay_moov+skip_trailer \ + -frag_duration 1 \ + - | cargo run --bin moq-pub -- "$URL" "$@" diff --git a/repos/moq-rs/dev/pub-1 b/repos/moq-rs/dev/pub-1 new file mode 100755 index 0000000..8a70a5c --- /dev/null +++ b/repos/moq-rs/dev/pub-1 @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +# Change directory to the root of the project +cd "$(dirname "$0")/.." + +# Connect to the 2nd relay by default. +export PORT="${PORT:-4444}" + +./dev/pub diff --git a/repos/moq-rs/dev/pub-file b/repos/moq-rs/dev/pub-file new file mode 100755 index 0000000..cef42ca --- /dev/null +++ b/repos/moq-rs/dev/pub-file @@ -0,0 +1,90 @@ +#!/bin/bash +set -euo pipefail + +# Change directory to the root of the project +cd "$(dirname "$0")/.." + +# Default to a source video +INPUT="${INPUT:-dev/source.mp4}" + +# Output the fragmented MP4 to disk for testing. +OUTPUT="${OUTPUT:-dev/output.mp4}" + +# Run ffmpeg the same as dev/pub, but: +# - print any errors/warnings +# - only loop twice +# +# Note this is artificially slowed down to real-time using the -re flag; you can remove it. +ffmpeg \ + -re \ + -y \ + -i "$INPUT" \ + -c copy \ + -fps_mode passthrough \ + -f mp4 -movflags cmaf+separate_moof+delay_moov+skip_trailer \ + -frag_duration 1 \ + "${OUTPUT}" + +# % ffmpeg -f mp4 --ffmpeg -h muxer=mov +# +# ffmpeg version 6.0 Copyright (c) 2000-2023 the FFmpeg developers +# Muxer mov [QuickTime / MOV]: +# Common extensions: mov. +# Default video codec: h264. +# Default audio codec: aac. +# mov/mp4/tgp/psp/tg2/ipod/ismv/f4v muxer AVOptions: +# -movflags E.......... MOV muxer flags (default 0) +# rtphint E.......... Add RTP hint tracks +# empty_moov E.......... Make the initial moov atom empty +# frag_keyframe E.......... Fragment at video keyframes +# frag_every_frame E.......... Fragment at every frame +# separate_moof E.......... Write separate moof/mdat atoms for each track +# frag_custom E.......... Flush fragments on caller requests +# isml E.......... Create a live smooth streaming feed (for pushing to a publishing point) +# faststart E.......... Run a second pass to put the index (moov atom) at the beginning of the file +# omit_tfhd_offset E.......... Omit the base data offset in tfhd atoms +# disable_chpl E.......... Disable Nero chapter atom +# default_base_moof E.......... Set the default-base-is-moof flag in tfhd atoms +# dash E.......... Write DASH compatible fragmented MP4 +# cmaf E.......... Write CMAF compatible fragmented MP4 +# frag_discont E.......... Signal that the next fragment is discontinuous from earlier ones +# delay_moov E.......... Delay writing the initial moov until the first fragment is cut, or until the first fragment flush +# global_sidx E.......... Write a global sidx index at the start of the file +# skip_sidx E.......... Skip writing of sidx atom +# write_colr E.......... Write colr atom even if the color info is unspecified (Experimental, may be renamed or changed, do not use from scripts) +# prefer_icc E.......... If writing colr atom prioritise usage of ICC profile if it exists in stream packet side data +# write_gama E.......... Write deprecated gama atom +# use_metadata_tags E.......... Use mdta atom for metadata. +# skip_trailer E.......... Skip writing the mfra/tfra/mfro trailer for fragmented files +# negative_cts_offsets E.......... Use negative CTS offsets (reducing the need for edit lists) +# -moov_size E.......... maximum moov size so it can be placed at the begin (from 0 to INT_MAX) (default 0) +# -rtpflags E.......... RTP muxer flags (default 0) +# latm E.......... Use MP4A-LATM packetization instead of MPEG4-GENERIC for AAC +# rfc2190 E.......... Use RFC 2190 packetization instead of RFC 4629 for H.263 +# skip_rtcp E.......... Don't send RTCP sender reports +# h264_mode0 E.......... Use mode 0 for H.264 in RTP +# send_bye E.......... Send RTCP BYE packets when finishing +# -skip_iods E.......... Skip writing iods atom. (default true) +# -iods_audio_profile E.......... iods audio profile atom. (from -1 to 255) (default -1) +# -iods_video_profile E.......... iods video profile atom. (from -1 to 255) (default -1) +# -frag_duration E.......... Maximum fragment duration (from 0 to INT_MAX) (default 0) +# -min_frag_duration E.......... Minimum fragment duration (from 0 to INT_MAX) (default 0) +# -frag_size E.......... Maximum fragment size (from 0 to INT_MAX) (default 0) +# -ism_lookahead E.......... Number of lookahead entries for ISM files (from 0 to 255) (default 0) +# -video_track_timescale E.......... set timescale of all video tracks (from 0 to INT_MAX) (default 0) +# -brand E.......... Override major brand +# -use_editlist E.......... use edit list (default auto) +# -fragment_index E.......... Fragment number of the next fragment (from 1 to INT_MAX) (default 1) +# -mov_gamma E.......... gamma value for gama atom (from 0 to 10) (default 0) +# -frag_interleave E.......... Interleave samples within fragments (max number of consecutive samples, lower is tighter interleaving, but with more overhead) (from 0 to INT_MAX) (default 0) +# -encryption_scheme E.......... Configures the encryption scheme, allowed values are none, cenc-aes-ctr +# -encryption_key E.......... The media encryption key (hex) +# -encryption_kid E.......... The media encryption key identifier (hex) +# -use_stream_ids_as_track_ids E.......... use stream ids as track ids (default false) +# -write_btrt E.......... force or disable writing btrt (default auto) +# -write_tmcd E.......... force or disable writing tmcd (default auto) +# -write_prft E.......... Write producer reference time box with specified time source (from 0 to 2) (default 0) +# wallclock 1 E.......... +# pts 2 E.......... +# -empty_hdlr_name E.......... write zero-length name string in hdlr atoms within mdia and minf atoms (default false) +# -movie_timescale E.......... set movie timescale (from 1 to INT_MAX) (default 1000) diff --git a/repos/moq-rs/dev/relay b/repos/moq-rs/dev/relay new file mode 100755 index 0000000..acad797 --- /dev/null +++ b/repos/moq-rs/dev/relay @@ -0,0 +1,44 @@ +#!/bin/bash +set -euo pipefail + +# Change directory to the root of the project +cd "$(dirname "$0")/.." + +# Use debug logging by default +export RUST_LOG="${RUST_LOG:-debug}" + +SCRIPT=$(realpath "$0") +CURRENT_DIR=$(dirname "$SCRIPT") + +if [[ -f "$CURRENT_DIR/.env" ]]; then + source $CURRENT_DIR/.env +fi + +# Default to a self-signed certificate +# TODO automatically generate if it doesn't exist. +CERT="${CERT:-dev/localhost.crt}" +KEY="${KEY:-dev/localhost.key}" + +# Default to listening on localhost:4443 +HOST="${HOST:-[::]}" +PORT="${PORT:-4443}" +LISTEN="${LISTEN:-$HOST:$PORT}" + +# A list of optional args +ARGS="" + +# Connect to the given URL to get origins. +# TODO default to a public instance? +if [ -n "${API-}" ]; then + ARGS="$ARGS --api $API" +fi + +# Provide our node URL when registering origins. +if [ -n "${NODE-}" ]; then + ARGS="$ARGS --api-node $NODE" +fi + +echo "Publish URL: https://quic.video/publish/?server=localhost:${PORT}" + +# Run the relay and forward any arguments +cargo --config profile.dev.debug-assertions=false run --bin moq-relay -- --listen "$LISTEN" --tls-cert "$CERT" --tls-key "$KEY" --dev $ARGS -- "$@" diff --git a/repos/moq-rs/dev/relay-0 b/repos/moq-rs/dev/relay-0 new file mode 100755 index 0000000..ec67141 --- /dev/null +++ b/repos/moq-rs/dev/relay-0 @@ -0,0 +1,12 @@ +#!/bin/bash +set -euo pipefail + +# Change directory to the root of the project +cd "$(dirname "$0")/.." + +# Run an instance that advertises itself to the origin API. +export PORT="${PORT:-4443}" +export API="${API:-http://localhost:4442}" # TODO support HTTPS +export NODE="${NODE:-https://localhost:$PORT}" + +./dev/relay diff --git a/repos/moq-rs/dev/relay-1 b/repos/moq-rs/dev/relay-1 new file mode 100755 index 0000000..0f5acc5 --- /dev/null +++ b/repos/moq-rs/dev/relay-1 @@ -0,0 +1,12 @@ +#!/bin/bash +set -euo pipefail + +# Change directory to the root of the project +cd "$(dirname "$0")/.." + +# Run an instance that advertises itself to the origin API. +export PORT="${PORT:-4444}" +export API="${API:-http://localhost:4442}" # TODO support HTTPS +export NODE="${NODE:-https://localhost:$PORT}" + +./dev/relay diff --git a/repos/moq-rs/dev/setup b/repos/moq-rs/dev/setup new file mode 100644 index 0000000..e5adf60 --- /dev/null +++ b/repos/moq-rs/dev/setup @@ -0,0 +1,2 @@ +#!/bin/bash +set -euo pipefail diff --git a/repos/moq-rs/entrypoint.sh b/repos/moq-rs/entrypoint.sh new file mode 100755 index 0000000..d81ec7e --- /dev/null +++ b/repos/moq-rs/entrypoint.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# This script is the entrypoint for the Docker container. +# If DEVELOPMENT is set to true, file changes will be monitored and the binary will be recompiled. +# +# ./entrypoint.sh <...args> + +set -e + +# Check if DEVELOPMENT is set to true +IS_DEVELOPMENT=${DEVELOPMENT:-false} +BIN=$1 +shift +ARGS=$@ +CARGO_ARGS="--config profile.dev.debug-assertions=false" + +trigger_init() { + sleep 1 + touch .trigger + rm .trigger +} + +if [ "$IS_DEVELOPMENT" = "true" ]; then + # Run the binary in development mode + echo "Running in development mode" + # Change to the project directory + cd /project + # Trigger the initial build + trigger_init & + # Run the binary in development mode + exec cargo $CARGO_ARGS watch -x "run --bin $BIN -- $ARGS" +else + # Run the binary in production mode + echo "Running in production mode" + exec $BIN $ARGS +fi diff --git a/repos/moq-rs/media/Big_Buck_Bunny_1080_10s_1MB.mp4 b/repos/moq-rs/media/Big_Buck_Bunny_1080_10s_1MB.mp4 new file mode 100644 index 0000000..f9f672c Binary files /dev/null and b/repos/moq-rs/media/Big_Buck_Bunny_1080_10s_1MB.mp4 differ diff --git a/repos/moq-rs/media/Big_Buck_Bunny_1080_10s_2MB.mp4 b/repos/moq-rs/media/Big_Buck_Bunny_1080_10s_2MB.mp4 new file mode 100644 index 0000000..d7045a5 Binary files /dev/null and b/repos/moq-rs/media/Big_Buck_Bunny_1080_10s_2MB.mp4 differ diff --git a/repos/moq-rs/media/Big_Buck_Bunny_1080_10s_5MB.mp4 b/repos/moq-rs/media/Big_Buck_Bunny_1080_10s_5MB.mp4 new file mode 100644 index 0000000..10e92c0 Binary files /dev/null and b/repos/moq-rs/media/Big_Buck_Bunny_1080_10s_5MB.mp4 differ diff --git a/repos/moq-rs/media/bbb_source.mp4 b/repos/moq-rs/media/bbb_source.mp4 new file mode 100644 index 0000000..f9f672c Binary files /dev/null and b/repos/moq-rs/media/bbb_source.mp4 differ diff --git a/repos/moq-rs/moq-api/Cargo.toml b/repos/moq-rs/moq-api/Cargo.toml new file mode 100644 index 0000000..df1f7a8 --- /dev/null +++ b/repos/moq-rs/moq-api/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "moq-api" +description = "Media over QUIC" +authors = ["Luke Curley"] +repository = "https://github.com/kixelated/moq-rs" +license = "MIT OR Apache-2.0" + +version = "0.0.1" +edition = "2021" + +keywords = ["quic", "http3", "webtransport", "media", "live"] +categories = ["multimedia", "network-programming", "web-programming"] + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# HTTP server +axum = "0.6" +hyper = { version = "0.14", features = ["full"] } +tokio = { version = "1", features = ["full"] } + +# HTTP client +reqwest = { version = "0.11", features = ["json", "rustls-tls"] } + +# JSON encoding +serde = "1" +serde_json = "1" + +# CLI +clap = { version = "4", features = ["derive"] } + +# Database +redis = { version = "0.23", features = [ + "tokio-rustls-comp", + "connection-manager", +] } +url = { version = "2", features = ["serde"] } + +# Error handling +log = "0.4" +env_logger = "0.9" +thiserror = "1" diff --git a/repos/moq-rs/moq-api/README.md b/repos/moq-rs/moq-api/README.md new file mode 100644 index 0000000..324dad8 --- /dev/null +++ b/repos/moq-rs/moq-api/README.md @@ -0,0 +1,4 @@ +# moq-api + +A thin HTTP API that wraps Redis. +Basically I didn't want the relays connecting to Redis directly. diff --git a/repos/moq-rs/moq-api/src/client.rs b/repos/moq-rs/moq-api/src/client.rs new file mode 100644 index 0000000..5f07d11 --- /dev/null +++ b/repos/moq-rs/moq-api/src/client.rs @@ -0,0 +1,56 @@ +use url::Url; + +use crate::{ApiError, Origin}; + +#[derive(Clone)] +pub struct Client { + // The address of the moq-api server + url: Url, + + client: reqwest::Client, +} + +impl Client { + pub fn new(url: Url) -> Self { + let client = reqwest::Client::new(); + Self { url, client } + } + + pub async fn get_origin(&self, id: &str) -> Result, ApiError> { + let url = self.url.join("origin/")?.join(id)?; + let resp = self.client.get(url).send().await?; + if resp.status() == reqwest::StatusCode::NOT_FOUND { + return Ok(None); + } + + let origin: Origin = resp.json().await?; + Ok(Some(origin)) + } + + pub async fn set_origin(&mut self, id: &str, origin: &Origin) -> Result<(), ApiError> { + let url = self.url.join("origin/")?.join(id)?; + + let resp = self.client.post(url).json(origin).send().await?; + resp.error_for_status()?; + + Ok(()) + } + + pub async fn delete_origin(&mut self, id: &str) -> Result<(), ApiError> { + let url = self.url.join("origin/")?.join(id)?; + + let resp = self.client.delete(url).send().await?; + resp.error_for_status()?; + + Ok(()) + } + + pub async fn patch_origin(&mut self, id: &str, origin: &Origin) -> Result<(), ApiError> { + let url = self.url.join("origin/")?.join(id)?; + + let resp = self.client.patch(url).json(origin).send().await?; + resp.error_for_status()?; + + Ok(()) + } +} diff --git a/repos/moq-rs/moq-api/src/error.rs b/repos/moq-rs/moq-api/src/error.rs new file mode 100644 index 0000000..e1891d9 --- /dev/null +++ b/repos/moq-rs/moq-api/src/error.rs @@ -0,0 +1,16 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ApiError { + #[error("redis error: {0}")] + Redis(#[from] redis::RedisError), + + #[error("reqwest error: {0}")] + Request(#[from] reqwest::Error), + + #[error("hyper error: {0}")] + Hyper(#[from] hyper::Error), + + #[error("url error: {0}")] + Url(#[from] url::ParseError), +} diff --git a/repos/moq-rs/moq-api/src/lib.rs b/repos/moq-rs/moq-api/src/lib.rs new file mode 100644 index 0000000..be117a0 --- /dev/null +++ b/repos/moq-rs/moq-api/src/lib.rs @@ -0,0 +1,7 @@ +mod client; +mod error; +mod model; + +pub use client::*; +pub use error::*; +pub use model::*; diff --git a/repos/moq-rs/moq-api/src/main.rs b/repos/moq-rs/moq-api/src/main.rs new file mode 100644 index 0000000..daeebe5 --- /dev/null +++ b/repos/moq-rs/moq-api/src/main.rs @@ -0,0 +1,14 @@ +use clap::Parser; + +mod server; +use moq_api::ApiError; +use server::{Server, ServerConfig}; + +#[tokio::main] +async fn main() -> Result<(), ApiError> { + env_logger::init(); + + let config = ServerConfig::parse(); + let server = Server::new(config); + server.run().await +} diff --git a/repos/moq-rs/moq-api/src/model.rs b/repos/moq-rs/moq-api/src/model.rs new file mode 100644 index 0000000..073e923 --- /dev/null +++ b/repos/moq-rs/moq-api/src/model.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +use url::Url; + +#[derive(Serialize, Deserialize, PartialEq, Eq)] +pub struct Origin { + pub url: Url, +} diff --git a/repos/moq-rs/moq-api/src/server.rs b/repos/moq-rs/moq-api/src/server.rs new file mode 100644 index 0000000..8a05c16 --- /dev/null +++ b/repos/moq-rs/moq-api/src/server.rs @@ -0,0 +1,171 @@ +use std::net; + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Response}, + routing::get, + Json, Router, +}; + +use clap::Parser; + +use redis::{aio::ConnectionManager, AsyncCommands}; + +use moq_api::{ApiError, Origin}; + +/// Runs a HTTP API to create/get origins for broadcasts. +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct ServerConfig { + /// Listen for HTTP requests on the given address + #[arg(long)] + pub listen: net::SocketAddr, + + /// Connect to the given redis instance + #[arg(long)] + pub redis: url::Url, +} + +pub struct Server { + config: ServerConfig, +} + +impl Server { + pub fn new(config: ServerConfig) -> Self { + Self { config } + } + + pub async fn run(self) -> Result<(), ApiError> { + log::info!("connecting to redis: url={}", self.config.redis); + + // Create the redis client. + let redis = redis::Client::open(self.config.redis)?; + let redis = redis + .get_tokio_connection_manager() // TODO get_tokio_connection_manager_with_backoff? + .await?; + + let app = Router::new() + .route( + "/origin/:id", + get(get_origin) + .post(set_origin) + .delete(delete_origin) + .patch(patch_origin), + ) + .with_state(redis); + + log::info!("serving requests: bind={}", self.config.listen); + + axum::Server::bind(&self.config.listen) + .serve(app.into_make_service()) + .await?; + + Ok(()) + } +} + +async fn get_origin( + Path(id): Path, + State(mut redis): State, +) -> Result, AppError> { + let key = origin_key(&id); + + let payload: Option = redis.get(&key).await?; + let payload = payload.ok_or(AppError::NotFound)?; + let origin: Origin = serde_json::from_str(&payload)?; + + Ok(Json(origin)) +} + +async fn set_origin( + State(mut redis): State, + Path(id): Path, + Json(origin): Json, +) -> Result<(), AppError> { + // TODO validate origin + + let key = origin_key(&id); + + // Convert the input back to JSON after validating it add adding any fields (TODO) + let payload = serde_json::to_string(&origin)?; + + let res: Option = redis::cmd("SET") + .arg(key) + .arg(payload) + .arg("NX") + .arg("EX") + .arg(600) // Set the key to expire in 10 minutes; the origin needs to keep refreshing it. + .query_async(&mut redis) + .await?; + + if res.is_none() { + return Err(AppError::Duplicate); + } + + Ok(()) +} + +async fn delete_origin(Path(id): Path, State(mut redis): State) -> Result<(), AppError> { + let key = origin_key(&id); + match redis.del(key).await? { + 0 => Err(AppError::NotFound), + _ => Ok(()), + } +} + +// Update the expiration deadline. +async fn patch_origin( + Path(id): Path, + State(mut redis): State, + Json(origin): Json, +) -> Result<(), AppError> { + let key = origin_key(&id); + + // Make sure the contents haven't changed + // TODO make a LUA script to do this all in one operation. + let payload: Option = redis.get(&key).await?; + let payload = payload.ok_or(AppError::NotFound)?; + let expected: Origin = serde_json::from_str(&payload)?; + + if expected != origin { + return Err(AppError::Duplicate); + } + + // Reset the timeout to 10 minutes. + match redis.expire(key, 600).await? { + 0 => Err(AppError::NotFound), + _ => Ok(()), + } +} + +fn origin_key(id: &str) -> String { + format!("origin.{}", id) +} + +#[derive(thiserror::Error, Debug)] +enum AppError { + #[error("redis error")] + Redis(#[from] redis::RedisError), + + #[error("json error")] + Json(#[from] serde_json::Error), + + #[error("not found")] + NotFound, + + #[error("duplicate ID")] + Duplicate, +} + +// Tell axum how to convert `AppError` into a response. +impl IntoResponse for AppError { + fn into_response(self) -> Response { + match self { + AppError::Redis(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("redis error: {}", e)).into_response(), + AppError::Json(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("json error: {}", e)).into_response(), + AppError::NotFound => StatusCode::NOT_FOUND.into_response(), + AppError::Duplicate => StatusCode::CONFLICT.into_response(), + } + } +} diff --git a/repos/moq-rs/moq-clock/Cargo.toml b/repos/moq-rs/moq-clock/Cargo.toml new file mode 100644 index 0000000..ccd0e5b --- /dev/null +++ b/repos/moq-rs/moq-clock/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "moq-clock" +description = "CLOCK over QUIC" +authors = ["Luke Curley"] +repository = "https://github.com/kixelated/moq-rs" +license = "MIT OR Apache-2.0" + +version = "0.1.0" +edition = "2021" + +keywords = ["quic", "http3", "webtransport", "media", "live"] +categories = ["multimedia", "network-programming", "web-programming"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +moq-transport = { path = "../moq-transport" } + +# QUIC +quinn = "0.10" +webtransport-quinn = "0.6.1" + +url = "2" + +# Crypto +rustls = { version = "0.21", features = ["dangerous_configuration"] } +rustls-native-certs = "0.6" +rustls-pemfile = "1" + +# Async stuff +tokio = { version = "1", features = ["full"] } + +# CLI, logging, error handling +clap = { version = "4", features = ["derive"] } +log = { version = "0.4", features = ["std"] } +env_logger = "0.9" +anyhow = { version = "1", features = ["backtrace"] } +tracing = "0.1" +tracing-subscriber = "0.3" + +# CLOCK STUFF +chrono = "0.4" + +[build-dependencies] +clap = { version = "4", features = ["derive"] } +clap_mangen = "0.2" +url = "2" diff --git a/repos/moq-rs/moq-clock/src/cli.rs b/repos/moq-rs/moq-clock/src/cli.rs new file mode 100644 index 0000000..32debc1 --- /dev/null +++ b/repos/moq-rs/moq-clock/src/cli.rs @@ -0,0 +1,46 @@ +use clap::Parser; +use std::{net, path}; +use url::Url; + +#[derive(Parser, Clone, Debug)] +pub struct Config { + /// Listen for UDP packets on the given address. + #[arg(long, default_value = "[::]:0")] + pub bind: net::SocketAddr, + + /// Connect to the given URL starting with https:// + #[arg(value_parser = moq_url)] + pub url: Url, + + /// Use the TLS root CA at this path, encoded as PEM. + /// + /// This value can be provided multiple times for multiple roots. + /// If this is empty, system roots will be used instead + #[arg(long)] + pub tls_root: Vec, + + /// Danger: Disable TLS certificate verification. + /// + /// Fine for local development, but should be used in caution in production. + #[arg(long)] + pub tls_disable_verify: bool, + + /// Publish the current time to the relay, otherwise only subscribe. + #[arg(long)] + pub publish: bool, + + /// The name of the clock track. + #[arg(long, default_value = "now")] + pub track: String, +} + +fn moq_url(s: &str) -> Result { + let url = Url::try_from(s).map_err(|e| e.to_string())?; + + // Make sure the scheme is moq + if url.scheme() != "https" { + return Err("url scheme must be https:// for WebTransport".to_string()); + } + + Ok(url) +} diff --git a/repos/moq-rs/moq-clock/src/clock.rs b/repos/moq-rs/moq-clock/src/clock.rs new file mode 100644 index 0000000..a72a0cf --- /dev/null +++ b/repos/moq-rs/moq-clock/src/clock.rs @@ -0,0 +1,148 @@ +use std::time; + +use anyhow::Context; +use moq_transport::{ + cache::{fragment, segment, track}, + VarInt, +}; + +use chrono::prelude::*; + +pub struct Publisher { + track: track::Publisher, +} + +impl Publisher { + pub fn new(track: track::Publisher) -> Self { + Self { track } + } + + pub async fn run(mut self) -> anyhow::Result<()> { + let start = Utc::now(); + let mut now = start; + + // Just for fun, don't start at zero. + let mut sequence = start.minute(); + + loop { + let segment = self + .track + .create_segment(segment::Info { + sequence: VarInt::from_u32(sequence), + priority: 0, + expires: Some(time::Duration::from_secs(60)), + }) + .context("failed to create minute segment")?; + + sequence += 1; + + tokio::spawn(async move { + if let Err(err) = Self::send_segment(segment, now).await { + log::warn!("failed to send minute: {:?}", err); + } + }); + + let next = now + chrono::Duration::minutes(1); + let next = next.with_second(0).unwrap().with_nanosecond(0).unwrap(); + + let delay = (next - now).to_std().unwrap(); + tokio::time::sleep(delay).await; + + now = next; // just assume we didn't undersleep + } + } + + async fn send_segment(mut segment: segment::Publisher, mut now: DateTime) -> anyhow::Result<()> { + // Everything but the second. + let base = now.format("%Y-%m-%d %H:%M:").to_string(); + + segment + .fragment(VarInt::ZERO, base.len())? + .chunk(base.clone().into()) + .context("failed to write base")?; + + loop { + let delta = now.format("%S").to_string(); + let sequence = VarInt::from_u32(now.second() + 1); + + segment + .fragment(sequence, delta.len())? + .chunk(delta.clone().into()) + .context("failed to write delta")?; + + println!("{}{}", base, delta); + + let next = now + chrono::Duration::seconds(1); + let next = next.with_nanosecond(0).unwrap(); + + let delay = (next - now).to_std().unwrap(); + tokio::time::sleep(delay).await; + + // Get the current time again to check if we overslept + let next = Utc::now(); + if next.minute() != now.minute() { + return Ok(()); + } + + now = next; + } + } +} +pub struct Subscriber { + track: track::Subscriber, +} + +impl Subscriber { + pub fn new(track: track::Subscriber) -> Self { + Self { track } + } + + pub async fn run(mut self) -> anyhow::Result<()> { + while let Some(segment) = self.track.segment().await.context("failed to get segment")? { + log::debug!("got segment: {:?}", segment); + tokio::spawn(async move { + if let Err(err) = Self::recv_segment(segment).await { + log::warn!("failed to receive segment: {:?}", err); + } + }); + } + + Ok(()) + } + + async fn recv_segment(mut segment: segment::Subscriber) -> anyhow::Result<()> { + let first = segment + .fragment() + .await + .context("failed to get first fragment")? + .context("no fragments in segment")?; + + log::debug!("got first: {:?}", first); + + if first.sequence.into_inner() != 0 { + anyhow::bail!("first object must be zero; I'm not going to implement a reassembly buffer"); + } + + let base = Self::recv_fragment(first, Vec::new()).await?; + + log::debug!("read base: {:?}", String::from_utf8_lossy(&base)); + + while let Some(fragment) = segment.fragment().await? { + log::debug!("next fragment: {:?}", fragment); + let value = Self::recv_fragment(fragment, base.clone()).await?; + let str = String::from_utf8(value).context("invalid UTF-8")?; + + println!("{}", str); + } + + Ok(()) + } + + async fn recv_fragment(mut fragment: fragment::Subscriber, mut buf: Vec) -> anyhow::Result> { + while let Some(data) = fragment.chunk().await? { + buf.extend_from_slice(&data); + } + + Ok(buf) + } +} diff --git a/repos/moq-rs/moq-clock/src/main.rs b/repos/moq-rs/moq-clock/src/main.rs new file mode 100644 index 0000000..219dab7 --- /dev/null +++ b/repos/moq-rs/moq-clock/src/main.rs @@ -0,0 +1,123 @@ +use std::{fs, io, sync::Arc, time}; + +use anyhow::Context; +use clap::Parser; + +mod cli; +mod clock; + +use moq_transport::cache::broadcast; + +// TODO: clap complete + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + + // Disable tracing so we don't get a bunch of Quinn spam. + let tracer = tracing_subscriber::FmtSubscriber::builder() + .with_max_level(tracing::Level::WARN) + .finish(); + tracing::subscriber::set_global_default(tracer).unwrap(); + + let config = cli::Config::parse(); + + // Create a list of acceptable root certificates. + let mut roots = rustls::RootCertStore::empty(); + + if config.tls_root.is_empty() { + // Add the platform's native root certificates. + for cert in rustls_native_certs::load_native_certs().context("could not load platform certs")? { + roots + .add(&rustls::Certificate(cert.0)) + .context("failed to add root cert")?; + } + } else { + // Add the specified root certificates. + for root in &config.tls_root { + let root = fs::File::open(root).context("failed to open root cert file")?; + let mut root = io::BufReader::new(root); + + let root = rustls_pemfile::certs(&mut root).context("failed to read root cert")?; + anyhow::ensure!(root.len() == 1, "expected a single root cert"); + let root = rustls::Certificate(root[0].to_owned()); + + roots.add(&root).context("failed to add root cert")?; + } + } + + let mut tls_config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(roots) + .with_no_client_auth(); + + // Allow disabling TLS verification altogether. + if config.tls_disable_verify { + let noop = NoCertificateVerification {}; + tls_config.dangerous().set_certificate_verifier(Arc::new(noop)); + } + + tls_config.alpn_protocols = vec![webtransport_quinn::ALPN.to_vec()]; // this one is important + + let arc_tls_config = std::sync::Arc::new(tls_config); + let quinn_client_config = quinn::ClientConfig::new(arc_tls_config); + + let mut endpoint = quinn::Endpoint::client(config.bind)?; + endpoint.set_default_client_config(quinn_client_config); + + log::info!("connecting to relay: url={}", config.url); + + let session = webtransport_quinn::connect(&endpoint, &config.url) + .await + .context("failed to create WebTransport session")?; + + let (mut publisher, subscriber) = broadcast::new(""); // TODO config.namespace + + if config.publish { + let session = moq_transport::session::Client::publisher(session, subscriber) + .await + .context("failed to create MoQ Transport session")?; + + let publisher = publisher + .create_track(&config.track) + .context("failed to create clock track")?; + let clock = clock::Publisher::new(publisher); + + tokio::select! { + res = session.run() => res.context("session error")?, + res = clock.run() => res.context("clock error")?, + } + } else { + let session = moq_transport::session::Client::subscriber(session, publisher) + .await + .context("failed to create MoQ Transport session")?; + + let subscriber = subscriber + .get_track(&config.track) + .context("failed to get clock track")?; + let clock = clock::Subscriber::new(subscriber); + + tokio::select! { + res = session.run() => res.context("session error")?, + res = clock.run() => res.context("clock error")?, + } + } + + Ok(()) +} + +pub struct NoCertificateVerification {} + +impl rustls::client::ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &rustls::Certificate, + _intermediates: &[rustls::Certificate], + _server_name: &rustls::ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: time::SystemTime, + ) -> Result { + Ok(rustls::client::ServerCertVerified::assertion()) + } +} diff --git a/repos/moq-rs/moq-pub/Cargo.toml b/repos/moq-rs/moq-pub/Cargo.toml new file mode 100644 index 0000000..7fe5707 --- /dev/null +++ b/repos/moq-rs/moq-pub/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "moq-pub" +description = "Media over QUIC" +authors = ["Mike English", "Luke Curley"] +repository = "https://github.com/kixelated/moq-rs" +license = "MIT OR Apache-2.0" + +version = "0.1.0" +edition = "2021" + +keywords = ["quic", "http3", "webtransport", "media", "live"] +categories = ["multimedia", "network-programming", "web-programming"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +moq-transport = { path = "../moq-transport" } + +# QUIC +quinn = "0.10" +webtransport-quinn = "0.6.1" +url = "2" + +# Crypto +rustls = { version = "0.21", features = ["dangerous_configuration"] } +rustls-native-certs = "0.6" +rustls-pemfile = "1" + +# Async stuff +tokio = { version = "1", features = ["full"] } + +# CLI, logging, error handling +clap = { version = "4", features = ["derive"] } +log = { version = "0.4", features = ["std"] } +env_logger = "0.9" +mp4 = "0.14" +anyhow = { version = "1", features = ["backtrace"] } +serde_json = "1" +rfc6381-codec = "0.1" +tracing = "0.1" +tracing-subscriber = "0.3" + +[build-dependencies] +clap = { version = "4", features = ["derive"] } +clap_mangen = "0.2" +url = "2" diff --git a/repos/moq-rs/moq-pub/README.md b/repos/moq-rs/moq-pub/README.md new file mode 100644 index 0000000..fe7327a --- /dev/null +++ b/repos/moq-rs/moq-pub/README.md @@ -0,0 +1,28 @@ +# moq-pub + +A command line tool for publishing media via Media over QUIC (MoQ). + +Expects to receive fragmented MP4 via standard input and connect to a MOQT relay. + +``` +ffmpeg ... - | moq-pub https://localhost:4443 +``` + +### Invoking `moq-pub`: + +Here's how I'm currently testing things, with a local copy of Big Buck Bunny named `bbb_source.mp4`: + +``` +$ ffmpeg -hide_banner -v quiet -stream_loop -1 -re -i bbb_source.mp4 -an -f mp4 -movflags empty_moov+frag_every_frame+separate_moof+omit_tfhd_offset - | RUST_LOG=moq_pub=info moq-pub https://localhost:4443 +``` + +This relies on having `moq-relay` (the relay server) already running locally in another shell. + +Note also that we're dropping the audio track (`-an`) above until audio playback is stabilized on the `moq-js` side. + +### Known issues + +- Expects only one H.264/AVC1-encoded video track (catalog generation doesn't support audio tracks yet) +- Doesn't yet gracefully handle EOF - workaround: never stop sending it media (`-stream_loop -1`) +- Probably still full of lots of bugs +- Various other TODOs you can find in the code diff --git a/repos/moq-rs/moq-pub/build.rs b/repos/moq-rs/moq-pub/build.rs new file mode 100644 index 0000000..27f92f2 --- /dev/null +++ b/repos/moq-rs/moq-pub/build.rs @@ -0,0 +1,15 @@ +include!("src/cli.rs"); + +use clap::CommandFactory; + +fn main() -> Result<(), Box> { + let out_dir = std::path::PathBuf::from( + std::env::var_os("OUT_DIR").ok_or(std::io::Error::new(std::io::ErrorKind::NotFound, "OUT_DIR not found"))?, + ); + let cmd = Config::command(); + let man = clap_mangen::Man::new(cmd); + let mut buffer: Vec = Default::default(); + man.render(&mut buffer)?; + std::fs::write(out_dir.join("moq-pub.1"), buffer)?; + Ok(()) +} diff --git a/repos/moq-rs/moq-pub/publish.sh b/repos/moq-rs/moq-pub/publish.sh new file mode 100755 index 0000000..2b0be95 --- /dev/null +++ b/repos/moq-rs/moq-pub/publish.sh @@ -0,0 +1,10 @@ +live=$1 + +if [[ $live == "live" ]]; then + echo "live" + ffmpeg -hide_banner -v quiet -framerate 30 -f avfoundation -i "0" -vf scale=320:-1 -an -f mp4 -movflags empty_moov+frag_every_frame+separate_moof+omit_tfhd_offset - | RUST_LOG=moq_pub=info cargo run -- -n zafer.video/live_stream_cam -i - +else + echo "not live" + ffmpeg -hide_banner -v quiet -stream_loop 100 -re -i ../media/bbb_source.mp4 -an -f mp4 -movflags empty_moov+frag_every_frame+separate_moof+omit_tfhd_offset - | RUST_LOG=moq_pub=info cargo run -- -n zafer.video/vod -i - + exit 0 +fi diff --git a/repos/moq-rs/moq-pub/src/cli.rs b/repos/moq-rs/moq-pub/src/cli.rs new file mode 100644 index 0000000..0d21e58 --- /dev/null +++ b/repos/moq-rs/moq-pub/src/cli.rs @@ -0,0 +1,53 @@ +use clap::Parser; +use std::{net, path}; +use url::Url; + +#[derive(Parser, Clone, Debug)] +pub struct Config { + /// Listen for UDP packets on the given address. + #[arg(long, default_value = "[::]:0")] + pub bind: net::SocketAddr, + + /// Advertise this frame rate in the catalog (informational) + // TODO auto-detect this from the input when not provided + #[arg(long, default_value = "24")] + pub fps: u8, + + /// Advertise this bit rate in the catalog (informational) + // TODO auto-detect this from the input when not provided + #[arg(long, default_value = "1500000")] + pub bitrate: u32, + + /// Advertise this bit rates in the catalog (informational) + // A comma-separated list of bitrates + #[arg(long, default_value = "1500000")] + pub bitrates: String, + + /// Connect to the given URL starting with https:// + #[arg(value_parser = moq_url)] + pub url: Url, + + /// Use the TLS root CA at this path, encoded as PEM. + /// + /// This value can be provided multiple times for multiple roots. + /// If this is empty, system roots will be used instead + #[arg(long)] + pub tls_root: Vec, + + /// Danger: Disable TLS certificate verification. + /// + /// Fine for local development, but should be used in caution in production. + #[arg(long)] + pub tls_disable_verify: bool, +} + +fn moq_url(s: &str) -> Result { + let url = Url::try_from(s).map_err(|e| e.to_string())?; + + // Make sure the scheme is moq + if url.scheme() != "https" { + return Err("url scheme must be https:// for WebTransport".to_string()); + } + + Ok(url) +} diff --git a/repos/moq-rs/moq-pub/src/main.rs b/repos/moq-rs/moq-pub/src/main.rs new file mode 100644 index 0000000..d00a89e --- /dev/null +++ b/repos/moq-rs/moq-pub/src/main.rs @@ -0,0 +1,107 @@ +use std::{fs, io, sync::Arc, time}; + +use anyhow::Context; +use clap::Parser; + +mod cli; +use cli::*; + +mod media; +use media::*; + +use moq_transport::cache::broadcast; + +// TODO: clap complete + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + + // Disable tracing so we don't get a bunch of Quinn spam. + let tracer = tracing_subscriber::FmtSubscriber::builder() + .with_max_level(tracing::Level::WARN) + .finish(); + tracing::subscriber::set_global_default(tracer).unwrap(); + + let config = Config::parse(); + + let (publisher, subscriber) = broadcast::new(""); + let mut media = Media::new(&config, publisher).await?; + + // Create a list of acceptable root certificates. + let mut roots = rustls::RootCertStore::empty(); + + if config.tls_root.is_empty() { + // Add the platform's native root certificates. + for cert in rustls_native_certs::load_native_certs().context("could not load platform certs")? { + roots + .add(&rustls::Certificate(cert.0)) + .context("failed to add root cert")?; + } + } else { + // Add the specified root certificates. + for root in &config.tls_root { + let root = fs::File::open(root).context("failed to open root cert file")?; + let mut root = io::BufReader::new(root); + + let root = rustls_pemfile::certs(&mut root).context("failed to read root cert")?; + anyhow::ensure!(root.len() == 1, "expected a single root cert"); + let root = rustls::Certificate(root[0].to_owned()); + + roots.add(&root).context("failed to add root cert")?; + } + } + + let mut tls_config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(roots) + .with_no_client_auth(); + + // Allow disabling TLS verification altogether. + if config.tls_disable_verify { + let noop = NoCertificateVerification {}; + tls_config.dangerous().set_certificate_verifier(Arc::new(noop)); + } + + tls_config.alpn_protocols = vec![webtransport_quinn::ALPN.to_vec()]; // this one is important + + let arc_tls_config = std::sync::Arc::new(tls_config); + let quinn_client_config = quinn::ClientConfig::new(arc_tls_config); + + let mut endpoint = quinn::Endpoint::client(config.bind)?; + endpoint.set_default_client_config(quinn_client_config); + + log::info!("connecting to relay: url={}", config.url); + + let session = webtransport_quinn::connect(&endpoint, &config.url) + .await + .context("failed to create WebTransport session")?; + + let session = moq_transport::session::Client::publisher(session, subscriber) + .await + .context("failed to create MoQ Transport session")?; + + // TODO run a task that returns a 404 for all unknown subscriptions. + tokio::select! { + res = session.run() => res.context("session error")?, + res = media.run() => res.context("media error")?, + } + + Ok(()) +} + +pub struct NoCertificateVerification {} + +impl rustls::client::ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &rustls::Certificate, + _intermediates: &[rustls::Certificate], + _server_name: &rustls::ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: time::SystemTime, + ) -> Result { + Ok(rustls::client::ServerCertVerified::assertion()) + } +} diff --git a/repos/moq-rs/moq-pub/src/media.rs b/repos/moq-rs/moq-pub/src/media.rs new file mode 100644 index 0000000..38c130d --- /dev/null +++ b/repos/moq-rs/moq-pub/src/media.rs @@ -0,0 +1,488 @@ +use crate::cli::Config; +use anyhow::{self, Context}; +use moq_transport::cache::{broadcast, fragment, segment, track}; +use moq_transport::VarInt; +use mp4::{self, ReadBox, WriteBox}; +use serde_json::json; +use std::cmp::max; +use std::collections::HashMap; +use std::io::{BufWriter, Cursor}; +use std::time; +use tokio::io::AsyncReadExt; + +pub struct Media { + // We hold on to publisher so we don't close then while media is still being published. + _broadcast: broadcast::Publisher, + _catalog: track::Publisher, + _init: track::Publisher, + + // Tracks based on their track ID. + tracks: HashMap, +} + +impl Media { + pub async fn new(_config: &Config, mut broadcast: broadcast::Publisher) -> anyhow::Result { + let mut stdin = tokio::io::stdin(); + + let ftyp: Vec; + loop { + match read_atom(&mut stdin).await { + Ok(atom) => { + ftyp = atom; + break; + } + Err(e) => { + log::warn!("could not parse ftyp atom: {}", e); + tokio::time::sleep(time::Duration::from_millis(100)).await; + continue; + } + } + } + + anyhow::ensure!(&ftyp[4..8] == b"ftyp", "expected ftyp atom"); + + let moov = read_atom(&mut stdin).await?; + anyhow::ensure!(&moov[4..8] == b"moov", "expected moov atom"); + + let mut init = ftyp; + init.extend(&moov); + + // We're going to parse the moov box. + // We have to read the moov box header to correctly advance the cursor for the mp4 crate. + let mut moov_reader = Cursor::new(&moov); + let moov_header = mp4::BoxHeader::read(&mut moov_reader)?; + + // Parse the moov box so we can detect the timescales for each track. + let moov = mp4::MoovBox::read_box(&mut moov_reader, moov_header.size)?; + + // Create the catalog track with a single segment. + let mut init_track = broadcast.create_track("0.mp4")?; + let init_segment = init_track.create_segment(segment::Info { + sequence: VarInt::ZERO, + priority: 0, + expires: None, + })?; + + // Create a single fragment, optionally setting the size + let mut init_fragment = init_segment.final_fragment(VarInt::ZERO)?; + + init_fragment.chunk(init.into())?; + + let mut tracks = HashMap::new(); + + for trak in &moov.traks { + let id = trak.tkhd.track_id; + let name = format!("{}.m4s", id); + + let timescale = track_timescale(&moov, id); + + // Store the track publisher in a map so we can update it later. + let track = broadcast.create_track(&name)?; + let track = Track::new(track, timescale); + tracks.insert(id, track); + } + + log::debug!("tracks: {:?}", tracks.keys().collect::>()); + + let mut catalog = broadcast.create_track(".catalog")?; + + // Create the catalog track + Self::serve_catalog(&mut catalog, &init_track.name, &moov, &_config)?; + + Ok(Media { + _broadcast: broadcast, + _catalog: catalog, + _init: init_track, + tracks, + }) + } + + pub async fn run(&mut self) -> anyhow::Result<()> { + let mut stdin = tokio::io::stdin(); + // The current track name + let mut current = None; + + loop { + let atom: Vec; + + loop { + match read_atom(&mut stdin).await { + Ok(at) => { + atom = at; + break; + } + Err(e) => { + log::warn!("skipping atom: {}", e); + tokio::time::sleep(time::Duration::from_millis(100)).await; + continue; + } + } + } + + let mut reader = Cursor::new(&atom); + let header = mp4::BoxHeader::read(&mut reader)?; + + match header.name { + mp4::BoxType::MoofBox => { + let moof = mp4::MoofBox::read_box(&mut reader, header.size).context("failed to read MP4")?; + + // Process the moof. + let fragment = Fragment::new(moof)?; + + // Get the track for this moof. + let track = self.tracks.get_mut(&fragment.track).context("failed to find track")?; + + // Save the track ID for the next iteration, which must be a mdat. + anyhow::ensure!(current.is_none(), "multiple moof atoms"); + current.replace(fragment.track); + + // Publish the moof header, creating a new segment if it's a keyframe. + track.header(atom, fragment).context("failed to publish moof")?; + } + mp4::BoxType::MdatBox => { + // Get the track ID from the previous moof. + let track = current.take().context("missing moof")?; + let track = self.tracks.get_mut(&track).context("failed to find track")?; + + // Publish the mdat atom. + track.data(atom).context("failed to publish mdat")?; + } + mp4::BoxType::PrftBox => { + let prft = mp4::PrftBox::read_box(&mut reader, header.size).context("failed to read MP4")?; + + // Put this prft to all tracks + for (track_id, track) in self.tracks.iter_mut() { + let mut t_prft = prft.clone(); + t_prft.reference_track_id = *track_id; + track.last_prft = t_prft; + } + } + + _ => { + // Skip unknown atoms + } + } + } + } + + fn serve_catalog( + track: &mut track::Publisher, + init_track_name: &str, + moov: &mp4::MoovBox, + config: &Config, + ) -> Result<(), anyhow::Error> { + let segment = track.create_segment(segment::Info { + sequence: VarInt::ZERO, + priority: 0, + expires: None, + })?; + + let bitrates = config.bitrates.split(',').collect::>(); + + let mut tracks = Vec::new(); + let mut counter = 0; + + for trak in &moov.traks { + log::debug!("trak: {:?}", trak); + let mut track = json!({ + "container": "mp4", + "init_track": init_track_name, + "data_track": format!("{}.m4s", trak.tkhd.track_id), + }); + + let stsd = &trak.mdia.minf.stbl.stsd; + if let Some(avc1) = &stsd.avc1 { + // avc1[.PPCCLL] + // + // let profile = 0x64; + // let constraints = 0x00; + // let level = 0x1f; + let profile = avc1.avcc.avc_profile_indication; + let constraints = avc1.avcc.profile_compatibility; // Not 100% certain here, but it's 0x00 on my current test video + let level = avc1.avcc.avc_level_indication; + + let width = avc1.width; + let height = avc1.height; + + let bitrate = bitrates[counter]; + + let codec = rfc6381_codec::Codec::avc1(profile, constraints, level); + let codec_str = codec.to_string(); + + track["kind"] = json!("video"); + track["codec"] = json!(codec_str); + track["width"] = json!(width); + track["height"] = json!(height); + track["bit_rate"] = json!(bitrate.parse::()?); + + counter += 1; + } else if let Some(_hev1) = &stsd.hev1 { + // TODO https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L106 + anyhow::bail!("HEVC not yet supported") + } else if let Some(mp4a) = &stsd.mp4a { + let desc = &mp4a + .esds + .as_ref() + .context("missing esds box for MP4a")? + .es_desc + .dec_config; + let codec_str = format!("mp4a.{:02x}.{}", desc.object_type_indication, desc.dec_specific.profile); + + track["kind"] = json!("audio"); + track["codec"] = json!(codec_str); + track["channel_count"] = json!(mp4a.channelcount); + track["sample_rate"] = json!(mp4a.samplerate.value()); + track["sample_size"] = json!(mp4a.samplesize); + + let bitrate = max(desc.max_bitrate, desc.avg_bitrate); + if bitrate > 0 { + track["bit_rate"] = json!(bitrate); + } + } else if let Some(vp09) = &stsd.vp09 { + // https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L238 + let vpcc = &vp09.vpcc; + let codec_str = format!("vp09.0.{:02x}.{:02x}.{:02x}", vpcc.profile, vpcc.level, vpcc.bit_depth); + + track["kind"] = json!("video"); + track["codec"] = json!(codec_str); + track["width"] = json!(vp09.width); // no idea if this needs to be multiplied + track["height"] = json!(vp09.height); // no idea if this needs to be multiplied + + // TODO Test if this actually works; I'm just guessing based on mp4box.js + anyhow::bail!("VP9 not yet supported") + } else { + // TODO add av01 support: https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L251 + anyhow::bail!("unknown codec for track: {}", trak.tkhd.track_id); + } + + tracks.push(track); + } + + let catalog = json!({ + "tracks": tracks + }); + + let catalog_str = serde_json::to_string_pretty(&catalog)?; + log::info!("catalog: {}", catalog_str); + + // Create a single fragment for the segment. + let mut fragment = segment.final_fragment(VarInt::ZERO)?; + + // Add the segment and add the fragment. + fragment.chunk(catalog_str.into())?; + + Ok(()) + } +} + +// Read a full MP4 atom into a vector. +async fn read_atom(reader: &mut R) -> anyhow::Result> { + // Read the 8 bytes for the size + type + let mut buf = [0u8; 8]; + if reader.read_exact(&mut buf).await.is_err() { + return Err(anyhow::anyhow!("failed to read atom header")); + } + + // Convert the first 4 bytes into the size. + let size = u32::from_be_bytes(buf[0..4].try_into()?) as u64; + + let mut raw = buf.to_vec(); + + let mut limit = match size { + // Runs until the end of the file. + 0 => reader.take(u64::MAX), + + // The next 8 bytes are the extended size to be used instead. + 1 => { + reader.read_exact(&mut buf).await?; + let size_large = u64::from_be_bytes(buf); + anyhow::ensure!(size_large >= 16, "impossible extended box size: {}", size_large); + + reader.take(size_large - 16) + } + + 2..=7 => { + anyhow::bail!("impossible box size: {}", size) + } + + size => reader.take(size - 8), + }; + + // Append to the vector and return it. + let _read_bytes = limit.read_to_end(&mut raw).await?; + + Ok(raw) +} + +struct Track { + // The track we're producing + track: track::Publisher, + + // The current segment + current: Option, + + // Last PRFT box for this track + last_prft: mp4::PrftBox, + + // The number of units per second. + timescale: u64, + + // The number of segments produced. + sequence: u64, +} + +impl Track { + fn new(track: track::Publisher, timescale: u64) -> Self { + Self { + track, + sequence: 0, + current: None, + last_prft: mp4::PrftBox::default(), + timescale, + } + } + + pub fn header(&mut self, raw: Vec, fragment: Fragment) -> anyhow::Result<()> { + // Apply the last PRFT box to the raw atom + let mut prft_buffer = BufWriter::new(Vec::new()); + self.last_prft.write_box(&mut prft_buffer)?; + let prft = prft_buffer.into_inner()?; + + if let Some(current) = self.current.as_mut() { + if !fragment.keyframe { + // Use the existing segment + current.chunk(prft.into())?; + current.chunk(raw.into())?; + return Ok(()); + } + } + + // Otherwise make a new segment + + // Compute the timestamp in milliseconds. + // Overflows after 583 million years, so we're fine. + let timestamp: u32 = fragment + .timestamp(self.timescale) + .as_millis() + .try_into() + .context("timestamp too large")?; + + // Create a new segment. + let segment = self.track.create_segment(segment::Info { + sequence: VarInt::try_from(self.sequence).context("sequence too large")?, + + // Newer segments are higher priority + priority: u32::MAX.checked_sub(timestamp).context("priority too large")?, + // priority: self.sequence.try_into().unwrap(), + + // Delete segments after 10s. + expires: Some(time::Duration::from_secs(10)), + })?; + + log::info!("serving segment | track:{:?} sequence:{:?} priority:{:?}", self.track.name, segment.sequence, segment.priority); + + // Create a single fragment for the segment that we will keep appending. + let mut fragment = segment.final_fragment(VarInt::ZERO)?; + + self.sequence += 1; + + // Insert the raw atom into the segment. + fragment.chunk(prft.into())?; + fragment.chunk(raw.into())?; + + // Save for the next iteration + self.current = Some(fragment); + + Ok(()) + } + + pub fn data(&mut self, raw: Vec) -> anyhow::Result<()> { + let fragment = self.current.as_mut().context("missing current fragment")?; + fragment.chunk(raw.into())?; + + Ok(()) + } +} + +struct Fragment { + // The track for this fragment. + track: u32, + + // The timestamp of the first sample in this fragment, in timescale units. + timestamp: u64, + + // True if this fragment is a keyframe. + keyframe: bool, +} + +impl Fragment { + fn new(moof: mp4::MoofBox) -> anyhow::Result { + // We can't split the mdat atom, so this is impossible to support + anyhow::ensure!(moof.trafs.len() == 1, "multiple tracks per moof atom"); + let track = moof.trafs[0].tfhd.track_id; + + // Parse the moof to get some timing information to sleep. + let timestamp = sample_timestamp(&moof).expect("couldn't find timestamp"); + + // Detect if we should start a new segment. + let keyframe = sample_keyframe(&moof); + + Ok(Self { + track, + timestamp, + keyframe, + }) + } + + // Convert from timescale units to a duration. + fn timestamp(&self, timescale: u64) -> time::Duration { + time::Duration::from_millis(1000 * self.timestamp / timescale) + } +} + +fn sample_timestamp(moof: &mp4::MoofBox) -> Option { + Some(moof.trafs.first()?.tfdt.as_ref()?.base_media_decode_time) +} + +fn sample_keyframe(moof: &mp4::MoofBox) -> bool { + for traf in &moof.trafs { + // TODO trak default flags if this is None + let default_flags = traf.tfhd.default_sample_flags.unwrap_or_default(); + let trun = match &traf.trun { + Some(t) => t, + None => return false, + }; + + for i in 0..trun.sample_count { + let mut flags = match trun.sample_flags.get(i as usize) { + Some(f) => *f, + None => default_flags, + }; + + if i == 0 && trun.first_sample_flags.is_some() { + flags = trun.first_sample_flags.unwrap(); + } + + // https://chromium.googlesource.com/chromium/src/media/+/master/formats/mp4/track_run_iterator.cc#177 + let keyframe = (flags >> 24) & 0x3 == 0x2; // kSampleDependsOnNoOther + let non_sync = (flags >> 16) & 0x1 == 0x1; // kSampleIsNonSyncSample + + if keyframe && !non_sync { + return true; + } + } + } + + false +} + +// Find the timescale for the given track. +fn track_timescale(moov: &mp4::MoovBox, track_id: u32) -> u64 { + let trak = moov + .traks + .iter() + .find(|trak| trak.tkhd.track_id == track_id) + .expect("failed to find trak"); + + trak.mdia.mdhd.timescale as u64 +} diff --git a/repos/moq-rs/moq-quinn/cert/.gitignore b/repos/moq-rs/moq-quinn/cert/.gitignore new file mode 100644 index 0000000..9879661 --- /dev/null +++ b/repos/moq-rs/moq-quinn/cert/.gitignore @@ -0,0 +1,3 @@ +*.crt +*.key +*.hex \ No newline at end of file diff --git a/repos/moq-rs/moq-quinn/cert/generate b/repos/moq-rs/moq-quinn/cert/generate new file mode 100755 index 0000000..5e90ca5 --- /dev/null +++ b/repos/moq-rs/moq-quinn/cert/generate @@ -0,0 +1,18 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" + +# Generate a new RSA key/cert for local development +HOST="localhost" +CRT="$HOST.crt" +KEY="$HOST.key" + +# Install the system certificate if it's not already +# NOTE: The ecdsa flag does nothing but I wish it did +go run filippo.io/mkcert -ecdsa -install + +# Generate a new certificate for localhost +# This fork of mkcert supports the -days flag. +# TODO remove the -days flag when Chrome accepts self-signed certs. +go run filippo.io/mkcert -ecdsa -days 10 -cert-file "$CRT" -key-file "$KEY" localhost 127.0.0.1 ::1 diff --git a/repos/moq-rs/moq-quinn/cert/go.mod b/repos/moq-rs/moq-quinn/cert/go.mod new file mode 100644 index 0000000..ac3c3d0 --- /dev/null +++ b/repos/moq-rs/moq-quinn/cert/go.mod @@ -0,0 +1,14 @@ +module github.com/kixelated/warp/cert + +go 1.18 + +require ( + filippo.io/mkcert v1.4.4 // indirect + golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 // indirect + golang.org/x/text v0.3.7 // indirect + howett.net/plist v1.0.0 // indirect + software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect +) + +replace filippo.io/mkcert => github.com/kixelated/mkcert v1.4.4-days diff --git a/repos/moq-rs/moq-quinn/cert/go.sum b/repos/moq-rs/moq-quinn/cert/go.sum new file mode 100644 index 0000000..94fb636 --- /dev/null +++ b/repos/moq-rs/moq-quinn/cert/go.sum @@ -0,0 +1,22 @@ +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/kixelated/mkcert v1.4.4-days h1:T2P9W4ruEfgLHOl5UljPwh0d79FbFWkSe2IONcUBxG8= +github.com/kixelated/mkcert v1.4.4-days/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 h1:yssD99+7tqHWO5Gwh81phT+67hg+KttniBr6UnEXOY8= +golang.org/x/net v0.0.0-20220421235706-1d1ef9303861/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= +software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= diff --git a/repos/moq-rs/moq-relay/Cargo.toml b/repos/moq-rs/moq-relay/Cargo.toml new file mode 100644 index 0000000..78b4b67 --- /dev/null +++ b/repos/moq-rs/moq-relay/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "moq-relay" +description = "Media over QUIC" +authors = ["Luke Curley"] +repository = "https://github.com/kixelated/moq-rs" +license = "MIT OR Apache-2.0" + +version = "0.1.0" +edition = "2021" + +keywords = ["quic", "http3", "webtransport", "media", "live"] +categories = ["multimedia", "network-programming", "web-programming"] + +[dependencies] +moq-transport = { path = "../moq-transport" } +moq-api = { path = "../moq-api" } + +# QUIC +quinn = "0.10" +webtransport-quinn = "0.6.1" +url = "2" + +# Crypto +ring = "0.16" +rustls = { version = "0.21", features = ["dangerous_configuration"] } +rustls-pemfile = "1" +rustls-native-certs = "0.6" +webpki = "0.22" + +# Async stuff +tokio = { version = "1", features = ["full"] } + +# Web server to serve the fingerprint +axum = { version = "0.6", features = ["tokio"] } +axum-server = { version = "0.5", features = ["tls-rustls"] } +hex = "0.4" +tower-http = { version = "0.4", features = ["cors"] } + +# Error handling +anyhow = { version = "1", features = ["backtrace"] } +thiserror = "1" + +# CLI +clap = { version = "4", features = ["derive"] } + +# Logging +log = { version = "0.4", features = ["std"] } +env_logger = "0.9" +tracing = "0.1" +tracing-subscriber = "0.3" diff --git a/repos/moq-rs/moq-relay/README.md b/repos/moq-rs/moq-relay/README.md new file mode 100644 index 0000000..04d4c2e --- /dev/null +++ b/repos/moq-rs/moq-relay/README.md @@ -0,0 +1,17 @@ +# moq-relay + +A server that connects publishing clients to subscribing clients. +All subscriptions are deduplicated and cached, so that a single publisher can serve many subscribers. + +## Usage + +The publisher must choose a unique name for their broadcast, sent as the WebTransport path when connecting to the server. +We currently do a dumb string comparison, so capatilization matters as do slashes. + +For example: `CONNECT https://relay.quic.video/BigBuckBunny` + +The MoqTransport handshake includes a `role` parameter, which must be `publisher` or `subscriber`. +The specification allows a `both` role but you'll get an error. + +You can have one publisher and any number of subscribers connected to the same path. +If the publisher disconnects, then all subscribers receive an error and will not get updates, even if a new publisher reuses the path. diff --git a/repos/moq-rs/moq-relay/src/config.rs b/repos/moq-rs/moq-relay/src/config.rs new file mode 100644 index 0000000..20c0420 --- /dev/null +++ b/repos/moq-rs/moq-relay/src/config.rs @@ -0,0 +1,55 @@ +use std::{net, path}; +use url::Url; + +use clap::Parser; + +/// Search for a pattern in a file and display the lines that contain it. +#[derive(Parser, Clone)] +pub struct Config { + /// Listen on this address + #[arg(long, default_value = "[::]:4443")] + pub listen: net::SocketAddr, + + /// Use the certificates at this path, encoded as PEM. + /// + /// You can use this option multiple times for multiple certificates. + /// The first match for the provided SNI will be used, otherwise the last cert will be used. + /// You also need to provide the private key multiple times via `key``. + #[arg(long)] + pub tls_cert: Vec, + + /// Use the private key at this path, encoded as PEM. + /// + /// There must be a key for every certificate provided via `cert`. + #[arg(long)] + pub tls_key: Vec, + + /// Use the TLS root at this path, encoded as PEM. + /// + /// This value can be provided multiple times for multiple roots. + /// If this is empty, system roots will be used instead + #[arg(long)] + pub tls_root: Vec, + + /// Danger: Disable TLS certificate verification. + /// + /// Fine for local development and between relays, but should be used in caution in production. + #[arg(long)] + pub tls_disable_verify: bool, + + /// Optional: Use the moq-api via HTTP to store origin information. + #[arg(long)] + pub api: Option, + + /// Our internal address which we advertise to other origins. + /// We use QUIC, so the certificate must be valid for this address. + /// This needs to be prefixed with https:// to use WebTransport. + /// This is only used when --api is set and only for publishing broadcasts. + #[arg(long)] + pub api_node: Option, + + /// Enable development mode. + /// Currently, this only listens on HTTPS and serves /fingerprint, for self-signed certificates + #[arg(long, action)] + pub dev: bool, +} diff --git a/repos/moq-rs/moq-relay/src/error.rs b/repos/moq-rs/moq-relay/src/error.rs new file mode 100644 index 0000000..b943a93 --- /dev/null +++ b/repos/moq-rs/moq-relay/src/error.rs @@ -0,0 +1,51 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum RelayError { + #[error("transport error: {0}")] + Transport(#[from] moq_transport::session::SessionError), + + #[error("cache error: {0}")] + Cache(#[from] moq_transport::cache::CacheError), + + #[error("api error: {0}")] + MoqApi(#[from] moq_api::ApiError), + + #[error("url error: {0}")] + Url(#[from] url::ParseError), + + #[error("webtransport client error: {0}")] + WebTransportClient(#[from] webtransport_quinn::ClientError), + + #[error("webtransport server error: {0}")] + WebTransportServer(#[from] webtransport_quinn::ServerError), + + #[error("missing node")] + MissingNode, +} + +impl moq_transport::MoqError for RelayError { + fn code(&self) -> u32 { + match self { + Self::Transport(err) => err.code(), + Self::Cache(err) => err.code(), + Self::MoqApi(_err) => 504, + Self::Url(_) => 500, + Self::MissingNode => 500, + Self::WebTransportClient(_) => 504, + Self::WebTransportServer(_) => 500, + } + } + + fn reason(&self) -> String { + match self { + Self::Transport(err) => format!("transport error: {}", err.reason()), + Self::Cache(err) => format!("cache error: {}", err.reason()), + Self::MoqApi(err) => format!("api error: {}", err), + Self::Url(err) => format!("url error: {}", err), + Self::MissingNode => "missing node".to_owned(), + Self::WebTransportServer(err) => format!("upstream server error: {}", err), + Self::WebTransportClient(err) => format!("upstream client error: {}", err), + } + } +} diff --git a/repos/moq-rs/moq-relay/src/main.rs b/repos/moq-rs/moq-relay/src/main.rs new file mode 100644 index 0000000..8239d7e --- /dev/null +++ b/repos/moq-rs/moq-relay/src/main.rs @@ -0,0 +1,51 @@ +use anyhow::Context; +use clap::Parser; + +mod config; +mod error; +mod origin; +mod quic; +mod session; +mod tls; +mod web; + +pub use config::*; +pub use error::*; +pub use origin::*; +pub use quic::*; +pub use session::*; +pub use tls::*; +pub use web::*; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + + // Disable tracing so we don't get a bunch of Quinn spam. + let tracer = tracing_subscriber::FmtSubscriber::builder() + .with_max_level(tracing::Level::WARN) + .finish(); + tracing::subscriber::set_global_default(tracer).unwrap(); + + let config = Config::parse(); + let tls = Tls::load(&config)?; + + // Create a QUIC server for media. + let quic = Quic::new(config.clone(), tls.clone()) + .await + .context("failed to create server")?; + + // Create the web server if the --dev flag was set. + // This is currently only useful in local development so it's not enabled by default. + if config.dev { + let web = Web::new(config, tls); + + // Unfortunately we can't use preconditions because Tokio still executes the branch; just ignore the result + tokio::select! { + res = quic.serve() => res.context("failed to run quic server"), + res = web.serve() => res.context("failed to run web server"), + } + } else { + quic.serve().await.context("failed to run quic server") + } +} diff --git a/repos/moq-rs/moq-relay/src/origin.rs b/repos/moq-rs/moq-relay/src/origin.rs new file mode 100644 index 0000000..7a665c9 --- /dev/null +++ b/repos/moq-rs/moq-relay/src/origin.rs @@ -0,0 +1,216 @@ +use std::ops::{Deref, DerefMut}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex, Weak}, +}; + +use moq_api::ApiError; +use moq_transport::cache::{broadcast, CacheError}; +use url::Url; + +use tokio::time; + +use crate::RelayError; + +#[derive(Clone)] +pub struct Origin { + // An API client used to get/set broadcasts. + // If None then we never use a remote origin. + // TODO: Stub this out instead. + api: Option, + + // The internal address of our node. + // If None then we can never advertise ourselves as an origin. + // TODO: Stub this out instead. + node: Option, + + // A map of active broadcasts by ID. + cache: Arc>>>, + + // A QUIC endpoint we'll use to fetch from other origins. + quic: quinn::Endpoint, +} + +impl Origin { + pub fn new(api: Option, node: Option, quic: quinn::Endpoint) -> Self { + Self { + api, + node, + cache: Default::default(), + quic, + } + } + + /// Create a new broadcast with the given ID. + /// + /// Publisher::run needs to be called to periodically refresh the origin cache. + pub async fn publish(&mut self, id: &str) -> Result { + let (publisher, subscriber) = broadcast::new(id); + + let subscriber = { + let mut cache = self.cache.lock().unwrap(); + + // Check if the broadcast already exists. + // TODO This is racey, because a new publisher could be created while existing subscribers are still active. + if cache.contains_key(id) { + return Err(CacheError::Duplicate.into()); + } + + // Create subscriber that will remove from the cache when dropped. + let subscriber = Arc::new(Subscriber { + broadcast: subscriber, + origin: self.clone(), + }); + + cache.insert(id.to_string(), Arc::downgrade(&subscriber)); + + subscriber + }; + + // Create a publisher that constantly updates itself as the origin in moq-api. + // It holds a reference to the subscriber to prevent dropping early. + let mut publisher = Publisher { + broadcast: publisher, + subscriber, + api: None, + }; + + // Insert the publisher into the database. + if let Some(api) = self.api.as_mut() { + // Make a URL for the broadcast. + let url = self.node.as_ref().ok_or(RelayError::MissingNode)?.clone().join(id)?; + let origin = moq_api::Origin { url }; + api.set_origin(id, &origin).await?; + + // Refresh every 5 minutes + publisher.api = Some((api.clone(), origin)); + } + + Ok(publisher) + } + + pub fn subscribe(&self, id: &str) -> Arc { + let mut cache = self.cache.lock().unwrap(); + + if let Some(broadcast) = cache.get(id) { + if let Some(broadcast) = broadcast.upgrade() { + return broadcast; + } + } + + let (publisher, subscriber) = broadcast::new(id); + let subscriber = Arc::new(Subscriber { + broadcast: subscriber, + origin: self.clone(), + }); + + cache.insert(id.to_string(), Arc::downgrade(&subscriber)); + + let mut this = self.clone(); + let id = id.to_string(); + + // Rather than fetching from the API and connecting via QUIC inline, we'll spawn a task to do it. + // This way we could stop polling this session and it won't impact other session. + // It also means we'll only connect the API and QUIC once if N subscribers suddenly show up. + // However, the downside is that we don't return an error immediately. + // If that's important, it can be done but it gets a bit racey. + tokio::spawn(async move { + if let Err(err) = this.serve(&id, publisher).await { + log::warn!("failed to serve remote broadcast: id={} err={}", id, err); + } + }); + + subscriber + } + + async fn serve(&mut self, id: &str, publisher: broadcast::Publisher) -> Result<(), RelayError> { + log::debug!("finding origin: id={}", id); + + // Fetch the origin from the API. + let origin = self + .api + .as_mut() + .ok_or(CacheError::NotFound)? + .get_origin(id) + .await? + .ok_or(CacheError::NotFound)?; + + log::debug!("fetching from origin: id={} url={}", id, origin.url); + + // Establish the webtransport session. + let session = webtransport_quinn::connect(&self.quic, &origin.url).await?; + let session = moq_transport::session::Client::subscriber(session, publisher).await?; + + session.run().await?; + + Ok(()) + } +} + +pub struct Subscriber { + pub broadcast: broadcast::Subscriber, + + origin: Origin, +} + +impl Drop for Subscriber { + fn drop(&mut self) { + self.origin.cache.lock().unwrap().remove(&self.broadcast.id); + } +} + +impl Deref for Subscriber { + type Target = broadcast::Subscriber; + + fn deref(&self) -> &Self::Target { + &self.broadcast + } +} + +pub struct Publisher { + pub broadcast: broadcast::Publisher, + + api: Option<(moq_api::Client, moq_api::Origin)>, + + #[allow(dead_code)] + subscriber: Arc, +} + +impl Publisher { + pub async fn run(&mut self) -> Result<(), ApiError> { + // Every 5m tell the API we're still alive. + // TODO don't hard-code these values + let mut interval = time::interval(time::Duration::from_secs(60 * 5)); + + loop { + if let Some((api, origin)) = self.api.as_mut() { + api.patch_origin(&self.broadcast.id, origin).await?; + } + + // TODO move to start of loop; this is just for testing + interval.tick().await; + } + } + + pub async fn close(&mut self) -> Result<(), ApiError> { + if let Some((api, _)) = self.api.as_mut() { + api.delete_origin(&self.broadcast.id).await?; + } + + Ok(()) + } +} + +impl Deref for Publisher { + type Target = broadcast::Publisher; + + fn deref(&self) -> &Self::Target { + &self.broadcast + } +} + +impl DerefMut for Publisher { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.broadcast + } +} diff --git a/repos/moq-rs/moq-relay/src/quic.rs b/repos/moq-rs/moq-relay/src/quic.rs new file mode 100644 index 0000000..563ad56 --- /dev/null +++ b/repos/moq-rs/moq-relay/src/quic.rs @@ -0,0 +1,85 @@ +use std::{sync::Arc, time}; + +use anyhow::Context; + +use tokio::task::JoinSet; + +use crate::{Config, Origin, Session, Tls}; + +pub struct Quic { + quic: quinn::Endpoint, + + // The active connections. + conns: JoinSet>, + + // The map of active broadcasts by path. + origin: Origin, +} + +impl Quic { + // Create a QUIC endpoint that can be used for both clients and servers. + pub async fn new(config: Config, tls: Tls) -> anyhow::Result { + let mut client_config = tls.client.clone(); + let mut server_config = tls.server.clone(); + client_config.alpn_protocols = vec![webtransport_quinn::ALPN.to_vec()]; + server_config.alpn_protocols = vec![webtransport_quinn::ALPN.to_vec()]; + + // Enable BBR congestion control + // TODO validate the implementation + let mut transport_config = quinn::TransportConfig::default(); + transport_config.max_idle_timeout(Some(time::Duration::from_secs(10).try_into().unwrap())); + transport_config.keep_alive_interval(Some(time::Duration::from_secs(4))); // TODO make this smarter + transport_config.congestion_controller_factory(Arc::new(quinn::congestion::BbrConfig::default())); + transport_config.mtu_discovery_config(None); // Disable MTU discovery + let transport_config = Arc::new(transport_config); + + let mut client_config = quinn::ClientConfig::new(Arc::new(client_config)); + let mut server_config = quinn::ServerConfig::with_crypto(Arc::new(server_config)); + server_config.transport_config(transport_config.clone()); + client_config.transport_config(transport_config); + + // There's a bit more boilerplate to make a generic endpoint. + let runtime = quinn::default_runtime().context("no async runtime")?; + let endpoint_config = quinn::EndpointConfig::default(); + let socket = std::net::UdpSocket::bind(config.listen).context("failed to bind UDP socket")?; + + // Create the generic QUIC endpoint. + let mut quic = quinn::Endpoint::new(endpoint_config, Some(server_config), socket, runtime) + .context("failed to create QUIC endpoint")?; + quic.set_default_client_config(client_config); + + let api = config.api.map(|url| { + log::info!("using moq-api: url={}", url); + moq_api::Client::new(url) + }); + + if let Some(ref node) = config.api_node { + log::info!("advertising origin: url={}", node); + } + + let origin = Origin::new(api, config.api_node, quic.clone()); + let conns = JoinSet::new(); + + Ok(Self { quic, origin, conns }) + } + + pub async fn serve(mut self) -> anyhow::Result<()> { + log::info!("listening on {}", self.quic.local_addr()?); + + loop { + tokio::select! { + res = self.quic.accept() => { + let conn = res.context("failed to accept QUIC connection")?; + let mut session = Session::new(self.origin.clone()); + self.conns.spawn(async move { session.run(conn).await }); + }, + res = self.conns.join_next(), if !self.conns.is_empty() => { + let res = res.expect("no tasks").expect("task aborted"); + if let Err(err) = res { + log::warn!("connection terminated: {:?}", err); + } + }, + } + } + } +} diff --git a/repos/moq-rs/moq-relay/src/session.rs b/repos/moq-rs/moq-relay/src/session.rs new file mode 100644 index 0000000..a7570f7 --- /dev/null +++ b/repos/moq-rs/moq-relay/src/session.rs @@ -0,0 +1,111 @@ +use anyhow::Context; + +use moq_transport::{session::Request, setup::Role, MoqError}; + +use crate::Origin; + +#[derive(Clone)] +pub struct Session { + origin: Origin, +} + +impl Session { + pub fn new(origin: Origin) -> Self { + Self { origin } + } + + pub async fn run(&mut self, conn: quinn::Connecting) -> anyhow::Result<()> { + log::debug!("received QUIC handshake: ip={:?}", conn.remote_address()); + + // Wait for the QUIC connection to be established. + let conn = conn.await.context("failed to establish QUIC connection")?; + + log::debug!( + "established QUIC connection: ip={:?} id={}", + conn.remote_address(), + conn.stable_id() + ); + let id = conn.stable_id(); + + // Wait for the CONNECT request. + let request = webtransport_quinn::accept(conn) + .await + .context("failed to receive WebTransport request")?; + + // Strip any leading and trailing slashes to get the broadcast name. + let path = request.url().path().trim_matches('/').to_string(); + + log::debug!("received WebTransport CONNECT: id={} path={}", id, path); + + // Accept the CONNECT request. + let session = request + .ok() + .await + .context("failed to respond to WebTransport request")?; + + // Perform the MoQ handshake. + let request = moq_transport::session::Server::accept(session) + .await + .context("failed to accept handshake")?; + + log::debug!("received MoQ SETUP: id={} role={:?}", id, request.role()); + + let role = request.role(); + + match role { + Role::Publisher => { + if let Err(err) = self.serve_publisher(id, request, &path).await { + log::warn!("error serving publisher: id={} path={} err={:#?}", id, path, err); + } + } + Role::Subscriber => { + if let Err(err) = self.serve_subscriber(id, request, &path).await { + log::warn!("error serving subscriber: id={} path={} err={:#?}", id, path, err); + } + } + Role::Both => { + log::warn!("role both not supported: id={}", id); + request.reject(300); + } + }; + + log::debug!("closing connection: id={}", id); + + Ok(()) + } + + async fn serve_publisher(&mut self, id: usize, request: Request, path: &str) -> anyhow::Result<()> { + log::info!("serving publisher: id={}, path={}", id, path); + + let mut origin = match self.origin.publish(path).await { + Ok(origin) => origin, + Err(err) => { + request.reject(err.code()); + return Err(err.into()); + } + }; + + let session = request.subscriber(origin.broadcast.clone()).await?; + + tokio::select! { + _ = session.run() => origin.close().await?, + _ = origin.run() => (), // TODO send error to session + }; + + Ok(()) + } + + async fn serve_subscriber(&mut self, id: usize, request: Request, path: &str) -> anyhow::Result<()> { + log::info!("serving subscriber: id={} path={}", id, path); + + let subscriber = self.origin.subscribe(path); + + let session = request.publisher(subscriber.broadcast.clone()).await?; + session.run().await?; + + // Make sure this doesn't get dropped too early + drop(subscriber); + + Ok(()) + } +} diff --git a/repos/moq-rs/moq-relay/src/tls.rs b/repos/moq-rs/moq-relay/src/tls.rs new file mode 100644 index 0000000..98e07b9 --- /dev/null +++ b/repos/moq-rs/moq-relay/src/tls.rs @@ -0,0 +1,182 @@ +use anyhow::Context; +use ring::digest::{digest, SHA256}; +use rustls::server::{ClientHello, ResolvesServerCert}; +use rustls::sign::CertifiedKey; +use rustls::{Certificate, PrivateKey, RootCertStore}; +use std::io::{self, Cursor, Read}; +use std::path; +use std::sync::Arc; +use std::{fs, time}; +use webpki::{DnsNameRef, EndEntityCert}; + +use crate::Config; + +#[derive(Clone)] +pub struct Tls { + pub server: rustls::ServerConfig, + pub client: rustls::ClientConfig, + pub fingerprints: Vec, +} + +impl Tls { + pub fn load(config: &Config) -> anyhow::Result { + let mut serve = ServeCerts::default(); + + // Load the certificate and key files based on their index. + anyhow::ensure!( + config.tls_cert.len() == config.tls_key.len(), + "--tls-cert and --tls-key counts differ" + ); + for (chain, key) in config.tls_cert.iter().zip(config.tls_key.iter()) { + serve.load(chain, key)?; + } + + // Create a list of acceptable root certificates. + let mut roots = RootCertStore::empty(); + + if config.tls_root.is_empty() { + // Add the platform's native root certificates. + for cert in rustls_native_certs::load_native_certs().context("could not load platform certs")? { + roots.add(&Certificate(cert.0)).context("failed to add root cert")?; + } + } else { + // Add the specified root certificates. + for root in &config.tls_root { + let root = fs::File::open(root).context("failed to open root cert file")?; + let mut root = io::BufReader::new(root); + let root = rustls_pemfile::certs(&mut root).context("failed to read root cert")?; + anyhow::ensure!(root.len() == 1, "expected a single root cert"); + let root = Certificate(root[0].to_owned()); + + roots.add(&root).context("failed to add root cert")?; + } + } + + // Create the TLS configuration we'll use as a client (relay -> relay) + let mut client = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(roots) + .with_no_client_auth(); + + // Allow disabling TLS verification altogether. + if config.tls_disable_verify { + let noop = NoCertificateVerification {}; + client.dangerous().set_certificate_verifier(Arc::new(noop)); + } + + let fingerprints = serve.fingerprints(); + + // Create the TLS configuration we'll use as a server (relay <- browser) + let server = rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_cert_resolver(Arc::new(serve)); + + let certs = Self { + server, + client, + fingerprints, + }; + + Ok(certs) + } +} + +#[derive(Default)] +struct ServeCerts { + list: Vec>, +} + +impl ServeCerts { + // Load a certificate and cooresponding key from a file + pub fn load(&mut self, chain: &path::PathBuf, key: &path::PathBuf) -> anyhow::Result<()> { + // Read the PEM certificate chain + let chain = fs::File::open(chain).context("failed to open cert file")?; + let mut chain = io::BufReader::new(chain); + + let chain: Vec = rustls_pemfile::certs(&mut chain)? + .into_iter() + .map(Certificate) + .collect(); + + anyhow::ensure!(!chain.is_empty(), "could not find certificate"); + + // Read the PEM private key + let mut keys = fs::File::open(key).context("failed to open key file")?; + + // Read the keys into a Vec so we can parse it twice. + let mut buf = Vec::new(); + keys.read_to_end(&mut buf)?; + + // Try to parse a PKCS#8 key + // -----BEGIN PRIVATE KEY----- + let mut keys = rustls_pemfile::pkcs8_private_keys(&mut Cursor::new(&buf))?; + + // Try again but with EC keys this time + // -----BEGIN EC PRIVATE KEY----- + if keys.is_empty() { + keys = rustls_pemfile::ec_private_keys(&mut Cursor::new(&buf))? + }; + + anyhow::ensure!(!keys.is_empty(), "could not find private key"); + anyhow::ensure!(keys.len() < 2, "expected a single key"); + + let key = PrivateKey(keys.remove(0)); + let key = rustls::sign::any_supported_type(&key)?; + + let certified = Arc::new(CertifiedKey::new(chain, key)); + self.list.push(certified); + + Ok(()) + } + + // Return the SHA256 fingerprint of our certificates. + pub fn fingerprints(&self) -> Vec { + self.list + .iter() + .map(|ck| { + let fingerprint = digest(&SHA256, ck.cert[0].as_ref()); + let fingerprint = hex::encode(fingerprint.as_ref()); + fingerprint + }) + .collect() + } +} + +impl ResolvesServerCert for ServeCerts { + fn resolve(&self, client_hello: ClientHello<'_>) -> Option> { + if let Some(name) = client_hello.server_name() { + if let Ok(dns_name) = DnsNameRef::try_from_ascii_str(name) { + for ck in &self.list { + // TODO I gave up on caching the parsed result because of lifetime hell. + // If this shows up on benchmarks, somebody should fix it. + let leaf = ck.cert.first().expect("missing certificate"); + let parsed = EndEntityCert::try_from(leaf.0.as_ref()).expect("failed to parse certificate"); + + if parsed.verify_is_valid_for_dns_name(dns_name).is_ok() { + return Some(ck.clone()); + } + } + } + } + + // Default to the last certificate if we couldn't find one. + self.list.last().cloned() + } +} + +pub struct NoCertificateVerification {} + +impl rustls::client::ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &rustls::Certificate, + _intermediates: &[rustls::Certificate], + _server_name: &rustls::ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: time::SystemTime, + ) -> Result { + Ok(rustls::client::ServerCertVerified::assertion()) + } +} diff --git a/repos/moq-rs/moq-relay/src/web.rs b/repos/moq-rs/moq-relay/src/web.rs new file mode 100644 index 0000000..75b185a --- /dev/null +++ b/repos/moq-rs/moq-relay/src/web.rs @@ -0,0 +1,44 @@ +use std::sync::Arc; + +use axum::{extract::State, http::Method, response::IntoResponse, routing::get, Router}; +use axum_server::{tls_rustls::RustlsAcceptor, Server}; +use tower_http::cors::{Any, CorsLayer}; + +use crate::{Config, Tls}; + +// Run a HTTP server using Axum +// TODO remove this when Chrome adds support for self-signed certificates using WebTransport +pub struct Web { + app: Router, + server: Server, +} + +impl Web { + pub fn new(config: Config, tls: Tls) -> Self { + // Get the first certificate's fingerprint. + // TODO serve all of them so we can support multiple signature algorithms. + let fingerprint = tls.fingerprints.first().expect("missing certificate").clone(); + + let mut tls_config = tls.server.clone(); + tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + let tls_config = axum_server::tls_rustls::RustlsConfig::from_config(Arc::new(tls_config)); + + let app = Router::new() + .route("/fingerprint", get(serve_fingerprint)) + .layer(CorsLayer::new().allow_origin(Any).allow_methods([Method::GET])) + .with_state(fingerprint); + + let server = axum_server::bind_rustls(config.listen, tls_config); + + Self { app, server } + } + + pub async fn serve(self) -> anyhow::Result<()> { + self.server.serve(self.app.into_make_service()).await?; + Ok(()) + } +} + +async fn serve_fingerprint(State(fingerprint): State) -> impl IntoResponse { + fingerprint +} diff --git a/repos/moq-rs/moq-transport/Cargo.toml b/repos/moq-rs/moq-transport/Cargo.toml new file mode 100644 index 0000000..50caae3 --- /dev/null +++ b/repos/moq-rs/moq-transport/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "moq-transport" +description = "Media over QUIC" +authors = ["Luke Curley"] +repository = "https://github.com/kixelated/moq-rs" +license = "MIT OR Apache-2.0" + +version = "0.2.0" +edition = "2021" + +keywords = ["quic", "http3", "webtransport", "media", "live"] +categories = ["multimedia", "network-programming", "web-programming"] + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bytes = "1" +thiserror = "1" +tokio = { version = "1", features = ["macros", "io-util", "sync"] } +log = "0.4" +indexmap = "2" + +quinn = "0.10" +webtransport-quinn = "0.6.1" + +async-trait = "0.1" +paste = "1" +chrono = "0.4.31" + +[dev-dependencies] +# QUIC +url = "2" + +# Crypto +rustls = { version = "0.21", features = ["dangerous_configuration"] } +rustls-native-certs = "0.6" +rustls-pemfile = "1" + +# Async stuff +tokio = { version = "1", features = ["full"] } + +# CLI, logging, error handling +clap = { version = "4", features = ["derive"] } +log = { version = "0.4", features = ["std"] } +env_logger = "0.9" +mp4 = "0.13" +anyhow = { version = "1", features = ["backtrace"] } +serde_json = "1" +rfc6381-codec = "0.1" +tracing = "0.1" +tracing-subscriber = "0.3" diff --git a/repos/moq-rs/moq-transport/README.md b/repos/moq-rs/moq-transport/README.md new file mode 100644 index 0000000..7788103 --- /dev/null +++ b/repos/moq-rs/moq-transport/README.md @@ -0,0 +1,10 @@ +[![Documentation](https://docs.rs/moq-transport/badge.svg)](https://docs.rs/moq-transport/) +[![Crates.io](https://img.shields.io/crates/v/moq-transport.svg)](https://crates.io/crates/moq-transport) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE-MIT) + +# moq-transport + +A Rust implementation of the proposed IETF standard. + +[Specification](https://datatracker.ietf.org/doc/draft-ietf-moq-transport/) +[Github](https://github.com/moq-wg/moq-transport) diff --git a/repos/moq-rs/moq-transport/src/cache/broadcast.rs b/repos/moq-rs/moq-transport/src/cache/broadcast.rs new file mode 100644 index 0000000..feb3824 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/cache/broadcast.rs @@ -0,0 +1,262 @@ +//! A broadcast is a collection of tracks, split into two handles: [Publisher] and [Subscriber]. +//! +//! The [Publisher] can create tracks, either manually or on request. +//! It receives all requests by a [Subscriber] for a tracks that don't exist. +//! The simplest implementation is to close every unknown track with [CacheError::NotFound]. +//! +//! A [Subscriber] can request tracks by name. +//! If the track already exists, it will be returned. +//! If the track doesn't exist, it will be sent to [Unknown] to be handled. +//! A [Subscriber] can be cloned to create multiple subscriptions. +//! +//! The broadcast is automatically closed with [CacheError::Closed] when [Publisher] is dropped, or all [Subscriber]s are dropped. +use std::{ + collections::{hash_map, HashMap, VecDeque}, + fmt, + ops::Deref, + sync::Arc, +}; + +use super::{track, CacheError, Watch}; + +/// Create a new broadcast. +pub fn new(id: &str) -> (Publisher, Subscriber) { + let state = Watch::new(State::default()); + let info = Arc::new(Info { id: id.to_string() }); + + let publisher = Publisher::new(state.clone(), info.clone()); + let subscriber = Subscriber::new(state, info); + + (publisher, subscriber) +} + +/// Static information about a broadcast. +#[derive(Debug)] +pub struct Info { + pub id: String, +} + +/// Dynamic information about the broadcast. +#[derive(Debug)] +struct State { + tracks: HashMap, + requested: VecDeque, + closed: Result<(), CacheError>, +} + +impl State { + pub fn get(&self, name: &str) -> Result, CacheError> { + // Don't check closed, so we can return from cache. + Ok(self.tracks.get(name).cloned()) + } + + pub fn insert(&mut self, track: track::Subscriber) -> Result<(), CacheError> { + self.closed.clone()?; + + match self.tracks.entry(track.name.clone()) { + hash_map::Entry::Occupied(_) => return Err(CacheError::Duplicate), + hash_map::Entry::Vacant(v) => v.insert(track), + }; + + Ok(()) + } + + pub fn request(&mut self, name: &str) -> Result { + self.closed.clone()?; + + // Create a new track. + let (publisher, subscriber) = track::new(name); + + // Insert the track into our Map so we deduplicate future requests. + self.tracks.insert(name.to_string(), subscriber.clone()); + + // Send the track to the Publisher to handle. + self.requested.push_back(publisher); + + Ok(subscriber) + } + + pub fn has_next(&self) -> Result { + // Check if there's any elements in the queue before checking closed. + if !self.requested.is_empty() { + return Ok(true); + } + + self.closed.clone()?; + Ok(false) + } + + pub fn next(&mut self) -> track::Publisher { + // We panic instead of erroring to avoid a nasty wakeup loop if you don't call has_next first. + self.requested.pop_front().expect("no entry in queue") + } + + pub fn close(&mut self, err: CacheError) -> Result<(), CacheError> { + self.closed.clone()?; + self.closed = Err(err); + Ok(()) + } +} + +impl Default for State { + fn default() -> Self { + Self { + tracks: HashMap::new(), + closed: Ok(()), + requested: VecDeque::new(), + } + } +} + +/// Publish new tracks for a broadcast by name. +// TODO remove Clone +#[derive(Clone)] +pub struct Publisher { + state: Watch, + info: Arc, + _dropped: Arc, +} + +impl Publisher { + fn new(state: Watch, info: Arc) -> Self { + let _dropped = Arc::new(Dropped::new(state.clone())); + Self { state, info, _dropped } + } + + /// Create a new track with the given name, inserting it into the broadcast. + pub fn create_track(&mut self, name: &str) -> Result { + let (publisher, subscriber) = track::new(name); + self.state.lock_mut().insert(subscriber)?; + Ok(publisher) + } + + /// Insert a track into the broadcast. + pub fn insert_track(&mut self, track: track::Subscriber) -> Result<(), CacheError> { + self.state.lock_mut().insert(track) + } + + /// Block until the next track requested by a subscriber. + pub async fn next_track(&mut self) -> Result { + loop { + let notify = { + let state = self.state.lock(); + if state.has_next()? { + return Ok(state.into_mut().next()); + } + + state.changed() + }; + + notify.await; + } + } + + /// Close the broadcast with an error. + pub fn close(self, err: CacheError) -> Result<(), CacheError> { + self.state.lock_mut().close(err) + } +} + +impl Deref for Publisher { + type Target = Info; + + fn deref(&self) -> &Self::Target { + &self.info + } +} + +impl fmt::Debug for Publisher { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Publisher") + .field("state", &self.state) + .field("info", &self.info) + .finish() + } +} + +/// Subscribe to a broadcast by requesting tracks. +/// +/// This can be cloned to create handles. +#[derive(Clone)] +pub struct Subscriber { + state: Watch, + info: Arc, + _dropped: Arc, +} + +impl Subscriber { + fn new(state: Watch, info: Arc) -> Self { + let _dropped = Arc::new(Dropped::new(state.clone())); + Self { state, info, _dropped } + } + + /// Get a track from the broadcast by name. + /// If the track does not exist, it will be created and potentially fufilled by the publisher (via Unknown). + /// Otherwise, it will return [CacheError::NotFound]. + pub fn get_track(&self, name: &str) -> Result { + let state = self.state.lock(); + if let Some(track) = state.get(name)? { + return Ok(track); + } + + // Request a new track if it does not exist. + state.into_mut().request(name) + } + + /// Check if the broadcast is closed, either because the publisher was dropped or called [Publisher::close]. + pub fn is_closed(&self) -> Option { + self.state.lock().closed.as_ref().err().cloned() + } + + /// Wait until if the broadcast is closed, either because the publisher was dropped or called [Publisher::close]. + pub async fn closed(&self) -> CacheError { + loop { + let notify = { + let state = self.state.lock(); + if let Some(err) = state.closed.as_ref().err() { + return err.clone(); + } + + state.changed() + }; + + notify.await; + } + } +} + +impl Deref for Subscriber { + type Target = Info; + + fn deref(&self) -> &Self::Target { + &self.info + } +} + +impl fmt::Debug for Subscriber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Subscriber") + .field("state", &self.state) + .field("info", &self.info) + .finish() + } +} + +// A handle that closes the broadcast when dropped: +// - when all Subscribers are dropped or +// - when Publisher and Unknown are dropped. +struct Dropped { + state: Watch, +} + +impl Dropped { + fn new(state: Watch) -> Self { + Self { state } + } +} + +impl Drop for Dropped { + fn drop(&mut self) { + self.state.lock_mut().close(CacheError::Closed).ok(); + } +} diff --git a/repos/moq-rs/moq-transport/src/cache/error.rs b/repos/moq-rs/moq-transport/src/cache/error.rs new file mode 100644 index 0000000..d3f907b --- /dev/null +++ b/repos/moq-rs/moq-transport/src/cache/error.rs @@ -0,0 +1,51 @@ +use thiserror::Error; + +use crate::MoqError; + +#[derive(Clone, Debug, Error)] +pub enum CacheError { + /// A clean termination, represented as error code 0. + /// This error is automatically used when publishers or subscribers are dropped without calling close. + #[error("closed")] + Closed, + + /// An ANNOUNCE_RESET or SUBSCRIBE_RESET was sent by the publisher. + #[error("reset code={0:?}")] + Reset(u32), + + /// An ANNOUNCE_STOP or SUBSCRIBE_STOP was sent by the subscriber. + #[error("stop")] + Stop, + + /// The requested resource was not found. + #[error("not found")] + NotFound, + + /// A resource already exists with that ID. + #[error("duplicate")] + Duplicate, +} + +impl MoqError for CacheError { + /// An integer code that is sent over the wire. + fn code(&self) -> u32 { + match self { + Self::Closed => 0, + Self::Reset(code) => *code, + Self::Stop => 206, + Self::NotFound => 404, + Self::Duplicate => 409, + } + } + + /// A reason that is sent over the wire. + fn reason(&self) -> String { + match self { + Self::Closed => "closed".to_owned(), + Self::Reset(code) => format!("reset code: {}", code), + Self::Stop => "stop".to_owned(), + Self::NotFound => "not found".to_owned(), + Self::Duplicate => "duplicate".to_owned(), + } + } +} diff --git a/repos/moq-rs/moq-transport/src/cache/fragment.rs b/repos/moq-rs/moq-transport/src/cache/fragment.rs new file mode 100644 index 0000000..4e08333 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/cache/fragment.rs @@ -0,0 +1,208 @@ +//! A fragment is a stream of bytes with a header, split into a [Publisher] and [Subscriber] handle. +//! +//! A [Publisher] writes an ordered stream of bytes in chunks. +//! There's no framing, so these chunks can be of any size or position, and won't be maintained over the network. +//! +//! A [Subscriber] reads an ordered stream of bytes in chunks. +//! These chunks are returned directly from the QUIC connection, so they may be of any size or position. +//! You can clone the [Subscriber] and each will read a copy of of all future chunks. (fanout) +//! +//! The fragment is closed with [CacheError::Closed] when all publishers or subscribers are dropped. +use core::fmt; +use std::{ops::Deref, sync::Arc}; + +use crate::VarInt; +use bytes::Bytes; + +use super::{CacheError, Watch}; + +/// Create a new segment with the given info. +pub fn new(info: Info) -> (Publisher, Subscriber) { + let state = Watch::new(State::default()); + let info = Arc::new(info); + + let publisher = Publisher::new(state.clone(), info.clone()); + let subscriber = Subscriber::new(state, info); + + (publisher, subscriber) +} + +/// Static information about the segment. +#[derive(Debug)] +pub struct Info { + // The sequence number of the fragment within the segment. + // NOTE: These may be received out of order or with gaps. + pub sequence: VarInt, + + // The size of the fragment, optionally None if this is the last fragment in a segment. + // TODO enforce this size. + pub size: Option, +} + +struct State { + // The data that has been received thus far. + chunks: Vec, + + // Set when the publisher is dropped. + closed: Result<(), CacheError>, +} + +impl State { + pub fn close(&mut self, err: CacheError) -> Result<(), CacheError> { + self.closed.clone()?; + self.closed = Err(err); + Ok(()) + } +} + +impl Default for State { + fn default() -> Self { + Self { + chunks: Vec::new(), + closed: Ok(()), + } + } +} + +impl fmt::Debug for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // We don't want to print out the contents, so summarize. + f.debug_struct("State").field("closed", &self.closed).finish() + } +} + +/// Used to write data to a segment and notify subscribers. +pub struct Publisher { + // Mutable segment state. + state: Watch, + + // Immutable segment state. + info: Arc, + + // Closes the segment when all Publishers are dropped. + _dropped: Arc, +} + +impl Publisher { + fn new(state: Watch, info: Arc) -> Self { + let _dropped = Arc::new(Dropped::new(state.clone())); + Self { state, info, _dropped } + } + + /// Write a new chunk of bytes. + pub fn chunk(&mut self, chunk: Bytes) -> Result<(), CacheError> { + let mut state = self.state.lock_mut(); + state.closed.clone()?; + state.chunks.push(chunk); + Ok(()) + } + + /// Close the segment with an error. + pub fn close(self, err: CacheError) -> Result<(), CacheError> { + self.state.lock_mut().close(err) + } +} + +impl Deref for Publisher { + type Target = Info; + + fn deref(&self) -> &Self::Target { + &self.info + } +} + +impl fmt::Debug for Publisher { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Publisher") + .field("state", &self.state) + .field("info", &self.info) + .finish() + } +} + +/// Notified when a segment has new data available. +#[derive(Clone)] +pub struct Subscriber { + // Modify the segment state. + state: Watch, + + // Immutable segment state. + info: Arc, + + // The number of chunks that we've read. + // NOTE: Cloned subscribers inherit this index, but then run in parallel. + index: usize, + + // Dropped when all Subscribers are dropped. + _dropped: Arc, +} + +impl Subscriber { + fn new(state: Watch, info: Arc) -> Self { + let _dropped = Arc::new(Dropped::new(state.clone())); + + Self { + state, + info, + index: 0, + _dropped, + } + } + + /// Block until the next chunk of bytes is available. + pub async fn chunk(&mut self) -> Result, CacheError> { + loop { + let notify = { + let state = self.state.lock(); + if self.index < state.chunks.len() { + let chunk = state.chunks[self.index].clone(); + self.index += 1; + return Ok(Some(chunk)); + } + + match &state.closed { + Err(CacheError::Closed) => return Ok(None), + Err(err) => return Err(err.clone()), + Ok(()) => state.changed(), + } + }; + + notify.await; // Try again when the state changes + } + } +} + +impl Deref for Subscriber { + type Target = Info; + + fn deref(&self) -> &Self::Target { + &self.info + } +} + +impl fmt::Debug for Subscriber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Subscriber") + .field("state", &self.state) + .field("info", &self.info) + .field("index", &self.index) + .finish() + } +} + +struct Dropped { + // Modify the segment state. + state: Watch, +} + +impl Dropped { + fn new(state: Watch) -> Self { + Self { state } + } +} + +impl Drop for Dropped { + fn drop(&mut self) { + self.state.lock_mut().close(CacheError::Closed).ok(); + } +} diff --git a/repos/moq-rs/moq-transport/src/cache/mod.rs b/repos/moq-rs/moq-transport/src/cache/mod.rs new file mode 100644 index 0000000..96228cf --- /dev/null +++ b/repos/moq-rs/moq-transport/src/cache/mod.rs @@ -0,0 +1,21 @@ +//! Allows a publisher to push updates, automatically caching and fanning it out to any subscribers. +//! +//! The hierarchy is: [broadcast] -> [track] -> [segment] -> [fragment] -> [Bytes](bytes::Bytes) +//! +//! The naming scheme doesn't match the spec because it's more strict, and bikeshedding of course: +//! +//! - [broadcast] is kinda like "track namespace" +//! - [track] is "track" +//! - [segment] is "group" but MUST use a single stream. +//! - [fragment] is "object" but MUST have the same properties as the segment. + +pub mod broadcast; +mod error; +pub mod fragment; +pub mod segment; +pub mod track; + +pub(crate) mod watch; +pub(crate) use watch::*; + +pub use error::*; diff --git a/repos/moq-rs/moq-transport/src/cache/segment.rs b/repos/moq-rs/moq-transport/src/cache/segment.rs new file mode 100644 index 0000000..01fa513 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/cache/segment.rs @@ -0,0 +1,228 @@ +//! A segment is a stream of fragments with a header, split into a [Publisher] and [Subscriber] handle. +//! +//! A [Publisher] writes an ordered stream of fragments. +//! Each fragment can have a sequence number, allowing the subscriber to detect gaps fragments. +//! +//! A [Subscriber] reads an ordered stream of fragments. +//! The subscriber can be cloned, in which case each subscriber receives a copy of each fragment. (fanout) +//! +//! The segment is closed with [CacheError::Closed] when all publishers or subscribers are dropped. +use core::fmt; +use std::{ops::Deref, sync::Arc, time}; + +use crate::VarInt; + +use super::{fragment, CacheError, Watch}; + +/// Create a new segment with the given info. +pub fn new(info: Info) -> (Publisher, Subscriber) { + let state = Watch::new(State::default()); + let info = Arc::new(info); + + let publisher = Publisher::new(state.clone(), info.clone()); + let subscriber = Subscriber::new(state, info); + + (publisher, subscriber) +} + +/// Static information about the segment. +#[derive(Debug)] +pub struct Info { + // The sequence number of the segment within the track. + // NOTE: These may be received out of order or with gaps. + pub sequence: VarInt, + + // The priority of the segment within the BROADCAST. + pub priority: u32, + + // Cache the segment for at most this long. + pub expires: Option, +} + +struct State { + // The data that has been received thus far. + fragments: Vec, + + // Set when the publisher is dropped. + closed: Result<(), CacheError>, +} + +impl State { + pub fn close(&mut self, err: CacheError) -> Result<(), CacheError> { + self.closed.clone()?; + self.closed = Err(err); + Ok(()) + } +} + +impl Default for State { + fn default() -> Self { + Self { + fragments: Vec::new(), + closed: Ok(()), + } + } +} + +impl fmt::Debug for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("State") + .field("fragments", &self.fragments) + .field("closed", &self.closed) + .finish() + } +} + +/// Used to write data to a segment and notify subscribers. +pub struct Publisher { + // Mutable segment state. + state: Watch, + + // Immutable segment state. + info: Arc, + + // Closes the segment when all Publishers are dropped. + _dropped: Arc, +} + +impl Publisher { + fn new(state: Watch, info: Arc) -> Self { + let _dropped = Arc::new(Dropped::new(state.clone())); + Self { state, info, _dropped } + } + + // Not public because it's a footgun. + pub(crate) fn push_fragment( + &mut self, + sequence: VarInt, + size: Option, + ) -> Result { + let (publisher, subscriber) = fragment::new(fragment::Info { sequence, size }); + + let mut state = self.state.lock_mut(); + state.closed.clone()?; + state.fragments.push(subscriber); + Ok(publisher) + } + + /// Write a fragment + pub fn fragment(&mut self, sequence: VarInt, size: usize) -> Result { + self.push_fragment(sequence, Some(size)) + } + + /// Write the last fragment, which means size can be unknown. + pub fn final_fragment(mut self, sequence: VarInt) -> Result { + self.push_fragment(sequence, None) + } + + /// Close the segment with an error. + pub fn close(self, err: CacheError) -> Result<(), CacheError> { + self.state.lock_mut().close(err) + } +} + +impl Deref for Publisher { + type Target = Info; + + fn deref(&self) -> &Self::Target { + &self.info + } +} + +impl fmt::Debug for Publisher { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Publisher") + .field("state", &self.state) + .field("info", &self.info) + .finish() + } +} + +/// Notified when a segment has new data available. +#[derive(Clone)] +pub struct Subscriber { + // Modify the segment state. + state: Watch, + + // Immutable segment state. + info: Arc, + + // The number of chunks that we've read. + // NOTE: Cloned subscribers inherit this index, but then run in parallel. + pub index: usize, + + // Dropped when all Subscribers are dropped. + _dropped: Arc, +} + +impl Subscriber { + fn new(state: Watch, info: Arc) -> Self { + let _dropped = Arc::new(Dropped::new(state.clone())); + + Self { + state, + info, + index: 0, + _dropped, + } + } + + /// Block until the next chunk of bytes is available. + pub async fn fragment(&mut self) -> Result, CacheError> { + loop { + let notify = { + let state = self.state.lock(); + // log::debug!("fragments length: {:?}", state.fragments.len()); + if self.index < state.fragments.len() { + let fragment = state.fragments[self.index].clone(); + self.index += 1; + // log::debug!("fragment received: {:?}", self.index); + return Ok(Some(fragment)); + } + + match &state.closed { + Err(CacheError::Closed) => return Ok(None), + Err(err) => return Err(err.clone()), + Ok(()) => state.changed(), + } + }; + + notify.await; // Try again when the state changes + } + } +} + +impl Deref for Subscriber { + type Target = Info; + + fn deref(&self) -> &Self::Target { + &self.info + } +} + +impl fmt::Debug for Subscriber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Subscriber") + .field("state", &self.state) + .field("info", &self.info) + .field("index", &self.index) + .finish() + } +} + +struct Dropped { + // Modify the segment state. + state: Watch, +} + +impl Dropped { + fn new(state: Watch) -> Self { + Self { state } + } +} + +impl Drop for Dropped { + fn drop(&mut self) { + self.state.lock_mut().close(CacheError::Closed).ok(); + } +} diff --git a/repos/moq-rs/moq-transport/src/cache/track.rs b/repos/moq-rs/moq-transport/src/cache/track.rs new file mode 100644 index 0000000..6e1081d --- /dev/null +++ b/repos/moq-rs/moq-transport/src/cache/track.rs @@ -0,0 +1,339 @@ +//! A track is a collection of semi-reliable and semi-ordered segments, split into a [Publisher] and [Subscriber] handle. +//! +//! A [Publisher] creates segments with a sequence number and priority. +//! The sequest number is used to determine the order of segments, while the priority is used to determine which segment to transmit first. +//! This may seem counter-intuitive, but is designed for live streaming where the newest segments may be higher priority. +//! A cloned [Publisher] can be used to create segments in parallel, but will error if a duplicate sequence number is used. +//! +//! A [Subscriber] may not receive all segments in order or at all. +//! These segments are meant to be transmitted over congested networks and the key to MoQ Tranport is to not block on them. +//! Segments will be cached for a potentially limited duration added to the unreliable nature. +//! A cloned [Subscriber] will receive a copy of all new segment going forward (fanout). +//! +//! The track is closed with [CacheError::Closed] when all publishers or subscribers are dropped. + +use std::{collections::BinaryHeap, fmt, ops::Deref, sync::Arc, time}; + +use indexmap::IndexMap; + +use super::{segment, CacheError, Watch}; +use crate::VarInt; + +/// Create a track with the given name. +pub fn new(name: &str) -> (Publisher, Subscriber) { + let state = Watch::new(State::default()); + let info = Arc::new(Info { name: name.to_string() }); + + let publisher = Publisher::new(state.clone(), info.clone()); + let subscriber = Subscriber::new(state, info); + + (publisher, subscriber) +} + +/// Static information about a track. +#[derive(Debug)] +pub struct Info { + pub name: String, +} + +struct State { + // Store segments in received order so subscribers can detect changes. + // The key is the segment sequence, which could have gaps. + // A None value means the segment has expired. + lookup: IndexMap>, + + // Store when segments will expire in a priority queue. + expires: BinaryHeap, + + // The number of None entries removed from the start of the lookup. + pruned: usize, + + // Set when the publisher is closed/dropped, or all subscribers are dropped. + closed: Result<(), CacheError>, +} + +impl State { + pub fn close(&mut self, err: CacheError) -> Result<(), CacheError> { + self.closed.clone()?; + self.closed = Err(err); + Ok(()) + } + + pub fn insert(&mut self, segment: segment::Subscriber) -> Result<(), CacheError> { + self.closed.clone()?; + + let entry = match self.lookup.entry(segment.sequence) { + indexmap::map::Entry::Occupied(_entry) => return Err(CacheError::Duplicate), + indexmap::map::Entry::Vacant(entry) => entry, + }; + + if let Some(expires) = segment.expires { + self.expires.push(SegmentExpiration { + sequence: segment.sequence, + expires: time::Instant::now() + expires, + }); + } + + entry.insert(Some(segment)); + + // Expire any existing segments on insert. + // This means if you don't insert then you won't expire... but it's probably fine since the cache won't grow. + // TODO Use a timer to expire segments at the correct time instead + self.expire(); + + Ok(()) + } + + // Try expiring any segments + pub fn expire(&mut self) { + let now = time::Instant::now(); + while let Some(segment) = self.expires.peek() { + if segment.expires > now { + break; + } + + // Update the entry to None while preserving the index. + match self.lookup.entry(segment.sequence) { + indexmap::map::Entry::Occupied(mut entry) => entry.insert(None), + indexmap::map::Entry::Vacant(_) => panic!("expired segment not found"), + }; + + self.expires.pop(); + } + + // Remove None entries from the start of the lookup. + while let Some((_, None)) = self.lookup.get_index(0) { + self.lookup.shift_remove_index(0); + self.pruned += 1; + } + } +} + +impl Default for State { + fn default() -> Self { + Self { + lookup: Default::default(), + expires: Default::default(), + pruned: 0, + closed: Ok(()), + } + } +} + +impl fmt::Debug for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("State") + .field("lookup", &self.lookup) + .field("pruned", &self.pruned) + .field("closed", &self.closed) + .finish() + } +} + +/// Creates new segments for a track. +pub struct Publisher { + state: Watch, + info: Arc, + _dropped: Arc, +} + +impl Publisher { + fn new(state: Watch, info: Arc) -> Self { + let _dropped = Arc::new(Dropped::new(state.clone())); + Self { state, info, _dropped } + } + + /// Insert a new segment. + pub fn insert_segment(&mut self, segment: segment::Subscriber) -> Result<(), CacheError> { + self.state.lock_mut().insert(segment) + } + + /// Create an insert a segment with the given info. + pub fn create_segment(&mut self, info: segment::Info) -> Result { + let (publisher, subscriber) = segment::new(info); + self.insert_segment(subscriber)?; + Ok(publisher) + } + + /// Close the segment with an error. + pub fn close(self, err: CacheError) -> Result<(), CacheError> { + self.state.lock_mut().close(err) + } +} + +impl Deref for Publisher { + type Target = Info; + + fn deref(&self) -> &Self::Target { + &self.info + } +} + +impl fmt::Debug for Publisher { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Publisher") + .field("state", &self.state) + .field("info", &self.info) + .finish() + } +} + +/// Receives new segments for a track. +#[derive(Clone)] +pub struct Subscriber { + state: Watch, + info: Arc, + + // The index of the next segment to return. + index: usize, + + // If there are multiple segments to return, we put them in here to return them in priority order. + pending: BinaryHeap, + + // Dropped when all subscribers are dropped. + _dropped: Arc, +} + +impl Subscriber { + fn new(state: Watch, info: Arc) -> Self { + let _dropped = Arc::new(Dropped::new(state.clone())); + Self { + state, + info, + index: 0, + pending: Default::default(), + _dropped, + } + } + + /// Block until the next segment arrives + pub async fn segment(&mut self) -> Result, CacheError> { + loop { + let notify = { + log::trace!("waiting for segment: {:?}", self); + let state = self.state.lock(); + + // Get our adjusted index, which could be negative if we've removed more broadcasts than read. + let mut index = self.index.saturating_sub(state.pruned); + + // Push all new segments into a priority queue. + while index < state.lookup.len() { + let (_, segment) = state.lookup.get_index(index).unwrap(); + + // Skip None values (expired segments). + // TODO These might actually be expired, so we should check the expiration time. + if let Some(segment) = segment { + self.pending.push(SegmentPriority(segment.clone())); + } + + index += 1; + } + + self.index = state.pruned + index; + + // Return the higher priority segment. + if let Some(segment) = self.pending.pop() { + log::trace!("got segment: {:?}", segment.0); + return Ok(Some(segment.0)); + } + + // Otherwise check if we need to return an error. + match &state.closed { + Err(CacheError::Closed) => return Ok(None), + Err(err) => return Err(err.clone()), + Ok(()) => state.changed(), + } + }; + + notify.await + } + } +} + +impl Deref for Subscriber { + type Target = Info; + + fn deref(&self) -> &Self::Target { + &self.info + } +} + +impl fmt::Debug for Subscriber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Subscriber") + .field("state", &self.state) + .field("info", &self.info) + .field("index", &self.index) + .finish() + } +} + +// Closes the track on Drop. +struct Dropped { + state: Watch, +} + +impl Dropped { + fn new(state: Watch) -> Self { + Self { state } + } +} + +impl Drop for Dropped { + fn drop(&mut self) { + self.state.lock_mut().close(CacheError::Closed).ok(); + } +} + +// Used to order segments by expiration time. +struct SegmentExpiration { + sequence: VarInt, + expires: time::Instant, +} + +impl Ord for SegmentExpiration { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // Reverse order so the earliest expiration is at the top of the heap. + other.expires.cmp(&self.expires) + } +} + +impl PartialOrd for SegmentExpiration { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for SegmentExpiration { + fn eq(&self, other: &Self) -> bool { + self.expires == other.expires + } +} + +impl Eq for SegmentExpiration {} + +// Used to order segments by priority +#[derive(Clone)] +struct SegmentPriority(pub segment::Subscriber); + +impl Ord for SegmentPriority { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // Reverse order so the highest priority is at the top of the heap. + // TODO I let CodePilot generate this code so yolo + other.0.priority.cmp(&self.0.priority) + } +} + +impl PartialOrd for SegmentPriority { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for SegmentPriority { + fn eq(&self, other: &Self) -> bool { + self.0.priority == other.0.priority + } +} + +impl Eq for SegmentPriority {} diff --git a/repos/moq-rs/moq-transport/src/cache/watch.rs b/repos/moq-rs/moq-transport/src/cache/watch.rs new file mode 100644 index 0000000..93c8475 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/cache/watch.rs @@ -0,0 +1,180 @@ +use std::{ + fmt, + future::Future, + ops::{Deref, DerefMut}, + pin::Pin, + sync::{Arc, Mutex, MutexGuard}, + task, +}; + +struct State { + value: T, + wakers: Vec, + epoch: usize, +} + +impl State { + pub fn new(value: T) -> Self { + Self { + value, + wakers: Vec::new(), + epoch: 0, + } + } + + pub fn register(&mut self, waker: &task::Waker) { + self.wakers.retain(|existing| !existing.will_wake(waker)); + self.wakers.push(waker.clone()); + } + + pub fn notify(&mut self) { + self.epoch += 1; + for waker in self.wakers.drain(..) { + waker.wake(); + } + } +} + +impl Default for State { + fn default() -> Self { + Self::new(T::default()) + } +} + +impl fmt::Debug for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.value.fmt(f) + } +} + +pub struct Watch { + state: Arc>>, +} + +impl Watch { + pub fn new(initial: T) -> Self { + let state = Arc::new(Mutex::new(State::new(initial))); + Self { state } + } + + pub fn lock(&self) -> WatchRef { + WatchRef { + state: self.state.clone(), + lock: self.state.lock().unwrap(), + } + } + + pub fn lock_mut(&self) -> WatchMut { + WatchMut { + lock: self.state.lock().unwrap(), + } + } +} + +impl Clone for Watch { + fn clone(&self) -> Self { + Self { + state: self.state.clone(), + } + } +} + +impl Default for Watch { + fn default() -> Self { + Self::new(T::default()) + } +} + +impl fmt::Debug for Watch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.state.try_lock() { + Ok(lock) => lock.value.fmt(f), + Err(_) => write!(f, ""), + } + } +} + +pub struct WatchRef<'a, T> { + state: Arc>>, + lock: MutexGuard<'a, State>, +} + +impl<'a, T> WatchRef<'a, T> { + // Release the lock and wait for a notification when next updated. + pub fn changed(self) -> WatchChanged { + WatchChanged { + state: self.state, + epoch: self.lock.epoch, + } + } + + // Upgrade to a mutable references that automatically calls notify on drop. + pub fn into_mut(self) -> WatchMut<'a, T> { + WatchMut { lock: self.lock } + } +} + +impl<'a, T> Deref for WatchRef<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.lock.value + } +} + +impl<'a, T: fmt::Debug> fmt::Debug for WatchRef<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.lock.fmt(f) + } +} + +pub struct WatchMut<'a, T> { + lock: MutexGuard<'a, State>, +} + +impl<'a, T> Deref for WatchMut<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.lock.value + } +} + +impl<'a, T> DerefMut for WatchMut<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.lock.value + } +} + +impl<'a, T> Drop for WatchMut<'a, T> { + fn drop(&mut self) { + self.lock.notify(); + } +} + +impl<'a, T: fmt::Debug> fmt::Debug for WatchMut<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.lock.fmt(f) + } +} + +pub struct WatchChanged { + state: Arc>>, + epoch: usize, +} + +impl Future for WatchChanged { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll { + // TODO is there an API we can make that doesn't drop this lock? + let mut state = self.state.lock().unwrap(); + + if state.epoch > self.epoch { + task::Poll::Ready(()) + } else { + state.register(cx.waker()); + task::Poll::Pending + } + } +} diff --git a/repos/moq-rs/moq-transport/src/coding/decode.rs b/repos/moq-rs/moq-transport/src/coding/decode.rs new file mode 100644 index 0000000..a6fe94e --- /dev/null +++ b/repos/moq-rs/moq-transport/src/coding/decode.rs @@ -0,0 +1,55 @@ +use super::{BoundsExceeded, VarInt}; +use std::{io, str}; + +use thiserror::Error; + +// I'm too lazy to add these trait bounds to every message type. +// TODO Use trait aliases when they're stable, or add these bounds to every method. +pub trait AsyncRead: tokio::io::AsyncRead + Unpin + Send {} +impl AsyncRead for webtransport_quinn::RecvStream {} +impl AsyncRead for tokio::io::Take<&mut T> where T: AsyncRead {} +impl + Unpin + Send> AsyncRead for io::Cursor {} + +#[async_trait::async_trait] +pub trait Decode: Sized { + async fn decode(r: &mut R) -> Result; +} + +/// A decode error. +#[derive(Error, Debug)] +pub enum DecodeError { + #[error("unexpected end of buffer")] + UnexpectedEnd, + + #[error("invalid string")] + InvalidString(#[from] str::Utf8Error), + + #[error("invalid message: {0:?}")] + InvalidMessage(VarInt), + + #[error("invalid role: {0:?}")] + InvalidRole(VarInt), + + #[error("invalid subscribe location")] + InvalidSubscribeLocation, + + #[error("varint bounds exceeded")] + BoundsExceeded(#[from] BoundsExceeded), + + // TODO move these to ParamError + #[error("duplicate parameter")] + DupliateParameter, + + #[error("missing parameter")] + MissingParameter, + + #[error("invalid parameter")] + InvalidParameter, + + #[error("io error: {0}")] + IoError(#[from] std::io::Error), + + // Used to signal that the stream has ended. + #[error("no more messages")] + Final, +} diff --git a/repos/moq-rs/moq-transport/src/coding/encode.rs b/repos/moq-rs/moq-transport/src/coding/encode.rs new file mode 100644 index 0000000..b03cdb9 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/coding/encode.rs @@ -0,0 +1,27 @@ +use super::BoundsExceeded; + +use thiserror::Error; + +// I'm too lazy to add these trait bounds to every message type. +// TODO Use trait aliases when they're stable, or add these bounds to every method. +pub trait AsyncWrite: tokio::io::AsyncWrite + Unpin + Send {} +impl AsyncWrite for webtransport_quinn::SendStream {} +impl AsyncWrite for Vec {} + +#[async_trait::async_trait] +pub trait Encode: Sized { + async fn encode(&self, w: &mut W) -> Result<(), EncodeError>; +} + +/// An encode error. +#[derive(Error, Debug)] +pub enum EncodeError { + #[error("varint too large")] + BoundsExceeded(#[from] BoundsExceeded), + + #[error("invalid value")] + InvalidValue, + + #[error("i/o error: {0}")] + IoError(#[from] std::io::Error), +} diff --git a/repos/moq-rs/moq-transport/src/coding/mod.rs b/repos/moq-rs/moq-transport/src/coding/mod.rs new file mode 100644 index 0000000..ff57b4c --- /dev/null +++ b/repos/moq-rs/moq-transport/src/coding/mod.rs @@ -0,0 +1,11 @@ +mod decode; +mod encode; +mod params; +mod string; +mod varint; + +pub use decode::*; +pub use encode::*; +pub use params::*; +pub use string::*; +pub use varint::*; diff --git a/repos/moq-rs/moq-transport/src/coding/params.rs b/repos/moq-rs/moq-transport/src/coding/params.rs new file mode 100644 index 0000000..9cfd6f3 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/coding/params.rs @@ -0,0 +1,85 @@ +use std::io::Cursor; +use std::{cmp::max, collections::HashMap}; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use crate::coding::{AsyncRead, AsyncWrite, Decode, Encode}; + +use crate::{ + coding::{DecodeError, EncodeError}, + VarInt, +}; + +#[derive(Default, Debug, Clone)] +pub struct Params(pub HashMap>); + +#[async_trait::async_trait] +impl Decode for Params { + async fn decode(mut r: &mut R) -> Result { + let mut params = HashMap::new(); + + // I hate this shit so much; let me encode my role and get on with my life. + let count = VarInt::decode(r).await?; + for _ in 0..count.into_inner() { + let kind = VarInt::decode(r).await?; + if params.contains_key(&kind) { + return Err(DecodeError::DupliateParameter); + } + + let size = VarInt::decode(r).await?; + + // Don't allocate the entire requested size to avoid a possible attack + // Instead, we allocate up to 1024 and keep appending as we read further. + let mut pr = r.take(size.into_inner()); + let mut buf = Vec::with_capacity(max(1024, pr.limit() as usize)); + pr.read_to_end(&mut buf).await?; + params.insert(kind, buf); + + r = pr.into_inner(); + } + + Ok(Params(params)) + } +} + +#[async_trait::async_trait] +impl Encode for Params { + async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + VarInt::try_from(self.0.len())?.encode(w).await?; + + for (kind, value) in self.0.iter() { + kind.encode(w).await?; + VarInt::try_from(value.len())?.encode(w).await?; + w.write_all(value).await?; + } + + Ok(()) + } +} + +impl Params { + pub fn new() -> Self { + Self::default() + } + + pub async fn set(&mut self, kind: VarInt, p: P) -> Result<(), EncodeError> { + let mut value = Vec::new(); + p.encode(&mut value).await?; + self.0.insert(kind, value); + + Ok(()) + } + + pub fn has(&self, kind: VarInt) -> bool { + self.0.contains_key(&kind) + } + + pub async fn get(&mut self, kind: VarInt) -> Result, DecodeError> { + if let Some(value) = self.0.remove(&kind) { + let mut cursor = Cursor::new(value); + Ok(Some(P::decode(&mut cursor).await?)) + } else { + Ok(None) + } + } +} diff --git a/repos/moq-rs/moq-transport/src/coding/string.rs b/repos/moq-rs/moq-transport/src/coding/string.rs new file mode 100644 index 0000000..2cdff4a --- /dev/null +++ b/repos/moq-rs/moq-transport/src/coding/string.rs @@ -0,0 +1,29 @@ +use std::cmp::min; + +use crate::coding::{AsyncRead, AsyncWrite}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use crate::VarInt; + +use super::{Decode, DecodeError, Encode, EncodeError}; + +#[async_trait::async_trait] +impl Encode for String { + async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + let size = VarInt::try_from(self.len())?; + size.encode(w).await?; + w.write_all(self.as_ref()).await?; + Ok(()) + } +} + +#[async_trait::async_trait] +impl Decode for String { + /// Decode a string with a varint length prefix. + async fn decode(r: &mut R) -> Result { + let size = VarInt::decode(r).await?.into_inner(); + let mut str = String::with_capacity(min(1024, size) as usize); + r.take(size).read_to_string(&mut str).await?; + Ok(str) + } +} diff --git a/repos/moq-rs/moq-transport/src/coding/varint.rs b/repos/moq-rs/moq-transport/src/coding/varint.rs new file mode 100644 index 0000000..8557de8 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/coding/varint.rs @@ -0,0 +1,232 @@ +// Based on quinn-proto +// https://github.com/quinn-rs/quinn/blob/main/quinn-proto/src/varint.rs +// Licensed via Apache 2.0 and MIT + +use std::convert::{TryFrom, TryInto}; +use std::fmt; + +use crate::coding::{AsyncRead, AsyncWrite}; +use thiserror::Error; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use super::{Decode, DecodeError, Encode, EncodeError}; + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Error)] +#[error("value out of range")] +pub struct BoundsExceeded; + +/// An integer less than 2^62 +/// +/// Values of this type are suitable for encoding as QUIC variable-length integer. +// It would be neat if we could express to Rust that the top two bits are available for use as enum +// discriminants +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct VarInt(u64); + +impl VarInt { + /// The largest possible value. + pub const MAX: Self = Self((1 << 62) - 1); + + /// The smallest possible value. + pub const ZERO: Self = Self(0); + + /// Construct a `VarInt` infallibly using the largest available type. + /// Larger values need to use `try_from` instead. + pub const fn from_u32(x: u32) -> Self { + Self(x as u64) + } + + /// Extract the integer value + pub const fn into_inner(self) -> u64 { + self.0 + } +} + +impl From for u64 { + fn from(x: VarInt) -> Self { + x.0 + } +} + +impl From for usize { + fn from(x: VarInt) -> Self { + x.0 as usize + } +} + +impl From for u128 { + fn from(x: VarInt) -> Self { + x.0 as u128 + } +} + +impl From for VarInt { + fn from(x: u8) -> Self { + Self(x.into()) + } +} + +impl From for VarInt { + fn from(x: u16) -> Self { + Self(x.into()) + } +} + +impl From for VarInt { + fn from(x: u32) -> Self { + Self(x.into()) + } +} + +impl TryFrom for VarInt { + type Error = BoundsExceeded; + + /// Succeeds iff `x` < 2^62 + fn try_from(x: u64) -> Result { + if x <= Self::MAX.into_inner() { + Ok(Self(x)) + } else { + Err(BoundsExceeded) + } + } +} + +impl TryFrom for VarInt { + type Error = BoundsExceeded; + + /// Succeeds iff `x` < 2^62 + fn try_from(x: u128) -> Result { + if x <= Self::MAX.into() { + Ok(Self(x as u64)) + } else { + Err(BoundsExceeded) + } + } +} + +impl TryFrom for VarInt { + type Error = BoundsExceeded; + + /// Succeeds iff `x` < 2^62 + fn try_from(x: usize) -> Result { + Self::try_from(x as u64) + } +} + +impl TryFrom for u32 { + type Error = BoundsExceeded; + + /// Succeeds iff `x` < 2^32 + fn try_from(x: VarInt) -> Result { + if x.0 <= u32::MAX.into() { + Ok(x.0 as u32) + } else { + Err(BoundsExceeded) + } + } +} + +impl TryFrom for u16 { + type Error = BoundsExceeded; + + /// Succeeds iff `x` < 2^16 + fn try_from(x: VarInt) -> Result { + if x.0 <= u16::MAX.into() { + Ok(x.0 as u16) + } else { + Err(BoundsExceeded) + } + } +} + +impl TryFrom for u8 { + type Error = BoundsExceeded; + + /// Succeeds iff `x` < 2^8 + fn try_from(x: VarInt) -> Result { + if x.0 <= u8::MAX.into() { + Ok(x.0 as u8) + } else { + Err(BoundsExceeded) + } + } +} + +impl fmt::Debug for VarInt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Display for VarInt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +#[async_trait::async_trait] +impl Decode for VarInt { + /// Decode a varint from the given reader. + async fn decode(r: &mut R) -> Result { + let b = r.read_u8().await?; + Self::decode_byte(b, r).await + } +} + +impl VarInt { + /// Decode a varint given the first byte, reading the rest as needed. + /// This is silly but useful for determining if the stream has ended. + pub async fn decode_byte(b: u8, r: &mut R) -> Result { + let tag = b >> 6; + + let mut buf = [0u8; 8]; + buf[0] = b & 0b0011_1111; + + let x = match tag { + 0b00 => u64::from(buf[0]), + 0b01 => { + r.read_exact(buf[1..2].as_mut()).await?; + u64::from(u16::from_be_bytes(buf[..2].try_into().unwrap())) + } + 0b10 => { + r.read_exact(buf[1..4].as_mut()).await?; + u64::from(u32::from_be_bytes(buf[..4].try_into().unwrap())) + } + 0b11 => { + r.read_exact(buf[1..8].as_mut()).await?; + u64::from_be_bytes(buf) + } + _ => unreachable!(), + }; + + Ok(Self(x)) + } +} + +#[async_trait::async_trait] +impl Encode for VarInt { + /// Encode a varint to the given writer. + async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + let x = self.0; + if x < 2u64.pow(6) { + w.write_u8(x as u8).await?; + } else if x < 2u64.pow(14) { + w.write_u16(0b01 << 14 | x as u16).await?; + } else if x < 2u64.pow(30) { + w.write_u32(0b10 << 30 | x as u32).await?; + } else if x < 2u64.pow(62) { + w.write_u64(0b11 << 62 | x).await?; + } else { + unreachable!("malformed VarInt"); + } + + Ok(()) + } +} + +// This is a fork of quinn::VarInt. +impl From for VarInt { + fn from(v: quinn::VarInt) -> Self { + Self(v.into_inner()) + } +} diff --git a/repos/moq-rs/moq-transport/src/error.rs b/repos/moq-rs/moq-transport/src/error.rs new file mode 100644 index 0000000..d070251 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/error.rs @@ -0,0 +1,7 @@ +pub trait MoqError { + /// An integer code that is sent over the wire. + fn code(&self) -> u32; + + /// An optional reason sometimes sent over the wire. + fn reason(&self) -> String; +} diff --git a/repos/moq-rs/moq-transport/src/lib.rs b/repos/moq-rs/moq-transport/src/lib.rs new file mode 100644 index 0000000..08f4485 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/lib.rs @@ -0,0 +1,18 @@ +//! An implementation of the MoQ Transport protocol. +//! +//! MoQ Transport is a pub/sub protocol over QUIC. +//! While originally designed for live media, MoQ Transport is generic and can be used for other live applications. +//! The specification is a work in progress and will change. +//! See the [specification](https://datatracker.ietf.org/doc/draft-ietf-moq-transport/) and [github](https://github.com/moq-wg/moq-transport) for any updates. +//! +//! This implementation has some required extensions until the draft stablizes. See: [Extensions](crate::setup::Extensions) +mod coding; +mod error; + +pub mod cache; +pub mod message; +pub mod session; +pub mod setup; + +pub use coding::VarInt; +pub use error::MoqError; diff --git a/repos/moq-rs/moq-transport/src/message/announce.rs b/repos/moq-rs/moq-transport/src/message/announce.rs new file mode 100644 index 0000000..281fffa --- /dev/null +++ b/repos/moq-rs/moq-transport/src/message/announce.rs @@ -0,0 +1,30 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError, Params}; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +/// Sent by the publisher to announce the availability of a group of tracks. +#[derive(Clone, Debug)] +pub struct Announce { + /// The track namespace + pub namespace: String, + + /// Optional parameters + pub params: Params, +} + +impl Announce { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let namespace = String::decode(r).await?; + let params = Params::decode(r).await?; + + Ok(Self { namespace, params }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.namespace.encode(w).await?; + self.params.encode(w).await?; + + Ok(()) + } +} diff --git a/repos/moq-rs/moq-transport/src/message/announce_ok.rs b/repos/moq-rs/moq-transport/src/message/announce_ok.rs new file mode 100644 index 0000000..300279e --- /dev/null +++ b/repos/moq-rs/moq-transport/src/message/announce_ok.rs @@ -0,0 +1,23 @@ +use crate::{ + coding::{AsyncRead, AsyncWrite, Decode, DecodeError, Encode, EncodeError}, + setup::Extensions, +}; + +/// Sent by the subscriber to accept an Announce. +#[derive(Clone, Debug)] +pub struct AnnounceOk { + // Echo back the namespace that was announced. + // TODO Propose using an ID to save bytes. + pub namespace: String, +} + +impl AnnounceOk { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let namespace = String::decode(r).await?; + Ok(Self { namespace }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.namespace.encode(w).await + } +} diff --git a/repos/moq-rs/moq-transport/src/message/announce_reset.rs b/repos/moq-rs/moq-transport/src/message/announce_reset.rs new file mode 100644 index 0000000..24d3f81 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/message/announce_reset.rs @@ -0,0 +1,39 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +/// Sent by the subscriber to reject an Announce. +#[derive(Clone, Debug)] +pub struct AnnounceError { + // Echo back the namespace that was reset + pub namespace: String, + + // An error code. + pub code: u32, + + // An optional, human-readable reason. + pub reason: String, +} + +impl AnnounceError { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let namespace = String::decode(r).await?; + let code = VarInt::decode(r).await?.try_into()?; + let reason = String::decode(r).await?; + + Ok(Self { + namespace, + code, + reason, + }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.namespace.encode(w).await?; + VarInt::from_u32(self.code).encode(w).await?; + self.reason.encode(w).await?; + + Ok(()) + } +} diff --git a/repos/moq-rs/moq-transport/src/message/go_away.rs b/repos/moq-rs/moq-transport/src/message/go_away.rs new file mode 100644 index 0000000..7999c9a --- /dev/null +++ b/repos/moq-rs/moq-transport/src/message/go_away.rs @@ -0,0 +1,21 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError}; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +/// Sent by the server to indicate that the client should connect to a different server. +#[derive(Clone, Debug)] +pub struct GoAway { + pub url: String, +} + +impl GoAway { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let url = String::decode(r).await?; + Ok(Self { url }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.url.encode(w).await + } +} diff --git a/repos/moq-rs/moq-transport/src/message/mod.rs b/repos/moq-rs/moq-transport/src/message/mod.rs new file mode 100644 index 0000000..ebca0b0 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/message/mod.rs @@ -0,0 +1,163 @@ +//! Low-level message sent over the wire, as defined in the specification. +//! +//! All of these messages are sent over a bidirectional QUIC stream. +//! This introduces some head-of-line blocking but preserves ordering. +//! The only exception are OBJECT "messages", which are sent over dedicated QUIC streams. +//! +//! Messages sent by the publisher: +//! - [Announce] +//! - [Unannounce] +//! - [SubscribeOk] +//! - [SubscribeError] +//! - [SubscribeReset] +//! - [Object] +//! +//! Messages sent by the subscriber: +//! - [Subscribe] +//! - [Unsubscribe] +//! - [AnnounceOk] +//! - [AnnounceError] +//! +//! Example flow: +//! ```test +//! -> ANNOUNCE namespace="foo" +//! <- ANNOUNCE_OK namespace="foo" +//! <- SUBSCRIBE id=0 namespace="foo" name="bar" +//! -> SUBSCRIBE_OK id=0 +//! -> OBJECT id=0 sequence=69 priority=4 expires=30 +//! -> OBJECT id=0 sequence=70 priority=4 expires=30 +//! -> OBJECT id=0 sequence=70 priority=4 expires=30 +//! <- SUBSCRIBE_STOP id=0 +//! -> SUBSCRIBE_RESET id=0 code=206 reason="closed by peer" +//! ``` +mod announce; +mod announce_ok; +mod announce_reset; +mod go_away; +mod object; +mod subscribe; +mod subscribe_error; +mod subscribe_fin; +mod subscribe_ok; +mod subscribe_reset; +mod unannounce; +mod unsubscribe; + +pub use announce::*; +pub use announce_ok::*; +pub use announce_reset::*; +pub use go_away::*; +pub use object::*; +pub use subscribe::*; +pub use subscribe_error::*; +pub use subscribe_fin::*; +pub use subscribe_ok::*; +pub use subscribe_reset::*; +pub use unannounce::*; +pub use unsubscribe::*; + +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; + +use std::fmt; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +// Use a macro to generate the message types rather than copy-paste. +// This implements a decode/encode method that uses the specified type. +macro_rules! message_types { + {$($name:ident = $val:expr,)*} => { + /// All supported message types. + #[derive(Clone)] + pub enum Message { + $($name($name)),* + } + + impl Message { + pub async fn decode(r: &mut R, ext: &Extensions) -> Result { + log::debug!("*** decode"); + let t = VarInt::decode(r).await?; + log::debug!("*** decode2 {:?}", t); + + match t.into_inner() { + $($val => { + let msg = $name::decode(r, ext).await?; + + Ok(Self::$name(msg)) + })* + _ => Err(DecodeError::InvalidMessage(t)), + } + } + + pub async fn encode(&self, w: &mut W, ext: &Extensions) -> Result<(), EncodeError> { + match self { + $(Self::$name(ref m) => { + VarInt::from_u32($val).encode(w).await?; + m.encode(w, ext).await + },)* + } + } + + pub fn id(&self) -> VarInt { + match self { + $(Self::$name(_) => { + VarInt::from_u32($val) + },)* + } + } + + pub fn name(&self) -> &'static str { + match self { + $(Self::$name(_) => { + stringify!($name) + },)* + } + } + } + + $(impl From<$name> for Message { + fn from(m: $name) -> Self { + Message::$name(m) + } + })* + + impl fmt::Debug for Message { + // Delegate to the message formatter + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + $(Self::$name(ref m) => m.fmt(f),)* + } + } + } + } +} + +// Each message is prefixed with the given VarInt type. +message_types! { + // NOTE: Object and Setup are in other modules. + // Object = 0x0 + // ObjectUnbounded = 0x2 + // SetupClient = 0x40 + // SetupServer = 0x41 + + // SUBSCRIBE family, sent by subscriber + Subscribe = 0x3, + Unsubscribe = 0xa, + + // SUBSCRIBE family, sent by publisher + SubscribeOk = 0x4, + SubscribeError = 0x5, + SubscribeFin = 0xb, + SubscribeReset = 0xc, + + // ANNOUNCE family, sent by publisher + Announce = 0x6, + Unannounce = 0x9, + + // ANNOUNCE family, sent by subscriber + AnnounceOk = 0x7, + AnnounceError = 0x8, + + // Misc + GoAway = 0x10, +} diff --git a/repos/moq-rs/moq-transport/src/message/object.rs b/repos/moq-rs/moq-transport/src/message/object.rs new file mode 100644 index 0000000..8e29e96 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/message/object.rs @@ -0,0 +1,125 @@ +use std::{io, time}; + +use tokio::io::AsyncReadExt; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; +use crate::setup; + +/// Sent by the publisher as the header of each data stream. +#[derive(Clone, Debug)] +pub struct Object { + // An ID for this track. + // Proposal: https://github.com/moq-wg/moq-transport/issues/209 + pub track: VarInt, + + // The sequence number within the track. + pub group: VarInt, + + // The sequence number within the group. + pub sequence: VarInt, + + // The priority, where **smaller** values are sent first. + pub priority: u32, + + // Cache the object for at most this many seconds. + // Zero means never expire. + pub expires: Option, + + /// An optional size, allowing multiple OBJECTs on the same stream. + pub size: Option, + + /// ntp timestamp + pub ntp_timestamp: Option, +} + +impl Object { + pub async fn decode(r: &mut R, extensions: &setup::Extensions) -> Result { + // Try reading the first byte, returning a special error if the stream naturally ended. + let typ = match r.read_u8().await { + Ok(b) => VarInt::decode_byte(b, r).await?, + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Err(DecodeError::Final), + Err(e) => return Err(e.into()), + }; + + let size_present = match typ.into_inner() { + 0 => false, + 2 => true, + _ => return Err(DecodeError::InvalidMessage(typ)), + }; + + let track = VarInt::decode(r).await?; + let group = VarInt::decode(r).await?; + let sequence = VarInt::decode(r).await?; + let priority = VarInt::decode(r).await?.try_into()?; + + let ntp_timestamp = match extensions.ntp_timestamp { + true => Some(VarInt::decode(r).await?), + false => None, + }; + + let expires = match extensions.object_expires { + true => match VarInt::decode(r).await?.into_inner() { + 0 => None, + secs => Some(time::Duration::from_secs(secs)), + }, + false => None, + }; + + // The presence of the size field depends on the type. + let size = match size_present { + true => Some(VarInt::decode(r).await?), + false => None, + }; + + // log::debug!("reading object: {:?}", ntp_timestamp); + + Ok(Self { + track, + group, + sequence, + priority, + ntp_timestamp, + expires, + size, + }) + } + + pub async fn encode(&self, w: &mut W, extensions: &setup::Extensions) -> Result<(), EncodeError> { + // The kind changes based on the presence of the size. + let kind = match self.size { + Some(_) => VarInt::from_u32(2), + None => VarInt::ZERO, + }; + + kind.encode(w).await?; + self.track.encode(w).await?; + self.group.encode(w).await?; + self.sequence.encode(w).await?; + VarInt::from_u32(self.priority).encode(w).await?; + + if extensions.ntp_timestamp { + if let Some(ntp_timestamp) = self.ntp_timestamp { + ntp_timestamp.encode(w).await?; + } + } + + // Round up if there's any decimal points. + let expires = match self.expires { + None => 0, + Some(time::Duration::ZERO) => return Err(EncodeError::InvalidValue), // there's no way of expressing zero currently. + Some(expires) if expires.subsec_nanos() > 0 => expires.as_secs() + 1, + Some(expires) => expires.as_secs(), + }; + + if extensions.object_expires { + VarInt::try_from(expires)?.encode(w).await?; + } + + if let Some(size) = self.size { + size.encode(w).await?; + } + + Ok(()) + } +} diff --git a/repos/moq-rs/moq-transport/src/message/subscribe.rs b/repos/moq-rs/moq-transport/src/message/subscribe.rs new file mode 100644 index 0000000..a3f39d4 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/message/subscribe.rs @@ -0,0 +1,166 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError, Params, VarInt}; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +/// Sent by the subscriber to request all future objects for the given track. +/// +/// Objects will use the provided ID instead of the full track name, to save bytes. +#[derive(Clone, Debug)] +pub struct Subscribe { + /// An ID we choose so we can map to the track_name. + // Proposal: https://github.com/moq-wg/moq-transport/issues/209 + pub id: VarInt, + + /// The track namespace. + /// + /// Must be None if `extensions.subscribe_split` is false. + pub namespace: Option, + + /// The track name. + pub name: String, + + /// The start/end group/object. + pub start_group: SubscribeLocation, + pub start_object: SubscribeLocation, + pub end_group: SubscribeLocation, + pub end_object: SubscribeLocation, + + /// Must be None if `extensions.switch_track_id` is false. + pub switch_track_id: Option, + + /// Optional parameters + pub params: Params, +} + +impl Subscribe { + pub async fn decode(r: &mut R, ext: &Extensions) -> Result { + let id = VarInt::decode(r).await?; + + let namespace = match ext.subscribe_split { + true => Some(String::decode(r).await?), + false => None, + }; + + let name = String::decode(r).await?; + + let start_group = SubscribeLocation::decode(r).await?; + let start_object = SubscribeLocation::decode(r).await?; + let end_group = SubscribeLocation::decode(r).await?; + let end_object = SubscribeLocation::decode(r).await?; + + // You can't have a start object without a start group. + if start_group == SubscribeLocation::None && start_object != SubscribeLocation::None { + return Err(DecodeError::InvalidSubscribeLocation); + } + + // You can't have an end object without an end group. + if end_group == SubscribeLocation::None && end_object != SubscribeLocation::None { + return Err(DecodeError::InvalidSubscribeLocation); + } + + log::debug!("ext.switch_track_id: {}", ext.switch_track_id); + + let switch_track_id = if ext.switch_track_id { + Some(VarInt::decode(r).await?) + } else { + None + }; + log::debug!("switch_track_id: {:?}", switch_track_id); + + + // NOTE: There's some more location restrictions in the draft, but they're enforced at a higher level. + + let params = Params::decode(r).await?; + + log::debug!("params: {:?}", params); + + Ok(Self { + id, + namespace, + name, + start_group, + start_object, + end_group, + end_object, + switch_track_id, + params, + }) + } + + pub async fn encode(&self, w: &mut W, ext: &Extensions) -> Result<(), EncodeError> { + self.id.encode(w).await?; + + if self.namespace.is_some() != ext.subscribe_split { + panic!("namespace must be None if subscribe_split is false"); + } + + if ext.subscribe_split { + self.namespace.as_ref().unwrap().encode(w).await?; + } + + self.name.encode(w).await?; + + self.start_group.encode(w).await?; + self.start_object.encode(w).await?; + self.end_group.encode(w).await?; + self.end_object.encode(w).await?; + + if self.switch_track_id.is_some() != ext.switch_track_id { + panic!("switch_track_id must be None if subscribe_switch_track is false"); + } + + if ext.switch_track_id { + self.switch_track_id.as_ref().unwrap().encode(w).await?; + } + + self.params.encode(w).await?; + + Ok(()) + } +} + +/// Signal where the subscription should begin, relative to the current cache. +#[derive(Clone, Debug, PartialEq)] +pub enum SubscribeLocation { + None, + Absolute(VarInt), + Latest(VarInt), + Future(VarInt), +} + +impl SubscribeLocation { + pub async fn decode(r: &mut R) -> Result { + let kind = VarInt::decode(r).await?; + + match kind.into_inner() { + 0 => Ok(Self::None), + 1 => Ok(Self::Absolute(VarInt::decode(r).await?)), + 2 => Ok(Self::Latest(VarInt::decode(r).await?)), + 3 => Ok(Self::Future(VarInt::decode(r).await?)), + _ => Err(DecodeError::InvalidSubscribeLocation), + } + } + + pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + match self { + Self::None => { + VarInt::from_u32(0).encode(w).await?; + } + Self::Absolute(val) => { + VarInt::from_u32(1).encode(w).await?; + val.encode(w).await?; + } + Self::Latest(val) => { + VarInt::from_u32(2).encode(w).await?; + val.encode(w).await?; + } + Self::Future(val) => { + VarInt::from_u32(3).encode(w).await?; + val.encode(w).await?; + } + } + + Ok(()) + } +} diff --git a/repos/moq-rs/moq-transport/src/message/subscribe_error.rs b/repos/moq-rs/moq-transport/src/message/subscribe_error.rs new file mode 100644 index 0000000..9ef4c91 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/message/subscribe_error.rs @@ -0,0 +1,36 @@ +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; +use crate::setup::Extensions; + +/// Sent by the publisher to reject a Subscribe. +#[derive(Clone, Debug)] +pub struct SubscribeError { + // NOTE: No full track name because of this proposal: https://github.com/moq-wg/moq-transport/issues/209 + + // The ID for this subscription. + pub id: VarInt, + + // An error code. + pub code: u32, + + // An optional, human-readable reason. + pub reason: String, +} + +impl SubscribeError { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let id = VarInt::decode(r).await?; + let code = VarInt::decode(r).await?.try_into()?; + let reason = String::decode(r).await?; + + Ok(Self { id, code, reason }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.id.encode(w).await?; + VarInt::from_u32(self.code).encode(w).await?; + self.reason.encode(w).await?; + + Ok(()) + } +} diff --git a/repos/moq-rs/moq-transport/src/message/subscribe_fin.rs b/repos/moq-rs/moq-transport/src/message/subscribe_fin.rs new file mode 100644 index 0000000..b070971 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/message/subscribe_fin.rs @@ -0,0 +1,37 @@ +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; +use crate::setup::Extensions; + +/// Sent by the publisher to cleanly terminate a Subscribe. +#[derive(Clone, Debug)] +pub struct SubscribeFin { + // NOTE: No full track name because of this proposal: https://github.com/moq-wg/moq-transport/issues/209 + /// The ID for this subscription. + pub id: VarInt, + + /// The final group/object sent on this subscription. + pub final_group: VarInt, + pub final_object: VarInt, +} + +impl SubscribeFin { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let id = VarInt::decode(r).await?; + let final_group = VarInt::decode(r).await?; + let final_object = VarInt::decode(r).await?; + + Ok(Self { + id, + final_group, + final_object, + }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.id.encode(w).await?; + self.final_group.encode(w).await?; + self.final_object.encode(w).await?; + + Ok(()) + } +} diff --git a/repos/moq-rs/moq-transport/src/message/subscribe_ok.rs b/repos/moq-rs/moq-transport/src/message/subscribe_ok.rs new file mode 100644 index 0000000..11864e6 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/message/subscribe_ok.rs @@ -0,0 +1,31 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +/// Sent by the publisher to accept a Subscribe. +#[derive(Clone, Debug)] +pub struct SubscribeOk { + // NOTE: No full track name because of this proposal: https://github.com/moq-wg/moq-transport/issues/209 + /// The ID for this track. + pub id: VarInt, + + /// The subscription will expire in this many milliseconds. + pub expires: VarInt, +} + +impl SubscribeOk { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let id = VarInt::decode(r).await?; + let expires = VarInt::decode(r).await?; + Ok(Self { id, expires }) + } +} + +impl SubscribeOk { + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.id.encode(w).await?; + self.expires.encode(w).await?; + Ok(()) + } +} diff --git a/repos/moq-rs/moq-transport/src/message/subscribe_reset.rs b/repos/moq-rs/moq-transport/src/message/subscribe_reset.rs new file mode 100644 index 0000000..e488b28 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/message/subscribe_reset.rs @@ -0,0 +1,50 @@ +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; +use crate::setup::Extensions; + +/// Sent by the publisher to terminate a Subscribe. +#[derive(Clone, Debug)] +pub struct SubscribeReset { + // NOTE: No full track name because of this proposal: https://github.com/moq-wg/moq-transport/issues/209 + /// The ID for this subscription. + pub id: VarInt, + + /// An error code. + pub code: u32, + + /// An optional, human-readable reason. + pub reason: String, + + /// The final group/object sent on this subscription. + pub final_group: VarInt, + pub final_object: VarInt, +} + +impl SubscribeReset { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let id = VarInt::decode(r).await?; + let code = VarInt::decode(r).await?.try_into()?; + let reason = String::decode(r).await?; + let final_group = VarInt::decode(r).await?; + let final_object = VarInt::decode(r).await?; + + Ok(Self { + id, + code, + reason, + final_group, + final_object, + }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.id.encode(w).await?; + VarInt::from_u32(self.code).encode(w).await?; + self.reason.encode(w).await?; + + self.final_group.encode(w).await?; + self.final_object.encode(w).await?; + + Ok(()) + } +} diff --git a/repos/moq-rs/moq-transport/src/message/unannounce.rs b/repos/moq-rs/moq-transport/src/message/unannounce.rs new file mode 100644 index 0000000..a2c2e39 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/message/unannounce.rs @@ -0,0 +1,25 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError}; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +/// Sent by the publisher to terminate an Announce. +#[derive(Clone, Debug)] +pub struct Unannounce { + // Echo back the namespace that was reset + pub namespace: String, +} + +impl Unannounce { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let namespace = String::decode(r).await?; + + Ok(Self { namespace }) + } + + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.namespace.encode(w).await?; + + Ok(()) + } +} diff --git a/repos/moq-rs/moq-transport/src/message/unsubscribe.rs b/repos/moq-rs/moq-transport/src/message/unsubscribe.rs new file mode 100644 index 0000000..5361f59 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/message/unsubscribe.rs @@ -0,0 +1,27 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; + +use crate::coding::{AsyncRead, AsyncWrite}; +use crate::setup::Extensions; + +/// Sent by the subscriber to terminate a Subscribe. +#[derive(Clone, Debug)] +pub struct Unsubscribe { + // NOTE: No full track name because of this proposal: https://github.com/moq-wg/moq-transport/issues/209 + + // The ID for this subscription. + pub id: VarInt, +} + +impl Unsubscribe { + pub async fn decode(r: &mut R, _ext: &Extensions) -> Result { + let id = VarInt::decode(r).await?; + Ok(Self { id }) + } +} + +impl Unsubscribe { + pub async fn encode(&self, w: &mut W, _ext: &Extensions) -> Result<(), EncodeError> { + self.id.encode(w).await?; + Ok(()) + } +} diff --git a/repos/moq-rs/moq-transport/src/session/client.rs b/repos/moq-rs/moq-transport/src/session/client.rs new file mode 100644 index 0000000..df839eb --- /dev/null +++ b/repos/moq-rs/moq-transport/src/session/client.rs @@ -0,0 +1,78 @@ +use super::{Control, Publisher, SessionError, Subscriber}; +use crate::{cache::broadcast, setup}; +use webtransport_quinn::Session; + +/// An endpoint that connects to a URL to publish and/or consume live streams. +pub struct Client {} + +impl Client { + /// Connect using an established WebTransport session, performing the MoQ handshake as a publisher. + pub async fn publisher(session: Session, source: broadcast::Subscriber) -> Result { + let control = Self::send_setup(&session, setup::Role::Publisher).await?; + let publisher = Publisher::new(session, control, source); + Ok(publisher) + } + + /// Connect using an established WebTransport session, performing the MoQ handshake as a subscriber. + pub async fn subscriber(session: Session, source: broadcast::Publisher) -> Result { + let control = Self::send_setup(&session, setup::Role::Subscriber).await?; + let subscriber = Subscriber::new(session, control, source); + Ok(subscriber) + } + + // TODO support performing both roles + /* + pub async fn connect(self) -> anyhow::Result<(Publisher, Subscriber)> { + self.connect_role(setup::Role::Both).await + } + */ + + async fn send_setup(session: &Session, role: setup::Role) -> Result { + let mut control = session.open_bi().await?; + + let versions: setup::Versions = [setup::Version::DRAFT_01, setup::Version::KIXEL_01].into(); + + let client = setup::Client { + role, + versions: versions.clone(), + params: Default::default(), + + // Offer all extensions + extensions: setup::Extensions { + object_expires: true, + subscriber_id: true, + subscribe_split: true, + ntp_timestamp: true, + switch_track_id: true, + }, + }; + + log::debug!("sending client SETUP: {:?}", client); + client.encode(&mut control.0).await?; + + let mut server = setup::Server::decode(&mut control.1).await?; + + log::debug!("received server SETUP: {:?}", server); + + match server.version { + setup::Version::DRAFT_01 => { + // We always require this extension + server.extensions.require_subscriber_id()?; + + if server.role.is_publisher() { + // We only require object expires if we're a subscriber, so we don't cache objects indefinitely. + server.extensions.require_object_expires()?; + } + } + setup::Version::KIXEL_01 => { + // KIXEL_01 didn't support extensions; all were enabled. + server.extensions = client.extensions.clone() + } + _ => return Err(SessionError::Version(versions, [server.version].into())), + } + + let control = Control::new(control.0, control.1, server.extensions); + + Ok(control) + } +} diff --git a/repos/moq-rs/moq-transport/src/session/control.rs b/repos/moq-rs/moq-transport/src/session/control.rs new file mode 100644 index 0000000..77f69c7 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/session/control.rs @@ -0,0 +1,47 @@ +// A helper class to guard sending control messages behind a Mutex. + +use std::{fmt, sync::Arc}; + +use tokio::sync::Mutex; +use webtransport_quinn::{RecvStream, SendStream}; + +use super::SessionError; +use crate::{message::Message, setup::Extensions}; + +#[derive(Debug, Clone)] +pub(crate) struct Control { + send: Arc>, + recv: Arc>, + pub ext: Extensions, +} + +impl Control { + pub fn new(send: SendStream, recv: RecvStream, ext: Extensions) -> Self { + Self { + send: Arc::new(Mutex::new(send)), + recv: Arc::new(Mutex::new(recv)), + ext, + } + } + + pub async fn send + fmt::Debug>(&self, msg: T) -> Result<(), SessionError> { + let mut stream = self.send.lock().await; + log::info!("sending message: {:?}", msg); + msg.into() + .encode(&mut *stream, &self.ext) + .await + .map_err(|e| SessionError::Unknown(e.to_string()))?; + Ok(()) + } + + // It's likely a mistake to call this from two different tasks, but it's easier to just support it. + pub async fn recv(&self) -> Result { + let mut stream = self.recv.lock().await; + log::debug!("waiting for message"); + let msg = Message::decode(&mut *stream, &self.ext) + .await + .map_err(|e| SessionError::Unknown(e.to_string()))?; + log::debug!("received message: {:?}", msg); + Ok(msg) + } +} diff --git a/repos/moq-rs/moq-transport/src/session/error.rs b/repos/moq-rs/moq-transport/src/session/error.rs new file mode 100644 index 0000000..228a4c8 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/session/error.rs @@ -0,0 +1,107 @@ +use crate::{cache, coding, setup, MoqError, VarInt}; + +#[derive(thiserror::Error, Debug)] +pub enum SessionError { + #[error("webtransport error: {0}")] + Session(#[from] webtransport_quinn::SessionError), + + #[error("cache error: {0}")] + Cache(#[from] cache::CacheError), + + #[error("encode error: {0}")] + Encode(#[from] coding::EncodeError), + + #[error("decode error: {0}")] + Decode(#[from] coding::DecodeError), + + #[error("unsupported versions: client={0:?} server={1:?}")] + Version(setup::Versions, setup::Versions), + + #[error("incompatible roles: client={0:?} server={1:?}")] + RoleIncompatible(setup::Role, setup::Role), + + /// An error occured while reading from the QUIC stream. + #[error("failed to read from stream: {0}")] + Read(#[from] webtransport_quinn::ReadError), + + /// An error occured while writing to the QUIC stream. + #[error("failed to write to stream: {0}")] + Write(#[from] webtransport_quinn::WriteError), + + /// The role negiotiated in the handshake was violated. For example, a publisher sent a SUBSCRIBE, or a subscriber sent an OBJECT. + #[error("role violation: msg={0}")] + RoleViolation(VarInt), + + /// Our enforced stream mapping was disrespected. + #[error("stream mapping conflict")] + StreamMapping, + + /// The priority was invalid. + #[error("invalid priority: {0}")] + InvalidPriority(VarInt), + + /// The size was invalid. + #[error("invalid size: {0}")] + InvalidSize(VarInt), + + /// A required extension was not offered. + #[error("required extension not offered: {0:?}")] + RequiredExtension(VarInt), + + /// Some VarInt was too large and we were too lazy to handle it + #[error("varint bounds exceeded")] + BoundsExceeded(#[from] coding::BoundsExceeded), + + /// An unclassified error because I'm lazy. TODO classify these errors + #[error("unknown error: {0}")] + Unknown(String), +} + +impl MoqError for SessionError { + /// An integer code that is sent over the wire. + fn code(&self) -> u32 { + match self { + Self::Cache(err) => err.code(), + Self::RoleIncompatible(..) => 406, + Self::RoleViolation(..) => 405, + Self::StreamMapping => 409, + Self::Unknown(_) => 500, + Self::Write(_) => 501, + Self::Read(_) => 502, + Self::Session(_) => 503, + Self::Version(..) => 406, + Self::Encode(_) => 500, + Self::Decode(_) => 500, + Self::InvalidPriority(_) => 400, + Self::InvalidSize(_) => 400, + Self::RequiredExtension(_) => 426, + Self::BoundsExceeded(_) => 500, + } + } + + /// A reason that is sent over the wire. + fn reason(&self) -> String { + match self { + Self::Cache(err) => err.reason(), + Self::RoleViolation(kind) => format!("role violation for message type {:?}", kind), + Self::RoleIncompatible(client, server) => { + format!( + "role incompatible: client wanted {:?} but server wanted {:?}", + client, server + ) + } + Self::Read(err) => format!("read error: {}", err), + Self::Write(err) => format!("write error: {}", err), + Self::Session(err) => format!("session error: {}", err), + Self::Unknown(err) => format!("unknown error: {}", err), + Self::Version(client, server) => format!("unsupported versions: client={:?} server={:?}", client, server), + Self::Encode(err) => format!("encode error: {}", err), + Self::Decode(err) => format!("decode error: {}", err), + Self::StreamMapping => "streaming mapping conflict".to_owned(), + Self::InvalidPriority(priority) => format!("invalid priority: {}", priority), + Self::InvalidSize(size) => format!("invalid size: {}", size), + Self::RequiredExtension(id) => format!("required extension was missing: {:?}", id), + Self::BoundsExceeded(_) => "varint bounds exceeded".to_string(), + } + } +} diff --git a/repos/moq-rs/moq-transport/src/session/mod.rs b/repos/moq-rs/moq-transport/src/session/mod.rs new file mode 100644 index 0000000..50b36a9 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/session/mod.rs @@ -0,0 +1,27 @@ +//! A MoQ Transport session, on top of a WebTransport session, on top of a QUIC connection. +//! +//! The handshake is relatively simple but split into different steps. +//! All of these handshakes slightly differ depending on if the endpoint is a client or server. +//! 1. Complete the QUIC handhake. +//! 2. Complete the WebTransport handshake. +//! 3. Complete the MoQ handshake. +//! +//! Use [Client] or [Server] for the MoQ handshake depending on the endpoint. +//! Then, decide if you want to create a [Publisher] or [Subscriber], or both (TODO). +//! +//! A [Publisher] can announce broadcasts, which will automatically be served over the network. +//! A [Subscriber] can subscribe to broadcasts, which will automatically be served over the network. + +mod client; +mod control; +mod error; +mod publisher; +mod server; +mod subscriber; + +pub use client::*; +pub(crate) use control::*; +pub use error::*; +pub use publisher::*; +pub use server::*; +pub use subscriber::*; diff --git a/repos/moq-rs/moq-transport/src/session/publisher.rs b/repos/moq-rs/moq-transport/src/session/publisher.rs new file mode 100644 index 0000000..84cf882 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/session/publisher.rs @@ -0,0 +1,403 @@ +use std::{ + collections::{hash_map, HashMap}, result, sync::{Arc, Mutex}, thread::JoinHandle, u32 +}; + +use tokio::task::AbortHandle; +use webtransport_quinn::Session; + +use crate::{ + cache::{broadcast, segment, track, CacheError}, + message, + message::Message, + MoqError, VarInt, +}; + +use super::{Control, SessionError}; + + +/// Serves broadcasts over the network, automatically handling subscriptions and caching. +// TODO Clone specific fields when a task actually needs it. +#[derive(Clone, Debug)] +pub struct Publisher { + // A map of active subscriptions, containing an abort handle to cancel them. + subscribes: Arc>>, + webtransport: Session, + control: Control, + source: broadcast::Subscriber, +} + +impl Publisher { + pub(crate) fn new(webtransport: Session, control: Control, source: broadcast::Subscriber) -> Self { + Self { + webtransport, + control, + subscribes: Default::default(), + source, + } + } + + // TODO Serve a broadcast without sending an ANNOUNCE. + // fn serve(&mut self, broadcast: broadcast::Subscriber) -> Result<(), SessionError> { + + // TODO Wait until the next subscribe that doesn't route to an ANNOUNCE. + // pub async fn subscribed(&mut self) -> Result { + + pub async fn run(mut self) -> Result<(), SessionError> { + let res = self.run_inner().await; + + // Terminate all active subscribes on error. + self.subscribes + .lock() + .unwrap() + .drain() + .for_each(|(_, abort)| abort.abort()); + + res + } + + pub async fn run_inner(&mut self) -> Result<(), SessionError> { + log::debug!("running publisher"); + loop { + tokio::select! { + stream = self.webtransport.accept_uni() => { + stream?; + return Err(SessionError::RoleViolation(VarInt::ZERO)); + }, + // NOTE: this is not cancel safe, but it's fine since the other branchs are fatal. + msg = self.control.recv() => { + let msg = msg?; + + log::info!("message received: {:?}", msg); + if let Err(err) = self.recv_message(&msg).await { + log::warn!("message error: {:?} {:?}", err, msg); + } + }, + // No more broadcasts are available. + err = self.source.closed() => { + self.webtransport.close(err.code(), err.reason().as_bytes()); + return Ok(()); + }, + } + } + } + + /* TODO: Uncomment here when it's implemented in webtransport-quinn + pub fn get_throughput(&self) -> u64 { + // self.webtransport.throughput() + 0 + } + */ + + async fn recv_message(&mut self, msg: &Message) -> Result<(), SessionError> { + log::info!("received message: {:?}", msg); + match msg { + Message::AnnounceOk(msg) => self.recv_announce_ok(msg).await, + Message::AnnounceError(msg) => self.recv_announce_error(msg).await, + Message::Subscribe(msg) => self.recv_subscribe(msg).await, + Message::Unsubscribe(msg) => self.recv_unsubscribe(msg).await, + _ => Err(SessionError::RoleViolation(msg.id())), + } + } + + async fn recv_announce_ok(&mut self, _msg: &message::AnnounceOk) -> Result<(), SessionError> { + // We didn't send an announce. + Err(CacheError::NotFound.into()) + } + + async fn recv_announce_error(&mut self, _msg: &message::AnnounceError) -> Result<(), SessionError> { + // We didn't send an announce. + Err(CacheError::NotFound.into()) + } + + async fn recv_subscribe(&mut self, msg: &message::Subscribe) -> Result<(), SessionError> { + log::info!("received subscribe: {:?}", msg); + // Assume that the subscribe ID is unique for now. + if msg.name.starts_with(".probe") { + let mut probe_size = 20000; + let mut probe_priority = 0; + if msg.name.starts_with(".probe:") { + let parameters = msg.name.split(":").collect::>(); + probe_size = parameters.get(1).unwrap().parse().unwrap(); + probe_priority = parameters.get(2).unwrap().parse().unwrap(); + } + let mut this = self.clone(); + let probe_msg = msg.clone(); + tokio::spawn(async move { + let res = this.send_probe_data(probe_msg.id, probe_size, probe_priority).await; + if let Err(err) = &res { + log::warn!("failed to send probe data: {:?}", err); + } + }); + } else { + let abort = match self.start_subscribe(msg.clone()) { + Ok(abort) => abort, + Err(err) => return self.reset_subscribe(msg.id, err).await, + }; + + // log + log::info!("subscribe started: {:?}", msg); + + // Insert the abort handle into the lookup table. + match self.subscribes.lock().unwrap().entry(msg.id) { + hash_map::Entry::Occupied(_) => return Err(CacheError::Duplicate.into()), // TODO fatal, because we already started the task + hash_map::Entry::Vacant(entry) => entry.insert(abort), + }; + } + + self.control + .send(message::SubscribeOk { + id: msg.id, + expires: VarInt::ZERO, + }) + .await + } + + async fn reset_subscribe(&mut self, id: VarInt, err: E) -> Result<(), SessionError> { + let msg = message::SubscribeReset { + id, + code: err.code(), + reason: err.reason(), + + // TODO properly populate these + // But first: https://github.com/moq-wg/moq-transport/issues/313 + final_group: VarInt::ZERO, + final_object: VarInt::ZERO, + }; + + self.control.send(msg).await + } + + async fn send_probe_data(&mut self, id: VarInt, probe_size: u32, probe_priority: u32) -> Result<(), SessionError>{ + log::info!("sending probe data"); + + let stream_priority: i32 = match probe_priority { + 1 => i32::MAX, + _ => 0, + }; + + let mut stream = self.webtransport.open_uni().await?; + // Convert the u32 to a i32, since the Quinn set_priority is signed. + stream.set_priority(stream_priority).ok(); + + let ntp_timestamp = match VarInt::try_from(chrono::Utc::now().timestamp_millis() as u64) { + Ok(ntp_timestamp) => ntp_timestamp, + Err(e) => return Err(SessionError::BoundsExceeded(e)), + }; + + let payload = vec![0_u8; probe_size.try_into().unwrap()]; + + // write the object + + + let object = message::Object { + track: id, + group: VarInt::from_u32(0), + priority: probe_priority, + sequence: VarInt::from_u32(0), + expires: None, + ntp_timestamp: Option::from(ntp_timestamp), + size: Some(VarInt::try_from(probe_size).unwrap()) + }; + + object + .encode(&mut stream, &self.control.ext) + .await + .map_err(|e| SessionError::Unknown(e.to_string()))?; + + // write the payload + let result = stream.write_all(&payload).await; + if let Err(err) = result { + log::warn!("failed to write probe data: {:?}", err); + } + log::info!("sent probe data"); + Ok(()) + } + + fn start_subscribe(&mut self, msg: message::Subscribe) -> Result { + // We currently don't use the namespace field in SUBSCRIBE + // Make sure the namespace is empty if it's provided. + if msg.namespace.as_ref().map_or(false, |namespace| !namespace.is_empty()) { + return Err(CacheError::NotFound.into()); + } + + // TODO only clone the fields we need + let mut this = self.clone(); + + let mut track = self.source.get_track(&msg.name)?; + + let handle = tokio::spawn(async move { + log::info!("serving track: name={}", track.name); + + if msg.switch_track_id.is_some() && msg.switch_track_id.unwrap() != VarInt::from_u32(0) && this.subscribes.lock().unwrap().get(&msg.switch_track_id.unwrap()).is_some() { + log::info!("closing track: name={:?}", msg.switch_track_id); + this.subscribes.lock().unwrap().remove(&msg.switch_track_id.unwrap()); + } + + let res = this.run_subscribe(msg.id, &mut track).await; + if let Err(err) = &res { + log::warn!("failed to serve track: name={} err={:#?}", track.name, err); + } + + if this.subscribes.lock().unwrap().get(&msg.id).is_none() { + // possibly it was unsubscribed by the client + log::warn!("subscribe not found: name={}", track.name); + } else { + log::info!("closing track: name={}", track.name); + // Make sure we send a reset at the end. + let err = res.err().unwrap_or(CacheError::Closed.into()); + this.reset_subscribe(msg.id, err).await.ok(); + + // We're all done, so clean up the abort handle. + this.subscribes.lock().unwrap().remove(&msg.id); + } + }); + + Ok(handle.abort_handle()) + } + + async fn run_subscribe(&self, id: VarInt, track: &mut track::Subscriber) -> Result<(), SessionError> { + // TODO add an Ok method to track::Publisher so we can send SUBSCRIBE_OK + + log::info!("in run_subscribe: {:?}", track); + + while let Some(mut segment) = track.segment().await? { + // Check if the subscribe was removed while waiting for the segment. + if self.subscribes.lock().unwrap().get(&id).is_none() { + log::info!("run_subscribe | subscription removed, exiting | track:{} sequence:{:?} priority:{} index:{}", id, segment.sequence, segment.priority, segment.index); + break + } + + if self.subscribes.lock().unwrap().get(&id).is_none() { + // possibly it was unsubscribed by the client + log::warn!("subscribe not found: {:?}", id); + return Ok(()); + } + + // TODO only clone the fields we need + let this = self.clone(); + + tokio::spawn(async move { + if let Err(err) = this.run_segment(id, &mut segment).await { + log::warn!("failed to serve segment: {:?} {:?}", id, err) + } + }); + + } + + Ok(()) + } + + async fn run_segment(&self, id: VarInt, segment: &mut segment::Subscriber) -> Result<(), SessionError> { + log::info!("serving segment | track:{} sequence:{:?} priority:{} index:{}", id, segment.sequence, segment.priority, segment.index); + + let mut stream = self.webtransport.open_uni().await?; + + // Convert the u32 to a i32, since the Quinn set_priority is signed. + let priority = (segment.priority as i64 - i32::MAX as i64) as i32; + stream.set_priority(priority).ok(); + + let mut sent_chunk_count = 0u32; + let mut chunk_count = 0u32; + let chunk_sending_rate = 0; + + let mut internal_buffer = Vec::new(); + + while let Some(mut fragment) = segment.fragment().await? { + log::info!("serving fragment | track:{} sequence:{:?} segment: {:?}", id, fragment.sequence, segment.sequence); + sent_chunk_count = 0; + chunk_count = 0; + + // TODO: use real NTP timestamp + // + let ntp_timestamp = match VarInt::try_from(chrono::Utc::now().timestamp_millis() as u64) { + Ok(ntp_timestamp) => ntp_timestamp, + Err(e) => return Err(SessionError::BoundsExceeded(e)), + }; + + let object = message::Object { + track: id, + + // Properties of the segment + group: segment.sequence, + priority: segment.priority, + expires: segment.expires, + + // Properties of the fragment + sequence: fragment.sequence, + + // timestamp for latency calculation + ntp_timestamp: Option::from(ntp_timestamp), + + size: fragment.size.map(VarInt::try_from).transpose()?, + }; + + object + .encode(&mut stream, &self.control.ext) + .await + .map_err(|e| SessionError::Unknown(e.to_string()))?; + + // TODO: + // parse boxes + // if box length > 200000, send it in one chunk + // waiting room + while let Some(chunk) = fragment.chunk().await? + { + log::trace!("writing chunk of track: {:?}", chunk); + if chunk.len() > 0 { + chunk_count += 1; + if chunk_sending_rate == 0 { + let result = stream.write_all(&chunk).await; + if let Err(err) = result { + log::warn!("failed to write some chunks ({:?}:{:?} last chunk: {}): {:?}", id, segment.index, chunk_count, err); + } else { + // log::debug!("sent chunk | track:{} sequence:{:?} segment: {} chunk_read:{}", id, segment.sequence, segment.index, chunk_count); + } + sent_chunk_count += 1; + } else { + internal_buffer.append(chunk.to_vec().as_mut()); + } + } + if chunk_sending_rate > 0 && chunk_count % chunk_sending_rate == 0 { + let result = stream.write_all(&internal_buffer).await; + if let Err(err) = result { + log::warn!("failed to write some chunks ({:?}:{:?} last chunk: {}): {:?}", id, segment.index, chunk_count, err); + } else { + // log::debug!("sent chunk | track:{} sequence:{:?} segment: {} chunk_read:{}", id, segment.sequence, segment.index, chunk_count); + } + sent_chunk_count += chunk_sending_rate; + internal_buffer.clear(); + } + } + // log::debug!("finished fragment |track:{:?} segment: {} chunk_read:{} chunk_sent:{}", id, segment.sequence, chunk_count, sent_chunk_count); + } + + if chunk_count == 0 { + log::warn!("no chunks sent for track: {:?}", id); + return Err(SessionError::Unknown("no chunks sent".to_string())); + } else if chunk_sending_rate > 0 && sent_chunk_count < chunk_count { + // we did not send the last remaining chunks + log::warn!("not all chunks sent for track: {:?} chunk count: {:?} sent: {}", id, chunk_count, sent_chunk_count); + let result = stream.write_all(&internal_buffer).await; + if let Err(err) = result { + log::warn!("failed to write some chunks ({:?}:{:?} last chunk: {}): {:?}", id, segment.index, chunk_count, err); + } else { + // log::debug!("sent chunk | track:{} sequence:{:?} segment: {} chunk_read:{} chunk_sent:{}", id, segment.sequence, segment.index, chunk_count, sent_chunk_count); + } + internal_buffer.clear(); + } + + Ok(()) + } + + async fn recv_unsubscribe(&mut self, msg: &message::Unsubscribe) -> Result<(), SessionError> { + let abort = self + .subscribes + .lock() + .unwrap() + .remove(&msg.id) + .ok_or(CacheError::NotFound)?; + abort.abort(); + + self.reset_subscribe(msg.id, CacheError::Stop).await + } +} diff --git a/repos/moq-rs/moq-transport/src/session/server.rs b/repos/moq-rs/moq-transport/src/session/server.rs new file mode 100644 index 0000000..d039f85 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/session/server.rs @@ -0,0 +1,118 @@ +use super::{Control, Publisher, SessionError, Subscriber}; +use crate::{cache::broadcast, setup}; + +use webtransport_quinn::{RecvStream, SendStream, Session}; + +/// An endpoint that accepts connections, publishing and/or consuming live streams. +pub struct Server {} + +impl Server { + /// Accept an established Webtransport session, performing the MoQ handshake. + /// + /// This returns a [Request] half-way through the handshake that allows the application to accept or deny the session. + pub async fn accept(session: Session) -> Result { + let mut control = session.accept_bi().await?; + + let mut client = setup::Client::decode(&mut control.1).await?; + + log::debug!("received client SETUP: {:?}", client); + + if client.versions.contains(&setup::Version::DRAFT_01) { + // We always require subscriber ID. + client.extensions.require_subscriber_id()?; + + // We require OBJECT_EXPIRES for publishers only. + if client.role.is_publisher() { + client.extensions.require_object_expires()?; + } + + // We don't require SUBSCRIBE_SPLIT since it's easy enough to support, but it's clearly an oversight. + // client.extensions.require(&Extension::SUBSCRIBE_SPLIT)?; + } else if client.versions.contains(&setup::Version::KIXEL_01) { + // Extensions didn't exist in KIXEL_01, so we set them manually. + client.extensions = setup::Extensions { + object_expires: true, + subscriber_id: true, + subscribe_split: true, + switch_track_id: true, + ntp_timestamp: true, + }; + } else { + return Err(SessionError::Version( + client.versions, + [setup::Version::DRAFT_01, setup::Version::KIXEL_01].into(), + )); + } + + Ok(Request { + session, + client, + control, + }) + } +} + +/// A partially complete MoQ Transport handshake. +pub struct Request { + session: Session, + client: setup::Client, + control: (SendStream, RecvStream), +} + +impl Request { + /// Accept the session as a publisher, using the provided broadcast to serve subscriptions. + pub async fn publisher(mut self, source: broadcast::Subscriber) -> Result { + let setup = self.setup(setup::Role::Publisher)?; + setup.encode(&mut self.control.0).await?; + + let control = Control::new(self.control.0, self.control.1, setup.extensions); + let publisher = Publisher::new(self.session, control, source); + Ok(publisher) + } + + /// Accept the session as a subscriber only. + pub async fn subscriber(mut self, source: broadcast::Publisher) -> Result { + let setup = self.setup(setup::Role::Subscriber)?; + setup.encode(&mut self.control.0).await?; + + let control = Control::new(self.control.0, self.control.1, setup.extensions); + let subscriber = Subscriber::new(self.session, control, source); + Ok(subscriber) + } + + // TODO Accept the session and perform both roles. + /* + pub async fn accept(self) -> anyhow::Result<(Publisher, Subscriber)> { + self.ok(setup::Role::Both).await + } + */ + + fn setup(&mut self, role: setup::Role) -> Result { + let server = setup::Server { + role, + version: setup::Version::DRAFT_01, + extensions: self.client.extensions.clone(), + params: Default::default(), + }; + + log::debug!("sending server SETUP: {:?}", server); + + // We need to sure we support the opposite of the client's role. + // ex. if the client is a publisher, we must be a subscriber ONLY. + if !self.client.role.is_compatible(server.role) { + return Err(SessionError::RoleIncompatible(self.client.role, server.role)); + } + + Ok(server) + } + + /// Reject the request, closing the Webtransport session. + pub fn reject(self, code: u32) { + self.session.close(code, b"") + } + + /// The role advertised by the client. + pub fn role(&self) -> setup::Role { + self.client.role + } +} diff --git a/repos/moq-rs/moq-transport/src/session/subscriber.rs b/repos/moq-rs/moq-transport/src/session/subscriber.rs new file mode 100644 index 0000000..e38404f --- /dev/null +++ b/repos/moq-rs/moq-transport/src/session/subscriber.rs @@ -0,0 +1,214 @@ +use webtransport_quinn::{RecvStream, Session}; + +use std::{ + collections::HashMap, + sync::{atomic, Arc, Mutex}, +}; + +use crate::{ + cache::{broadcast, segment, track, CacheError}, + coding::DecodeError, + message, + message::Message, + session::{Control, SessionError}, + VarInt, +}; + +/// Receives broadcasts over the network, automatically handling subscriptions and caching. +// TODO Clone specific fields when a task actually needs it. +#[derive(Clone, Debug)] +pub struct Subscriber { + // The webtransport session. + webtransport: Session, + + // The list of active subscriptions, each guarded by an mutex. + subscribes: Arc>>, + + // The sequence number for the next subscription. + next: Arc, + + // A channel for sending messages. + control: Control, + + // All unknown subscribes comes here. + source: broadcast::Publisher, +} + +impl Subscriber { + pub(crate) fn new(webtransport: Session, control: Control, source: broadcast::Publisher) -> Self { + Self { + webtransport, + subscribes: Default::default(), + next: Default::default(), + control, + source, + } + } + + pub async fn run(self) -> Result<(), SessionError> { + let inbound = self.clone().run_inbound(); + let streams = self.clone().run_streams(); + let source = self.clone().run_source(); + + // Return the first error. + tokio::select! { + res = inbound => res, + res = streams => res, + res = source => res, + } + } + + async fn run_inbound(mut self) -> Result<(), SessionError> { + loop { + let msg = self.control.recv().await?; + + log::info!("message received: {:?}", msg); + if let Err(err) = self.recv_message(&msg) { + log::warn!("message error: {:?} {:?}", err, msg); + } + } + } + + fn recv_message(&mut self, msg: &Message) -> Result<(), SessionError> { + match msg { + Message::Announce(_) => Ok(()), // don't care + Message::Unannounce(_) => Ok(()), // also don't care + Message::SubscribeOk(_msg) => Ok(()), // don't care + Message::SubscribeReset(msg) => self.recv_subscribe_error(msg.id, CacheError::Reset(msg.code)), + Message::SubscribeFin(msg) => self.recv_subscribe_error(msg.id, CacheError::Closed), + Message::SubscribeError(msg) => self.recv_subscribe_error(msg.id, CacheError::Reset(msg.code)), + Message::GoAway(_msg) => unimplemented!("GOAWAY"), + _ => Err(SessionError::RoleViolation(msg.id())), + } + } + + fn recv_subscribe_error(&mut self, id: VarInt, err: CacheError) -> Result<(), SessionError> { + let mut subscribes = self.subscribes.lock().unwrap(); + let subscribe = subscribes.remove(&id).ok_or(CacheError::NotFound)?; + subscribe.close(err)?; + + Ok(()) + } + + async fn run_streams(self) -> Result<(), SessionError> { + loop { + // Accept all incoming unidirectional streams. + let stream = self.webtransport.accept_uni().await?; + let this = self.clone(); + + tokio::spawn(async move { + if let Err(err) = this.run_stream(stream).await { + log::warn!("failed to receive stream: err={:#?}", err); + } + }); + } + } + + async fn run_stream(self, mut stream: RecvStream) -> Result<(), SessionError> { + // Decode the object on the data stream. + let mut object = message::Object::decode(&mut stream, &self.control.ext) + .await + .map_err(|e| SessionError::Unknown(e.to_string()))?; + + log::trace!("first object: {:?}", object); + + // A new scope is needed because the async compiler is dumb + let mut segment = { + let mut subscribes = self.subscribes.lock().unwrap(); + let track = subscribes.get_mut(&object.track).ok_or(CacheError::NotFound)?; + + track.create_segment(segment::Info { + sequence: object.group, + priority: object.priority, + expires: object.expires, + })? + }; + + log::trace!("received segment: {:?}", segment); + + // Create the first fragment + let mut fragment = segment.push_fragment(object.sequence, object.size.map(usize::from))?; + let mut remain = object.size.map(usize::from); + + loop { + if let Some(0) = remain { + // Decode the next object from the stream. + let next = match message::Object::decode(&mut stream, &self.control.ext).await { + Ok(next) => next, + + // No more objects + Err(DecodeError::Final) => break, + + // Unknown error + Err(err) => return Err(err.into()), + }; + + log::trace!("next object: {:?}", object); + + // NOTE: This is a custom restriction; not part of the moq-transport draft. + // We require every OBJECT to contain the same priority since prioritization is done per-stream. + // We also require every OBJECT to contain the same group so we know when the group ends, and can detect gaps. + if next.priority != object.priority && next.group != object.group { + return Err(SessionError::StreamMapping); + } + + object = next; + + // Create a new object. + fragment = segment.push_fragment(object.sequence, object.size.map(usize::from))?; + remain = object.size.map(usize::from); + + log::trace!("next fragment: {:?}", fragment); + } + + match stream.read_chunk(remain.unwrap_or(usize::MAX), true).await? { + // Unbounded object has ended + None if remain.is_none() => break, + + // Bounded object ended early, oops. + None => return Err(DecodeError::UnexpectedEnd.into()), + + // NOTE: This does not make a copy! + // Bytes are immutable and ref counted. + Some(data) => { + remain = remain.map(|r| r - data.bytes.len()); + + log::trace!("next chunk: {:?}", data); + fragment.chunk(data.bytes)?; + } + } + } + + Ok(()) + } + + async fn run_source(mut self) -> Result<(), SessionError> { + log::debug!("running source"); + loop { + // NOTE: This returns Closed when the source is closed. + let track = self.source.next_track().await?; + let name = track.name.clone(); + + let id = VarInt::from_u32(self.next.fetch_add(1, atomic::Ordering::SeqCst)); + self.subscribes.lock().unwrap().insert(id, track); + + let msg = message::Subscribe { + id, + namespace: self.control.ext.subscribe_split.then(|| "".to_string()), + name, + + // TODO correctly support these + start_group: message::SubscribeLocation::Latest(VarInt::ZERO), + start_object: message::SubscribeLocation::Absolute(VarInt::ZERO), + end_group: message::SubscribeLocation::None, + end_object: message::SubscribeLocation::None, + + switch_track_id: Some(VarInt::ZERO), + + params: Default::default(), + }; + + self.control.send(msg).await?; + } + } +} diff --git a/repos/moq-rs/moq-transport/src/setup/client.rs b/repos/moq-rs/moq-transport/src/setup/client.rs new file mode 100644 index 0000000..a18eb7d --- /dev/null +++ b/repos/moq-rs/moq-transport/src/setup/client.rs @@ -0,0 +1,72 @@ +use super::{Extensions, Role, Versions}; +use crate::{ + coding::{Decode, DecodeError, Encode, EncodeError, Params}, + VarInt, +}; + +use crate::coding::{AsyncRead, AsyncWrite}; + +/// Sent by the client to setup the session. +// NOTE: This is not a message type, but rather the control stream header. +// Proposal: https://github.com/moq-wg/moq-transport/issues/138 +#[derive(Debug)] +pub struct Client { + /// The list of supported versions in preferred order. + pub versions: Versions, + + /// Indicate if the client is a publisher, a subscriber, or both. + pub role: Role, + + /// A list of known/offered extensions. + pub extensions: Extensions, + + /// Unknown parameters. + pub params: Params, +} + +impl Client { + /// Decode a client setup message. + pub async fn decode(r: &mut R) -> Result { + let typ = VarInt::decode(r).await?; + if typ.into_inner() != 0x40 { + return Err(DecodeError::InvalidMessage(typ)); + } + + let versions = Versions::decode(r).await?; + let mut params = Params::decode(r).await?; + + let role = params + .get::(VarInt::from_u32(0)) + .await? + .ok_or(DecodeError::MissingParameter)?; + + // Make sure the PATH parameter isn't used + // TODO: This assumes WebTransport support only + if params.has(VarInt::from_u32(1)) { + return Err(DecodeError::InvalidParameter); + } + + let extensions = Extensions::load(&mut params).await?; + + Ok(Self { + versions, + role, + extensions, + params, + }) + } + + /// Encode a server setup message. + pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + VarInt::from_u32(0x40).encode(w).await?; + self.versions.encode(w).await?; + + let mut params = self.params.clone(); + params.set(VarInt::from_u32(0), self.role).await?; + self.extensions.store(&mut params).await?; + + params.encode(w).await?; + + Ok(()) + } +} diff --git a/repos/moq-rs/moq-transport/src/setup/extension.rs b/repos/moq-rs/moq-transport/src/setup/extension.rs new file mode 100644 index 0000000..0a197b7 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/setup/extension.rs @@ -0,0 +1,88 @@ +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::coding::{Decode, DecodeError, Encode, EncodeError, Params}; +use crate::session::SessionError; +use crate::VarInt; +use paste::paste; + +/// This is a custom extension scheme to allow/require draft PRs. +/// +/// By convention, the extension number is the PR number + 0xe0000. + +macro_rules! extensions { + {$($name:ident = $val:expr,)*} => { + #[derive(Clone, Default, Debug)] + pub struct Extensions { + $( + pub $name: bool, + )* + } + + impl Extensions { + pub async fn load(params: &mut Params) -> Result { + let mut extensions = Self::default(); + + $( + if let Some(_) = params.get::(VarInt::from_u32($val)).await? { + extensions.$name = true + } + )* + + Ok(extensions) + } + + pub async fn store(&self, params: &mut Params) -> Result<(), EncodeError> { + $( + if self.$name { + params.set(VarInt::from_u32($val), ExtensionExists{}).await?; + } + )* + + Ok(()) + } + + paste! { + $( + pub fn [](&self) -> Result<(), SessionError> { + match self.$name { + true => Ok(()), + false => Err(SessionError::RequiredExtension(VarInt::from_u32($val))), + } + } + )* + } + } + } +} + +struct ExtensionExists; + +#[async_trait::async_trait] +impl Decode for ExtensionExists { + async fn decode(_r: &mut R) -> Result { + Ok(ExtensionExists {}) + } +} + +#[async_trait::async_trait] +impl Encode for ExtensionExists { + async fn encode(&self, _w: &mut W) -> Result<(), EncodeError> { + Ok(()) + } +} + +extensions! { + // required for publishers: OBJECT contains expires VarInt in seconds: https://github.com/moq-wg/moq-transport/issues/249 + // TODO write up a PR + object_expires = 0xe00f9, + + // required: SUBSCRIBE chooses track ID: https://github.com/moq-wg/moq-transport/pull/258 + subscriber_id = 0xe0102, + + // optional: SUBSCRIBE contains namespace/name tuple: https://github.com/moq-wg/moq-transport/pull/277 + subscribe_split = 0xe0115, + + ntp_timestamp = 0xe0116, // TODO - ZG -write up a PR + + switch_track_id = 0xe0117, // TODO - ZG -write up a PR +} diff --git a/repos/moq-rs/moq-transport/src/setup/mod.rs b/repos/moq-rs/moq-transport/src/setup/mod.rs new file mode 100644 index 0000000..e7662e7 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/setup/mod.rs @@ -0,0 +1,17 @@ +//! Messages used for the MoQ Transport handshake. +//! +//! After establishing the WebTransport session, the client creates a bidirectional QUIC stream. +//! The client sends the [Client] message and the server responds with the [Server] message. +//! Both sides negotate the [Version] and [Role]. + +mod client; +mod extension; +mod role; +mod server; +mod version; + +pub use client::*; +pub use extension::*; +pub use role::*; +pub use server::*; +pub use version::*; diff --git a/repos/moq-rs/moq-transport/src/setup/role.rs b/repos/moq-rs/moq-transport/src/setup/role.rs new file mode 100644 index 0000000..10b30e0 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/setup/role.rs @@ -0,0 +1,74 @@ +use crate::coding::{AsyncRead, AsyncWrite}; + +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; + +/// Indicates the endpoint is a publisher, subscriber, or both. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Role { + Publisher, + Subscriber, + Both, +} + +impl Role { + /// Returns true if the role is publisher. + pub fn is_publisher(&self) -> bool { + match self { + Self::Publisher | Self::Both => true, + Self::Subscriber => false, + } + } + + /// Returns true if the role is a subscriber. + pub fn is_subscriber(&self) -> bool { + match self { + Self::Subscriber | Self::Both => true, + Self::Publisher => false, + } + } + + /// Returns true if two endpoints are compatible. + pub fn is_compatible(&self, other: Role) -> bool { + self.is_publisher() == other.is_subscriber() && self.is_subscriber() == other.is_publisher() + } +} + +impl From for VarInt { + fn from(r: Role) -> Self { + VarInt::from_u32(match r { + Role::Publisher => 0x1, + Role::Subscriber => 0x2, + Role::Both => 0x3, + }) + } +} + +impl TryFrom for Role { + type Error = DecodeError; + + fn try_from(v: VarInt) -> Result { + match v.into_inner() { + 0x1 => Ok(Self::Publisher), + 0x2 => Ok(Self::Subscriber), + 0x3 => Ok(Self::Both), + _ => Err(DecodeError::InvalidRole(v)), + } + } +} + +#[async_trait::async_trait] +impl Decode for Role { + /// Decode the role. + async fn decode(r: &mut R) -> Result { + let v = VarInt::decode(r).await?; + v.try_into() + } +} + +#[async_trait::async_trait] +impl Encode for Role { + /// Encode the role. + async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + VarInt::from(*self).encode(w).await + } +} diff --git a/repos/moq-rs/moq-transport/src/setup/server.rs b/repos/moq-rs/moq-transport/src/setup/server.rs new file mode 100644 index 0000000..7f73119 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/setup/server.rs @@ -0,0 +1,71 @@ +use super::{Extensions, Role, Version}; +use crate::{ + coding::{Decode, DecodeError, Encode, EncodeError, Params}, + VarInt, +}; + +use crate::coding::{AsyncRead, AsyncWrite}; + +/// Sent by the server in response to a client setup. +// NOTE: This is not a message type, but rather the control stream header. +// Proposal: https://github.com/moq-wg/moq-transport/issues/138 +#[derive(Debug)] +pub struct Server { + /// The list of supported versions in preferred order. + pub version: Version, + + /// Indicate if the server is a publisher, a subscriber, or both. + // Proposal: moq-wg/moq-transport#151 + pub role: Role, + + /// Custom extensions. + pub extensions: Extensions, + + /// Unknown parameters. + pub params: Params, +} + +impl Server { + /// Decode the server setup. + pub async fn decode(r: &mut R) -> Result { + let typ = VarInt::decode(r).await?; + if typ.into_inner() != 0x41 { + return Err(DecodeError::InvalidMessage(typ)); + } + + let version = Version::decode(r).await?; + let mut params = Params::decode(r).await?; + + let role = params + .get::(VarInt::from_u32(0)) + .await? + .ok_or(DecodeError::MissingParameter)?; + + // Make sure the PATH parameter isn't used + if params.has(VarInt::from_u32(1)) { + return Err(DecodeError::InvalidParameter); + } + + let extensions = Extensions::load(&mut params).await?; + + Ok(Self { + version, + role, + extensions, + params, + }) + } + + /// Encode the server setup. + pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + VarInt::from_u32(0x41).encode(w).await?; + self.version.encode(w).await?; + + let mut params = self.params.clone(); + params.set(VarInt::from_u32(0), self.role).await?; + self.extensions.store(&mut params).await?; + params.encode(w).await?; + + Ok(()) + } +} diff --git a/repos/moq-rs/moq-transport/src/setup/version.rs b/repos/moq-rs/moq-transport/src/setup/version.rs new file mode 100644 index 0000000..8933039 --- /dev/null +++ b/repos/moq-rs/moq-transport/src/setup/version.rs @@ -0,0 +1,155 @@ +use crate::coding::{Decode, DecodeError, Encode, EncodeError, VarInt}; + +use crate::coding::{AsyncRead, AsyncWrite}; + +use std::ops::Deref; + +/// A version number negotiated during the setup. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Version(pub VarInt); + +impl Version { + /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-00.html + pub const DRAFT_00: Version = Version(VarInt::from_u32(0xff000000)); + + /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-01.html + pub const DRAFT_01: Version = Version(VarInt::from_u32(0xff000001)); + + /// Fork of draft-ietf-moq-transport-00. + /// + /// Rough list of differences: + /// + /// # Messages + /// - Messages are sent over a control stream or a data stream. + /// - Data streams: each unidirectional stream contains a single OBJECT message. + /// - Control stream: a (client-initiated) bidirectional stream containing SETUP and then all other messages. + /// - Messages do not contain a length; unknown messages are fatal. + /// + /// # SETUP + /// - SETUP is split into SETUP_CLIENT and SETUP_SERVER with separate IDs. + /// - SETUP uses version `0xff00` for draft-00. + /// - SETUP no longer contains optional parameters; all are encoded in order and possibly zero. + /// - SETUP `role` indicates the role of the sender, not the role of the server. + /// - SETUP `path` field removed; use WebTransport for path. + /// + /// # SUBSCRIBE + /// - SUBSCRIBE `full_name` is split into separate `namespace` and `name` fields. + /// - SUBSCRIBE no longer contains optional parameters; all are encoded in order and possibly zero. + /// - SUBSCRIBE no longer contains the `auth` parameter; use WebTransport for auth. + /// - SUBSCRIBE no longer contains the `group` parameter; concept no longer exists. + /// - SUBSCRIBE contains the `id` instead of SUBSCRIBE_OK. + /// - SUBSCRIBE_OK and SUBSCRIBE_ERROR reference the subscription `id` the instead of the track `full_name`. + /// - SUBSCRIBE_ERROR was renamed to SUBSCRIBE_RESET, sent by publisher to terminate a SUBSCRIBE. + /// - SUBSCRIBE_STOP was added, sent by the subscriber to terminate a SUBSCRIBE. + /// - SUBSCRIBE_OK no longer has `expires`. + /// + /// # ANNOUNCE + /// - ANNOUNCE no longer contains optional parameters; all are encoded in order and possibly zero. + /// - ANNOUNCE no longer contains the `auth` field; use WebTransport for auth. + /// - ANNOUNCE_ERROR was renamed to ANNOUNCE_RESET, sent by publisher to terminate an ANNOUNCE. + /// - ANNOUNCE_STOP was added, sent by the subscriber to terminate an ANNOUNCE. + /// + /// # OBJECT + /// - OBJECT uses a dedicated QUIC stream. + /// - OBJECT has no size and continues until stream FIN. + /// - OBJECT `priority` is a i32 instead of a varint. (for practical reasons) + /// - OBJECT `expires` was added, a varint in seconds. + /// - OBJECT `group` was removed. + /// + /// # GROUP + /// - GROUP concept was removed, replaced with OBJECT as a QUIC stream. + pub const KIXEL_00: Version = Version(VarInt::from_u32(0xbad00)); + + /// Fork of draft-ietf-moq-transport-01. + /// + /// Most of the KIXEL_00 changes made it into the draft, or were reverted. + /// This was only used for a short time until extensions were created. + /// + /// - SUBSCRIBE contains a separate track namespace and track name field (accidental revert). [#277](https://github.com/moq-wg/moq-transport/pull/277) + /// - SUBSCRIBE contains the `track_id` instead of SUBSCRIBE_OK. [#145](https://github.com/moq-wg/moq-transport/issues/145) + /// - SUBSCRIBE_* reference `track_id` the instead of the `track_full_name`. [#145](https://github.com/moq-wg/moq-transport/issues/145) + /// - OBJECT `priority` is still a VarInt, but the max value is a u32 (implementation reasons) + /// - OBJECT messages within the same `group` MUST be on the same QUIC stream. + pub const KIXEL_01: Version = Version(VarInt::from_u32(0xbad01)); +} + +impl From for Version { + fn from(v: VarInt) -> Self { + Self(v) + } +} + +impl From for VarInt { + fn from(v: Version) -> Self { + v.0 + } +} + +impl Version { + /// Decode the version number. + pub async fn decode(r: &mut R) -> Result { + let v = VarInt::decode(r).await?; + Ok(Self(v)) + } + + /// Encode the version number. + pub async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + self.0.encode(w).await?; + Ok(()) + } +} + +/// A list of versions in arbitrary order. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Versions(Vec); + +#[async_trait::async_trait] +impl Decode for Versions { + /// Decode the version list. + async fn decode(r: &mut R) -> Result { + let count = VarInt::decode(r).await?.into_inner(); + let mut vs = Vec::new(); + + for _ in 0..count { + let v = Version::decode(r).await?; + vs.push(v); + } + + Ok(Self(vs)) + } +} + +#[async_trait::async_trait] +impl Encode for Versions { + /// Encode the version list. + async fn encode(&self, w: &mut W) -> Result<(), EncodeError> { + let size: VarInt = self.0.len().try_into()?; + size.encode(w).await?; + + for v in &self.0 { + v.encode(w).await?; + } + + Ok(()) + } +} + +impl Deref for Versions { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for Versions { + fn from(vs: Vec) -> Self { + Self(vs) + } +} + +impl From<[Version; N]> for Versions { + fn from(vs: [Version; N]) -> Self { + Self(vs.to_vec()) + } +} diff --git a/repos/moq-rs/tc_profiles/FCCamazone b/repos/moq-rs/tc_profiles/FCCamazone new file mode 100644 index 0000000..2d4489b --- /dev/null +++ b/repos/moq-rs/tc_profiles/FCCamazone @@ -0,0 +1,2400 @@ +rate 1418kbit +delay 50ms +loss 0.08% +wait 1s +rate 1399kbit +delay 50ms +loss 0.08% +wait 1s +rate 1474kbit +delay 50ms +loss 0.08% +wait 1s +rate 1507kbit +delay 50ms +loss 0.08% +wait 1s +rate 1484kbit +delay 50ms +loss 0.08% +wait 1s +rate 1576kbit +delay 50ms +loss 0.08% +wait 1s +rate 1479kbit +delay 50ms +loss 0.08% +wait 1s +rate 1462kbit +delay 50ms +loss 0.08% +wait 1s +rate 1576kbit +delay 50ms +loss 0.08% +wait 1s +rate 1452kbit +delay 50ms +loss 0.08% +wait 1s +rate 1486kbit +delay 50ms +loss 0.08% +wait 1s +rate 1499kbit +delay 50ms +loss 0.08% +wait 1s +rate 1448kbit +delay 50ms +loss 0.08% +wait 1s +rate 1468kbit +delay 50ms +loss 0.08% +wait 1s +rate 1362kbit +delay 50ms +loss 0.08% +wait 1s +rate 1564kbit +delay 50ms +loss 0.08% +wait 1s +rate 1514kbit +delay 50ms +loss 0.08% +wait 1s +rate 1541kbit +delay 50ms +loss 0.08% +wait 1s +rate 1498kbit +delay 50ms +loss 0.08% +wait 1s +rate 1454kbit +delay 50ms +loss 0.08% +wait 1s +rate 1510kbit +delay 50ms +loss 0.08% +wait 1s +rate 1383kbit +delay 50ms +loss 0.08% +wait 1s +rate 1471kbit +delay 50ms +loss 0.08% +wait 1s +rate 1497kbit +delay 50ms +loss 0.08% +wait 1s +rate 4928kbit +delay 50ms +loss 0.08% +wait 1s +rate 4725kbit +delay 50ms +loss 0.08% +wait 1s +rate 4862kbit +delay 50ms +loss 0.08% +wait 1s +rate 4695kbit +delay 50ms +loss 0.08% +wait 1s +rate 5307kbit +delay 50ms +loss 0.08% +wait 1s +rate 4854kbit +delay 50ms +loss 0.08% +wait 1s +rate 5045kbit +delay 50ms +loss 0.08% +wait 1s +rate 4358kbit +delay 50ms +loss 0.08% +wait 1s +rate 5371kbit +delay 50ms +loss 0.08% +wait 1s +rate 4486kbit +delay 50ms +loss 0.08% +wait 1s +rate 5374kbit +delay 50ms +loss 0.08% +wait 1s +rate 4773kbit +delay 50ms +loss 0.08% +wait 1s +rate 4401kbit +delay 50ms +loss 0.08% +wait 1s +rate 5049kbit +delay 50ms +loss 0.08% +wait 1s +rate 4534kbit +delay 50ms +loss 0.08% +wait 1s +rate 5248kbit +delay 50ms +loss 0.08% +wait 1s +rate 4771kbit +delay 50ms +loss 0.08% +wait 1s +rate 4883kbit +delay 50ms +loss 0.08% +wait 1s +rate 4821kbit +delay 50ms +loss 0.08% +wait 1s +rate 5147kbit +delay 50ms +loss 0.08% +wait 1s +rate 4966kbit +delay 50ms +loss 0.08% +wait 1s +rate 4624kbit +delay 50ms +loss 0.08% +wait 1s +rate 4930kbit +delay 50ms +loss 0.08% +wait 1s +rate 4597kbit +delay 50ms +loss 0.08% +wait 1s +rate 5161kbit +delay 50ms +loss 0.08% +wait 1s +rate 1340kbit +delay 50ms +loss 0.08% +wait 1s +rate 5090kbit +delay 50ms +loss 0.08% +wait 1s +rate 5000kbit +delay 50ms +loss 0.08% +wait 1s +rate 5252kbit +delay 50ms +loss 0.08% +wait 1s +rate 6574kbit +delay 50ms +loss 0.08% +wait 1s +rate 4694kbit +delay 50ms +loss 0.08% +wait 1s +rate 3956kbit +delay 50ms +loss 0.08% +wait 1s +rate 4623kbit +delay 50ms +loss 0.08% +wait 1s +rate 5200kbit +delay 50ms +loss 0.08% +wait 1s +rate 4710kbit +delay 50ms +loss 0.08% +wait 1s +rate 4662kbit +delay 50ms +loss 0.08% +wait 1s +rate 4946kbit +delay 50ms +loss 0.08% +wait 1s +rate 4600kbit +delay 50ms +loss 0.08% +wait 1s +rate 4687kbit +delay 50ms +loss 0.08% +wait 1s +rate 4768kbit +delay 50ms +loss 0.08% +wait 1s +rate 5015kbit +delay 50ms +loss 0.08% +wait 1s +rate 6271kbit +delay 50ms +loss 0.08% +wait 1s +rate 924kbit +delay 50ms +loss 0.08% +wait 1s +rate 897kbit +delay 50ms +loss 0.08% +wait 1s +rate 897kbit +delay 50ms +loss 0.08% +wait 1s +rate 935kbit +delay 50ms +loss 0.08% +wait 1s +rate 935kbit +delay 50ms +loss 0.08% +wait 1s +rate 929kbit +delay 50ms +loss 0.08% +wait 1s +rate 911kbit +delay 50ms +loss 0.08% +wait 1s +rate 920kbit +delay 50ms +loss 0.08% +wait 1s +rate 918kbit +delay 50ms +loss 0.08% +wait 1s +rate 922kbit +delay 50ms +loss 0.08% +wait 1s +rate 922kbit +delay 50ms +loss 0.08% +wait 1s +rate 891kbit +delay 50ms +loss 0.08% +wait 1s +rate 914kbit +delay 50ms +loss 0.08% +wait 1s +rate 911kbit +delay 50ms +loss 0.08% +wait 1s +rate 895kbit +delay 50ms +loss 0.08% +wait 1s +rate 908kbit +delay 50ms +loss 0.08% +wait 1s +rate 874kbit +delay 50ms +loss 0.08% +wait 1s +rate 900kbit +delay 50ms +loss 0.08% +wait 1s +rate 880kbit +delay 50ms +loss 0.08% +wait 1s +rate 908kbit +delay 50ms +loss 0.08% +wait 1s +rate 908kbit +delay 50ms +loss 0.08% +wait 1s +rate 913kbit +delay 50ms +loss 0.08% +wait 1s +rate 901kbit +delay 50ms +loss 0.08% +wait 1s +rate 923kbit +delay 50ms +loss 0.08% +wait 1s +rate 925kbit +delay 50ms +loss 0.08% +wait 1s +rate 899kbit +delay 50ms +loss 0.08% +wait 1s +rate 926kbit +delay 50ms +loss 0.08% +wait 1s +rate 913kbit +delay 50ms +loss 0.08% +wait 1s +rate 901kbit +delay 50ms +loss 0.08% +wait 1s +rate 923kbit +delay 50ms +loss 0.08% +wait 1s +rate 926kbit +delay 50ms +loss 0.08% +wait 1s +rate 898kbit +delay 50ms +loss 0.08% +wait 1s +rate 909kbit +delay 50ms +loss 0.08% +wait 1s +rate 910kbit +delay 50ms +loss 0.08% +wait 1s +rate 911kbit +delay 50ms +loss 0.08% +wait 1s +rate 915kbit +delay 50ms +loss 0.08% +wait 1s +rate 921kbit +delay 50ms +loss 0.08% +wait 1s +rate 922kbit +delay 50ms +loss 0.08% +wait 1s +rate 893kbit +delay 50ms +loss 0.08% +wait 1s +rate 922kbit +delay 50ms +loss 0.08% +wait 1s +rate 944kbit +delay 50ms +loss 0.08% +wait 1s +rate 895kbit +delay 50ms +loss 0.08% +wait 1s +rate 925kbit +delay 50ms +loss 0.08% +wait 1s +rate 915kbit +delay 50ms +loss 0.08% +wait 1s +rate 873kbit +delay 50ms +loss 0.08% +wait 1s +rate 926kbit +delay 50ms +loss 0.08% +wait 1s +rate 932kbit +delay 50ms +loss 0.08% +wait 1s +rate 911kbit +delay 50ms +loss 0.08% +wait 1s +rate 847kbit +delay 50ms +loss 0.08% +wait 1s +rate 904kbit +delay 50ms +loss 0.08% +wait 1s +rate 891kbit +delay 50ms +loss 0.08% +wait 1s +rate 899kbit +delay 50ms +loss 0.08% +wait 1s +rate 916kbit +delay 50ms +loss 0.08% +wait 1s +rate 899kbit +delay 50ms +loss 0.08% +wait 1s +rate 740kbit +delay 50ms +loss 0.08% +wait 1s +rate 897kbit +delay 50ms +loss 0.08% +wait 1s +rate 916kbit +delay 50ms +loss 0.08% +wait 1s +rate 906kbit +delay 50ms +loss 0.08% +wait 1s +rate 932kbit +delay 50ms +loss 0.08% +wait 1s +rate 942kbit +delay 50ms +loss 0.08% +wait 1s +rate 910kbit +delay 50ms +loss 0.08% +wait 1s +rate 869kbit +delay 50ms +loss 0.08% +wait 1s +rate 860kbit +delay 50ms +loss 0.08% +wait 1s +rate 750kbit +delay 50ms +loss 0.08% +wait 1s +rate 903kbit +delay 50ms +loss 0.08% +wait 1s +rate 945kbit +delay 50ms +loss 0.08% +wait 1s +rate 938kbit +delay 50ms +loss 0.08% +wait 1s +rate 928kbit +delay 50ms +loss 0.08% +wait 1s +rate 941kbit +delay 50ms +loss 0.08% +wait 1s +rate 918kbit +delay 50ms +loss 0.08% +wait 1s +rate 940kbit +delay 50ms +loss 0.08% +wait 1s +rate 903kbit +delay 50ms +loss 0.08% +wait 1s +rate 942kbit +delay 50ms +loss 0.08% +wait 1s +rate 946kbit +delay 50ms +loss 0.08% +wait 1s +rate 910kbit +delay 50ms +loss 0.08% +wait 1s +rate 912kbit +delay 50ms +loss 0.08% +wait 1s +rate 902kbit +delay 50ms +loss 0.08% +wait 1s +rate 927kbit +delay 50ms +loss 0.08% +wait 1s +rate 948kbit +delay 50ms +loss 0.08% +wait 1s +rate 896kbit +delay 50ms +loss 0.08% +wait 1s +rate 914kbit +delay 50ms +loss 0.08% +wait 1s +rate 923kbit +delay 50ms +loss 0.08% +wait 1s +rate 934kbit +delay 50ms +loss 0.08% +wait 1s +rate 927kbit +delay 50ms +loss 0.08% +wait 1s +rate 925kbit +delay 50ms +loss 0.08% +wait 1s +rate 928kbit +delay 50ms +loss 0.08% +wait 1s +rate 905kbit +delay 50ms +loss 0.08% +wait 1s +rate 890kbit +delay 50ms +loss 0.08% +wait 1s +rate 5073kbit +delay 50ms +loss 0.08% +wait 1s +rate 5007kbit +delay 50ms +loss 0.08% +wait 1s +rate 4080kbit +delay 50ms +loss 0.08% +wait 1s +rate 4477kbit +delay 50ms +loss 0.08% +wait 1s +rate 4783kbit +delay 50ms +loss 0.08% +wait 1s +rate 4925kbit +delay 50ms +loss 0.08% +wait 1s +rate 5464kbit +delay 50ms +loss 0.08% +wait 1s +rate 5089kbit +delay 50ms +loss 0.08% +wait 1s +rate 4460kbit +delay 50ms +loss 0.08% +wait 1s +rate 5172kbit +delay 50ms +loss 0.08% +wait 1s +rate 4515kbit +delay 50ms +loss 0.08% +wait 1s +rate 4423kbit +delay 50ms +loss 0.08% +wait 1s +rate 4786kbit +delay 50ms +loss 0.08% +wait 1s +rate 4858kbit +delay 50ms +loss 0.08% +wait 1s +rate 900kbit +delay 50ms +loss 0.08% +wait 1s +rate 4998kbit +delay 50ms +loss 0.08% +wait 1s +rate 4827kbit +delay 50ms +loss 0.08% +wait 1s +rate 4868kbit +delay 50ms +loss 0.08% +wait 1s +rate 5251kbit +delay 50ms +loss 0.08% +wait 1s +rate 4750kbit +delay 50ms +loss 0.08% +wait 1s +rate 907kbit +delay 50ms +loss 0.08% +wait 1s +rate 5004kbit +delay 50ms +loss 0.08% +wait 1s +rate 4470kbit +delay 50ms +loss 0.08% +wait 1s +rate 5163kbit +delay 50ms +loss 0.08% +wait 1s +rate 3943kbit +delay 50ms +loss 0.08% +wait 1s +rate 4254kbit +delay 50ms +loss 0.08% +wait 1s +rate 4783kbit +delay 50ms +loss 0.08% +wait 1s +rate 4835kbit +delay 50ms +loss 0.08% +wait 1s +rate 5133kbit +delay 50ms +loss 0.08% +wait 1s +rate 5106kbit +delay 50ms +loss 0.08% +wait 1s +rate 4934kbit +delay 50ms +loss 0.08% +wait 1s +rate 4744kbit +delay 50ms +loss 0.08% +wait 1s +rate 4189kbit +delay 50ms +loss 0.08% +wait 1s +rate 4507kbit +delay 50ms +loss 0.08% +wait 1s +rate 1330kbit +delay 50ms +loss 0.08% +wait 1s +rate 4253kbit +delay 50ms +loss 0.08% +wait 1s +rate 4454kbit +delay 50ms +loss 0.08% +wait 1s +rate 4432kbit +delay 50ms +loss 0.08% +wait 1s +rate 907kbit +delay 50ms +loss 0.08% +wait 1s +rate 4436kbit +delay 50ms +loss 0.08% +wait 1s +rate 4627kbit +delay 50ms +loss 0.08% +wait 1s +rate 5102kbit +delay 50ms +loss 0.08% +wait 1s +rate 906kbit +delay 50ms +loss 0.08% +wait 1s +rate 4775kbit +delay 50ms +loss 0.08% +wait 1s +rate 4751kbit +delay 50ms +loss 0.08% +wait 1s +rate 4590kbit +delay 50ms +loss 0.08% +wait 1s +rate 4837kbit +delay 50ms +loss 0.08% +wait 1s +rate 4552kbit +delay 50ms +loss 0.08% +wait 1s +rate 4491kbit +delay 50ms +loss 0.08% +wait 1s +rate 4641kbit +delay 50ms +loss 0.08% +wait 1s +rate 934kbit +delay 50ms +loss 0.08% +wait 1s +rate 4977kbit +delay 50ms +loss 0.08% +wait 1s +rate 7686kbit +delay 50ms +loss 0.08% +wait 1s +rate 933kbit +delay 50ms +loss 0.08% +wait 1s +rate 6580kbit +delay 50ms +loss 0.08% +wait 1s +rate 4277kbit +delay 50ms +loss 0.08% +wait 1s +rate 5171kbit +delay 50ms +loss 0.08% +wait 1s +rate 4512kbit +delay 50ms +loss 0.08% +wait 1s +rate 4583kbit +delay 50ms +loss 0.08% +wait 1s +rate 4729kbit +delay 50ms +loss 0.08% +wait 1s +rate 4876kbit +delay 50ms +loss 0.08% +wait 1s +rate 5265kbit +delay 50ms +loss 0.08% +wait 1s +rate 924kbit +delay 50ms +loss 0.08% +wait 1s +rate 5272kbit +delay 50ms +loss 0.08% +wait 1s +rate 4945kbit +delay 50ms +loss 0.08% +wait 1s +rate 4676kbit +delay 50ms +loss 0.08% +wait 1s +rate 5159kbit +delay 50ms +loss 0.08% +wait 1s +rate 4825kbit +delay 50ms +loss 0.08% +wait 1s +rate 4578kbit +delay 50ms +loss 0.08% +wait 1s +rate 3952kbit +delay 50ms +loss 0.08% +wait 1s +rate 4527kbit +delay 50ms +loss 0.08% +wait 1s +rate 4370kbit +delay 50ms +loss 0.08% +wait 1s +rate 4944kbit +delay 50ms +loss 0.08% +wait 1s +rate 4657kbit +delay 50ms +loss 0.08% +wait 1s +rate 959kbit +delay 50ms +loss 0.08% +wait 1s +rate 6868kbit +delay 50ms +loss 0.08% +wait 1s +rate 4798kbit +delay 50ms +loss 0.08% +wait 1s +rate 4511kbit +delay 50ms +loss 0.08% +wait 1s +rate 5066kbit +delay 50ms +loss 0.08% +wait 1s +rate 4852kbit +delay 50ms +loss 0.08% +wait 1s +rate 4707kbit +delay 50ms +loss 0.08% +wait 1s +rate 4598kbit +delay 50ms +loss 0.08% +wait 1s +rate 4431kbit +delay 50ms +loss 0.08% +wait 1s +rate 4074kbit +delay 50ms +loss 0.08% +wait 1s +rate 4532kbit +delay 50ms +loss 0.08% +wait 1s +rate 4846kbit +delay 50ms +loss 0.08% +wait 1s +rate 4701kbit +delay 50ms +loss 0.08% +wait 1s +rate 4263kbit +delay 50ms +loss 0.08% +wait 1s +rate 4886kbit +delay 50ms +loss 0.08% +wait 1s +rate 4757kbit +delay 50ms +loss 0.08% +wait 1s +rate 4819kbit +delay 50ms +loss 0.08% +wait 1s +rate 4538kbit +delay 50ms +loss 0.08% +wait 1s +rate 4903kbit +delay 50ms +loss 0.08% +wait 1s +rate 4668kbit +delay 50ms +loss 0.08% +wait 1s +rate 4775kbit +delay 50ms +loss 0.08% +wait 1s +rate 6419kbit +delay 50ms +loss 0.08% +wait 1s +rate 3359kbit +delay 50ms +loss 0.08% +wait 1s +rate 4481kbit +delay 50ms +loss 0.08% +wait 1s +rate 4346kbit +delay 50ms +loss 0.08% +wait 1s +rate 5181kbit +delay 50ms +loss 0.08% +wait 1s +rate 6044kbit +delay 50ms +loss 0.08% +wait 1s +rate 5259kbit +delay 50ms +loss 0.08% +wait 1s +rate 4723kbit +delay 50ms +loss 0.08% +wait 1s +rate 4841kbit +delay 50ms +loss 0.08% +wait 1s +rate 4083kbit +delay 50ms +loss 0.08% +wait 1s +rate 6574kbit +delay 50ms +loss 0.08% +wait 1s +rate 4686kbit +delay 50ms +loss 0.08% +wait 1s +rate 4449kbit +delay 50ms +loss 0.08% +wait 1s +rate 4509kbit +delay 50ms +loss 0.08% +wait 1s +rate 4410kbit +delay 50ms +loss 0.08% +wait 1s +rate 4428kbit +delay 50ms +loss 0.08% +wait 1s +rate 917kbit +delay 50ms +loss 0.08% +wait 1s +rate 893kbit +delay 50ms +loss 0.08% +wait 1s +rate 894kbit +delay 50ms +loss 0.08% +wait 1s +rate 877kbit +delay 50ms +loss 0.08% +wait 1s +rate 937kbit +delay 50ms +loss 0.08% +wait 1s +rate 931kbit +delay 50ms +loss 0.08% +wait 1s +rate 904kbit +delay 50ms +loss 0.08% +wait 1s +rate 924kbit +delay 50ms +loss 0.08% +wait 1s +rate 899kbit +delay 50ms +loss 0.08% +wait 1s +rate 896kbit +delay 50ms +loss 0.08% +wait 1s +rate 932kbit +delay 50ms +loss 0.08% +wait 1s +rate 937kbit +delay 50ms +loss 0.08% +wait 1s +rate 906kbit +delay 50ms +loss 0.08% +wait 1s +rate 916kbit +delay 50ms +loss 0.08% +wait 1s +rate 920kbit +delay 50ms +loss 0.08% +wait 1s +rate 943kbit +delay 50ms +loss 0.08% +wait 1s +rate 919kbit +delay 50ms +loss 0.08% +wait 1s +rate 883kbit +delay 50ms +loss 0.08% +wait 1s +rate 873kbit +delay 50ms +loss 0.08% +wait 1s +rate 936kbit +delay 50ms +loss 0.08% +wait 1s +rate 914kbit +delay 50ms +loss 0.08% +wait 1s +rate 911kbit +delay 50ms +loss 0.08% +wait 1s +rate 893kbit +delay 50ms +loss 0.08% +wait 1s +rate 933kbit +delay 50ms +loss 0.08% +wait 1s +rate 939kbit +delay 50ms +loss 0.08% +wait 1s +rate 915kbit +delay 50ms +loss 0.08% +wait 1s +rate 939kbit +delay 50ms +loss 0.08% +wait 1s +rate 952kbit +delay 50ms +loss 0.08% +wait 1s +rate 918kbit +delay 50ms +loss 0.08% +wait 1s +rate 924kbit +delay 50ms +loss 0.08% +wait 1s +rate 931kbit +delay 50ms +loss 0.08% +wait 1s +rate 878kbit +delay 50ms +loss 0.08% +wait 1s +rate 919kbit +delay 50ms +loss 0.08% +wait 1s +rate 907kbit +delay 50ms +loss 0.08% +wait 1s +rate 905kbit +delay 50ms +loss 0.08% +wait 1s +rate 873kbit +delay 50ms +loss 0.08% +wait 1s +rate 938kbit +delay 50ms +loss 0.08% +wait 1s +rate 916kbit +delay 50ms +loss 0.08% +wait 1s +rate 937kbit +delay 50ms +loss 0.08% +wait 1s +rate 940kbit +delay 50ms +loss 0.08% +wait 1s +rate 911kbit +delay 50ms +loss 0.08% +wait 1s +rate 915kbit +delay 50ms +loss 0.08% +wait 1s +rate 923kbit +delay 50ms +loss 0.08% +wait 1s +rate 933kbit +delay 50ms +loss 0.08% +wait 1s +rate 922kbit +delay 50ms +loss 0.08% +wait 1s +rate 899kbit +delay 50ms +loss 0.08% +wait 1s +rate 890kbit +delay 50ms +loss 0.08% +wait 1s +rate 935kbit +delay 50ms +loss 0.08% +wait 1s +rate 896kbit +delay 50ms +loss 0.08% +wait 1s +rate 919kbit +delay 50ms +loss 0.08% +wait 1s +rate 951kbit +delay 50ms +loss 0.08% +wait 1s +rate 903kbit +delay 50ms +loss 0.08% +wait 1s +rate 938kbit +delay 50ms +loss 0.08% +wait 1s +rate 913kbit +delay 50ms +loss 0.08% +wait 1s +rate 932kbit +delay 50ms +loss 0.08% +wait 1s +rate 918kbit +delay 50ms +loss 0.08% +wait 1s +rate 750kbit +delay 50ms +loss 0.08% +wait 1s +rate 933kbit +delay 50ms +loss 0.08% +wait 1s +rate 927kbit +delay 50ms +loss 0.08% +wait 1s +rate 928kbit +delay 50ms +loss 0.08% +wait 1s +rate 927kbit +delay 50ms +loss 0.08% +wait 1s +rate 920kbit +delay 50ms +loss 0.08% +wait 1s +rate 918kbit +delay 50ms +loss 0.08% +wait 1s +rate 907kbit +delay 50ms +loss 0.08% +wait 1s +rate 947kbit +delay 50ms +loss 0.08% +wait 1s +rate 947kbit +delay 50ms +loss 0.08% +wait 1s +rate 920kbit +delay 50ms +loss 0.08% +wait 1s +rate 918kbit +delay 50ms +loss 0.08% +wait 1s +rate 868kbit +delay 50ms +loss 0.08% +wait 1s +rate 928kbit +delay 50ms +loss 0.08% +wait 1s +rate 903kbit +delay 50ms +loss 0.08% +wait 1s +rate 944kbit +delay 50ms +loss 0.08% +wait 1s +rate 899kbit +delay 50ms +loss 0.08% +wait 1s +rate 941kbit +delay 50ms +loss 0.08% +wait 1s +rate 949kbit +delay 50ms +loss 0.08% +wait 1s +rate 915kbit +delay 50ms +loss 0.08% +wait 1s +rate 918kbit +delay 50ms +loss 0.08% +wait 1s +rate 923kbit +delay 50ms +loss 0.08% +wait 1s +rate 901kbit +delay 50ms +loss 0.08% +wait 1s +rate 901kbit +delay 50ms +loss 0.08% +wait 1s +rate 898kbit +delay 50ms +loss 0.08% +wait 1s +rate 880kbit +delay 50ms +loss 0.08% +wait 1s +rate 896kbit +delay 50ms +loss 0.08% +wait 1s +rate 894kbit +delay 50ms +loss 0.08% +wait 1s +rate 944kbit +delay 50ms +loss 0.08% +wait 1s +rate 900kbit +delay 50ms +loss 0.08% +wait 1s +rate 906kbit +delay 50ms +loss 0.08% +wait 1s +rate 895kbit +delay 50ms +loss 0.08% +wait 1s +rate 905kbit +delay 50ms +loss 0.08% +wait 1s +rate 4543kbit +delay 50ms +loss 0.08% +wait 1s +rate 4394kbit +delay 50ms +loss 0.08% +wait 1s +rate 4902kbit +delay 50ms +loss 0.08% +wait 1s +rate 6070kbit +delay 50ms +loss 0.08% +wait 1s +rate 898kbit +delay 50ms +loss 0.08% +wait 1s +rate 4689kbit +delay 50ms +loss 0.08% +wait 1s +rate 5211kbit +delay 50ms +loss 0.08% +wait 1s +rate 4842kbit +delay 50ms +loss 0.08% +wait 1s +rate 4463kbit +delay 50ms +loss 0.08% +wait 1s +rate 4738kbit +delay 50ms +loss 0.08% +wait 1s +rate 4902kbit +delay 50ms +loss 0.08% +wait 1s +rate 4350kbit +delay 50ms +loss 0.08% +wait 1s +rate 4795kbit +delay 50ms +loss 0.08% +wait 1s +rate 4775kbit +delay 50ms +loss 0.08% +wait 1s +rate 4635kbit +delay 50ms +loss 0.08% +wait 1s +rate 1418kbit +delay 50ms +loss 0.08% +wait 1s +rate 1399kbit +delay 50ms +loss 0.08% +wait 1s +rate 1474kbit +delay 50ms +loss 0.08% +wait 1s +rate 1507kbit +delay 50ms +loss 0.08% +wait 1s +rate 1484kbit +delay 50ms +loss 0.08% +wait 1s +rate 1576kbit +delay 50ms +loss 0.08% +wait 1s +rate 1479kbit +delay 50ms +loss 0.08% +wait 1s +rate 1462kbit +delay 50ms +loss 0.08% +wait 1s +rate 1576kbit +delay 50ms +loss 0.08% +wait 1s +rate 1452kbit +delay 50ms +loss 0.08% +wait 1s +rate 1486kbit +delay 50ms +loss 0.08% +wait 1s +rate 1499kbit +delay 50ms +loss 0.08% +wait 1s +rate 1448kbit +delay 50ms +loss 0.08% +wait 1s +rate 1468kbit +delay 50ms +loss 0.08% +wait 1s +rate 1362kbit +delay 50ms +loss 0.08% +wait 1s +rate 1564kbit +delay 50ms +loss 0.08% +wait 1s +rate 1514kbit +delay 50ms +loss 0.08% +wait 1s +rate 1541kbit +delay 50ms +loss 0.08% +wait 1s +rate 1498kbit +delay 50ms +loss 0.08% +wait 1s +rate 1454kbit +delay 50ms +loss 0.08% +wait 1s +rate 1510kbit +delay 50ms +loss 0.08% +wait 1s +rate 1383kbit +delay 50ms +loss 0.08% +wait 1s +rate 1471kbit +delay 50ms +loss 0.08% +wait 1s +rate 1497kbit +delay 50ms +loss 0.08% +wait 1s +rate 4928kbit +delay 50ms +loss 0.08% +wait 1s +rate 4725kbit +delay 50ms +loss 0.08% +wait 1s +rate 4862kbit +delay 50ms +loss 0.08% +wait 1s +rate 4695kbit +delay 50ms +loss 0.08% +wait 1s +rate 5307kbit +delay 50ms +loss 0.08% +wait 1s +rate 4854kbit +delay 50ms +loss 0.08% +wait 1s +rate 5045kbit +delay 50ms +loss 0.08% +wait 1s +rate 4358kbit +delay 50ms +loss 0.08% +wait 1s +rate 5371kbit +delay 50ms +loss 0.08% +wait 1s +rate 4486kbit +delay 50ms +loss 0.08% +wait 1s +rate 5374kbit +delay 50ms +loss 0.08% +wait 1s +rate 4773kbit +delay 50ms +loss 0.08% +wait 1s +rate 4401kbit +delay 50ms +loss 0.08% +wait 1s +rate 5049kbit +delay 50ms +loss 0.08% +wait 1s +rate 4534kbit +delay 50ms +loss 0.08% +wait 1s +rate 5248kbit +delay 50ms +loss 0.08% +wait 1s +rate 4771kbit +delay 50ms +loss 0.08% +wait 1s +rate 4883kbit +delay 50ms +loss 0.08% +wait 1s +rate 4821kbit +delay 50ms +loss 0.08% +wait 1s +rate 5147kbit +delay 50ms +loss 0.08% +wait 1s +rate 4966kbit +delay 50ms +loss 0.08% +wait 1s +rate 4624kbit +delay 50ms +loss 0.08% +wait 1s +rate 4930kbit +delay 50ms +loss 0.08% +wait 1s +rate 4597kbit +delay 50ms +loss 0.08% +wait 1s +rate 5161kbit +delay 50ms +loss 0.08% +wait 1s +rate 1340kbit +delay 50ms +loss 0.08% +wait 1s +rate 5090kbit +delay 50ms +loss 0.08% +wait 1s +rate 5000kbit +delay 50ms +loss 0.08% +wait 1s +rate 5252kbit +delay 50ms +loss 0.08% +wait 1s +rate 6574kbit +delay 50ms +loss 0.08% +wait 1s +rate 4694kbit +delay 50ms +loss 0.08% +wait 1s +rate 3956kbit +delay 50ms +loss 0.08% +wait 1s +rate 4623kbit +delay 50ms +loss 0.08% +wait 1s +rate 5200kbit +delay 50ms +loss 0.08% +wait 1s +rate 4710kbit +delay 50ms +loss 0.08% +wait 1s +rate 4662kbit +delay 50ms +loss 0.08% +wait 1s +rate 4946kbit +delay 50ms +loss 0.08% +wait 1s +rate 4600kbit +delay 50ms +loss 0.08% +wait 1s +rate 4687kbit +delay 50ms +loss 0.08% +wait 1s +rate 4768kbit +delay 50ms +loss 0.08% +wait 1s +rate 5015kbit +delay 50ms +loss 0.08% +wait 1s +rate 6271kbit +delay 50ms +loss 0.08% +wait 1s +rate 924kbit +delay 50ms +loss 0.08% +wait 1s +rate 897kbit +delay 50ms +loss 0.08% +wait 1s +rate 897kbit +delay 50ms +loss 0.08% +wait 1s +rate 935kbit +delay 50ms +loss 0.08% +wait 1s +rate 935kbit +delay 50ms +loss 0.08% +wait 1s +rate 929kbit +delay 50ms +loss 0.08% +wait 1s +rate 911kbit +delay 50ms +loss 0.08% +wait 1s +rate 920kbit +delay 50ms +loss 0.08% +wait 1s +rate 918kbit +delay 50ms +loss 0.08% +wait 1s +rate 922kbit +delay 50ms +loss 0.08% +wait 1s +rate 922kbit +delay 50ms +loss 0.08% +wait 1s +rate 891kbit +delay 50ms +loss 0.08% +wait 1s +rate 914kbit +delay 50ms +loss 0.08% +wait 1s +rate 911kbit +delay 50ms +loss 0.08% +wait 1s +rate 895kbit +delay 50ms +loss 0.08% +wait 1s +rate 908kbit +delay 50ms +loss 0.08% +wait 1s +rate 874kbit +delay 50ms +loss 0.08% +wait 1s +rate 900kbit +delay 50ms +loss 0.08% +wait 1s +rate 880kbit +delay 50ms +loss 0.08% +wait 1s +rate 908kbit +delay 50ms +loss 0.08% +wait 1s +rate 908kbit +delay 50ms +loss 0.08% +wait 1s +rate 913kbit +delay 50ms +loss 0.08% +wait 1s +rate 901kbit +delay 50ms +loss 0.08% +wait 1s +rate 923kbit +delay 50ms +loss 0.08% +wait 1s +rate 925kbit +delay 50ms +loss 0.08% +wait 1s +rate 899kbit +delay 50ms +loss 0.08% +wait 1s +rate 926kbit +delay 50ms +loss 0.08% +wait 1s +rate 913kbit +delay 50ms +loss 0.08% +wait 1s +rate 901kbit +delay 50ms +loss 0.08% +wait 1s +rate 923kbit +delay 50ms +loss 0.08% +wait 1s +rate 926kbit +delay 50ms +loss 0.08% +wait 1s +rate 898kbit +delay 50ms +loss 0.08% +wait 1s +rate 909kbit +delay 50ms +loss 0.08% +wait 1s +rate 910kbit +delay 50ms +loss 0.08% +wait 1s +rate 911kbit +delay 50ms +loss 0.08% +wait 1s +rate 915kbit +delay 50ms +loss 0.08% +wait 1s +rate 921kbit +delay 50ms +loss 0.08% +wait 1s +rate 922kbit +delay 50ms +loss 0.08% +wait 1s +rate 893kbit +delay 50ms +loss 0.08% +wait 1s +rate 922kbit +delay 50ms +loss 0.08% +wait 1s +rate 944kbit +delay 50ms +loss 0.08% +wait 1s +rate 895kbit +delay 50ms +loss 0.08% +wait 1s +rate 925kbit +delay 50ms +loss 0.08% +wait 1s +rate 915kbit +delay 50ms +loss 0.08% +wait 1s +rate 873kbit +delay 50ms +loss 0.08% +wait 1s +rate 926kbit +delay 50ms +loss 0.08% +wait 1s +rate 932kbit +delay 50ms +loss 0.08% +wait 1s +rate 911kbit +delay 50ms +loss 0.08% +wait 1s +rate 847kbit +delay 50ms +loss 0.08% +wait 1s +rate 904kbit +delay 50ms +loss 0.08% +wait 1s +rate 891kbit +delay 50ms +loss 0.08% +wait 1s +rate 899kbit +delay 50ms +loss 0.08% +wait 1s +rate 916kbit +delay 50ms +loss 0.08% +wait 1s +rate 899kbit +delay 50ms +loss 0.08% +wait 1s +rate 740kbit +delay 50ms +loss 0.08% +wait 1s +rate 897kbit +delay 50ms +loss 0.08% +wait 1s +rate 916kbit +delay 50ms +loss 0.08% +wait 1s +rate 906kbit +delay 50ms +loss 0.08% +wait 1s +rate 932kbit +delay 50ms +loss 0.08% +wait 1s +rate 942kbit +delay 50ms +loss 0.08% +wait 1s +rate 910kbit +delay 50ms +loss 0.08% +wait 1s +rate 869kbit +delay 50ms +loss 0.08% +wait 1s +rate 860kbit +delay 50ms +loss 0.08% +wait 1s +rate 750kbit +delay 50ms +loss 0.08% +wait 1s +rate 903kbit +delay 50ms +loss 0.08% +wait 1s +rate 945kbit +delay 50ms +loss 0.08% +wait 1s +rate 938kbit +delay 50ms +loss 0.08% +wait 1s +rate 928kbit +delay 50ms +loss 0.08% +wait 1s +rate 941kbit +delay 50ms +loss 0.08% +wait 1s +rate 918kbit +delay 50ms +loss 0.08% +wait 1s +rate 940kbit +delay 50ms +loss 0.08% +wait 1s +rate 903kbit +delay 50ms +loss 0.08% +wait 1s +rate 942kbit +delay 50ms +loss 0.08% +wait 1s +rate 946kbit +delay 50ms +loss 0.08% +wait 1s +rate 910kbit +delay 50ms +loss 0.08% +wait 1s +rate 912kbit +delay 50ms +loss 0.08% +wait 1s +rate 902kbit +delay 50ms +loss 0.08% +wait 1s +rate 927kbit +delay 50ms +loss 0.08% +wait 1s +rate 948kbit +delay 50ms +loss 0.08% +wait 1s +rate 896kbit +delay 50ms +loss 0.08% +wait 1s +rate 914kbit +delay 50ms +loss 0.08% +wait 1s +rate 923kbit +delay 50ms +loss 0.08% +wait 1s +rate 934kbit +delay 50ms +loss 0.08% +wait 1s +rate 927kbit +delay 50ms +loss 0.08% +wait 1s +rate 925kbit +delay 50ms +loss 0.08% +wait 1s +rate 928kbit +delay 50ms +loss 0.08% +wait 1s +rate 905kbit +delay 50ms +loss 0.08% +wait 1s +rate 890kbit +delay 50ms +loss 0.08% +wait 1s +rate 5073kbit +delay 50ms +loss 0.08% +wait 1s +rate 5007kbit +delay 50ms +loss 0.08% +wait 1s +rate 4080kbit +delay 50ms +loss 0.08% +wait 1s +rate 4477kbit +delay 50ms +loss 0.08% +wait 1s +rate 4783kbit +delay 50ms +loss 0.08% +wait 1s +rate 4925kbit +delay 50ms +loss 0.08% +wait 1s +rate 5464kbit +delay 50ms +loss 0.08% +wait 1s +rate 5089kbit +delay 50ms +loss 0.08% +wait 1s +rate 4460kbit +delay 50ms +loss 0.08% +wait 1s +rate 5172kbit +delay 50ms +loss 0.08% +wait 1s +rate 4515kbit +delay 50ms +loss 0.08% +wait 1s +rate 4423kbit +delay 50ms +loss 0.08% +wait 1s +rate 4786kbit +delay 50ms +loss 0.08% +wait 1s +rate 4858kbit +delay 50ms +loss 0.08% +wait 1s +rate 900kbit +delay 50ms +loss 0.08% +wait 1s +rate 4998kbit +delay 50ms +loss 0.08% +wait 1s +rate 4827kbit +delay 50ms +loss 0.08% +wait 1s +rate 4868kbit +delay 50ms +loss 0.08% +wait 1s +rate 5251kbit +delay 50ms +loss 0.08% +wait 1s +rate 4750kbit +delay 50ms +loss 0.08% +wait 1s +rate 907kbit +delay 50ms +loss 0.08% +wait 1s +rate 5004kbit +delay 50ms +loss 0.08% +wait 1s +rate 4470kbit +delay 50ms +loss 0.08% +wait 1s +rate 5163kbit +delay 50ms +loss 0.08% +wait 1s +rate 3943kbit +delay 50ms +loss 0.08% +wait 1s +rate 4254kbit +delay 50ms +loss 0.08% +wait 1s +rate 4783kbit +delay 50ms +loss 0.08% +wait 1s +rate 4835kbit +delay 50ms +loss 0.08% +wait 1s +rate 5133kbit +delay 50ms +loss 0.08% +wait 1s +rate 5106kbit +delay 50ms +loss 0.08% +wait 1s +rate 4934kbit +delay 50ms +loss 0.08% +wait 1s +rate 4744kbit +delay 50ms +loss 0.08% +wait 1s +rate 4189kbit +delay 50ms +loss 0.08% +wait 1s +rate 4507kbit +delay 50ms +loss 0.08% +wait 1s +rate 1330kbit +delay 50ms +loss 0.08% +wait 1s +rate 4253kbit +delay 50ms +loss 0.08% +wait 1s +rate 4454kbit +delay 50ms +loss 0.08% +wait 1s +rate 4432kbit +delay 50ms +loss 0.08% +wait 1s +rate 907kbit +delay 50ms +loss 0.08% +wait 1s +rate 4436kbit +delay 50ms +loss 0.08% +wait 1s +rate 4627kbit +delay 50ms +loss 0.08% +wait 1s +rate 5102kbit +delay 50ms +loss 0.08% +wait 1s +rate 906kbit +delay 50ms +loss 0.08% +wait 1s +rate 4775kbit +delay 50ms +loss 0.08% +wait 1s +rate 4751kbit +delay 50ms +loss 0.08% +wait 1s +rate 4590kbit +delay 50ms +loss 0.08% +wait 1s +rate 4837kbit +delay 50ms +loss 0.08% +wait 1s +rate 4552kbit +delay 50ms +loss 0.08% +wait 1s +rate 4491kbit +delay 50ms +loss 0.08% +wait 1s +rate 4641kbit +delay 50ms +loss 0.08% +wait 1s +rate 934kbit +delay 50ms +loss 0.08% +wait 1s +rate 4977kbit +delay 50ms +loss 0.08% +wait 1s +rate 7686kbit +delay 50ms +loss 0.08% +wait 1s +rate 933kbit +delay 50ms +loss 0.08% +wait 1s +rate 6580kbit +delay 50ms +loss 0.08% +wait 1s +rate 4277kbit +delay 50ms +loss 0.08% +wait 1s +rate 5171kbit +delay 50ms +loss 0.08% +wait 1s +rate 4512kbit +delay 50ms +loss 0.08% +wait 1s +rate 4583kbit +delay 50ms +loss 0.08% +wait 1s +rate 4729kbit +delay 50ms +loss 0.08% +wait 1s +rate 4876kbit +delay 50ms +loss 0.08% +wait 1s +rate 5265kbit +delay 50ms +loss 0.08% +wait 1s +rate 924kbit +delay 50ms +loss 0.08% +wait 1s +rate 5272kbit +delay 50ms +loss 0.08% +wait 1s +rate 4945kbit +delay 50ms +loss 0.08% +wait 1s +rate 4676kbit +delay 50ms +loss 0.08% +wait 1s +rate 5159kbit +delay 50ms +loss 0.08% +wait 1s +rate 4825kbit +delay 50ms +loss 0.08% +wait 1s +rate 4578kbit +delay 50ms +loss 0.08% +wait 1s +rate 3952kbit +delay 50ms +loss 0.08% +wait 1s +rate 4527kbit +delay 50ms +loss 0.08% +wait 1s +rate 4370kbit +delay 50ms +loss 0.08% +wait 1s +rate 4944kbit +delay 50ms +loss 0.08% +wait 1s +rate 4657kbit +delay 50ms +loss 0.08% +wait 1s +rate 959kbit +delay 50ms +loss 0.08% +wait 1s +rate 6868kbit +delay 50ms +loss 0.08% +wait 1s +rate 4798kbit +delay 50ms +loss 0.08% +wait 1s \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/FCCamazone_x0.25 b/repos/moq-rs/tc_profiles/FCCamazone_x0.25 new file mode 100644 index 0000000..b2743a1 --- /dev/null +++ b/repos/moq-rs/tc_profiles/FCCamazone_x0.25 @@ -0,0 +1,2400 @@ +rate 354kbit +delay 50ms +loss 0.0008 +wait 1s +rate 349kbit +delay 50ms +loss 0.0008 +wait 1s +rate 368kbit +delay 50ms +loss 0.0008 +wait 1s +rate 376kbit +delay 50ms +loss 0.0008 +wait 1s +rate 371kbit +delay 50ms +loss 0.0008 +wait 1s +rate 394kbit +delay 50ms +loss 0.0008 +wait 1s +rate 369kbit +delay 50ms +loss 0.0008 +wait 1s +rate 365kbit +delay 50ms +loss 0.0008 +wait 1s +rate 394kbit +delay 50ms +loss 0.0008 +wait 1s +rate 363kbit +delay 50ms +loss 0.0008 +wait 1s +rate 371kbit +delay 50ms +loss 0.0008 +wait 1s +rate 374kbit +delay 50ms +loss 0.0008 +wait 1s +rate 362kbit +delay 50ms +loss 0.0008 +wait 1s +rate 367kbit +delay 50ms +loss 0.0008 +wait 1s +rate 340kbit +delay 50ms +loss 0.0008 +wait 1s +rate 391kbit +delay 50ms +loss 0.0008 +wait 1s +rate 378kbit +delay 50ms +loss 0.0008 +wait 1s +rate 385kbit +delay 50ms +loss 0.0008 +wait 1s +rate 374kbit +delay 50ms +loss 0.0008 +wait 1s +rate 363kbit +delay 50ms +loss 0.0008 +wait 1s +rate 377kbit +delay 50ms +loss 0.0008 +wait 1s +rate 345kbit +delay 50ms +loss 0.0008 +wait 1s +rate 367kbit +delay 50ms +loss 0.0008 +wait 1s +rate 374kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1232kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1181kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1215kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1173kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1326kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1213kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1261kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1089kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1342kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1121kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1343kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1193kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1100kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1262kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1133kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1312kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1192kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1220kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1205kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1286kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1241kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1156kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1232kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1149kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1290kbit +delay 50ms +loss 0.0008 +wait 1s +rate 335kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1272kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1250kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1313kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1643kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1173kbit +delay 50ms +loss 0.0008 +wait 1s +rate 989kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1155kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1300kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1177kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1165kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1236kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1150kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1171kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1192kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1253kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1567kbit +delay 50ms +loss 0.0008 +wait 1s +rate 231kbit +delay 50ms +loss 0.0008 +wait 1s +rate 224kbit +delay 50ms +loss 0.0008 +wait 1s +rate 224kbit +delay 50ms +loss 0.0008 +wait 1s +rate 233kbit +delay 50ms +loss 0.0008 +wait 1s +rate 233kbit +delay 50ms +loss 0.0008 +wait 1s +rate 232kbit +delay 50ms +loss 0.0008 +wait 1s +rate 227kbit +delay 50ms +loss 0.0008 +wait 1s +rate 230kbit +delay 50ms +loss 0.0008 +wait 1s +rate 229kbit +delay 50ms +loss 0.0008 +wait 1s +rate 230kbit +delay 50ms +loss 0.0008 +wait 1s +rate 230kbit +delay 50ms +loss 0.0008 +wait 1s +rate 222kbit +delay 50ms +loss 0.0008 +wait 1s +rate 228kbit +delay 50ms +loss 0.0008 +wait 1s +rate 227kbit +delay 50ms +loss 0.0008 +wait 1s +rate 223kbit +delay 50ms +loss 0.0008 +wait 1s +rate 227kbit +delay 50ms +loss 0.0008 +wait 1s +rate 218kbit +delay 50ms +loss 0.0008 +wait 1s +rate 225kbit +delay 50ms +loss 0.0008 +wait 1s +rate 220kbit +delay 50ms +loss 0.0008 +wait 1s +rate 227kbit +delay 50ms +loss 0.0008 +wait 1s +rate 227kbit +delay 50ms +loss 0.0008 +wait 1s +rate 228kbit +delay 50ms +loss 0.0008 +wait 1s +rate 225kbit +delay 50ms +loss 0.0008 +wait 1s +rate 230kbit +delay 50ms +loss 0.0008 +wait 1s +rate 231kbit +delay 50ms +loss 0.0008 +wait 1s +rate 224kbit +delay 50ms +loss 0.0008 +wait 1s +rate 231kbit +delay 50ms +loss 0.0008 +wait 1s +rate 228kbit +delay 50ms +loss 0.0008 +wait 1s +rate 225kbit +delay 50ms +loss 0.0008 +wait 1s +rate 230kbit +delay 50ms +loss 0.0008 +wait 1s +rate 231kbit +delay 50ms +loss 0.0008 +wait 1s +rate 224kbit +delay 50ms +loss 0.0008 +wait 1s +rate 227kbit +delay 50ms +loss 0.0008 +wait 1s +rate 227kbit +delay 50ms +loss 0.0008 +wait 1s +rate 227kbit +delay 50ms +loss 0.0008 +wait 1s +rate 228kbit +delay 50ms +loss 0.0008 +wait 1s +rate 230kbit +delay 50ms +loss 0.0008 +wait 1s +rate 230kbit +delay 50ms +loss 0.0008 +wait 1s +rate 223kbit +delay 50ms +loss 0.0008 +wait 1s +rate 230kbit +delay 50ms +loss 0.0008 +wait 1s +rate 236kbit +delay 50ms +loss 0.0008 +wait 1s +rate 223kbit +delay 50ms +loss 0.0008 +wait 1s +rate 231kbit +delay 50ms +loss 0.0008 +wait 1s +rate 228kbit +delay 50ms +loss 0.0008 +wait 1s +rate 218kbit +delay 50ms +loss 0.0008 +wait 1s +rate 231kbit +delay 50ms +loss 0.0008 +wait 1s +rate 233kbit +delay 50ms +loss 0.0008 +wait 1s +rate 227kbit +delay 50ms +loss 0.0008 +wait 1s +rate 211kbit +delay 50ms +loss 0.0008 +wait 1s +rate 226kbit +delay 50ms +loss 0.0008 +wait 1s +rate 222kbit +delay 50ms +loss 0.0008 +wait 1s +rate 224kbit +delay 50ms +loss 0.0008 +wait 1s +rate 229kbit +delay 50ms +loss 0.0008 +wait 1s +rate 224kbit +delay 50ms +loss 0.0008 +wait 1s +rate 185kbit +delay 50ms +loss 0.0008 +wait 1s +rate 224kbit +delay 50ms +loss 0.0008 +wait 1s +rate 229kbit +delay 50ms +loss 0.0008 +wait 1s +rate 226kbit +delay 50ms +loss 0.0008 +wait 1s +rate 233kbit +delay 50ms +loss 0.0008 +wait 1s +rate 235kbit +delay 50ms +loss 0.0008 +wait 1s +rate 227kbit +delay 50ms +loss 0.0008 +wait 1s +rate 217kbit +delay 50ms +loss 0.0008 +wait 1s +rate 215kbit +delay 50ms +loss 0.0008 +wait 1s +rate 187kbit +delay 50ms +loss 0.0008 +wait 1s +rate 225kbit +delay 50ms +loss 0.0008 +wait 1s +rate 236kbit +delay 50ms +loss 0.0008 +wait 1s +rate 234kbit +delay 50ms +loss 0.0008 +wait 1s +rate 232kbit +delay 50ms +loss 0.0008 +wait 1s +rate 235kbit +delay 50ms +loss 0.0008 +wait 1s +rate 229kbit +delay 50ms +loss 0.0008 +wait 1s +rate 235kbit +delay 50ms +loss 0.0008 +wait 1s +rate 225kbit +delay 50ms +loss 0.0008 +wait 1s +rate 235kbit +delay 50ms +loss 0.0008 +wait 1s +rate 236kbit +delay 50ms +loss 0.0008 +wait 1s +rate 227kbit +delay 50ms +loss 0.0008 +wait 1s +rate 228kbit +delay 50ms +loss 0.0008 +wait 1s +rate 225kbit +delay 50ms +loss 0.0008 +wait 1s +rate 231kbit +delay 50ms +loss 0.0008 +wait 1s +rate 237kbit +delay 50ms +loss 0.0008 +wait 1s +rate 224kbit +delay 50ms +loss 0.0008 +wait 1s +rate 228kbit +delay 50ms +loss 0.0008 +wait 1s +rate 230kbit +delay 50ms +loss 0.0008 +wait 1s +rate 233kbit +delay 50ms +loss 0.0008 +wait 1s +rate 231kbit +delay 50ms +loss 0.0008 +wait 1s +rate 231kbit +delay 50ms +loss 0.0008 +wait 1s +rate 232kbit +delay 50ms +loss 0.0008 +wait 1s +rate 226kbit +delay 50ms +loss 0.0008 +wait 1s +rate 222kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1268kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1251kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1020kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1119kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1195kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1231kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1366kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1272kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1115kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1293kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1128kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1105kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1196kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1214kbit +delay 50ms +loss 0.0008 +wait 1s +rate 225kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1249kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1206kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1217kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1312kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1187kbit +delay 50ms +loss 0.0008 +wait 1s +rate 226kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1251kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1117kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1290kbit +delay 50ms +loss 0.0008 +wait 1s +rate 985kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1063kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1195kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1208kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1283kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1276kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1233kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1186kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1047kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1126kbit +delay 50ms +loss 0.0008 +wait 1s +rate 332kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1063kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1113kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1108kbit +delay 50ms +loss 0.0008 +wait 1s +rate 226kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1109kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1156kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1275kbit +delay 50ms +loss 0.0008 +wait 1s +rate 226kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1193kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1187kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1147kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1209kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1138kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1122kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1160kbit +delay 50ms +loss 0.0008 +wait 1s +rate 233kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1244kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1921kbit +delay 50ms +loss 0.0008 +wait 1s +rate 233kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1645kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1069kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1292kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1128kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1145kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1182kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1219kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1316kbit +delay 50ms +loss 0.0008 +wait 1s +rate 231kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1318kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1236kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1169kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1289kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1206kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1144kbit +delay 50ms +loss 0.0008 +wait 1s +rate 988kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1131kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1092kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1236kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1164kbit +delay 50ms +loss 0.0008 +wait 1s +rate 239kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1717kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1199kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1127kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1266kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1213kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1176kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1149kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1107kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1018kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1133kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1211kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1175kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1065kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1221kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1189kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1204kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1134kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1225kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1167kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1193kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1604kbit +delay 50ms +loss 0.0008 +wait 1s +rate 839kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1120kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1086kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1295kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1511kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1314kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1180kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1210kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1020kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1643kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1171kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1112kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1127kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1102kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1107kbit +delay 50ms +loss 0.0008 +wait 1s +rate 229kbit +delay 50ms +loss 0.0008 +wait 1s +rate 223kbit +delay 50ms +loss 0.0008 +wait 1s +rate 223kbit +delay 50ms +loss 0.0008 +wait 1s +rate 219kbit +delay 50ms +loss 0.0008 +wait 1s +rate 234kbit +delay 50ms +loss 0.0008 +wait 1s +rate 232kbit +delay 50ms +loss 0.0008 +wait 1s +rate 226kbit +delay 50ms +loss 0.0008 +wait 1s +rate 231kbit +delay 50ms +loss 0.0008 +wait 1s +rate 224kbit +delay 50ms +loss 0.0008 +wait 1s +rate 224kbit +delay 50ms +loss 0.0008 +wait 1s +rate 233kbit +delay 50ms +loss 0.0008 +wait 1s +rate 234kbit +delay 50ms +loss 0.0008 +wait 1s +rate 226kbit +delay 50ms +loss 0.0008 +wait 1s +rate 229kbit +delay 50ms +loss 0.0008 +wait 1s +rate 230kbit +delay 50ms +loss 0.0008 +wait 1s +rate 235kbit +delay 50ms +loss 0.0008 +wait 1s +rate 229kbit +delay 50ms +loss 0.0008 +wait 1s +rate 220kbit +delay 50ms +loss 0.0008 +wait 1s +rate 218kbit +delay 50ms +loss 0.0008 +wait 1s +rate 234kbit +delay 50ms +loss 0.0008 +wait 1s +rate 228kbit +delay 50ms +loss 0.0008 +wait 1s +rate 227kbit +delay 50ms +loss 0.0008 +wait 1s +rate 223kbit +delay 50ms +loss 0.0008 +wait 1s +rate 233kbit +delay 50ms +loss 0.0008 +wait 1s +rate 234kbit +delay 50ms +loss 0.0008 +wait 1s +rate 228kbit +delay 50ms +loss 0.0008 +wait 1s +rate 234kbit +delay 50ms +loss 0.0008 +wait 1s +rate 238kbit +delay 50ms +loss 0.0008 +wait 1s +rate 229kbit +delay 50ms +loss 0.0008 +wait 1s +rate 231kbit +delay 50ms +loss 0.0008 +wait 1s +rate 232kbit +delay 50ms +loss 0.0008 +wait 1s +rate 219kbit +delay 50ms +loss 0.0008 +wait 1s +rate 229kbit +delay 50ms +loss 0.0008 +wait 1s +rate 226kbit +delay 50ms +loss 0.0008 +wait 1s +rate 226kbit +delay 50ms +loss 0.0008 +wait 1s +rate 218kbit +delay 50ms +loss 0.08% +wait 1s +rate 234kbit +delay 50ms +loss 0.08% +wait 1s +rate 229kbit +delay 50ms +loss 0.08% +wait 1s +rate 234kbit +delay 50ms +loss 0.08% +wait 1s +rate 235kbit +delay 50ms +loss 0.08% +wait 1s +rate 227kbit +delay 50ms +loss 0.08% +wait 1s +rate 228kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 233kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 222kbit +delay 50ms +loss 0.08% +wait 1s +rate 233kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 229kbit +delay 50ms +loss 0.08% +wait 1s +rate 237kbit +delay 50ms +loss 0.08% +wait 1s +rate 225kbit +delay 50ms +loss 0.08% +wait 1s +rate 234kbit +delay 50ms +loss 0.08% +wait 1s +rate 228kbit +delay 50ms +loss 0.08% +wait 1s +rate 233kbit +delay 50ms +loss 0.08% +wait 1s +rate 229kbit +delay 50ms +loss 0.08% +wait 1s +rate 187kbit +delay 50ms +loss 0.08% +wait 1s +rate 233kbit +delay 50ms +loss 0.08% +wait 1s +rate 231kbit +delay 50ms +loss 0.08% +wait 1s +rate 232kbit +delay 50ms +loss 0.08% +wait 1s +rate 231kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 229kbit +delay 50ms +loss 0.08% +wait 1s +rate 226kbit +delay 50ms +loss 0.08% +wait 1s +rate 236kbit +delay 50ms +loss 0.08% +wait 1s +rate 236kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 229kbit +delay 50ms +loss 0.08% +wait 1s +rate 217kbit +delay 50ms +loss 0.08% +wait 1s +rate 232kbit +delay 50ms +loss 0.08% +wait 1s +rate 225kbit +delay 50ms +loss 0.08% +wait 1s +rate 236kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 235kbit +delay 50ms +loss 0.08% +wait 1s +rate 237kbit +delay 50ms +loss 0.08% +wait 1s +rate 228kbit +delay 50ms +loss 0.08% +wait 1s +rate 229kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 225kbit +delay 50ms +loss 0.08% +wait 1s +rate 225kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 220kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 223kbit +delay 50ms +loss 0.08% +wait 1s +rate 236kbit +delay 50ms +loss 0.08% +wait 1s +rate 225kbit +delay 50ms +loss 0.08% +wait 1s +rate 226kbit +delay 50ms +loss 0.08% +wait 1s +rate 223kbit +delay 50ms +loss 0.08% +wait 1s +rate 226kbit +delay 50ms +loss 0.08% +wait 1s +rate 1135kbit +delay 50ms +loss 0.08% +wait 1s +rate 1098kbit +delay 50ms +loss 0.08% +wait 1s +rate 1225kbit +delay 50ms +loss 0.08% +wait 1s +rate 1517kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 1172kbit +delay 50ms +loss 0.08% +wait 1s +rate 1302kbit +delay 50ms +loss 0.08% +wait 1s +rate 1210kbit +delay 50ms +loss 0.08% +wait 1s +rate 1115kbit +delay 50ms +loss 0.08% +wait 1s +rate 1184kbit +delay 50ms +loss 0.08% +wait 1s +rate 1225kbit +delay 50ms +loss 0.08% +wait 1s +rate 1087kbit +delay 50ms +loss 0.08% +wait 1s +rate 1198kbit +delay 50ms +loss 0.08% +wait 1s +rate 1193kbit +delay 50ms +loss 0.08% +wait 1s +rate 1158kbit +delay 50ms +loss 0.08% +wait 1s +rate 354kbit +delay 50ms +loss 0.08% +wait 1s +rate 349kbit +delay 50ms +loss 0.08% +wait 1s +rate 368kbit +delay 50ms +loss 0.08% +wait 1s +rate 376kbit +delay 50ms +loss 0.08% +wait 1s +rate 371kbit +delay 50ms +loss 0.08% +wait 1s +rate 394kbit +delay 50ms +loss 0.08% +wait 1s +rate 369kbit +delay 50ms +loss 0.08% +wait 1s +rate 365kbit +delay 50ms +loss 0.08% +wait 1s +rate 394kbit +delay 50ms +loss 0.08% +wait 1s +rate 363kbit +delay 50ms +loss 0.08% +wait 1s +rate 371kbit +delay 50ms +loss 0.08% +wait 1s +rate 374kbit +delay 50ms +loss 0.08% +wait 1s +rate 362kbit +delay 50ms +loss 0.08% +wait 1s +rate 367kbit +delay 50ms +loss 0.08% +wait 1s +rate 340kbit +delay 50ms +loss 0.08% +wait 1s +rate 391kbit +delay 50ms +loss 0.08% +wait 1s +rate 378kbit +delay 50ms +loss 0.08% +wait 1s +rate 385kbit +delay 50ms +loss 0.08% +wait 1s +rate 374kbit +delay 50ms +loss 0.08% +wait 1s +rate 363kbit +delay 50ms +loss 0.08% +wait 1s +rate 377kbit +delay 50ms +loss 0.08% +wait 1s +rate 345kbit +delay 50ms +loss 0.08% +wait 1s +rate 367kbit +delay 50ms +loss 0.08% +wait 1s +rate 374kbit +delay 50ms +loss 0.08% +wait 1s +rate 1232kbit +delay 50ms +loss 0.08% +wait 1s +rate 1181kbit +delay 50ms +loss 0.08% +wait 1s +rate 1215kbit +delay 50ms +loss 0.08% +wait 1s +rate 1173kbit +delay 50ms +loss 0.08% +wait 1s +rate 1326kbit +delay 50ms +loss 0.08% +wait 1s +rate 1213kbit +delay 50ms +loss 0.08% +wait 1s +rate 1261kbit +delay 50ms +loss 0.08% +wait 1s +rate 1089kbit +delay 50ms +loss 0.08% +wait 1s +rate 1342kbit +delay 50ms +loss 0.08% +wait 1s +rate 1121kbit +delay 50ms +loss 0.08% +wait 1s +rate 1343kbit +delay 50ms +loss 0.08% +wait 1s +rate 1193kbit +delay 50ms +loss 0.08% +wait 1s +rate 1100kbit +delay 50ms +loss 0.08% +wait 1s +rate 1262kbit +delay 50ms +loss 0.08% +wait 1s +rate 1133kbit +delay 50ms +loss 0.08% +wait 1s +rate 1312kbit +delay 50ms +loss 0.08% +wait 1s +rate 1192kbit +delay 50ms +loss 0.08% +wait 1s +rate 1220kbit +delay 50ms +loss 0.08% +wait 1s +rate 1205kbit +delay 50ms +loss 0.08% +wait 1s +rate 1286kbit +delay 50ms +loss 0.08% +wait 1s +rate 1241kbit +delay 50ms +loss 0.08% +wait 1s +rate 1156kbit +delay 50ms +loss 0.08% +wait 1s +rate 1232kbit +delay 50ms +loss 0.08% +wait 1s +rate 1149kbit +delay 50ms +loss 0.08% +wait 1s +rate 1290kbit +delay 50ms +loss 0.08% +wait 1s +rate 335kbit +delay 50ms +loss 0.08% +wait 1s +rate 1272kbit +delay 50ms +loss 0.08% +wait 1s +rate 1250kbit +delay 50ms +loss 0.08% +wait 1s +rate 1313kbit +delay 50ms +loss 0.08% +wait 1s +rate 1643kbit +delay 50ms +loss 0.08% +wait 1s +rate 1173kbit +delay 50ms +loss 0.08% +wait 1s +rate 989kbit +delay 50ms +loss 0.08% +wait 1s +rate 1155kbit +delay 50ms +loss 0.08% +wait 1s +rate 1300kbit +delay 50ms +loss 0.08% +wait 1s +rate 1177kbit +delay 50ms +loss 0.08% +wait 1s +rate 1165kbit +delay 50ms +loss 0.08% +wait 1s +rate 1236kbit +delay 50ms +loss 0.08% +wait 1s +rate 1150kbit +delay 50ms +loss 0.08% +wait 1s +rate 1171kbit +delay 50ms +loss 0.08% +wait 1s +rate 1192kbit +delay 50ms +loss 0.08% +wait 1s +rate 1253kbit +delay 50ms +loss 0.08% +wait 1s +rate 1567kbit +delay 50ms +loss 0.08% +wait 1s +rate 231kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 233kbit +delay 50ms +loss 0.08% +wait 1s +rate 233kbit +delay 50ms +loss 0.08% +wait 1s +rate 232kbit +delay 50ms +loss 0.08% +wait 1s +rate 227kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 229kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 222kbit +delay 50ms +loss 0.08% +wait 1s +rate 228kbit +delay 50ms +loss 0.08% +wait 1s +rate 227kbit +delay 50ms +loss 0.08% +wait 1s +rate 223kbit +delay 50ms +loss 0.08% +wait 1s +rate 227kbit +delay 50ms +loss 0.08% +wait 1s +rate 218kbit +delay 50ms +loss 0.08% +wait 1s +rate 225kbit +delay 50ms +loss 0.08% +wait 1s +rate 220kbit +delay 50ms +loss 0.08% +wait 1s +rate 227kbit +delay 50ms +loss 0.08% +wait 1s +rate 227kbit +delay 50ms +loss 0.08% +wait 1s +rate 228kbit +delay 50ms +loss 0.08% +wait 1s +rate 225kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 231kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 231kbit +delay 50ms +loss 0.08% +wait 1s +rate 228kbit +delay 50ms +loss 0.08% +wait 1s +rate 225kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 231kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 227kbit +delay 50ms +loss 0.08% +wait 1s +rate 227kbit +delay 50ms +loss 0.08% +wait 1s +rate 227kbit +delay 50ms +loss 0.08% +wait 1s +rate 228kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 223kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 236kbit +delay 50ms +loss 0.08% +wait 1s +rate 223kbit +delay 50ms +loss 0.08% +wait 1s +rate 231kbit +delay 50ms +loss 0.08% +wait 1s +rate 228kbit +delay 50ms +loss 0.08% +wait 1s +rate 218kbit +delay 50ms +loss 0.08% +wait 1s +rate 231kbit +delay 50ms +loss 0.08% +wait 1s +rate 233kbit +delay 50ms +loss 0.08% +wait 1s +rate 227kbit +delay 50ms +loss 0.08% +wait 1s +rate 211kbit +delay 50ms +loss 0.08% +wait 1s +rate 226kbit +delay 50ms +loss 0.08% +wait 1s +rate 222kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 229kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 185kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 229kbit +delay 50ms +loss 0.08% +wait 1s +rate 226kbit +delay 50ms +loss 0.08% +wait 1s +rate 233kbit +delay 50ms +loss 0.08% +wait 1s +rate 235kbit +delay 50ms +loss 0.08% +wait 1s +rate 227kbit +delay 50ms +loss 0.08% +wait 1s +rate 217kbit +delay 50ms +loss 0.08% +wait 1s +rate 215kbit +delay 50ms +loss 0.08% +wait 1s +rate 187kbit +delay 50ms +loss 0.08% +wait 1s +rate 225kbit +delay 50ms +loss 0.08% +wait 1s +rate 236kbit +delay 50ms +loss 0.08% +wait 1s +rate 234kbit +delay 50ms +loss 0.08% +wait 1s +rate 232kbit +delay 50ms +loss 0.08% +wait 1s +rate 235kbit +delay 50ms +loss 0.08% +wait 1s +rate 229kbit +delay 50ms +loss 0.08% +wait 1s +rate 235kbit +delay 50ms +loss 0.08% +wait 1s +rate 225kbit +delay 50ms +loss 0.08% +wait 1s +rate 235kbit +delay 50ms +loss 0.08% +wait 1s +rate 236kbit +delay 50ms +loss 0.08% +wait 1s +rate 227kbit +delay 50ms +loss 0.08% +wait 1s +rate 228kbit +delay 50ms +loss 0.08% +wait 1s +rate 225kbit +delay 50ms +loss 0.08% +wait 1s +rate 231kbit +delay 50ms +loss 0.08% +wait 1s +rate 237kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 228kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 233kbit +delay 50ms +loss 0.08% +wait 1s +rate 231kbit +delay 50ms +loss 0.08% +wait 1s +rate 231kbit +delay 50ms +loss 0.08% +wait 1s +rate 232kbit +delay 50ms +loss 0.08% +wait 1s +rate 226kbit +delay 50ms +loss 0.08% +wait 1s +rate 222kbit +delay 50ms +loss 0.08% +wait 1s +rate 1268kbit +delay 50ms +loss 0.08% +wait 1s +rate 1251kbit +delay 50ms +loss 0.08% +wait 1s +rate 1020kbit +delay 50ms +loss 0.08% +wait 1s +rate 1119kbit +delay 50ms +loss 0.08% +wait 1s +rate 1195kbit +delay 50ms +loss 0.08% +wait 1s +rate 1231kbit +delay 50ms +loss 0.08% +wait 1s +rate 1366kbit +delay 50ms +loss 0.08% +wait 1s +rate 1272kbit +delay 50ms +loss 0.08% +wait 1s +rate 1115kbit +delay 50ms +loss 0.08% +wait 1s +rate 1293kbit +delay 50ms +loss 0.08% +wait 1s +rate 1128kbit +delay 50ms +loss 0.08% +wait 1s +rate 1105kbit +delay 50ms +loss 0.08% +wait 1s +rate 1196kbit +delay 50ms +loss 0.08% +wait 1s +rate 1214kbit +delay 50ms +loss 0.08% +wait 1s +rate 225kbit +delay 50ms +loss 0.08% +wait 1s +rate 1249kbit +delay 50ms +loss 0.08% +wait 1s +rate 1206kbit +delay 50ms +loss 0.08% +wait 1s +rate 1217kbit +delay 50ms +loss 0.08% +wait 1s +rate 1312kbit +delay 50ms +loss 0.08% +wait 1s +rate 1187kbit +delay 50ms +loss 0.08% +wait 1s +rate 226kbit +delay 50ms +loss 0.08% +wait 1s +rate 1251kbit +delay 50ms +loss 0.08% +wait 1s +rate 1117kbit +delay 50ms +loss 0.08% +wait 1s +rate 1290kbit +delay 50ms +loss 0.08% +wait 1s +rate 985kbit +delay 50ms +loss 0.08% +wait 1s +rate 1063kbit +delay 50ms +loss 0.08% +wait 1s +rate 1195kbit +delay 50ms +loss 0.08% +wait 1s +rate 1208kbit +delay 50ms +loss 0.08% +wait 1s +rate 1283kbit +delay 50ms +loss 0.08% +wait 1s +rate 1276kbit +delay 50ms +loss 0.08% +wait 1s +rate 1233kbit +delay 50ms +loss 0.08% +wait 1s +rate 1186kbit +delay 50ms +loss 0.08% +wait 1s +rate 1047kbit +delay 50ms +loss 0.08% +wait 1s +rate 1126kbit +delay 50ms +loss 0.08% +wait 1s +rate 332kbit +delay 50ms +loss 0.08% +wait 1s +rate 1063kbit +delay 50ms +loss 0.08% +wait 1s +rate 1113kbit +delay 50ms +loss 0.08% +wait 1s +rate 1108kbit +delay 50ms +loss 0.08% +wait 1s +rate 226kbit +delay 50ms +loss 0.08% +wait 1s +rate 1109kbit +delay 50ms +loss 0.08% +wait 1s +rate 1156kbit +delay 50ms +loss 0.08% +wait 1s +rate 1275kbit +delay 50ms +loss 0.08% +wait 1s +rate 226kbit +delay 50ms +loss 0.08% +wait 1s +rate 1193kbit +delay 50ms +loss 0.08% +wait 1s +rate 1187kbit +delay 50ms +loss 0.08% +wait 1s +rate 1147kbit +delay 50ms +loss 0.08% +wait 1s +rate 1209kbit +delay 50ms +loss 0.08% +wait 1s +rate 1138kbit +delay 50ms +loss 0.08% +wait 1s +rate 1122kbit +delay 50ms +loss 0.08% +wait 1s +rate 1160kbit +delay 50ms +loss 0.08% +wait 1s +rate 233kbit +delay 50ms +loss 0.08% +wait 1s +rate 1244kbit +delay 50ms +loss 0.08% +wait 1s +rate 1921kbit +delay 50ms +loss 0.08% +wait 1s +rate 233kbit +delay 50ms +loss 0.08% +wait 1s +rate 1645kbit +delay 50ms +loss 0.08% +wait 1s +rate 1069kbit +delay 50ms +loss 0.08% +wait 1s +rate 1292kbit +delay 50ms +loss 0.08% +wait 1s +rate 1128kbit +delay 50ms +loss 0.08% +wait 1s +rate 1145kbit +delay 50ms +loss 0.08% +wait 1s +rate 1182kbit +delay 50ms +loss 0.08% +wait 1s +rate 1219kbit +delay 50ms +loss 0.08% +wait 1s +rate 1316kbit +delay 50ms +loss 0.08% +wait 1s +rate 231kbit +delay 50ms +loss 0.08% +wait 1s +rate 1318kbit +delay 50ms +loss 0.08% +wait 1s +rate 1236kbit +delay 50ms +loss 0.08% +wait 1s +rate 1169kbit +delay 50ms +loss 0.08% +wait 1s +rate 1289kbit +delay 50ms +loss 0.08% +wait 1s +rate 1206kbit +delay 50ms +loss 0.08% +wait 1s +rate 1144kbit +delay 50ms +loss 0.08% +wait 1s +rate 988kbit +delay 50ms +loss 0.08% +wait 1s +rate 1131kbit +delay 50ms +loss 0.08% +wait 1s +rate 1092kbit +delay 50ms +loss 0.08% +wait 1s +rate 1236kbit +delay 50ms +loss 0.08% +wait 1s +rate 1164kbit +delay 50ms +loss 0.08% +wait 1s +rate 239kbit +delay 50ms +loss 0.08% +wait 1s +rate 1717kbit +delay 50ms +loss 0.08% +wait 1s +rate 1199kbit +delay 50ms +loss 0.08% +wait 1s \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/NYUbus b/repos/moq-rs/tc_profiles/NYUbus new file mode 100644 index 0000000..595d851 --- /dev/null +++ b/repos/moq-rs/tc_profiles/NYUbus @@ -0,0 +1,2400 @@ +rate 3970kbit +delay 50ms +loss 0.08% +wait 1s +rate 4430kbit +delay 50ms +loss 0.08% +wait 1s +rate 3640kbit +delay 50ms +loss 0.08% +wait 1s +rate 4830kbit +delay 50ms +loss 0.08% +wait 1s +rate 3830kbit +delay 50ms +loss 0.08% +wait 1s +rate 4460kbit +delay 50ms +loss 0.08% +wait 1s +rate 4080kbit +delay 50ms +loss 0.08% +wait 1s +rate 4950kbit +delay 50ms +loss 0.08% +wait 1s +rate 4080kbit +delay 50ms +loss 0.08% +wait 1s +rate 6290kbit +delay 50ms +loss 0.08% +wait 1s +rate 8480kbit +delay 50ms +loss 0.08% +wait 1s +rate 4040kbit +delay 50ms +loss 0.08% +wait 1s +rate 6150kbit +delay 50ms +loss 0.08% +wait 1s +rate 4960kbit +delay 50ms +loss 0.08% +wait 1s +rate 5670kbit +delay 50ms +loss 0.08% +wait 1s +rate 8170kbit +delay 50ms +loss 0.08% +wait 1s +rate 6040kbit +delay 50ms +loss 0.08% +wait 1s +rate 7090kbit +delay 50ms +loss 0.08% +wait 1s +rate 6110kbit +delay 50ms +loss 0.08% +wait 1s +rate 3360kbit +delay 50ms +loss 0.08% +wait 1s +rate 2660kbit +delay 50ms +loss 0.08% +wait 1s +rate 2530kbit +delay 50ms +loss 0.08% +wait 1s +rate 11600kbit +delay 50ms +loss 0.08% +wait 1s +rate 14800kbit +delay 50ms +loss 0.08% +wait 1s +rate 16100kbit +delay 50ms +loss 0.08% +wait 1s +rate 5890kbit +delay 50ms +loss 0.08% +wait 1s +rate 3960kbit +delay 50ms +loss 0.08% +wait 1s +rate 2130kbit +delay 50ms +loss 0.08% +wait 1s +rate 2530kbit +delay 50ms +loss 0.08% +wait 1s +rate 2740kbit +delay 50ms +loss 0.08% +wait 1s +rate 2680kbit +delay 50ms +loss 0.08% +wait 1s +rate 3000kbit +delay 50ms +loss 0.08% +wait 1s +rate 3290kbit +delay 50ms +loss 0.08% +wait 1s +rate 3760kbit +delay 50ms +loss 0.08% +wait 1s +rate 3740kbit +delay 50ms +loss 0.08% +wait 1s +rate 3850kbit +delay 50ms +loss 0.08% +wait 1s +rate 3690kbit +delay 50ms +loss 0.08% +wait 1s +rate 4770kbit +delay 50ms +loss 0.08% +wait 1s +rate 2750kbit +delay 50ms +loss 0.08% +wait 1s +rate 8070kbit +delay 50ms +loss 0.08% +wait 1s +rate 5500kbit +delay 50ms +loss 0.08% +wait 1s +rate 5040kbit +delay 50ms +loss 0.08% +wait 1s +rate 4620kbit +delay 50ms +loss 0.08% +wait 1s +rate 6740kbit +delay 50ms +loss 0.08% +wait 1s +rate 4040kbit +delay 50ms +loss 0.08% +wait 1s +rate 4380kbit +delay 50ms +loss 0.08% +wait 1s +rate 3760kbit +delay 50ms +loss 0.08% +wait 1s +rate 5210kbit +delay 50ms +loss 0.08% +wait 1s +rate 4060kbit +delay 50ms +loss 0.08% +wait 1s +rate 5180kbit +delay 50ms +loss 0.08% +wait 1s +rate 3780kbit +delay 50ms +loss 0.08% +wait 1s +rate 7960kbit +delay 50ms +loss 0.08% +wait 1s +rate 2880kbit +delay 50ms +loss 0.08% +wait 1s +rate 3830kbit +delay 50ms +loss 0.08% +wait 1s +rate 4980kbit +delay 50ms +loss 0.08% +wait 1s +rate 5500kbit +delay 50ms +loss 0.08% +wait 1s +rate 7130kbit +delay 50ms +loss 0.08% +wait 1s +rate 7020kbit +delay 50ms +loss 0.08% +wait 1s +rate 6650kbit +delay 50ms +loss 0.08% +wait 1s +rate 6290kbit +delay 50ms +loss 0.08% +wait 1s +rate 5920kbit +delay 50ms +loss 0.08% +wait 1s +rate 8810kbit +delay 50ms +loss 0.08% +wait 1s +rate 8420kbit +delay 50ms +loss 0.08% +wait 1s +rate 9840kbit +delay 50ms +loss 0.08% +wait 1s +rate 9260kbit +delay 50ms +loss 0.08% +wait 1s +rate 5030kbit +delay 50ms +loss 0.08% +wait 1s +rate 5250kbit +delay 50ms +loss 0.08% +wait 1s +rate 4740kbit +delay 50ms +loss 0.08% +wait 1s +rate 2680kbit +delay 50ms +loss 0.08% +wait 1s +rate 2350kbit +delay 50ms +loss 0.08% +wait 1s +rate 3730kbit +delay 50ms +loss 0.08% +wait 1s +rate 3960kbit +delay 50ms +loss 0.08% +wait 1s +rate 2750kbit +delay 50ms +loss 0.08% +wait 1s +rate 2880kbit +delay 50ms +loss 0.08% +wait 1s +rate 2870kbit +delay 50ms +loss 0.08% +wait 1s +rate 3430kbit +delay 50ms +loss 0.08% +wait 1s +rate 5120kbit +delay 50ms +loss 0.08% +wait 1s +rate 3800kbit +delay 50ms +loss 0.08% +wait 1s +rate 4410kbit +delay 50ms +loss 0.08% +wait 1s +rate 3850kbit +delay 50ms +loss 0.08% +wait 1s +rate 3980kbit +delay 50ms +loss 0.08% +wait 1s +rate 4320kbit +delay 50ms +loss 0.08% +wait 1s +rate 3100kbit +delay 50ms +loss 0.08% +wait 1s +rate 3630kbit +delay 50ms +loss 0.08% +wait 1s +rate 4410kbit +delay 50ms +loss 0.08% +wait 1s +rate 3210kbit +delay 50ms +loss 0.08% +wait 1s +rate 5020kbit +delay 50ms +loss 0.08% +wait 1s +rate 2580kbit +delay 50ms +loss 0.08% +wait 1s +rate 4860kbit +delay 50ms +loss 0.08% +wait 1s +rate 4140kbit +delay 50ms +loss 0.08% +wait 1s +rate 4630kbit +delay 50ms +loss 0.08% +wait 1s +rate 6530kbit +delay 50ms +loss 0.08% +wait 1s +rate 5520kbit +delay 50ms +loss 0.08% +wait 1s +rate 3830kbit +delay 50ms +loss 0.08% +wait 1s +rate 2440kbit +delay 50ms +loss 0.08% +wait 1s +rate 4890kbit +delay 50ms +loss 0.08% +wait 1s +rate 5800kbit +delay 50ms +loss 0.08% +wait 1s +rate 6230kbit +delay 50ms +loss 0.08% +wait 1s +rate 3070kbit +delay 50ms +loss 0.08% +wait 1s +rate 2730kbit +delay 50ms +loss 0.08% +wait 1s +rate 4110kbit +delay 50ms +loss 0.08% +wait 1s +rate 10900kbit +delay 50ms +loss 0.08% +wait 1s +rate 15300kbit +delay 50ms +loss 0.08% +wait 1s +rate 16600kbit +delay 50ms +loss 0.08% +wait 1s +rate 7890kbit +delay 50ms +loss 0.08% +wait 1s +rate 11400kbit +delay 50ms +loss 0.08% +wait 1s +rate 12800kbit +delay 50ms +loss 0.08% +wait 1s +rate 13300kbit +delay 50ms +loss 0.08% +wait 1s +rate 9860kbit +delay 50ms +loss 0.08% +wait 1s +rate 14200kbit +delay 50ms +loss 0.08% +wait 1s +rate 11300kbit +delay 50ms +loss 0.08% +wait 1s +rate 11100kbit +delay 50ms +loss 0.08% +wait 1s +rate 10400kbit +delay 50ms +loss 0.08% +wait 1s +rate 6080kbit +delay 50ms +loss 0.08% +wait 1s +rate 8980kbit +delay 50ms +loss 0.08% +wait 1s +rate 13800kbit +delay 50ms +loss 0.08% +wait 1s +rate 16400kbit +delay 50ms +loss 0.08% +wait 1s +rate 12700kbit +delay 50ms +loss 0.08% +wait 1s +rate 12200kbit +delay 50ms +loss 0.08% +wait 1s +rate 12400kbit +delay 50ms +loss 0.08% +wait 1s +rate 11400kbit +delay 50ms +loss 0.08% +wait 1s +rate 12800kbit +delay 50ms +loss 0.08% +wait 1s +rate 10700kbit +delay 50ms +loss 0.08% +wait 1s +rate 8950kbit +delay 50ms +loss 0.08% +wait 1s +rate 7970kbit +delay 50ms +loss 0.08% +wait 1s +rate 9410kbit +delay 50ms +loss 0.08% +wait 1s +rate 9580kbit +delay 50ms +loss 0.08% +wait 1s +rate 9220kbit +delay 50ms +loss 0.08% +wait 1s +rate 11600kbit +delay 50ms +loss 0.08% +wait 1s +rate 10800kbit +delay 50ms +loss 0.08% +wait 1s +rate 11200kbit +delay 50ms +loss 0.08% +wait 1s +rate 8330kbit +delay 50ms +loss 0.08% +wait 1s +rate 6440kbit +delay 50ms +loss 0.08% +wait 1s +rate 6160kbit +delay 50ms +loss 0.08% +wait 1s +rate 3430kbit +delay 50ms +loss 0.08% +wait 1s +rate 5640kbit +delay 50ms +loss 0.08% +wait 1s +rate 6820kbit +delay 50ms +loss 0.08% +wait 1s +rate 8840kbit +delay 50ms +loss 0.08% +wait 1s +rate 8760kbit +delay 50ms +loss 0.08% +wait 1s +rate 8490kbit +delay 50ms +loss 0.08% +wait 1s +rate 7030kbit +delay 50ms +loss 0.08% +wait 1s +rate 5030kbit +delay 50ms +loss 0.08% +wait 1s +rate 5780kbit +delay 50ms +loss 0.08% +wait 1s +rate 6090kbit +delay 50ms +loss 0.08% +wait 1s +rate 6000kbit +delay 50ms +loss 0.08% +wait 1s +rate 5670kbit +delay 50ms +loss 0.08% +wait 1s +rate 7770kbit +delay 50ms +loss 0.08% +wait 1s +rate 7190kbit +delay 50ms +loss 0.08% +wait 1s +rate 5610kbit +delay 50ms +loss 0.08% +wait 1s +rate 5480kbit +delay 50ms +loss 0.08% +wait 1s +rate 5500kbit +delay 50ms +loss 0.08% +wait 1s +rate 6670kbit +delay 50ms +loss 0.08% +wait 1s +rate 8120kbit +delay 50ms +loss 0.08% +wait 1s +rate 8640kbit +delay 50ms +loss 0.08% +wait 1s +rate 11200kbit +delay 50ms +loss 0.08% +wait 1s +rate 9410kbit +delay 50ms +loss 0.08% +wait 1s +rate 11200kbit +delay 50ms +loss 0.08% +wait 1s +rate 15700kbit +delay 50ms +loss 0.08% +wait 1s +rate 13000kbit +delay 50ms +loss 0.08% +wait 1s +rate 15500kbit +delay 50ms +loss 0.08% +wait 1s +rate 22300kbit +delay 50ms +loss 0.08% +wait 1s +rate 16300kbit +delay 50ms +loss 0.08% +wait 1s +rate 18700kbit +delay 50ms +loss 0.08% +wait 1s +rate 23200kbit +delay 50ms +loss 0.08% +wait 1s +rate 14800kbit +delay 50ms +loss 0.08% +wait 1s +rate 14500kbit +delay 50ms +loss 0.08% +wait 1s +rate 16500kbit +delay 50ms +loss 0.08% +wait 1s +rate 16900kbit +delay 50ms +loss 0.08% +wait 1s +rate 16400kbit +delay 50ms +loss 0.08% +wait 1s +rate 12300kbit +delay 50ms +loss 0.08% +wait 1s +rate 14200kbit +delay 50ms +loss 0.08% +wait 1s +rate 11300kbit +delay 50ms +loss 0.08% +wait 1s +rate 7110kbit +delay 50ms +loss 0.08% +wait 1s +rate 1540kbit +delay 50ms +loss 0.08% +wait 1s +rate 2250kbit +delay 50ms +loss 0.08% +wait 1s +rate 3050kbit +delay 50ms +loss 0.08% +wait 1s +rate 4120kbit +delay 50ms +loss 0.08% +wait 1s +rate 5600kbit +delay 50ms +loss 0.08% +wait 1s +rate 6100kbit +delay 50ms +loss 0.08% +wait 1s +rate 6880kbit +delay 50ms +loss 0.08% +wait 1s +rate 11200kbit +delay 50ms +loss 0.08% +wait 1s +rate 15900kbit +delay 50ms +loss 0.08% +wait 1s +rate 12900kbit +delay 50ms +loss 0.08% +wait 1s +rate 19900kbit +delay 50ms +loss 0.08% +wait 1s +rate 9800kbit +delay 50ms +loss 0.08% +wait 1s +rate 12100kbit +delay 50ms +loss 0.08% +wait 1s +rate 9520kbit +delay 50ms +loss 0.08% +wait 1s +rate 12500kbit +delay 50ms +loss 0.08% +wait 1s +rate 13200kbit +delay 50ms +loss 0.08% +wait 1s +rate 9450kbit +delay 50ms +loss 0.08% +wait 1s +rate 10400kbit +delay 50ms +loss 0.08% +wait 1s +rate 6730kbit +delay 50ms +loss 0.08% +wait 1s +rate 10100kbit +delay 50ms +loss 0.08% +wait 1s +rate 8410kbit +delay 50ms +loss 0.08% +wait 1s +rate 11500kbit +delay 50ms +loss 0.08% +wait 1s +rate 8150kbit +delay 50ms +loss 0.08% +wait 1s +rate 7660kbit +delay 50ms +loss 0.08% +wait 1s +rate 11600kbit +delay 50ms +loss 0.08% +wait 1s +rate 10800kbit +delay 50ms +loss 0.08% +wait 1s +rate 10600kbit +delay 50ms +loss 0.08% +wait 1s +rate 13600kbit +delay 50ms +loss 0.08% +wait 1s +rate 18700kbit +delay 50ms +loss 0.08% +wait 1s +rate 11300kbit +delay 50ms +loss 0.08% +wait 1s +rate 15500kbit +delay 50ms +loss 0.08% +wait 1s +rate 13600kbit +delay 50ms +loss 0.08% +wait 1s +rate 15500kbit +delay 50ms +loss 0.08% +wait 1s +rate 10700kbit +delay 50ms +loss 0.08% +wait 1s +rate 7500kbit +delay 50ms +loss 0.08% +wait 1s +rate 5090kbit +delay 50ms +loss 0.08% +wait 1s +rate 5150kbit +delay 50ms +loss 0.08% +wait 1s +rate 4470kbit +delay 50ms +loss 0.08% +wait 1s +rate 5220kbit +delay 50ms +loss 0.08% +wait 1s +rate 5890kbit +delay 50ms +loss 0.08% +wait 1s +rate 6280kbit +delay 50ms +loss 0.08% +wait 1s +rate 6500kbit +delay 50ms +loss 0.08% +wait 1s +rate 5420kbit +delay 50ms +loss 0.08% +wait 1s +rate 6070kbit +delay 50ms +loss 0.08% +wait 1s +rate 6950kbit +delay 50ms +loss 0.08% +wait 1s +rate 7450kbit +delay 50ms +loss 0.08% +wait 1s +rate 7020kbit +delay 50ms +loss 0.08% +wait 1s +rate 9630kbit +delay 50ms +loss 0.08% +wait 1s +rate 10900kbit +delay 50ms +loss 0.08% +wait 1s +rate 11300kbit +delay 50ms +loss 0.08% +wait 1s +rate 12300kbit +delay 50ms +loss 0.08% +wait 1s +rate 11400kbit +delay 50ms +loss 0.08% +wait 1s +rate 14600kbit +delay 50ms +loss 0.08% +wait 1s +rate 15400kbit +delay 50ms +loss 0.08% +wait 1s +rate 15300kbit +delay 50ms +loss 0.08% +wait 1s +rate 14900kbit +delay 50ms +loss 0.08% +wait 1s +rate 13200kbit +delay 50ms +loss 0.08% +wait 1s +rate 14400kbit +delay 50ms +loss 0.08% +wait 1s +rate 15800kbit +delay 50ms +loss 0.08% +wait 1s +rate 16300kbit +delay 50ms +loss 0.08% +wait 1s +rate 14100kbit +delay 50ms +loss 0.08% +wait 1s +rate 12200kbit +delay 50ms +loss 0.08% +wait 1s +rate 12900kbit +delay 50ms +loss 0.08% +wait 1s +rate 14900kbit +delay 50ms +loss 0.08% +wait 1s +rate 14400kbit +delay 50ms +loss 0.08% +wait 1s +rate 17500kbit +delay 50ms +loss 0.08% +wait 1s +rate 12300kbit +delay 50ms +loss 0.08% +wait 1s +rate 12700kbit +delay 50ms +loss 0.08% +wait 1s +rate 13300kbit +delay 50ms +loss 0.08% +wait 1s +rate 13100kbit +delay 50ms +loss 0.08% +wait 1s +rate 12200kbit +delay 50ms +loss 0.08% +wait 1s +rate 12400kbit +delay 50ms +loss 0.08% +wait 1s +rate 13600kbit +delay 50ms +loss 0.08% +wait 1s +rate 16000kbit +delay 50ms +loss 0.08% +wait 1s +rate 8050kbit +delay 50ms +loss 0.08% +wait 1s +rate 10500kbit +delay 50ms +loss 0.08% +wait 1s +rate 12600kbit +delay 50ms +loss 0.08% +wait 1s +rate 10900kbit +delay 50ms +loss 0.08% +wait 1s +rate 13000kbit +delay 50ms +loss 0.08% +wait 1s +rate 10500kbit +delay 50ms +loss 0.08% +wait 1s +rate 14800kbit +delay 50ms +loss 0.08% +wait 1s +rate 11200kbit +delay 50ms +loss 0.08% +wait 1s +rate 10500kbit +delay 50ms +loss 0.08% +wait 1s +rate 9250kbit +delay 50ms +loss 0.08% +wait 1s +rate 15100kbit +delay 50ms +loss 0.08% +wait 1s +rate 19100kbit +delay 50ms +loss 0.08% +wait 1s +rate 16000kbit +delay 50ms +loss 0.08% +wait 1s +rate 22200kbit +delay 50ms +loss 0.08% +wait 1s +rate 20700kbit +delay 50ms +loss 0.08% +wait 1s +rate 22000kbit +delay 50ms +loss 0.08% +wait 1s +rate 18900kbit +delay 50ms +loss 0.08% +wait 1s +rate 16700kbit +delay 50ms +loss 0.08% +wait 1s +rate 17000kbit +delay 50ms +loss 0.08% +wait 1s +rate 20200kbit +delay 50ms +loss 0.08% +wait 1s +rate 14200kbit +delay 50ms +loss 0.08% +wait 1s +rate 19000kbit +delay 50ms +loss 0.08% +wait 1s +rate 16800kbit +delay 50ms +loss 0.08% +wait 1s +rate 19200kbit +delay 50ms +loss 0.08% +wait 1s +rate 16400kbit +delay 50ms +loss 0.08% +wait 1s +rate 15800kbit +delay 50ms +loss 0.08% +wait 1s +rate 19000kbit +delay 50ms +loss 0.08% +wait 1s +rate 18700kbit +delay 50ms +loss 0.08% +wait 1s +rate 15800kbit +delay 50ms +loss 0.08% +wait 1s +rate 17000kbit +delay 50ms +loss 0.08% +wait 1s +rate 16500kbit +delay 50ms +loss 0.08% +wait 1s +rate 13000kbit +delay 50ms +loss 0.08% +wait 1s +rate 14100kbit +delay 50ms +loss 0.08% +wait 1s +rate 12900kbit +delay 50ms +loss 0.08% +wait 1s +rate 12900kbit +delay 50ms +loss 0.08% +wait 1s +rate 13600kbit +delay 50ms +loss 0.08% +wait 1s +rate 11900kbit +delay 50ms +loss 0.08% +wait 1s +rate 16600kbit +delay 50ms +loss 0.08% +wait 1s +rate 15400kbit +delay 50ms +loss 0.08% +wait 1s +rate 10400kbit +delay 50ms +loss 0.08% +wait 1s +rate 12600kbit +delay 50ms +loss 0.08% +wait 1s +rate 9400kbit +delay 50ms +loss 0.08% +wait 1s +rate 11100kbit +delay 50ms +loss 0.08% +wait 1s +rate 12100kbit +delay 50ms +loss 0.08% +wait 1s +rate 14200kbit +delay 50ms +loss 0.08% +wait 1s +rate 10600kbit +delay 50ms +loss 0.08% +wait 1s +rate 11100kbit +delay 50ms +loss 0.08% +wait 1s +rate 10300kbit +delay 50ms +loss 0.08% +wait 1s +rate 9090kbit +delay 50ms +loss 0.08% +wait 1s +rate 5180kbit +delay 50ms +loss 0.08% +wait 1s +rate 6740kbit +delay 50ms +loss 0.08% +wait 1s +rate 8900kbit +delay 50ms +loss 0.08% +wait 1s +rate 3490kbit +delay 50ms +loss 0.08% +wait 1s +rate 2940kbit +delay 50ms +loss 0.08% +wait 1s +rate 7650kbit +delay 50ms +loss 0.08% +wait 1s +rate 6330kbit +delay 50ms +loss 0.08% +wait 1s +rate 9420kbit +delay 50ms +loss 0.08% +wait 1s +rate 10000kbit +delay 50ms +loss 0.08% +wait 1s +rate 10400kbit +delay 50ms +loss 0.08% +wait 1s +rate 11900kbit +delay 50ms +loss 0.08% +wait 1s +rate 12000kbit +delay 50ms +loss 0.08% +wait 1s +rate 15000kbit +delay 50ms +loss 0.08% +wait 1s +rate 15000kbit +delay 50ms +loss 0.08% +wait 1s +rate 15600kbit +delay 50ms +loss 0.08% +wait 1s +rate 13100kbit +delay 50ms +loss 0.08% +wait 1s +rate 11800kbit +delay 50ms +loss 0.08% +wait 1s +rate 13500kbit +delay 50ms +loss 0.08% +wait 1s +rate 13500kbit +delay 50ms +loss 0.08% +wait 1s +rate 16500kbit +delay 50ms +loss 0.08% +wait 1s +rate 18400kbit +delay 50ms +loss 0.08% +wait 1s +rate 17700kbit +delay 50ms +loss 0.08% +wait 1s +rate 15300kbit +delay 50ms +loss 0.08% +wait 1s +rate 16500kbit +delay 50ms +loss 0.08% +wait 1s +rate 13700kbit +delay 50ms +loss 0.08% +wait 1s +rate 12900kbit +delay 50ms +loss 0.08% +wait 1s +rate 17600kbit +delay 50ms +loss 0.08% +wait 1s +rate 14800kbit +delay 50ms +loss 0.08% +wait 1s +rate 16600kbit +delay 50ms +loss 0.08% +wait 1s +rate 13900kbit +delay 50ms +loss 0.08% +wait 1s +rate 16100kbit +delay 50ms +loss 0.08% +wait 1s +rate 14800kbit +delay 50ms +loss 0.08% +wait 1s +rate 21800kbit +delay 50ms +loss 0.08% +wait 1s +rate 18900kbit +delay 50ms +loss 0.08% +wait 1s +rate 15600kbit +delay 50ms +loss 0.08% +wait 1s +rate 15500kbit +delay 50ms +loss 0.08% +wait 1s +rate 15800kbit +delay 50ms +loss 0.08% +wait 1s +rate 13600kbit +delay 50ms +loss 0.08% +wait 1s +rate 16600kbit +delay 50ms +loss 0.08% +wait 1s +rate 20400kbit +delay 50ms +loss 0.08% +wait 1s +rate 17500kbit +delay 50ms +loss 0.08% +wait 1s +rate 20100kbit +delay 50ms +loss 0.08% +wait 1s +rate 20800kbit +delay 50ms +loss 0.08% +wait 1s +rate 21900kbit +delay 50ms +loss 0.08% +wait 1s +rate 22000kbit +delay 50ms +loss 0.08% +wait 1s +rate 18100kbit +delay 50ms +loss 0.08% +wait 1s +rate 21800kbit +delay 50ms +loss 0.08% +wait 1s +rate 19500kbit +delay 50ms +loss 0.08% +wait 1s +rate 19300kbit +delay 50ms +loss 0.08% +wait 1s +rate 18000kbit +delay 50ms +loss 0.08% +wait 1s +rate 19300kbit +delay 50ms +loss 0.08% +wait 1s +rate 17400kbit +delay 50ms +loss 0.08% +wait 1s +rate 21300kbit +delay 50ms +loss 0.08% +wait 1s +rate 18300kbit +delay 50ms +loss 0.08% +wait 1s +rate 20500kbit +delay 50ms +loss 0.08% +wait 1s +rate 17500kbit +delay 50ms +loss 0.08% +wait 1s +rate 17600kbit +delay 50ms +loss 0.08% +wait 1s +rate 19700kbit +delay 50ms +loss 0.08% +wait 1s +rate 21400kbit +delay 50ms +loss 0.08% +wait 1s +rate 20400kbit +delay 50ms +loss 0.08% +wait 1s +rate 18400kbit +delay 50ms +loss 0.08% +wait 1s +rate 18200kbit +delay 50ms +loss 0.08% +wait 1s +rate 22500kbit +delay 50ms +loss 0.08% +wait 1s +rate 18400kbit +delay 50ms +loss 0.08% +wait 1s +rate 21600kbit +delay 50ms +loss 0.08% +wait 1s +rate 20600kbit +delay 50ms +loss 0.08% +wait 1s +rate 19300kbit +delay 50ms +loss 0.08% +wait 1s +rate 23200kbit +delay 50ms +loss 0.08% +wait 1s +rate 17300kbit +delay 50ms +loss 0.08% +wait 1s +rate 19000kbit +delay 50ms +loss 0.08% +wait 1s +rate 19200kbit +delay 50ms +loss 0.08% +wait 1s +rate 15900kbit +delay 50ms +loss 0.08% +wait 1s +rate 15300kbit +delay 50ms +loss 0.08% +wait 1s +rate 15100kbit +delay 50ms +loss 0.08% +wait 1s +rate 17400kbit +delay 50ms +loss 0.08% +wait 1s +rate 15200kbit +delay 50ms +loss 0.08% +wait 1s +rate 16100kbit +delay 50ms +loss 0.08% +wait 1s +rate 16700kbit +delay 50ms +loss 0.08% +wait 1s +rate 18600kbit +delay 50ms +loss 0.08% +wait 1s +rate 15000kbit +delay 50ms +loss 0.08% +wait 1s +rate 15100kbit +delay 50ms +loss 0.08% +wait 1s +rate 13200kbit +delay 50ms +loss 0.08% +wait 1s +rate 14600kbit +delay 50ms +loss 0.08% +wait 1s +rate 14400kbit +delay 50ms +loss 0.08% +wait 1s +rate 16000kbit +delay 50ms +loss 0.08% +wait 1s +rate 20900kbit +delay 50ms +loss 0.08% +wait 1s +rate 15700kbit +delay 50ms +loss 0.08% +wait 1s +rate 16000kbit +delay 50ms +loss 0.08% +wait 1s +rate 11900kbit +delay 50ms +loss 0.08% +wait 1s +rate 13100kbit +delay 50ms +loss 0.08% +wait 1s +rate 15200kbit +delay 50ms +loss 0.08% +wait 1s +rate 11800kbit +delay 50ms +loss 0.08% +wait 1s +rate 12000kbit +delay 50ms +loss 0.08% +wait 1s +rate 5710kbit +delay 50ms +loss 0.08% +wait 1s +rate 8380kbit +delay 50ms +loss 0.08% +wait 1s +rate 3890kbit +delay 50ms +loss 0.08% +wait 1s +rate 3440kbit +delay 50ms +loss 0.08% +wait 1s +rate 4850kbit +delay 50ms +loss 0.08% +wait 1s +rate 5760kbit +delay 50ms +loss 0.08% +wait 1s +rate 6220kbit +delay 50ms +loss 0.08% +wait 1s +rate 6130kbit +delay 50ms +loss 0.08% +wait 1s +rate 4760kbit +delay 50ms +loss 0.08% +wait 1s +rate 4790kbit +delay 50ms +loss 0.08% +wait 1s +rate 6860kbit +delay 50ms +loss 0.08% +wait 1s +rate 5500kbit +delay 50ms +loss 0.08% +wait 1s +rate 9320kbit +delay 50ms +loss 0.08% +wait 1s +rate 7810kbit +delay 50ms +loss 0.08% +wait 1s +rate 6780kbit +delay 50ms +loss 0.08% +wait 1s +rate 7660kbit +delay 50ms +loss 0.08% +wait 1s +rate 8330kbit +delay 50ms +loss 0.08% +wait 1s +rate 6670kbit +delay 50ms +loss 0.08% +wait 1s +rate 6340kbit +delay 50ms +loss 0.08% +wait 1s +rate 6540kbit +delay 50ms +loss 0.08% +wait 1s +rate 7050kbit +delay 50ms +loss 0.08% +wait 1s +rate 6190kbit +delay 50ms +loss 0.08% +wait 1s +rate 6800kbit +delay 50ms +loss 0.08% +wait 1s +rate 5000kbit +delay 50ms +loss 0.08% +wait 1s +rate 3730kbit +delay 50ms +loss 0.08% +wait 1s +rate 3530kbit +delay 50ms +loss 0.08% +wait 1s +rate 3410kbit +delay 50ms +loss 0.08% +wait 1s +rate 4170kbit +delay 50ms +loss 0.08% +wait 1s +rate 5160kbit +delay 50ms +loss 0.08% +wait 1s +rate 3490kbit +delay 50ms +loss 0.08% +wait 1s +rate 5810kbit +delay 50ms +loss 0.08% +wait 1s +rate 6070kbit +delay 50ms +loss 0.08% +wait 1s +rate 4220kbit +delay 50ms +loss 0.08% +wait 1s +rate 5330kbit +delay 50ms +loss 0.08% +wait 1s +rate 4860kbit +delay 50ms +loss 0.08% +wait 1s +rate 4300kbit +delay 50ms +loss 0.08% +wait 1s +rate 6810kbit +delay 50ms +loss 0.08% +wait 1s +rate 6190kbit +delay 50ms +loss 0.08% +wait 1s +rate 8880kbit +delay 50ms +loss 0.08% +wait 1s +rate 9040kbit +delay 50ms +loss 0.08% +wait 1s +rate 8460kbit +delay 50ms +loss 0.08% +wait 1s +rate 7150kbit +delay 50ms +loss 0.08% +wait 1s +rate 6820kbit +delay 50ms +loss 0.08% +wait 1s +rate 7130kbit +delay 50ms +loss 0.08% +wait 1s +rate 6810kbit +delay 50ms +loss 0.08% +wait 1s +rate 8820kbit +delay 50ms +loss 0.08% +wait 1s +rate 7490kbit +delay 50ms +loss 0.08% +wait 1s +rate 8780kbit +delay 50ms +loss 0.08% +wait 1s +rate 8210kbit +delay 50ms +loss 0.08% +wait 1s +rate 7240kbit +delay 50ms +loss 0.08% +wait 1s +rate 8590kbit +delay 50ms +loss 0.08% +wait 1s +rate 8060kbit +delay 50ms +loss 0.08% +wait 1s +rate 8640kbit +delay 50ms +loss 0.08% +wait 1s +rate 11200kbit +delay 50ms +loss 0.08% +wait 1s +rate 6260kbit +delay 50ms +loss 0.08% +wait 1s +rate 5620kbit +delay 50ms +loss 0.08% +wait 1s +rate 6900kbit +delay 50ms +loss 0.08% +wait 1s +rate 7150kbit +delay 50ms +loss 0.08% +wait 1s +rate 6940kbit +delay 50ms +loss 0.08% +wait 1s +rate 8980kbit +delay 50ms +loss 0.08% +wait 1s +rate 9170kbit +delay 50ms +loss 0.08% +wait 1s +rate 7300kbit +delay 50ms +loss 0.08% +wait 1s +rate 7130kbit +delay 50ms +loss 0.08% +wait 1s +rate 5760kbit +delay 50ms +loss 0.08% +wait 1s +rate 7520kbit +delay 50ms +loss 0.08% +wait 1s +rate 6630kbit +delay 50ms +loss 0.08% +wait 1s +rate 5290kbit +delay 50ms +loss 0.08% +wait 1s +rate 4030kbit +delay 50ms +loss 0.08% +wait 1s +rate 3570kbit +delay 50ms +loss 0.08% +wait 1s +rate 4410kbit +delay 50ms +loss 0.08% +wait 1s +rate 4970kbit +delay 50ms +loss 0.08% +wait 1s +rate 6980kbit +delay 50ms +loss 0.08% +wait 1s +rate 5520kbit +delay 50ms +loss 0.08% +wait 1s +rate 5600kbit +delay 50ms +loss 0.08% +wait 1s +rate 5250kbit +delay 50ms +loss 0.08% +wait 1s +rate 4420kbit +delay 50ms +loss 0.08% +wait 1s +rate 5850kbit +delay 50ms +loss 0.08% +wait 1s +rate 5480kbit +delay 50ms +loss 0.08% +wait 1s +rate 7110kbit +delay 50ms +loss 0.08% +wait 1s +rate 5840kbit +delay 50ms +loss 0.08% +wait 1s +rate 6500kbit +delay 50ms +loss 0.08% +wait 1s +rate 5820kbit +delay 50ms +loss 0.08% +wait 1s +rate 4460kbit +delay 50ms +loss 0.08% +wait 1s +rate 3960kbit +delay 50ms +loss 0.08% +wait 1s +rate 4450kbit +delay 50ms +loss 0.08% +wait 1s +rate 4500kbit +delay 50ms +loss 0.08% +wait 1s +rate 6510kbit +delay 50ms +loss 0.08% +wait 1s +rate 5780kbit +delay 50ms +loss 0.08% +wait 1s +rate 7030kbit +delay 50ms +loss 0.08% +wait 1s +rate 6480kbit +delay 50ms +loss 0.08% +wait 1s +rate 7130kbit +delay 50ms +loss 0.08% +wait 1s +rate 7850kbit +delay 50ms +loss 0.08% +wait 1s +rate 7240kbit +delay 50ms +loss 0.08% +wait 1s +rate 6900kbit +delay 50ms +loss 0.08% +wait 1s +rate 6720kbit +delay 50ms +loss 0.08% +wait 1s +rate 7420kbit +delay 50ms +loss 0.08% +wait 1s +rate 9070kbit +delay 50ms +loss 0.08% +wait 1s +rate 6680kbit +delay 50ms +loss 0.08% +wait 1s +rate 7510kbit +delay 50ms +loss 0.08% +wait 1s +rate 7650kbit +delay 50ms +loss 0.08% +wait 1s +rate 6600kbit +delay 50ms +loss 0.08% +wait 1s +rate 9760kbit +delay 50ms +loss 0.08% +wait 1s +rate 6550kbit +delay 50ms +loss 0.08% +wait 1s +rate 12400kbit +delay 50ms +loss 0.08% +wait 1s +rate 15000kbit +delay 50ms +loss 0.08% +wait 1s +rate 14000kbit +delay 50ms +loss 0.08% +wait 1s +rate 19800kbit +delay 50ms +loss 0.08% +wait 1s +rate 18600kbit +delay 50ms +loss 0.08% +wait 1s +rate 17100kbit +delay 50ms +loss 0.08% +wait 1s +rate 19000kbit +delay 50ms +loss 0.08% +wait 1s +rate 10800kbit +delay 50ms +loss 0.08% +wait 1s +rate 11400kbit +delay 50ms +loss 0.08% +wait 1s +rate 6160kbit +delay 50ms +loss 0.08% +wait 1s +rate 10800kbit +delay 50ms +loss 0.08% +wait 1s +rate 8600kbit +delay 50ms +loss 0.08% +wait 1s +rate 9030kbit +delay 50ms +loss 0.08% +wait 1s +rate 9810kbit +delay 50ms +loss 0.08% +wait 1s +rate 7630kbit +delay 50ms +loss 0.08% +wait 1s +rate 4870kbit +delay 50ms +loss 0.08% +wait 1s +rate 4740kbit +delay 50ms +loss 0.08% +wait 1s +rate 8180kbit +delay 50ms +loss 0.08% +wait 1s +rate 8070kbit +delay 50ms +loss 0.08% +wait 1s +rate 4940kbit +delay 50ms +loss 0.08% +wait 1s +rate 6820kbit +delay 50ms +loss 0.08% +wait 1s +rate 7860kbit +delay 50ms +loss 0.08% +wait 1s +rate 8600kbit +delay 50ms +loss 0.08% +wait 1s +rate 9380kbit +delay 50ms +loss 0.08% +wait 1s +rate 5340kbit +delay 50ms +loss 0.08% +wait 1s +rate 5020kbit +delay 50ms +loss 0.08% +wait 1s +rate 7980kbit +delay 50ms +loss 0.08% +wait 1s +rate 6050kbit +delay 50ms +loss 0.08% +wait 1s +rate 6220kbit +delay 50ms +loss 0.08% +wait 1s +rate 7860kbit +delay 50ms +loss 0.08% +wait 1s +rate 9500kbit +delay 50ms +loss 0.08% +wait 1s +rate 9530kbit +delay 50ms +loss 0.08% +wait 1s +rate 12700kbit +delay 50ms +loss 0.08% +wait 1s +rate 11200kbit +delay 50ms +loss 0.08% +wait 1s +rate 13100kbit +delay 50ms +loss 0.08% +wait 1s +rate 12200kbit +delay 50ms +loss 0.08% +wait 1s +rate 11000kbit +delay 50ms +loss 0.08% +wait 1s +rate 10100kbit +delay 50ms +loss 0.08% +wait 1s +rate 15000kbit +delay 50ms +loss 0.08% +wait 1s +rate 13400kbit +delay 50ms +loss 0.08% +wait 1s +rate 10600kbit +delay 50ms +loss 0.08% +wait 1s +rate 12400kbit +delay 50ms +loss 0.08% +wait 1s +rate 9750kbit +delay 50ms +loss 0.08% +wait 1s +rate 7980kbit +delay 50ms +loss 0.08% +wait 1s +rate 6790kbit +delay 50ms +loss 0.08% +wait 1s +rate 8080kbit +delay 50ms +loss 0.08% +wait 1s +rate 8870kbit +delay 50ms +loss 0.08% +wait 1s +rate 8350kbit +delay 50ms +loss 0.08% +wait 1s +rate 6910kbit +delay 50ms +loss 0.08% +wait 1s +rate 12100kbit +delay 50ms +loss 0.08% +wait 1s +rate 9900kbit +delay 50ms +loss 0.08% +wait 1s +rate 8280kbit +delay 50ms +loss 0.08% +wait 1s +rate 10200kbit +delay 50ms +loss 0.08% +wait 1s +rate 9090kbit +delay 50ms +loss 0.08% +wait 1s +rate 8540kbit +delay 50ms +loss 0.08% +wait 1s +rate 13600kbit +delay 50ms +loss 0.08% +wait 1s +rate 12600kbit +delay 50ms +loss 0.08% +wait 1s +rate 8640kbit +delay 50ms +loss 0.08% +wait 1s +rate 8430kbit +delay 50ms +loss 0.08% +wait 1s +rate 9340kbit +delay 50ms +loss 0.08% +wait 1s +rate 12000kbit +delay 50ms +loss 0.08% +wait 1s +rate 13500kbit +delay 50ms +loss 0.08% +wait 1s +rate 9360kbit +delay 50ms +loss 0.08% +wait 1s +rate 8560kbit +delay 50ms +loss 0.08% +wait 1s +rate 10200kbit +delay 50ms +loss 0.08% +wait 1s +rate 15300kbit +delay 50ms +loss 0.08% +wait 1s +rate 16900kbit +delay 50ms +loss 0.08% +wait 1s +rate 17300kbit +delay 50ms +loss 0.08% +wait 1s +rate 16400kbit +delay 50ms +loss 0.08% +wait 1s +rate 13900kbit +delay 50ms +loss 0.08% +wait 1s +rate 13800kbit +delay 50ms +loss 0.08% +wait 1s +rate 13700kbit +delay 50ms +loss 0.08% +wait 1s +rate 14700kbit +delay 50ms +loss 0.08% +wait 1s +rate 10800kbit +delay 50ms +loss 0.08% +wait 1s +rate 10700kbit +delay 50ms +loss 0.08% +wait 1s +rate 8160kbit +delay 50ms +loss 0.08% +wait 1s +rate 7690kbit +delay 50ms +loss 0.08% +wait 1s +rate 7550kbit +delay 50ms +loss 0.08% +wait 1s +rate 1930kbit +delay 50ms +loss 0.08% +wait 1s +rate 3450kbit +delay 50ms +loss 0.08% +wait 1s +rate 2780kbit +delay 50ms +loss 0.08% +wait 1s +rate 2040kbit +delay 50ms +loss 0.08% +wait 1s +rate 3430kbit +delay 50ms +loss 0.08% +wait 1s +rate 791kbit +delay 50ms +loss 0.08% +wait 1s +rate 2440kbit +delay 50ms +loss 0.08% +wait 1s +rate 1260kbit +delay 50ms +loss 0.08% +wait 1s +rate 2200kbit +delay 50ms +loss 0.08% +wait 1s +rate 650kbit +delay 50ms +loss 0.08% +wait 1s +rate 997kbit +delay 50ms +loss 0.08% +wait 1s +rate 2210kbit +delay 50ms +loss 0.08% +wait 1s +rate 1180kbit +delay 50ms +loss 0.08% +wait 1s +rate 1080kbit +delay 50ms +loss 0.08% +wait 1s +rate 1140kbit +delay 50ms +loss 0.08% +wait 1s +rate 1340kbit +delay 50ms +loss 0.08% +wait 1s +rate 899kbit +delay 50ms +loss 0.08% +wait 1s +rate 43kbit +delay 50ms +loss 0.08% +wait 1s +rate 726kbit +delay 50ms +loss 0.08% +wait 1s +rate 76kbit +delay 50ms +loss 0.08% +wait 1s +rate 694kbit +delay 50ms +loss 0.08% +wait 1s +rate 130kbit +delay 50ms +loss 0.08% +wait 1s +rate 162kbit +delay 50ms +loss 0.08% +wait 1s +rate 33kbit +delay 50ms +loss 0.08% +wait 1s +rate 173kbit +delay 50ms +loss 0.08% +wait 1s +rate 141kbit +delay 50ms +loss 0.08% +wait 1s +rate 173kbit +delay 50ms +loss 0.08% +wait 1s +rate 98kbit +delay 50ms +loss 0.08% +wait 1s +rate 336kbit +delay 50ms +loss 0.08% +wait 1s +rate 563kbit +delay 50ms +loss 0.08% +wait 1s \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/NYUbus_x0.25 b/repos/moq-rs/tc_profiles/NYUbus_x0.25 new file mode 100644 index 0000000..3b2fcd3 --- /dev/null +++ b/repos/moq-rs/tc_profiles/NYUbus_x0.25 @@ -0,0 +1,2400 @@ +rate 992kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1107kbit +delay 50ms +loss 0.0008 +wait 1s +rate 910kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1207kbit +delay 50ms +loss 0.0008 +wait 1s +rate 957kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1115kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1020kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1237kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1020kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1572kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2120kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1010kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1537kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1240kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1417kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2042kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1510kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1772kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1527kbit +delay 50ms +loss 0.0008 +wait 1s +rate 840kbit +delay 50ms +loss 0.0008 +wait 1s +rate 665kbit +delay 50ms +loss 0.0008 +wait 1s +rate 632kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2900kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3700kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4025kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1472kbit +delay 50ms +loss 0.0008 +wait 1s +rate 990kbit +delay 50ms +loss 0.0008 +wait 1s +rate 532kbit +delay 50ms +loss 0.0008 +wait 1s +rate 632kbit +delay 50ms +loss 0.0008 +wait 1s +rate 685kbit +delay 50ms +loss 0.0008 +wait 1s +rate 670kbit +delay 50ms +loss 0.0008 +wait 1s +rate 750kbit +delay 50ms +loss 0.0008 +wait 1s +rate 822kbit +delay 50ms +loss 0.0008 +wait 1s +rate 940kbit +delay 50ms +loss 0.0008 +wait 1s +rate 935kbit +delay 50ms +loss 0.0008 +wait 1s +rate 962kbit +delay 50ms +loss 0.0008 +wait 1s +rate 922kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1192kbit +delay 50ms +loss 0.0008 +wait 1s +rate 687kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2017kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1375kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1260kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1155kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1685kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1010kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1095kbit +delay 50ms +loss 0.0008 +wait 1s +rate 940kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1302kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1015kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1295kbit +delay 50ms +loss 0.0008 +wait 1s +rate 945kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1990kbit +delay 50ms +loss 0.0008 +wait 1s +rate 720kbit +delay 50ms +loss 0.0008 +wait 1s +rate 957kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1245kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1375kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1782kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1755kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1662kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1572kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1480kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2202kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2105kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2460kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2315kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1257kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1312kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1185kbit +delay 50ms +loss 0.0008 +wait 1s +rate 670kbit +delay 50ms +loss 0.0008 +wait 1s +rate 587kbit +delay 50ms +loss 0.0008 +wait 1s +rate 932kbit +delay 50ms +loss 0.0008 +wait 1s +rate 990kbit +delay 50ms +loss 0.0008 +wait 1s +rate 687kbit +delay 50ms +loss 0.0008 +wait 1s +rate 720kbit +delay 50ms +loss 0.0008 +wait 1s +rate 717kbit +delay 50ms +loss 0.0008 +wait 1s +rate 857kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1280kbit +delay 50ms +loss 0.0008 +wait 1s +rate 950kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1102kbit +delay 50ms +loss 0.0008 +wait 1s +rate 962kbit +delay 50ms +loss 0.0008 +wait 1s +rate 995kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1080kbit +delay 50ms +loss 0.0008 +wait 1s +rate 775kbit +delay 50ms +loss 0.0008 +wait 1s +rate 907kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1102kbit +delay 50ms +loss 0.0008 +wait 1s +rate 802kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1255kbit +delay 50ms +loss 0.0008 +wait 1s +rate 645kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1215kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1035kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1157kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1632kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1380kbit +delay 50ms +loss 0.0008 +wait 1s +rate 957kbit +delay 50ms +loss 0.0008 +wait 1s +rate 610kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1222kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1450kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1557kbit +delay 50ms +loss 0.0008 +wait 1s +rate 767kbit +delay 50ms +loss 0.0008 +wait 1s +rate 682kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1027kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2725kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3825kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4150kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1972kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2850kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3200kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3325kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2465kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3550kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2825kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2775kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2600kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1520kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2245kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3450kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4100kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3175kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3050kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3100kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2850kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3200kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2675kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2237kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1992kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2352kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2395kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2305kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2900kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2700kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2800kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2082kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1610kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1540kbit +delay 50ms +loss 0.0008 +wait 1s +rate 857kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1410kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1705kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2210kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2190kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2122kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1757kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1257kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1445kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1522kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1500kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1417kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1942kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1797kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1402kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1370kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1375kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1667kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2030kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2160kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2800kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2352kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2800kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3925kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3250kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3875kbit +delay 50ms +loss 0.0008 +wait 1s +rate 5575kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4075kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4675kbit +delay 50ms +loss 0.0008 +wait 1s +rate 5800kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3700kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3625kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4125kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4225kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4100kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3075kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3550kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2825kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1777kbit +delay 50ms +loss 0.0008 +wait 1s +rate 385kbit +delay 50ms +loss 0.0008 +wait 1s +rate 562kbit +delay 50ms +loss 0.0008 +wait 1s +rate 762kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1030kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1400kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1525kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1720kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2800kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3975kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3225kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4975kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2450kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3025kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2380kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3125kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3300kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2362kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2600kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1682kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2525kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2102kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2875kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2037kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1915kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2900kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2700kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2650kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3400kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4675kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2825kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3875kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3400kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3875kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2675kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1875kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1272kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1287kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1117kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1305kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1472kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1570kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1625kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1355kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1517kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1737kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1862kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1755kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2407kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2725kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2825kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3075kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2850kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3650kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3850kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3825kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3725kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3300kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3600kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3950kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4075kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3525kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3050kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3225kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3725kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3600kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4375kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3075kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3175kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3325kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3275kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3050kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3100kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3400kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4000kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2012kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2625kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3150kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2725kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3250kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2625kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3700kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2800kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2625kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2312kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3775kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4775kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4000kbit +delay 50ms +loss 0.0008 +wait 1s +rate 5550kbit +delay 50ms +loss 0.0008 +wait 1s +rate 5175kbit +delay 50ms +loss 0.0008 +wait 1s +rate 5500kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4725kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4175kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4250kbit +delay 50ms +loss 0.0008 +wait 1s +rate 5050kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3550kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4750kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4200kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4800kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4100kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3950kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4750kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4675kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3950kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4250kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4125kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3250kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3525kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3225kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3225kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3400kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2975kbit +delay 50ms +loss 0.0008 +wait 1s +rate 4150kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3850kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2600kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3150kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2350kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2775kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3025kbit +delay 50ms +loss 0.0008 +wait 1s +rate 3550kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2650kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2775kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2575kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2272kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1295kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1685kbit +delay 50ms +loss 0.0008 +wait 1s +rate 2225kbit +delay 50ms +loss 0.0008 +wait 1s +rate 872kbit +delay 50ms +loss 0.0008 +wait 1s +rate 735kbit +delay 50ms +loss 0.08% +wait 1s +rate 1912kbit +delay 50ms +loss 0.08% +wait 1s +rate 1582kbit +delay 50ms +loss 0.08% +wait 1s +rate 2355kbit +delay 50ms +loss 0.08% +wait 1s +rate 2500kbit +delay 50ms +loss 0.08% +wait 1s +rate 2600kbit +delay 50ms +loss 0.08% +wait 1s +rate 2975kbit +delay 50ms +loss 0.08% +wait 1s +rate 3000kbit +delay 50ms +loss 0.08% +wait 1s +rate 3750kbit +delay 50ms +loss 0.08% +wait 1s +rate 3750kbit +delay 50ms +loss 0.08% +wait 1s +rate 3900kbit +delay 50ms +loss 0.08% +wait 1s +rate 3275kbit +delay 50ms +loss 0.08% +wait 1s +rate 2950kbit +delay 50ms +loss 0.08% +wait 1s +rate 3375kbit +delay 50ms +loss 0.08% +wait 1s +rate 3375kbit +delay 50ms +loss 0.08% +wait 1s +rate 4125kbit +delay 50ms +loss 0.08% +wait 1s +rate 4600kbit +delay 50ms +loss 0.08% +wait 1s +rate 4425kbit +delay 50ms +loss 0.08% +wait 1s +rate 3825kbit +delay 50ms +loss 0.08% +wait 1s +rate 4125kbit +delay 50ms +loss 0.08% +wait 1s +rate 3425kbit +delay 50ms +loss 0.08% +wait 1s +rate 3225kbit +delay 50ms +loss 0.08% +wait 1s +rate 4400kbit +delay 50ms +loss 0.08% +wait 1s +rate 3700kbit +delay 50ms +loss 0.08% +wait 1s +rate 4150kbit +delay 50ms +loss 0.08% +wait 1s +rate 3475kbit +delay 50ms +loss 0.08% +wait 1s +rate 4025kbit +delay 50ms +loss 0.08% +wait 1s +rate 3700kbit +delay 50ms +loss 0.08% +wait 1s +rate 5450kbit +delay 50ms +loss 0.08% +wait 1s +rate 4725kbit +delay 50ms +loss 0.08% +wait 1s +rate 3900kbit +delay 50ms +loss 0.08% +wait 1s +rate 3875kbit +delay 50ms +loss 0.08% +wait 1s +rate 3950kbit +delay 50ms +loss 0.08% +wait 1s +rate 3400kbit +delay 50ms +loss 0.08% +wait 1s +rate 4150kbit +delay 50ms +loss 0.08% +wait 1s +rate 5100kbit +delay 50ms +loss 0.08% +wait 1s +rate 4375kbit +delay 50ms +loss 0.08% +wait 1s +rate 5025kbit +delay 50ms +loss 0.08% +wait 1s +rate 5200kbit +delay 50ms +loss 0.08% +wait 1s +rate 5475kbit +delay 50ms +loss 0.08% +wait 1s +rate 5500kbit +delay 50ms +loss 0.08% +wait 1s +rate 4525kbit +delay 50ms +loss 0.08% +wait 1s +rate 5450kbit +delay 50ms +loss 0.08% +wait 1s +rate 4875kbit +delay 50ms +loss 0.08% +wait 1s +rate 4825kbit +delay 50ms +loss 0.08% +wait 1s +rate 4500kbit +delay 50ms +loss 0.08% +wait 1s +rate 4825kbit +delay 50ms +loss 0.08% +wait 1s +rate 4350kbit +delay 50ms +loss 0.08% +wait 1s +rate 5325kbit +delay 50ms +loss 0.08% +wait 1s +rate 4575kbit +delay 50ms +loss 0.08% +wait 1s +rate 5125kbit +delay 50ms +loss 0.08% +wait 1s +rate 4375kbit +delay 50ms +loss 0.08% +wait 1s +rate 4400kbit +delay 50ms +loss 0.08% +wait 1s +rate 4925kbit +delay 50ms +loss 0.08% +wait 1s +rate 5350kbit +delay 50ms +loss 0.08% +wait 1s +rate 5100kbit +delay 50ms +loss 0.08% +wait 1s +rate 4600kbit +delay 50ms +loss 0.08% +wait 1s +rate 4550kbit +delay 50ms +loss 0.08% +wait 1s +rate 5625kbit +delay 50ms +loss 0.08% +wait 1s +rate 4600kbit +delay 50ms +loss 0.08% +wait 1s +rate 5400kbit +delay 50ms +loss 0.08% +wait 1s +rate 5150kbit +delay 50ms +loss 0.08% +wait 1s +rate 4825kbit +delay 50ms +loss 0.08% +wait 1s +rate 5800kbit +delay 50ms +loss 0.08% +wait 1s +rate 4325kbit +delay 50ms +loss 0.08% +wait 1s +rate 4750kbit +delay 50ms +loss 0.08% +wait 1s +rate 4800kbit +delay 50ms +loss 0.08% +wait 1s +rate 3975kbit +delay 50ms +loss 0.08% +wait 1s +rate 3825kbit +delay 50ms +loss 0.08% +wait 1s +rate 3775kbit +delay 50ms +loss 0.08% +wait 1s +rate 4350kbit +delay 50ms +loss 0.08% +wait 1s +rate 3800kbit +delay 50ms +loss 0.08% +wait 1s +rate 4025kbit +delay 50ms +loss 0.08% +wait 1s +rate 4175kbit +delay 50ms +loss 0.08% +wait 1s +rate 4650kbit +delay 50ms +loss 0.08% +wait 1s +rate 3750kbit +delay 50ms +loss 0.08% +wait 1s +rate 3775kbit +delay 50ms +loss 0.08% +wait 1s +rate 3300kbit +delay 50ms +loss 0.08% +wait 1s +rate 3650kbit +delay 50ms +loss 0.08% +wait 1s +rate 3600kbit +delay 50ms +loss 0.08% +wait 1s +rate 4000kbit +delay 50ms +loss 0.08% +wait 1s +rate 5225kbit +delay 50ms +loss 0.08% +wait 1s +rate 3925kbit +delay 50ms +loss 0.08% +wait 1s +rate 4000kbit +delay 50ms +loss 0.08% +wait 1s +rate 2975kbit +delay 50ms +loss 0.08% +wait 1s +rate 3275kbit +delay 50ms +loss 0.08% +wait 1s +rate 3800kbit +delay 50ms +loss 0.08% +wait 1s +rate 2950kbit +delay 50ms +loss 0.08% +wait 1s +rate 3000kbit +delay 50ms +loss 0.08% +wait 1s +rate 1427kbit +delay 50ms +loss 0.08% +wait 1s +rate 2095kbit +delay 50ms +loss 0.08% +wait 1s +rate 972kbit +delay 50ms +loss 0.08% +wait 1s +rate 860kbit +delay 50ms +loss 0.08% +wait 1s +rate 1212kbit +delay 50ms +loss 0.08% +wait 1s +rate 1440kbit +delay 50ms +loss 0.08% +wait 1s +rate 1555kbit +delay 50ms +loss 0.08% +wait 1s +rate 1532kbit +delay 50ms +loss 0.08% +wait 1s +rate 1190kbit +delay 50ms +loss 0.08% +wait 1s +rate 1197kbit +delay 50ms +loss 0.08% +wait 1s +rate 1715kbit +delay 50ms +loss 0.08% +wait 1s +rate 1375kbit +delay 50ms +loss 0.08% +wait 1s +rate 2330kbit +delay 50ms +loss 0.08% +wait 1s +rate 1952kbit +delay 50ms +loss 0.08% +wait 1s +rate 1695kbit +delay 50ms +loss 0.08% +wait 1s +rate 1915kbit +delay 50ms +loss 0.08% +wait 1s +rate 2082kbit +delay 50ms +loss 0.08% +wait 1s +rate 1667kbit +delay 50ms +loss 0.08% +wait 1s +rate 1585kbit +delay 50ms +loss 0.08% +wait 1s +rate 1635kbit +delay 50ms +loss 0.08% +wait 1s +rate 1762kbit +delay 50ms +loss 0.08% +wait 1s +rate 1547kbit +delay 50ms +loss 0.08% +wait 1s +rate 1700kbit +delay 50ms +loss 0.08% +wait 1s +rate 1250kbit +delay 50ms +loss 0.08% +wait 1s +rate 932kbit +delay 50ms +loss 0.08% +wait 1s +rate 882kbit +delay 50ms +loss 0.08% +wait 1s +rate 852kbit +delay 50ms +loss 0.08% +wait 1s +rate 1042kbit +delay 50ms +loss 0.08% +wait 1s +rate 1290kbit +delay 50ms +loss 0.08% +wait 1s +rate 872kbit +delay 50ms +loss 0.08% +wait 1s +rate 1452kbit +delay 50ms +loss 0.08% +wait 1s +rate 1517kbit +delay 50ms +loss 0.08% +wait 1s +rate 1055kbit +delay 50ms +loss 0.08% +wait 1s +rate 1332kbit +delay 50ms +loss 0.08% +wait 1s +rate 1215kbit +delay 50ms +loss 0.08% +wait 1s +rate 1075kbit +delay 50ms +loss 0.08% +wait 1s +rate 1702kbit +delay 50ms +loss 0.08% +wait 1s +rate 1547kbit +delay 50ms +loss 0.08% +wait 1s +rate 2220kbit +delay 50ms +loss 0.08% +wait 1s +rate 2260kbit +delay 50ms +loss 0.08% +wait 1s +rate 2115kbit +delay 50ms +loss 0.08% +wait 1s +rate 1787kbit +delay 50ms +loss 0.08% +wait 1s +rate 1705kbit +delay 50ms +loss 0.08% +wait 1s +rate 1782kbit +delay 50ms +loss 0.08% +wait 1s +rate 1702kbit +delay 50ms +loss 0.08% +wait 1s +rate 2205kbit +delay 50ms +loss 0.08% +wait 1s +rate 1872kbit +delay 50ms +loss 0.08% +wait 1s +rate 2195kbit +delay 50ms +loss 0.08% +wait 1s +rate 2052kbit +delay 50ms +loss 0.08% +wait 1s +rate 1810kbit +delay 50ms +loss 0.08% +wait 1s +rate 2147kbit +delay 50ms +loss 0.08% +wait 1s +rate 2015kbit +delay 50ms +loss 0.08% +wait 1s +rate 2160kbit +delay 50ms +loss 0.08% +wait 1s +rate 2800kbit +delay 50ms +loss 0.08% +wait 1s +rate 1565kbit +delay 50ms +loss 0.08% +wait 1s +rate 1405kbit +delay 50ms +loss 0.08% +wait 1s +rate 1725kbit +delay 50ms +loss 0.08% +wait 1s +rate 1787kbit +delay 50ms +loss 0.08% +wait 1s +rate 1735kbit +delay 50ms +loss 0.08% +wait 1s +rate 2245kbit +delay 50ms +loss 0.08% +wait 1s +rate 2292kbit +delay 50ms +loss 0.08% +wait 1s +rate 1825kbit +delay 50ms +loss 0.08% +wait 1s +rate 1782kbit +delay 50ms +loss 0.08% +wait 1s +rate 1440kbit +delay 50ms +loss 0.08% +wait 1s +rate 1880kbit +delay 50ms +loss 0.08% +wait 1s +rate 1657kbit +delay 50ms +loss 0.08% +wait 1s +rate 1322kbit +delay 50ms +loss 0.08% +wait 1s +rate 1007kbit +delay 50ms +loss 0.08% +wait 1s +rate 892kbit +delay 50ms +loss 0.08% +wait 1s +rate 1102kbit +delay 50ms +loss 0.08% +wait 1s +rate 1242kbit +delay 50ms +loss 0.08% +wait 1s +rate 1745kbit +delay 50ms +loss 0.08% +wait 1s +rate 1380kbit +delay 50ms +loss 0.08% +wait 1s +rate 1400kbit +delay 50ms +loss 0.08% +wait 1s +rate 1312kbit +delay 50ms +loss 0.08% +wait 1s +rate 1105kbit +delay 50ms +loss 0.08% +wait 1s +rate 1462kbit +delay 50ms +loss 0.08% +wait 1s +rate 1370kbit +delay 50ms +loss 0.08% +wait 1s +rate 1777kbit +delay 50ms +loss 0.08% +wait 1s +rate 1460kbit +delay 50ms +loss 0.08% +wait 1s +rate 1625kbit +delay 50ms +loss 0.08% +wait 1s +rate 1455kbit +delay 50ms +loss 0.08% +wait 1s +rate 1115kbit +delay 50ms +loss 0.08% +wait 1s +rate 990kbit +delay 50ms +loss 0.08% +wait 1s +rate 1112kbit +delay 50ms +loss 0.08% +wait 1s +rate 1125kbit +delay 50ms +loss 0.08% +wait 1s +rate 1627kbit +delay 50ms +loss 0.08% +wait 1s +rate 1445kbit +delay 50ms +loss 0.08% +wait 1s +rate 1757kbit +delay 50ms +loss 0.08% +wait 1s +rate 1620kbit +delay 50ms +loss 0.08% +wait 1s +rate 1782kbit +delay 50ms +loss 0.08% +wait 1s +rate 1962kbit +delay 50ms +loss 0.08% +wait 1s +rate 1810kbit +delay 50ms +loss 0.08% +wait 1s +rate 1725kbit +delay 50ms +loss 0.08% +wait 1s +rate 1680kbit +delay 50ms +loss 0.08% +wait 1s +rate 1855kbit +delay 50ms +loss 0.08% +wait 1s +rate 2267kbit +delay 50ms +loss 0.08% +wait 1s +rate 1670kbit +delay 50ms +loss 0.08% +wait 1s +rate 1877kbit +delay 50ms +loss 0.08% +wait 1s +rate 1912kbit +delay 50ms +loss 0.08% +wait 1s +rate 1650kbit +delay 50ms +loss 0.08% +wait 1s +rate 2440kbit +delay 50ms +loss 0.08% +wait 1s +rate 1637kbit +delay 50ms +loss 0.08% +wait 1s +rate 3100kbit +delay 50ms +loss 0.08% +wait 1s +rate 3750kbit +delay 50ms +loss 0.08% +wait 1s +rate 3500kbit +delay 50ms +loss 0.08% +wait 1s +rate 4950kbit +delay 50ms +loss 0.08% +wait 1s +rate 4650kbit +delay 50ms +loss 0.08% +wait 1s +rate 4275kbit +delay 50ms +loss 0.08% +wait 1s +rate 4750kbit +delay 50ms +loss 0.08% +wait 1s +rate 2700kbit +delay 50ms +loss 0.08% +wait 1s +rate 2850kbit +delay 50ms +loss 0.08% +wait 1s +rate 1540kbit +delay 50ms +loss 0.08% +wait 1s +rate 2700kbit +delay 50ms +loss 0.08% +wait 1s +rate 2150kbit +delay 50ms +loss 0.08% +wait 1s +rate 2257kbit +delay 50ms +loss 0.08% +wait 1s +rate 2452kbit +delay 50ms +loss 0.08% +wait 1s +rate 1907kbit +delay 50ms +loss 0.08% +wait 1s +rate 1217kbit +delay 50ms +loss 0.08% +wait 1s +rate 1185kbit +delay 50ms +loss 0.08% +wait 1s +rate 2045kbit +delay 50ms +loss 0.08% +wait 1s +rate 2017kbit +delay 50ms +loss 0.08% +wait 1s +rate 1235kbit +delay 50ms +loss 0.08% +wait 1s +rate 1705kbit +delay 50ms +loss 0.08% +wait 1s +rate 1965kbit +delay 50ms +loss 0.08% +wait 1s +rate 2150kbit +delay 50ms +loss 0.08% +wait 1s +rate 2345kbit +delay 50ms +loss 0.08% +wait 1s +rate 1335kbit +delay 50ms +loss 0.08% +wait 1s +rate 1255kbit +delay 50ms +loss 0.08% +wait 1s +rate 1995kbit +delay 50ms +loss 0.08% +wait 1s +rate 1512kbit +delay 50ms +loss 0.08% +wait 1s +rate 1555kbit +delay 50ms +loss 0.08% +wait 1s +rate 1965kbit +delay 50ms +loss 0.08% +wait 1s +rate 2375kbit +delay 50ms +loss 0.08% +wait 1s +rate 2382kbit +delay 50ms +loss 0.08% +wait 1s +rate 3175kbit +delay 50ms +loss 0.08% +wait 1s +rate 2800kbit +delay 50ms +loss 0.08% +wait 1s +rate 3275kbit +delay 50ms +loss 0.08% +wait 1s +rate 3050kbit +delay 50ms +loss 0.08% +wait 1s +rate 2750kbit +delay 50ms +loss 0.08% +wait 1s +rate 2525kbit +delay 50ms +loss 0.08% +wait 1s +rate 3750kbit +delay 50ms +loss 0.08% +wait 1s +rate 3350kbit +delay 50ms +loss 0.08% +wait 1s +rate 2650kbit +delay 50ms +loss 0.08% +wait 1s +rate 3100kbit +delay 50ms +loss 0.08% +wait 1s +rate 2437kbit +delay 50ms +loss 0.08% +wait 1s +rate 1995kbit +delay 50ms +loss 0.08% +wait 1s +rate 1697kbit +delay 50ms +loss 0.08% +wait 1s +rate 2020kbit +delay 50ms +loss 0.08% +wait 1s +rate 2217kbit +delay 50ms +loss 0.08% +wait 1s +rate 2087kbit +delay 50ms +loss 0.08% +wait 1s +rate 1727kbit +delay 50ms +loss 0.08% +wait 1s +rate 3025kbit +delay 50ms +loss 0.08% +wait 1s +rate 2475kbit +delay 50ms +loss 0.08% +wait 1s +rate 2070kbit +delay 50ms +loss 0.08% +wait 1s +rate 2550kbit +delay 50ms +loss 0.08% +wait 1s +rate 2272kbit +delay 50ms +loss 0.08% +wait 1s +rate 2135kbit +delay 50ms +loss 0.08% +wait 1s +rate 3400kbit +delay 50ms +loss 0.08% +wait 1s +rate 3150kbit +delay 50ms +loss 0.08% +wait 1s +rate 2160kbit +delay 50ms +loss 0.08% +wait 1s +rate 2107kbit +delay 50ms +loss 0.08% +wait 1s +rate 2335kbit +delay 50ms +loss 0.08% +wait 1s +rate 3000kbit +delay 50ms +loss 0.08% +wait 1s +rate 3375kbit +delay 50ms +loss 0.08% +wait 1s +rate 2340kbit +delay 50ms +loss 0.08% +wait 1s +rate 2140kbit +delay 50ms +loss 0.08% +wait 1s +rate 2550kbit +delay 50ms +loss 0.08% +wait 1s +rate 3825kbit +delay 50ms +loss 0.08% +wait 1s +rate 4225kbit +delay 50ms +loss 0.08% +wait 1s +rate 4325kbit +delay 50ms +loss 0.08% +wait 1s +rate 4100kbit +delay 50ms +loss 0.08% +wait 1s +rate 3475kbit +delay 50ms +loss 0.08% +wait 1s +rate 3450kbit +delay 50ms +loss 0.08% +wait 1s +rate 3425kbit +delay 50ms +loss 0.08% +wait 1s +rate 3675kbit +delay 50ms +loss 0.08% +wait 1s +rate 2700kbit +delay 50ms +loss 0.08% +wait 1s +rate 2675kbit +delay 50ms +loss 0.08% +wait 1s +rate 2040kbit +delay 50ms +loss 0.08% +wait 1s +rate 1922kbit +delay 50ms +loss 0.08% +wait 1s +rate 1887kbit +delay 50ms +loss 0.08% +wait 1s +rate 482kbit +delay 50ms +loss 0.08% +wait 1s +rate 862kbit +delay 50ms +loss 0.08% +wait 1s +rate 695kbit +delay 50ms +loss 0.08% +wait 1s +rate 510kbit +delay 50ms +loss 0.08% +wait 1s +rate 857kbit +delay 50ms +loss 0.08% +wait 1s +rate 197kbit +delay 50ms +loss 0.08% +wait 1s +rate 610kbit +delay 50ms +loss 0.08% +wait 1s +rate 315kbit +delay 50ms +loss 0.08% +wait 1s +rate 550kbit +delay 50ms +loss 0.08% +wait 1s +rate 162kbit +delay 50ms +loss 0.08% +wait 1s +rate 249kbit +delay 50ms +loss 0.08% +wait 1s +rate 552kbit +delay 50ms +loss 0.08% +wait 1s +rate 295kbit +delay 50ms +loss 0.08% +wait 1s +rate 270kbit +delay 50ms +loss 0.08% +wait 1s +rate 285kbit +delay 50ms +loss 0.08% +wait 1s +rate 335kbit +delay 50ms +loss 0.08% +wait 1s +rate 224kbit +delay 50ms +loss 0.08% +wait 1s +rate 10kbit +delay 50ms +loss 0.08% +wait 1s +rate 181kbit +delay 50ms +loss 0.08% +wait 1s +rate 19kbit +delay 50ms +loss 0.08% +wait 1s +rate 173kbit +delay 50ms +loss 0.08% +wait 1s +rate 32kbit +delay 50ms +loss 0.08% +wait 1s +rate 40kbit +delay 50ms +loss 0.08% +wait 1s +rate 8kbit +delay 50ms +loss 0.08% +wait 1s +rate 43kbit +delay 50ms +loss 0.08% +wait 1s +rate 35kbit +delay 50ms +loss 0.08% +wait 1s +rate 43kbit +delay 50ms +loss 0.08% +wait 1s +rate 24kbit +delay 50ms +loss 0.08% +wait 1s +rate 84kbit +delay 50ms +loss 0.08% +wait 1s +rate 140kbit +delay 50ms +loss 0.08% +wait 1s \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/Synthtic b/repos/moq-rs/tc_profiles/Synthtic new file mode 100644 index 0000000..e304e51 --- /dev/null +++ b/repos/moq-rs/tc_profiles/Synthtic @@ -0,0 +1,2400 @@ +rate 2240kbit +delay 50ms +loss 0.08% +wait 1s +rate 240kbit +delay 50ms +loss 0.08% +wait 1s +rate 170kbit +delay 50ms +loss 0.08% +wait 1s +rate 1480kbit +delay 50ms +loss 0.08% +wait 1s +rate 2590kbit +delay 50ms +loss 0.08% +wait 1s +rate 4310kbit +delay 50ms +loss 0.08% +wait 1s +rate 1000kbit +delay 50ms +loss 0.08% +wait 1s +rate 3990kbit +delay 50ms +loss 0.08% +wait 1s +rate 4240kbit +delay 50ms +loss 0.08% +wait 1s +rate 2030kbit +delay 50ms +loss 0.08% +wait 1s +rate 1930kbit +delay 50ms +loss 0.08% +wait 1s +rate 3080kbit +delay 50ms +loss 0.08% +wait 1s +rate 2020kbit +delay 50ms +loss 0.08% +wait 1s +rate 2200kbit +delay 50ms +loss 0.08% +wait 1s +rate 2730kbit +delay 50ms +loss 0.08% +wait 1s +rate 3150kbit +delay 50ms +loss 0.08% +wait 1s +rate 1900kbit +delay 50ms +loss 0.08% +wait 1s +rate 1350kbit +delay 50ms +loss 0.08% +wait 1s +rate 3740kbit +delay 50ms +loss 0.08% +wait 1s +rate 3250kbit +delay 50ms +loss 0.08% +wait 1s +rate 1430kbit +delay 50ms +loss 0.08% +wait 1s +rate 2590kbit +delay 50ms +loss 0.08% +wait 1s +rate 3560kbit +delay 50ms +loss 0.08% +wait 1s +rate 1850kbit +delay 50ms +loss 0.08% +wait 1s +rate 3230kbit +delay 50ms +loss 0.08% +wait 1s +rate 1000kbit +delay 50ms +loss 0.08% +wait 1s +rate 2590kbit +delay 50ms +loss 0.08% +wait 1s +rate 4910kbit +delay 50ms +loss 0.08% +wait 1s +rate 490kbit +delay 50ms +loss 0.08% +wait 1s +rate 1830kbit +delay 50ms +loss 0.08% +wait 1s +rate 1130kbit +delay 50ms +loss 0.08% +wait 1s +rate 510kbit +delay 50ms +loss 0.08% +wait 1s +rate 3400kbit +delay 50ms +loss 0.08% +wait 1s +rate 160kbit +delay 50ms +loss 0.08% +wait 1s +rate 750kbit +delay 50ms +loss 0.08% +wait 1s +rate 4710kbit +delay 50ms +loss 0.08% +wait 1s +rate 910kbit +delay 50ms +loss 0.08% +wait 1s +rate 2390kbit +delay 50ms +loss 0.08% +wait 1s +rate 460kbit +delay 50ms +loss 0.08% +wait 1s +rate 1100kbit +delay 50ms +loss 0.08% +wait 1s +rate 2300kbit +delay 50ms +loss 0.08% +wait 1s +rate 240kbit +delay 50ms +loss 0.08% +wait 1s +rate 1840kbit +delay 50ms +loss 0.08% +wait 1s +rate 4890kbit +delay 50ms +loss 0.08% +wait 1s +rate 3750kbit +delay 50ms +loss 0.08% +wait 1s +rate 1340kbit +delay 50ms +loss 0.08% +wait 1s +rate 3000kbit +delay 50ms +loss 0.08% +wait 1s +rate 4480kbit +delay 50ms +loss 0.08% +wait 1s +rate 1690kbit +delay 50ms +loss 0.08% +wait 1s +rate 3080kbit +delay 50ms +loss 0.08% +wait 1s +rate 1670kbit +delay 50ms +loss 0.08% +wait 1s +rate 850kbit +delay 50ms +loss 0.08% +wait 1s +rate 700kbit +delay 50ms +loss 0.08% +wait 1s +rate 970kbit +delay 50ms +loss 0.08% +wait 1s +rate 210kbit +delay 50ms +loss 0.08% +wait 1s +rate 1580kbit +delay 50ms +loss 0.08% +wait 1s +rate 360kbit +delay 50ms +loss 0.08% +wait 1s +rate 4730kbit +delay 50ms +loss 0.08% +wait 1s +rate 240kbit +delay 50ms +loss 0.08% +wait 1s +rate 5900kbit +delay 50ms +loss 0.08% +wait 1s +rate 830kbit +delay 50ms +loss 0.08% +wait 1s +rate 1500kbit +delay 50ms +loss 0.08% +wait 1s +rate 1880kbit +delay 50ms +loss 0.08% +wait 1s +rate 1540kbit +delay 50ms +loss 0.08% +wait 1s +rate 2520kbit +delay 50ms +loss 0.08% +wait 1s +rate 2420kbit +delay 50ms +loss 0.08% +wait 1s +rate 1800kbit +delay 50ms +loss 0.08% +wait 1s +rate 480kbit +delay 50ms +loss 0.08% +wait 1s +rate 3320kbit +delay 50ms +loss 0.08% +wait 1s +rate 3080kbit +delay 50ms +loss 0.08% +wait 1s +rate 70kbit +delay 50ms +loss 0.08% +wait 1s +rate 3760kbit +delay 50ms +loss 0.08% +wait 1s +rate 2880kbit +delay 50ms +loss 0.08% +wait 1s +rate 3190kbit +delay 50ms +loss 0.08% +wait 1s +rate 3610kbit +delay 50ms +loss 0.08% +wait 1s +rate 1950kbit +delay 50ms +loss 0.08% +wait 1s +rate 2690kbit +delay 50ms +loss 0.08% +wait 1s +rate 4490kbit +delay 50ms +loss 0.08% +wait 1s +rate 4410kbit +delay 50ms +loss 0.08% +wait 1s +rate 2670kbit +delay 50ms +loss 0.08% +wait 1s +rate 1960kbit +delay 50ms +loss 0.08% +wait 1s +rate 4050kbit +delay 50ms +loss 0.08% +wait 1s +rate 3220kbit +delay 50ms +loss 0.08% +wait 1s +rate 1650kbit +delay 50ms +loss 0.08% +wait 1s +rate 1360kbit +delay 50ms +loss 0.08% +wait 1s +rate 4840kbit +delay 50ms +loss 0.08% +wait 1s +rate 1120kbit +delay 50ms +loss 0.08% +wait 1s +rate 2070kbit +delay 50ms +loss 0.08% +wait 1s +rate 2200kbit +delay 50ms +loss 0.08% +wait 1s +rate 2300kbit +delay 50ms +loss 0.08% +wait 1s +rate 740kbit +delay 50ms +loss 0.08% +wait 1s +rate 2270kbit +delay 50ms +loss 0.08% +wait 1s +rate 2720kbit +delay 50ms +loss 0.08% +wait 1s +rate 5220kbit +delay 50ms +loss 0.08% +wait 1s +rate 210kbit +delay 50ms +loss 0.08% +wait 1s +rate 510kbit +delay 50ms +loss 0.08% +wait 1s +rate 3680kbit +delay 50ms +loss 0.08% +wait 1s +rate 290kbit +delay 50ms +loss 0.08% +wait 1s +rate 4460kbit +delay 50ms +loss 0.08% +wait 1s +rate 2760kbit +delay 50ms +loss 0.08% +wait 1s +rate 2510kbit +delay 50ms +loss 0.08% +wait 1s +rate 4190kbit +delay 50ms +loss 0.08% +wait 1s +rate 1130kbit +delay 50ms +loss 0.08% +wait 1s +rate 920kbit +delay 50ms +loss 0.08% +wait 1s +rate 5240kbit +delay 50ms +loss 0.08% +wait 1s +rate 4390kbit +delay 50ms +loss 0.08% +wait 1s +rate 1340kbit +delay 50ms +loss 0.08% +wait 1s +rate 3190kbit +delay 50ms +loss 0.08% +wait 1s +rate 190kbit +delay 50ms +loss 0.08% +wait 1s +rate 4270kbit +delay 50ms +loss 0.08% +wait 1s +rate 3900kbit +delay 50ms +loss 0.08% +wait 1s +rate 3080kbit +delay 50ms +loss 0.08% +wait 1s +rate 570kbit +delay 50ms +loss 0.08% +wait 1s +rate 2810kbit +delay 50ms +loss 0.08% +wait 1s +rate 2050kbit +delay 50ms +loss 0.08% +wait 1s +rate 1310kbit +delay 50ms +loss 0.08% +wait 1s +rate 3380kbit +delay 50ms +loss 0.08% +wait 1s +rate 1540kbit +delay 50ms +loss 0.08% +wait 1s +rate 2180kbit +delay 50ms +loss 0.08% +wait 1s +rate 3910kbit +delay 50ms +loss 0.08% +wait 1s +rate 110kbit +delay 50ms +loss 0.08% +wait 1s +rate 2830kbit +delay 50ms +loss 0.08% +wait 1s +rate 2510kbit +delay 50ms +loss 0.08% +wait 1s +rate 650kbit +delay 50ms +loss 0.08% +wait 1s +rate 740kbit +delay 50ms +loss 0.08% +wait 1s +rate 4210kbit +delay 50ms +loss 0.08% +wait 1s +rate 1000kbit +delay 50ms +loss 0.08% +wait 1s +rate 730kbit +delay 50ms +loss 0.08% +wait 1s +rate 2840kbit +delay 50ms +loss 0.08% +wait 1s +rate 3850kbit +delay 50ms +loss 0.08% +wait 1s +rate 2230kbit +delay 50ms +loss 0.08% +wait 1s +rate 4450kbit +delay 50ms +loss 0.08% +wait 1s +rate 620kbit +delay 50ms +loss 0.08% +wait 1s +rate 3370kbit +delay 50ms +loss 0.08% +wait 1s +rate 2040kbit +delay 50ms +loss 0.08% +wait 1s +rate 780kbit +delay 50ms +loss 0.08% +wait 1s +rate 3110kbit +delay 50ms +loss 0.08% +wait 1s +rate 50kbit +delay 50ms +loss 0.08% +wait 1s +rate 2570kbit +delay 50ms +loss 0.08% +wait 1s +rate 1620kbit +delay 50ms +loss 0.08% +wait 1s +rate 1420kbit +delay 50ms +loss 0.08% +wait 1s +rate 1810kbit +delay 50ms +loss 0.08% +wait 1s +rate 780kbit +delay 50ms +loss 0.08% +wait 1s +rate 4810kbit +delay 50ms +loss 0.08% +wait 1s +rate 3270kbit +delay 50ms +loss 0.08% +wait 1s +rate 1860kbit +delay 50ms +loss 0.08% +wait 1s +rate 4550kbit +delay 50ms +loss 0.08% +wait 1s +rate 4630kbit +delay 50ms +loss 0.08% +wait 1s +rate 5040kbit +delay 50ms +loss 0.08% +wait 1s +rate 2830kbit +delay 50ms +loss 0.08% +wait 1s +rate 790kbit +delay 50ms +loss 0.08% +wait 1s +rate 1040kbit +delay 50ms +loss 0.08% +wait 1s +rate 1100kbit +delay 50ms +loss 0.08% +wait 1s +rate 3110kbit +delay 50ms +loss 0.08% +wait 1s +rate 760kbit +delay 50ms +loss 0.08% +wait 1s +rate 2490kbit +delay 50ms +loss 0.08% +wait 1s +rate 3880kbit +delay 50ms +loss 0.08% +wait 1s +rate 2160kbit +delay 50ms +loss 0.08% +wait 1s +rate 2160kbit +delay 50ms +loss 0.08% +wait 1s +rate 1790kbit +delay 50ms +loss 0.08% +wait 1s +rate 2730kbit +delay 50ms +loss 0.08% +wait 1s +rate 1760kbit +delay 50ms +loss 0.08% +wait 1s +rate 1560kbit +delay 50ms +loss 0.08% +wait 1s +rate 4600kbit +delay 50ms +loss 0.08% +wait 1s +rate 3310kbit +delay 50ms +loss 0.08% +wait 1s +rate 1700kbit +delay 50ms +loss 0.08% +wait 1s +rate 260kbit +delay 50ms +loss 0.08% +wait 1s +rate 4450kbit +delay 50ms +loss 0.08% +wait 1s +rate 1840kbit +delay 50ms +loss 0.08% +wait 1s +rate 2480kbit +delay 50ms +loss 0.08% +wait 1s +rate 2330kbit +delay 50ms +loss 0.08% +wait 1s +rate 1010kbit +delay 50ms +loss 0.08% +wait 1s +rate 1140kbit +delay 50ms +loss 0.08% +wait 1s +rate 830kbit +delay 50ms +loss 0.08% +wait 1s +rate 4450kbit +delay 50ms +loss 0.08% +wait 1s +rate 3720kbit +delay 50ms +loss 0.08% +wait 1s +rate 1680kbit +delay 50ms +loss 0.08% +wait 1s +rate 4700kbit +delay 50ms +loss 0.08% +wait 1s +rate 730kbit +delay 50ms +loss 0.08% +wait 1s +rate 2710kbit +delay 50ms +loss 0.08% +wait 1s +rate 3240kbit +delay 50ms +loss 0.08% +wait 1s +rate 1810kbit +delay 50ms +loss 0.08% +wait 1s +rate 4040kbit +delay 50ms +loss 0.08% +wait 1s +rate 210kbit +delay 50ms +loss 0.08% +wait 1s +rate 4760kbit +delay 50ms +loss 0.08% +wait 1s +rate 3140kbit +delay 50ms +loss 0.08% +wait 1s +rate 920kbit +delay 50ms +loss 0.08% +wait 1s +rate 2940kbit +delay 50ms +loss 0.08% +wait 1s +rate 1030kbit +delay 50ms +loss 0.08% +wait 1s +rate 1050kbit +delay 50ms +loss 0.08% +wait 1s +rate 2550kbit +delay 50ms +loss 0.08% +wait 1s +rate 1210kbit +delay 50ms +loss 0.08% +wait 1s +rate 2490kbit +delay 50ms +loss 0.08% +wait 1s +rate 2420kbit +delay 50ms +loss 0.08% +wait 1s +rate 4020kbit +delay 50ms +loss 0.08% +wait 1s +rate 3590kbit +delay 50ms +loss 0.08% +wait 1s +rate 2570kbit +delay 50ms +loss 0.08% +wait 1s +rate 2490kbit +delay 50ms +loss 0.08% +wait 1s +rate 630kbit +delay 50ms +loss 0.08% +wait 1s +rate 2500kbit +delay 50ms +loss 0.08% +wait 1s +rate 4160kbit +delay 50ms +loss 0.08% +wait 1s +rate 4060kbit +delay 50ms +loss 0.08% +wait 1s +rate 3900kbit +delay 50ms +loss 0.08% +wait 1s +rate 2430kbit +delay 50ms +loss 0.08% +wait 1s +rate 4990kbit +delay 50ms +loss 0.08% +wait 1s +rate 3690kbit +delay 50ms +loss 0.08% +wait 1s +rate 2960kbit +delay 50ms +loss 0.08% +wait 1s +rate 2190kbit +delay 50ms +loss 0.08% +wait 1s +rate 1000kbit +delay 50ms +loss 0.08% +wait 1s +rate 1770kbit +delay 50ms +loss 0.08% +wait 1s +rate 640kbit +delay 50ms +loss 0.08% +wait 1s +rate 3900kbit +delay 50ms +loss 0.08% +wait 1s +rate 2700kbit +delay 50ms +loss 0.08% +wait 1s +rate 3460kbit +delay 50ms +loss 0.08% +wait 1s +rate 1170kbit +delay 50ms +loss 0.08% +wait 1s +rate 3640kbit +delay 50ms +loss 0.08% +wait 1s +rate 2300kbit +delay 50ms +loss 0.08% +wait 1s +rate 1790kbit +delay 50ms +loss 0.08% +wait 1s +rate 4680kbit +delay 50ms +loss 0.08% +wait 1s +rate 1880kbit +delay 50ms +loss 0.08% +wait 1s +rate 160kbit +delay 50ms +loss 0.08% +wait 1s +rate 4450kbit +delay 50ms +loss 0.08% +wait 1s +rate 310kbit +delay 50ms +loss 0.08% +wait 1s +rate 3650kbit +delay 50ms +loss 0.08% +wait 1s +rate 540kbit +delay 50ms +loss 0.08% +wait 1s +rate 520kbit +delay 50ms +loss 0.08% +wait 1s +rate 4630kbit +delay 50ms +loss 0.08% +wait 1s +rate 2570kbit +delay 50ms +loss 0.08% +wait 1s +rate 1790kbit +delay 50ms +loss 0.08% +wait 1s +rate 2580kbit +delay 50ms +loss 0.08% +wait 1s +rate 3650kbit +delay 50ms +loss 0.08% +wait 1s +rate 3850kbit +delay 50ms +loss 0.08% +wait 1s +rate 1250kbit +delay 50ms +loss 0.08% +wait 1s +rate 1090kbit +delay 50ms +loss 0.08% +wait 1s +rate 160kbit +delay 50ms +loss 0.08% +wait 1s +rate 3950kbit +delay 50ms +loss 0.08% +wait 1s +rate 1090kbit +delay 50ms +loss 0.08% +wait 1s +rate 2810kbit +delay 50ms +loss 0.08% +wait 1s +rate 270kbit +delay 50ms +loss 0.08% +wait 1s +rate 1550kbit +delay 50ms +loss 0.08% +wait 1s +rate 4340kbit +delay 50ms +loss 0.08% +wait 1s +rate 3310kbit +delay 50ms +loss 0.08% +wait 1s +rate 2320kbit +delay 50ms +loss 0.08% +wait 1s +rate 3470kbit +delay 50ms +loss 0.08% +wait 1s +rate 3360kbit +delay 50ms +loss 0.08% +wait 1s +rate 1210kbit +delay 50ms +loss 0.08% +wait 1s +rate 2310kbit +delay 50ms +loss 0.08% +wait 1s +rate 810kbit +delay 50ms +loss 0.08% +wait 1s +rate 2120kbit +delay 50ms +loss 0.08% +wait 1s +rate 4760kbit +delay 50ms +loss 0.08% +wait 1s +rate 3700kbit +delay 50ms +loss 0.08% +wait 1s +rate 3090kbit +delay 50ms +loss 0.08% +wait 1s +rate 2490kbit +delay 50ms +loss 0.08% +wait 1s +rate 1740kbit +delay 50ms +loss 0.08% +wait 1s +rate 2990kbit +delay 50ms +loss 0.08% +wait 1s +rate 1140kbit +delay 50ms +loss 0.08% +wait 1s +rate 2580kbit +delay 50ms +loss 0.08% +wait 1s +rate 2370kbit +delay 50ms +loss 0.08% +wait 1s +rate 1760kbit +delay 50ms +loss 0.08% +wait 1s +rate 3590kbit +delay 50ms +loss 0.08% +wait 1s +rate 4920kbit +delay 50ms +loss 0.08% +wait 1s +rate 410kbit +delay 50ms +loss 0.08% +wait 1s +rate 4280kbit +delay 50ms +loss 0.08% +wait 1s +rate 1000kbit +delay 50ms +loss 0.08% +wait 1s +rate 1980kbit +delay 50ms +loss 0.08% +wait 1s +rate 3350kbit +delay 50ms +loss 0.08% +wait 1s +rate 1350kbit +delay 50ms +loss 0.08% +wait 1s +rate 1580kbit +delay 50ms +loss 0.08% +wait 1s +rate 2880kbit +delay 50ms +loss 0.08% +wait 1s +rate 1690kbit +delay 50ms +loss 0.08% +wait 1s +rate 1650kbit +delay 50ms +loss 0.08% +wait 1s +rate 1520kbit +delay 50ms +loss 0.08% +wait 1s +rate 2400kbit +delay 50ms +loss 0.08% +wait 1s +rate 1890kbit +delay 50ms +loss 0.08% +wait 1s +rate 1650kbit +delay 50ms +loss 0.08% +wait 1s +rate 4340kbit +delay 50ms +loss 0.08% +wait 1s +rate 4990kbit +delay 50ms +loss 0.08% +wait 1s +rate 3710kbit +delay 50ms +loss 0.08% +wait 1s +rate 3590kbit +delay 50ms +loss 0.08% +wait 1s +rate 580kbit +delay 50ms +loss 0.08% +wait 1s +rate 1820kbit +delay 50ms +loss 0.08% +wait 1s +rate 1090kbit +delay 50ms +loss 0.08% +wait 1s +rate 330kbit +delay 50ms +loss 0.08% +wait 1s +rate 1850kbit +delay 50ms +loss 0.08% +wait 1s +rate 2700kbit +delay 50ms +loss 0.08% +wait 1s +rate 3560kbit +delay 50ms +loss 0.08% +wait 1s +rate 3510kbit +delay 50ms +loss 0.08% +wait 1s +rate 940kbit +delay 50ms +loss 0.08% +wait 1s +rate 920kbit +delay 50ms +loss 0.08% +wait 1s +rate 5310kbit +delay 50ms +loss 0.08% +wait 1s +rate 4650kbit +delay 50ms +loss 0.08% +wait 1s +rate 4250kbit +delay 50ms +loss 0.08% +wait 1s +rate 2590kbit +delay 50ms +loss 0.08% +wait 1s +rate 1290kbit +delay 50ms +loss 0.08% +wait 1s +rate 4480kbit +delay 50ms +loss 0.08% +wait 1s +rate 260kbit +delay 50ms +loss 0.08% +wait 1s +rate 370kbit +delay 50ms +loss 0.08% +wait 1s +rate 1280kbit +delay 50ms +loss 0.08% +wait 1s +rate 3930kbit +delay 50ms +loss 0.08% +wait 1s +rate 3730kbit +delay 50ms +loss 0.08% +wait 1s +rate 4670kbit +delay 50ms +loss 0.08% +wait 1s +rate 2810kbit +delay 50ms +loss 0.08% +wait 1s +rate 1920kbit +delay 50ms +loss 0.08% +wait 1s +rate 620kbit +delay 50ms +loss 0.08% +wait 1s +rate 1800kbit +delay 50ms +loss 0.08% +wait 1s +rate 2560kbit +delay 50ms +loss 0.08% +wait 1s +rate 990kbit +delay 50ms +loss 0.08% +wait 1s +rate 1620kbit +delay 50ms +loss 0.08% +wait 1s +rate 3660kbit +delay 50ms +loss 0.08% +wait 1s +rate 4050kbit +delay 50ms +loss 0.08% +wait 1s +rate 3090kbit +delay 50ms +loss 0.08% +wait 1s +rate 430kbit +delay 50ms +loss 0.08% +wait 1s +rate 2100kbit +delay 50ms +loss 0.08% +wait 1s +rate 2210kbit +delay 50ms +loss 0.08% +wait 1s +rate 2880kbit +delay 50ms +loss 0.08% +wait 1s +rate 1300kbit +delay 50ms +loss 0.08% +wait 1s +rate 380kbit +delay 50ms +loss 0.08% +wait 1s +rate 2650kbit +delay 50ms +loss 0.08% +wait 1s +rate 760kbit +delay 50ms +loss 0.08% +wait 1s +rate 1460kbit +delay 50ms +loss 0.08% +wait 1s +rate 3660kbit +delay 50ms +loss 0.08% +wait 1s +rate 2050kbit +delay 50ms +loss 0.08% +wait 1s +rate 4060kbit +delay 50ms +loss 0.08% +wait 1s +rate 3840kbit +delay 50ms +loss 0.08% +wait 1s +rate 2200kbit +delay 50ms +loss 0.08% +wait 1s +rate 1280kbit +delay 50ms +loss 0.08% +wait 1s +rate 3590kbit +delay 50ms +loss 0.08% +wait 1s +rate 3570kbit +delay 50ms +loss 0.08% +wait 1s +rate 2950kbit +delay 50ms +loss 0.08% +wait 1s +rate 710kbit +delay 50ms +loss 0.08% +wait 1s +rate 2560kbit +delay 50ms +loss 0.08% +wait 1s +rate 50kbit +delay 50ms +loss 0.08% +wait 1s +rate 2070kbit +delay 50ms +loss 0.08% +wait 1s +rate 5150kbit +delay 50ms +loss 0.08% +wait 1s +rate 600kbit +delay 50ms +loss 0.08% +wait 1s +rate 3120kbit +delay 50ms +loss 0.08% +wait 1s +rate 1150kbit +delay 50ms +loss 0.08% +wait 1s +rate 2740kbit +delay 50ms +loss 0.08% +wait 1s +rate 4600kbit +delay 50ms +loss 0.08% +wait 1s +rate 2290kbit +delay 50ms +loss 0.08% +wait 1s +rate 2980kbit +delay 50ms +loss 0.08% +wait 1s +rate 2710kbit +delay 50ms +loss 0.08% +wait 1s +rate 260kbit +delay 50ms +loss 0.08% +wait 1s +rate 2050kbit +delay 50ms +loss 0.08% +wait 1s +rate 610kbit +delay 50ms +loss 0.08% +wait 1s +rate 4140kbit +delay 50ms +loss 0.08% +wait 1s +rate 4250kbit +delay 50ms +loss 0.08% +wait 1s +rate 2790kbit +delay 50ms +loss 0.08% +wait 1s +rate 2560kbit +delay 50ms +loss 0.08% +wait 1s +rate 440kbit +delay 50ms +loss 0.08% +wait 1s +rate 1940kbit +delay 50ms +loss 0.08% +wait 1s +rate 30kbit +delay 50ms +loss 0.08% +wait 1s +rate 5140kbit +delay 50ms +loss 0.08% +wait 1s +rate 1610kbit +delay 50ms +loss 0.08% +wait 1s +rate 3510kbit +delay 50ms +loss 0.08% +wait 1s +rate 1960kbit +delay 50ms +loss 0.08% +wait 1s +rate 5030kbit +delay 50ms +loss 0.08% +wait 1s +rate 340kbit +delay 50ms +loss 0.08% +wait 1s +rate 3650kbit +delay 50ms +loss 0.08% +wait 1s +rate 2750kbit +delay 50ms +loss 0.08% +wait 1s +rate 2750kbit +delay 50ms +loss 0.08% +wait 1s +rate 1360kbit +delay 50ms +loss 0.08% +wait 1s +rate 2900kbit +delay 50ms +loss 0.08% +wait 1s +rate 3580kbit +delay 50ms +loss 0.08% +wait 1s +rate 1500kbit +delay 50ms +loss 0.08% +wait 1s +rate 4600kbit +delay 50ms +loss 0.08% +wait 1s +rate 490kbit +delay 50ms +loss 0.08% +wait 1s +rate 150kbit +delay 50ms +loss 0.08% +wait 1s +rate 1740kbit +delay 50ms +loss 0.08% +wait 1s +rate 250kbit +delay 50ms +loss 0.08% +wait 1s +rate 3140kbit +delay 50ms +loss 0.08% +wait 1s +rate 2650kbit +delay 50ms +loss 0.08% +wait 1s +rate 580kbit +delay 50ms +loss 0.08% +wait 1s +rate 4770kbit +delay 50ms +loss 0.08% +wait 1s +rate 4030kbit +delay 50ms +loss 0.08% +wait 1s +rate 3160kbit +delay 50ms +loss 0.08% +wait 1s +rate 2410kbit +delay 50ms +loss 0.08% +wait 1s +rate 3280kbit +delay 50ms +loss 0.08% +wait 1s +rate 2260kbit +delay 50ms +loss 0.08% +wait 1s +rate 530kbit +delay 50ms +loss 0.08% +wait 1s +rate 3120kbit +delay 50ms +loss 0.08% +wait 1s +rate 600kbit +delay 50ms +loss 0.08% +wait 1s +rate 370kbit +delay 50ms +loss 0.08% +wait 1s +rate 880kbit +delay 50ms +loss 0.08% +wait 1s +rate 4090kbit +delay 50ms +loss 0.08% +wait 1s +rate 3370kbit +delay 50ms +loss 0.08% +wait 1s +rate 1530kbit +delay 50ms +loss 0.08% +wait 1s +rate 4280kbit +delay 50ms +loss 0.08% +wait 1s +rate 1780kbit +delay 50ms +loss 0.08% +wait 1s +rate 320kbit +delay 50ms +loss 0.08% +wait 1s +rate 3280kbit +delay 50ms +loss 0.08% +wait 1s +rate 2270kbit +delay 50ms +loss 0.08% +wait 1s +rate 80kbit +delay 50ms +loss 0.08% +wait 1s +rate 440kbit +delay 50ms +loss 0.08% +wait 1s +rate 3490kbit +delay 50ms +loss 0.08% +wait 1s +rate 2580kbit +delay 50ms +loss 0.08% +wait 1s +rate 10kbit +delay 50ms +loss 0.08% +wait 1s +rate 390kbit +delay 50ms +loss 0.08% +wait 1s +rate 4620kbit +delay 50ms +loss 0.08% +wait 1s +rate 2940kbit +delay 50ms +loss 0.08% +wait 1s +rate 1500kbit +delay 50ms +loss 0.08% +wait 1s +rate 990kbit +delay 50ms +loss 0.08% +wait 1s +rate 1680kbit +delay 50ms +loss 0.08% +wait 1s +rate 3850kbit +delay 50ms +loss 0.08% +wait 1s +rate 2510kbit +delay 50ms +loss 0.08% +wait 1s +rate 2590kbit +delay 50ms +loss 0.08% +wait 1s +rate 4680kbit +delay 50ms +loss 0.08% +wait 1s +rate 2510kbit +delay 50ms +loss 0.08% +wait 1s +rate 1080kbit +delay 50ms +loss 0.08% +wait 1s +rate 530kbit +delay 50ms +loss 0.08% +wait 1s +rate 3120kbit +delay 50ms +loss 0.08% +wait 1s +rate 4890kbit +delay 50ms +loss 0.08% +wait 1s +rate 2610kbit +delay 50ms +loss 0.08% +wait 1s +rate 2660kbit +delay 50ms +loss 0.08% +wait 1s +rate 3100kbit +delay 50ms +loss 0.08% +wait 1s +rate 4580kbit +delay 50ms +loss 0.08% +wait 1s +rate 1280kbit +delay 50ms +loss 0.08% +wait 1s +rate 2770kbit +delay 50ms +loss 0.08% +wait 1s +rate 90kbit +delay 50ms +loss 0.08% +wait 1s +rate 2610kbit +delay 50ms +loss 0.08% +wait 1s +rate 1570kbit +delay 50ms +loss 0.08% +wait 1s +rate 3830kbit +delay 50ms +loss 0.08% +wait 1s +rate 4380kbit +delay 50ms +loss 0.08% +wait 1s +rate 4010kbit +delay 50ms +loss 0.08% +wait 1s +rate 3730kbit +delay 50ms +loss 0.08% +wait 1s +rate 260kbit +delay 50ms +loss 0.08% +wait 1s +rate 1340kbit +delay 50ms +loss 0.08% +wait 1s +rate 3150kbit +delay 50ms +loss 0.08% +wait 1s +rate 4770kbit +delay 50ms +loss 0.08% +wait 1s +rate 1820kbit +delay 50ms +loss 0.08% +wait 1s +rate 1040kbit +delay 50ms +loss 0.08% +wait 1s +rate 100kbit +delay 50ms +loss 0.08% +wait 1s +rate 3490kbit +delay 50ms +loss 0.08% +wait 1s +rate 2200kbit +delay 50ms +loss 0.08% +wait 1s +rate 3530kbit +delay 50ms +loss 0.08% +wait 1s +rate 2610kbit +delay 50ms +loss 0.08% +wait 1s +rate 110kbit +delay 50ms +loss 0.08% +wait 1s +rate 1910kbit +delay 50ms +loss 0.08% +wait 1s +rate 2240kbit +delay 50ms +loss 0.08% +wait 1s +rate 3630kbit +delay 50ms +loss 0.08% +wait 1s +rate 1370kbit +delay 50ms +loss 0.08% +wait 1s +rate 3490kbit +delay 50ms +loss 0.08% +wait 1s +rate 620kbit +delay 50ms +loss 0.08% +wait 1s +rate 1040kbit +delay 50ms +loss 0.08% +wait 1s +rate 1410kbit +delay 50ms +loss 0.08% +wait 1s +rate 320kbit +delay 50ms +loss 0.08% +wait 1s +rate 3390kbit +delay 50ms +loss 0.08% +wait 1s +rate 60kbit +delay 50ms +loss 0.08% +wait 1s +rate 2990kbit +delay 50ms +loss 0.08% +wait 1s +rate 3890kbit +delay 50ms +loss 0.08% +wait 1s +rate 1750kbit +delay 50ms +loss 0.08% +wait 1s +rate 2920kbit +delay 50ms +loss 0.08% +wait 1s +rate 780kbit +delay 50ms +loss 0.08% +wait 1s +rate 960kbit +delay 50ms +loss 0.08% +wait 1s +rate 2810kbit +delay 50ms +loss 0.08% +wait 1s +rate 1900kbit +delay 50ms +loss 0.08% +wait 1s +rate 1650kbit +delay 50ms +loss 0.08% +wait 1s +rate 2180kbit +delay 50ms +loss 0.08% +wait 1s +rate 2800kbit +delay 50ms +loss 0.08% +wait 1s +rate 2550kbit +delay 50ms +loss 0.08% +wait 1s +rate 1510kbit +delay 50ms +loss 0.08% +wait 1s +rate 3800kbit +delay 50ms +loss 0.08% +wait 1s +rate 2190kbit +delay 50ms +loss 0.08% +wait 1s +rate 2850kbit +delay 50ms +loss 0.08% +wait 1s +rate 1920kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 2010kbit +delay 50ms +loss 0.08% +wait 1s +rate 1210kbit +delay 50ms +loss 0.08% +wait 1s +rate 2960kbit +delay 50ms +loss 0.08% +wait 1s +rate 1880kbit +delay 50ms +loss 0.08% +wait 1s +rate 460kbit +delay 50ms +loss 0.08% +wait 1s +rate 4070kbit +delay 50ms +loss 0.08% +wait 1s +rate 4820kbit +delay 50ms +loss 0.08% +wait 1s +rate 50kbit +delay 50ms +loss 0.08% +wait 1s +rate 800kbit +delay 50ms +loss 0.08% +wait 1s +rate 2620kbit +delay 50ms +loss 0.08% +wait 1s +rate 330kbit +delay 50ms +loss 0.08% +wait 1s +rate 3090kbit +delay 50ms +loss 0.08% +wait 1s +rate 3080kbit +delay 50ms +loss 0.08% +wait 1s +rate 4140kbit +delay 50ms +loss 0.08% +wait 1s +rate 3870kbit +delay 50ms +loss 0.08% +wait 1s +rate 2990kbit +delay 50ms +loss 0.08% +wait 1s +rate 1760kbit +delay 50ms +loss 0.08% +wait 1s +rate 410kbit +delay 50ms +loss 0.08% +wait 1s +rate 3970kbit +delay 50ms +loss 0.08% +wait 1s +rate 3270kbit +delay 50ms +loss 0.08% +wait 1s +rate 2410kbit +delay 50ms +loss 0.08% +wait 1s +rate 1880kbit +delay 50ms +loss 0.08% +wait 1s +rate 2280kbit +delay 50ms +loss 0.08% +wait 1s +rate 2490kbit +delay 50ms +loss 0.08% +wait 1s +rate 4190kbit +delay 50ms +loss 0.08% +wait 1s +rate 4740kbit +delay 50ms +loss 0.08% +wait 1s +rate 1560kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 4290kbit +delay 50ms +loss 0.08% +wait 1s +rate 1510kbit +delay 50ms +loss 0.08% +wait 1s +rate 4480kbit +delay 50ms +loss 0.08% +wait 1s +rate 2570kbit +delay 50ms +loss 0.08% +wait 1s +rate 2630kbit +delay 50ms +loss 0.08% +wait 1s +rate 4400kbit +delay 50ms +loss 0.08% +wait 1s +rate 1840kbit +delay 50ms +loss 0.08% +wait 1s +rate 3640kbit +delay 50ms +loss 0.08% +wait 1s +rate 310kbit +delay 50ms +loss 0.08% +wait 1s +rate 3600kbit +delay 50ms +loss 0.08% +wait 1s +rate 960kbit +delay 50ms +loss 0.08% +wait 1s +rate 2610kbit +delay 50ms +loss 0.08% +wait 1s +rate 1990kbit +delay 50ms +loss 0.08% +wait 1s +rate 1920kbit +delay 50ms +loss 0.08% +wait 1s +rate 2270kbit +delay 50ms +loss 0.08% +wait 1s +rate 3980kbit +delay 50ms +loss 0.08% +wait 1s +rate 3650kbit +delay 50ms +loss 0.08% +wait 1s +rate 2660kbit +delay 50ms +loss 0.08% +wait 1s +rate 3660kbit +delay 50ms +loss 0.08% +wait 1s +rate 1780kbit +delay 50ms +loss 0.08% +wait 1s +rate 600kbit +delay 50ms +loss 0.08% +wait 1s +rate 1320kbit +delay 50ms +loss 0.08% +wait 1s +rate 1530kbit +delay 50ms +loss 0.08% +wait 1s +rate 10kbit +delay 50ms +loss 0.08% +wait 1s +rate 1010kbit +delay 50ms +loss 0.08% +wait 1s +rate 1760kbit +delay 50ms +loss 0.08% +wait 1s +rate 1050kbit +delay 50ms +loss 0.08% +wait 1s +rate 340kbit +delay 50ms +loss 0.08% +wait 1s +rate 3700kbit +delay 50ms +loss 0.08% +wait 1s +rate 820kbit +delay 50ms +loss 0.08% +wait 1s +rate 3750kbit +delay 50ms +loss 0.08% +wait 1s +rate 2340kbit +delay 50ms +loss 0.08% +wait 1s +rate 4730kbit +delay 50ms +loss 0.08% +wait 1s +rate 1870kbit +delay 50ms +loss 0.08% +wait 1s +rate 3890kbit +delay 50ms +loss 0.08% +wait 1s +rate 2790kbit +delay 50ms +loss 0.08% +wait 1s +rate 4360kbit +delay 50ms +loss 0.08% +wait 1s +rate 3610kbit +delay 50ms +loss 0.08% +wait 1s +rate 1290kbit +delay 50ms +loss 0.08% +wait 1s +rate 90kbit +delay 50ms +loss 0.08% +wait 1s +rate 4060kbit +delay 50ms +loss 0.08% +wait 1s +rate 4680kbit +delay 50ms +loss 0.08% +wait 1s +rate 670kbit +delay 50ms +loss 0.08% +wait 1s +rate 120kbit +delay 50ms +loss 0.08% +wait 1s +rate 230kbit +delay 50ms +loss 0.08% +wait 1s +rate 1400kbit +delay 50ms +loss 0.08% +wait 1s +rate 3650kbit +delay 50ms +loss 0.08% +wait 1s +rate 2370kbit +delay 50ms +loss 0.08% +wait 1s +rate 4410kbit +delay 50ms +loss 0.08% +wait 1s +rate 300kbit +delay 50ms +loss 0.08% +wait 1s +rate 1340kbit +delay 50ms +loss 0.08% +wait 1s +rate 2160kbit +delay 50ms +loss 0.08% +wait 1s +rate 570kbit +delay 50ms +loss 0.08% +wait 1s +rate 2420kbit +delay 50ms +loss 0.08% +wait 1s +rate 4300kbit +delay 50ms +loss 0.08% +wait 1s +rate 2660kbit +delay 50ms +loss 0.08% +wait 1s +rate 3510kbit +delay 50ms +loss 0.08% +wait 1s +rate 1560kbit +delay 50ms +loss 0.08% +wait 1s +rate 170kbit +delay 50ms +loss 0.08% +wait 1s +rate 1440kbit +delay 50ms +loss 0.08% +wait 1s +rate 950kbit +delay 50ms +loss 0.08% +wait 1s +rate 2400kbit +delay 50ms +loss 0.08% +wait 1s +rate 1800kbit +delay 50ms +loss 0.08% +wait 1s +rate 950kbit +delay 50ms +loss 0.08% +wait 1s +rate 410kbit +delay 50ms +loss 0.08% +wait 1s +rate 1000kbit +delay 50ms +loss 0.08% +wait 1s +rate 90kbit +delay 50ms +loss 0.08% +wait 1s +rate 4160kbit +delay 50ms +loss 0.08% +wait 1s +rate 3570kbit +delay 50ms +loss 0.08% +wait 1s +rate 3000kbit +delay 50ms +loss 0.08% +wait 1s +rate 560kbit +delay 50ms +loss 0.08% +wait 1s +rate 1960kbit +delay 50ms +loss 0.08% +wait 1s +rate 2840kbit +delay 50ms +loss 0.08% +wait 1s +rate 3550kbit +delay 50ms +loss 0.08% +wait 1s +rate 4290kbit +delay 50ms +loss 0.08% +wait 1s +rate 120kbit +delay 50ms +loss 0.08% +wait 1s +rate 340kbit +delay 50ms +loss 0.08% +wait 1s +rate 4420kbit +delay 50ms +loss 0.08% +wait 1s +rate 5040kbit +delay 50ms +loss 0.08% +wait 1s +rate 5130kbit +delay 50ms +loss 0.08% +wait 1s +rate 850kbit +delay 50ms +loss 0.08% +wait 1s +rate 1240kbit +delay 50ms +loss 0.08% +wait 1s +rate 3240kbit +delay 50ms +loss 0.08% +wait 1s +rate 410kbit +delay 50ms +loss 0.08% +wait 1s +rate 2780kbit +delay 50ms +loss 0.08% +wait 1s +rate 3290kbit +delay 50ms +loss 0.08% +wait 1s +rate 3180kbit +delay 50ms +loss 0.08% +wait 1s +rate 4450kbit +delay 50ms +loss 0.08% +wait 1s +rate 1020kbit +delay 50ms +loss 0.08% +wait 1s +rate 2820kbit +delay 50ms +loss 0.08% +wait 1s +rate 1750kbit +delay 50ms +loss 0.08% +wait 1s +rate 1170kbit +delay 50ms +loss 0.08% +wait 1s +rate 700kbit +delay 50ms +loss 0.08% +wait 1s +rate 2030kbit +delay 50ms +loss 0.08% +wait 1s +rate 3600kbit +delay 50ms +loss 0.08% +wait 1s +rate 800kbit +delay 50ms +loss 0.08% +wait 1s +rate 1810kbit +delay 50ms +loss 0.08% +wait 1s +rate 2070kbit +delay 50ms +loss 0.08% +wait 1s +rate 1690kbit +delay 50ms +loss 0.08% +wait 1s +rate 1100kbit +delay 50ms +loss 0.08% +wait 1s +rate 370kbit +delay 50ms +loss 0.08% +wait 1s +rate 400kbit +delay 50ms +loss 0.08% +wait 1s +rate 4480kbit +delay 50ms +loss 0.08% +wait 1s +rate 2250kbit +delay 50ms +loss 0.08% +wait 1s +rate 1260kbit +delay 50ms +loss 0.08% +wait 1s +rate 80kbit +delay 50ms +loss 0.08% +wait 1s \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/Synthtic_x0.25 b/repos/moq-rs/tc_profiles/Synthtic_x0.25 new file mode 100644 index 0000000..0bd01ff --- /dev/null +++ b/repos/moq-rs/tc_profiles/Synthtic_x0.25 @@ -0,0 +1,2400 @@ +rate 560kbit +delay 50ms +loss 0.0008 +wait 1s +rate 60kbit +delay 50ms +loss 0.0008 +wait 1s +rate 42kbit +delay 50ms +loss 0.0008 +wait 1s +rate 370kbit +delay 50ms +loss 0.0008 +wait 1s +rate 647kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1077kbit +delay 50ms +loss 0.0008 +wait 1s +rate 250kbit +delay 50ms +loss 0.0008 +wait 1s +rate 997kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1060kbit +delay 50ms +loss 0.0008 +wait 1s +rate 507kbit +delay 50ms +loss 0.0008 +wait 1s +rate 482kbit +delay 50ms +loss 0.0008 +wait 1s +rate 770kbit +delay 50ms +loss 0.0008 +wait 1s +rate 505kbit +delay 50ms +loss 0.0008 +wait 1s +rate 550kbit +delay 50ms +loss 0.0008 +wait 1s +rate 682kbit +delay 50ms +loss 0.0008 +wait 1s +rate 787kbit +delay 50ms +loss 0.0008 +wait 1s +rate 475kbit +delay 50ms +loss 0.0008 +wait 1s +rate 337kbit +delay 50ms +loss 0.0008 +wait 1s +rate 935kbit +delay 50ms +loss 0.0008 +wait 1s +rate 812kbit +delay 50ms +loss 0.0008 +wait 1s +rate 357kbit +delay 50ms +loss 0.0008 +wait 1s +rate 647kbit +delay 50ms +loss 0.0008 +wait 1s +rate 890kbit +delay 50ms +loss 0.0008 +wait 1s +rate 462kbit +delay 50ms +loss 0.0008 +wait 1s +rate 807kbit +delay 50ms +loss 0.0008 +wait 1s +rate 250kbit +delay 50ms +loss 0.0008 +wait 1s +rate 647kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1227kbit +delay 50ms +loss 0.0008 +wait 1s +rate 122kbit +delay 50ms +loss 0.0008 +wait 1s +rate 457kbit +delay 50ms +loss 0.0008 +wait 1s +rate 282kbit +delay 50ms +loss 0.0008 +wait 1s +rate 127kbit +delay 50ms +loss 0.0008 +wait 1s +rate 850kbit +delay 50ms +loss 0.0008 +wait 1s +rate 40kbit +delay 50ms +loss 0.0008 +wait 1s +rate 187kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1177kbit +delay 50ms +loss 0.0008 +wait 1s +rate 227kbit +delay 50ms +loss 0.0008 +wait 1s +rate 597kbit +delay 50ms +loss 0.0008 +wait 1s +rate 115kbit +delay 50ms +loss 0.0008 +wait 1s +rate 275kbit +delay 50ms +loss 0.0008 +wait 1s +rate 575kbit +delay 50ms +loss 0.0008 +wait 1s +rate 60kbit +delay 50ms +loss 0.0008 +wait 1s +rate 460kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1222kbit +delay 50ms +loss 0.0008 +wait 1s +rate 937kbit +delay 50ms +loss 0.0008 +wait 1s +rate 335kbit +delay 50ms +loss 0.0008 +wait 1s +rate 750kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1120kbit +delay 50ms +loss 0.0008 +wait 1s +rate 422kbit +delay 50ms +loss 0.0008 +wait 1s +rate 770kbit +delay 50ms +loss 0.0008 +wait 1s +rate 417kbit +delay 50ms +loss 0.0008 +wait 1s +rate 212kbit +delay 50ms +loss 0.0008 +wait 1s +rate 175kbit +delay 50ms +loss 0.0008 +wait 1s +rate 242kbit +delay 50ms +loss 0.0008 +wait 1s +rate 52kbit +delay 50ms +loss 0.0008 +wait 1s +rate 395kbit +delay 50ms +loss 0.0008 +wait 1s +rate 90kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1182kbit +delay 50ms +loss 0.0008 +wait 1s +rate 60kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1475kbit +delay 50ms +loss 0.0008 +wait 1s +rate 207kbit +delay 50ms +loss 0.0008 +wait 1s +rate 375kbit +delay 50ms +loss 0.0008 +wait 1s +rate 470kbit +delay 50ms +loss 0.0008 +wait 1s +rate 385kbit +delay 50ms +loss 0.0008 +wait 1s +rate 630kbit +delay 50ms +loss 0.0008 +wait 1s +rate 605kbit +delay 50ms +loss 0.0008 +wait 1s +rate 450kbit +delay 50ms +loss 0.0008 +wait 1s +rate 120kbit +delay 50ms +loss 0.0008 +wait 1s +rate 830kbit +delay 50ms +loss 0.0008 +wait 1s +rate 770kbit +delay 50ms +loss 0.0008 +wait 1s +rate 17kbit +delay 50ms +loss 0.0008 +wait 1s +rate 940kbit +delay 50ms +loss 0.0008 +wait 1s +rate 720kbit +delay 50ms +loss 0.0008 +wait 1s +rate 797kbit +delay 50ms +loss 0.0008 +wait 1s +rate 902kbit +delay 50ms +loss 0.0008 +wait 1s +rate 487kbit +delay 50ms +loss 0.0008 +wait 1s +rate 672kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1122kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1102kbit +delay 50ms +loss 0.0008 +wait 1s +rate 667kbit +delay 50ms +loss 0.0008 +wait 1s +rate 490kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1012kbit +delay 50ms +loss 0.0008 +wait 1s +rate 805kbit +delay 50ms +loss 0.0008 +wait 1s +rate 412kbit +delay 50ms +loss 0.0008 +wait 1s +rate 340kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1210kbit +delay 50ms +loss 0.0008 +wait 1s +rate 280kbit +delay 50ms +loss 0.0008 +wait 1s +rate 517kbit +delay 50ms +loss 0.0008 +wait 1s +rate 550kbit +delay 50ms +loss 0.0008 +wait 1s +rate 575kbit +delay 50ms +loss 0.0008 +wait 1s +rate 185kbit +delay 50ms +loss 0.0008 +wait 1s +rate 567kbit +delay 50ms +loss 0.0008 +wait 1s +rate 680kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1305kbit +delay 50ms +loss 0.0008 +wait 1s +rate 52kbit +delay 50ms +loss 0.0008 +wait 1s +rate 127kbit +delay 50ms +loss 0.0008 +wait 1s +rate 920kbit +delay 50ms +loss 0.0008 +wait 1s +rate 72kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1115kbit +delay 50ms +loss 0.0008 +wait 1s +rate 690kbit +delay 50ms +loss 0.0008 +wait 1s +rate 627kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1047kbit +delay 50ms +loss 0.0008 +wait 1s +rate 282kbit +delay 50ms +loss 0.0008 +wait 1s +rate 230kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1310kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1097kbit +delay 50ms +loss 0.0008 +wait 1s +rate 335kbit +delay 50ms +loss 0.0008 +wait 1s +rate 797kbit +delay 50ms +loss 0.0008 +wait 1s +rate 47kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1067kbit +delay 50ms +loss 0.0008 +wait 1s +rate 975kbit +delay 50ms +loss 0.0008 +wait 1s +rate 770kbit +delay 50ms +loss 0.0008 +wait 1s +rate 142kbit +delay 50ms +loss 0.0008 +wait 1s +rate 702kbit +delay 50ms +loss 0.0008 +wait 1s +rate 512kbit +delay 50ms +loss 0.0008 +wait 1s +rate 327kbit +delay 50ms +loss 0.0008 +wait 1s +rate 845kbit +delay 50ms +loss 0.0008 +wait 1s +rate 385kbit +delay 50ms +loss 0.0008 +wait 1s +rate 545kbit +delay 50ms +loss 0.0008 +wait 1s +rate 977kbit +delay 50ms +loss 0.0008 +wait 1s +rate 27kbit +delay 50ms +loss 0.0008 +wait 1s +rate 707kbit +delay 50ms +loss 0.0008 +wait 1s +rate 627kbit +delay 50ms +loss 0.0008 +wait 1s +rate 162kbit +delay 50ms +loss 0.0008 +wait 1s +rate 185kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1052kbit +delay 50ms +loss 0.0008 +wait 1s +rate 250kbit +delay 50ms +loss 0.0008 +wait 1s +rate 182kbit +delay 50ms +loss 0.0008 +wait 1s +rate 710kbit +delay 50ms +loss 0.0008 +wait 1s +rate 962kbit +delay 50ms +loss 0.0008 +wait 1s +rate 557kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1112kbit +delay 50ms +loss 0.0008 +wait 1s +rate 155kbit +delay 50ms +loss 0.0008 +wait 1s +rate 842kbit +delay 50ms +loss 0.0008 +wait 1s +rate 510kbit +delay 50ms +loss 0.0008 +wait 1s +rate 195kbit +delay 50ms +loss 0.0008 +wait 1s +rate 777kbit +delay 50ms +loss 0.0008 +wait 1s +rate 12kbit +delay 50ms +loss 0.0008 +wait 1s +rate 642kbit +delay 50ms +loss 0.0008 +wait 1s +rate 405kbit +delay 50ms +loss 0.0008 +wait 1s +rate 355kbit +delay 50ms +loss 0.0008 +wait 1s +rate 452kbit +delay 50ms +loss 0.0008 +wait 1s +rate 195kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1202kbit +delay 50ms +loss 0.0008 +wait 1s +rate 817kbit +delay 50ms +loss 0.0008 +wait 1s +rate 465kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1137kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1157kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1260kbit +delay 50ms +loss 0.0008 +wait 1s +rate 707kbit +delay 50ms +loss 0.0008 +wait 1s +rate 197kbit +delay 50ms +loss 0.0008 +wait 1s +rate 260kbit +delay 50ms +loss 0.0008 +wait 1s +rate 275kbit +delay 50ms +loss 0.0008 +wait 1s +rate 777kbit +delay 50ms +loss 0.0008 +wait 1s +rate 190kbit +delay 50ms +loss 0.0008 +wait 1s +rate 622kbit +delay 50ms +loss 0.0008 +wait 1s +rate 970kbit +delay 50ms +loss 0.0008 +wait 1s +rate 540kbit +delay 50ms +loss 0.0008 +wait 1s +rate 540kbit +delay 50ms +loss 0.0008 +wait 1s +rate 447kbit +delay 50ms +loss 0.0008 +wait 1s +rate 682kbit +delay 50ms +loss 0.0008 +wait 1s +rate 440kbit +delay 50ms +loss 0.0008 +wait 1s +rate 390kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1150kbit +delay 50ms +loss 0.0008 +wait 1s +rate 827kbit +delay 50ms +loss 0.0008 +wait 1s +rate 425kbit +delay 50ms +loss 0.0008 +wait 1s +rate 65kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1112kbit +delay 50ms +loss 0.0008 +wait 1s +rate 460kbit +delay 50ms +loss 0.0008 +wait 1s +rate 620kbit +delay 50ms +loss 0.0008 +wait 1s +rate 582kbit +delay 50ms +loss 0.0008 +wait 1s +rate 252kbit +delay 50ms +loss 0.0008 +wait 1s +rate 285kbit +delay 50ms +loss 0.0008 +wait 1s +rate 207kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1112kbit +delay 50ms +loss 0.0008 +wait 1s +rate 930kbit +delay 50ms +loss 0.0008 +wait 1s +rate 420kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1175kbit +delay 50ms +loss 0.0008 +wait 1s +rate 182kbit +delay 50ms +loss 0.0008 +wait 1s +rate 677kbit +delay 50ms +loss 0.0008 +wait 1s +rate 810kbit +delay 50ms +loss 0.0008 +wait 1s +rate 452kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1010kbit +delay 50ms +loss 0.0008 +wait 1s +rate 52kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1190kbit +delay 50ms +loss 0.0008 +wait 1s +rate 785kbit +delay 50ms +loss 0.0008 +wait 1s +rate 230kbit +delay 50ms +loss 0.0008 +wait 1s +rate 735kbit +delay 50ms +loss 0.0008 +wait 1s +rate 257kbit +delay 50ms +loss 0.0008 +wait 1s +rate 262kbit +delay 50ms +loss 0.0008 +wait 1s +rate 637kbit +delay 50ms +loss 0.0008 +wait 1s +rate 302kbit +delay 50ms +loss 0.0008 +wait 1s +rate 622kbit +delay 50ms +loss 0.0008 +wait 1s +rate 605kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1005kbit +delay 50ms +loss 0.0008 +wait 1s +rate 897kbit +delay 50ms +loss 0.0008 +wait 1s +rate 642kbit +delay 50ms +loss 0.0008 +wait 1s +rate 622kbit +delay 50ms +loss 0.0008 +wait 1s +rate 157kbit +delay 50ms +loss 0.0008 +wait 1s +rate 625kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1040kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1015kbit +delay 50ms +loss 0.0008 +wait 1s +rate 975kbit +delay 50ms +loss 0.0008 +wait 1s +rate 607kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1247kbit +delay 50ms +loss 0.0008 +wait 1s +rate 922kbit +delay 50ms +loss 0.0008 +wait 1s +rate 740kbit +delay 50ms +loss 0.0008 +wait 1s +rate 547kbit +delay 50ms +loss 0.0008 +wait 1s +rate 250kbit +delay 50ms +loss 0.0008 +wait 1s +rate 442kbit +delay 50ms +loss 0.0008 +wait 1s +rate 160kbit +delay 50ms +loss 0.0008 +wait 1s +rate 975kbit +delay 50ms +loss 0.0008 +wait 1s +rate 675kbit +delay 50ms +loss 0.0008 +wait 1s +rate 865kbit +delay 50ms +loss 0.0008 +wait 1s +rate 292kbit +delay 50ms +loss 0.0008 +wait 1s +rate 910kbit +delay 50ms +loss 0.0008 +wait 1s +rate 575kbit +delay 50ms +loss 0.0008 +wait 1s +rate 447kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1170kbit +delay 50ms +loss 0.0008 +wait 1s +rate 470kbit +delay 50ms +loss 0.0008 +wait 1s +rate 40kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1112kbit +delay 50ms +loss 0.0008 +wait 1s +rate 77kbit +delay 50ms +loss 0.0008 +wait 1s +rate 912kbit +delay 50ms +loss 0.0008 +wait 1s +rate 135kbit +delay 50ms +loss 0.0008 +wait 1s +rate 130kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1157kbit +delay 50ms +loss 0.0008 +wait 1s +rate 642kbit +delay 50ms +loss 0.0008 +wait 1s +rate 447kbit +delay 50ms +loss 0.0008 +wait 1s +rate 645kbit +delay 50ms +loss 0.0008 +wait 1s +rate 912kbit +delay 50ms +loss 0.0008 +wait 1s +rate 962kbit +delay 50ms +loss 0.0008 +wait 1s +rate 312kbit +delay 50ms +loss 0.0008 +wait 1s +rate 272kbit +delay 50ms +loss 0.0008 +wait 1s +rate 40kbit +delay 50ms +loss 0.0008 +wait 1s +rate 987kbit +delay 50ms +loss 0.0008 +wait 1s +rate 272kbit +delay 50ms +loss 0.0008 +wait 1s +rate 702kbit +delay 50ms +loss 0.0008 +wait 1s +rate 67kbit +delay 50ms +loss 0.0008 +wait 1s +rate 387kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1085kbit +delay 50ms +loss 0.0008 +wait 1s +rate 827kbit +delay 50ms +loss 0.0008 +wait 1s +rate 580kbit +delay 50ms +loss 0.0008 +wait 1s +rate 867kbit +delay 50ms +loss 0.0008 +wait 1s +rate 840kbit +delay 50ms +loss 0.0008 +wait 1s +rate 302kbit +delay 50ms +loss 0.0008 +wait 1s +rate 577kbit +delay 50ms +loss 0.0008 +wait 1s +rate 202kbit +delay 50ms +loss 0.0008 +wait 1s +rate 530kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1190kbit +delay 50ms +loss 0.0008 +wait 1s +rate 925kbit +delay 50ms +loss 0.0008 +wait 1s +rate 772kbit +delay 50ms +loss 0.0008 +wait 1s +rate 622kbit +delay 50ms +loss 0.0008 +wait 1s +rate 435kbit +delay 50ms +loss 0.0008 +wait 1s +rate 747kbit +delay 50ms +loss 0.0008 +wait 1s +rate 285kbit +delay 50ms +loss 0.0008 +wait 1s +rate 645kbit +delay 50ms +loss 0.0008 +wait 1s +rate 592kbit +delay 50ms +loss 0.0008 +wait 1s +rate 440kbit +delay 50ms +loss 0.0008 +wait 1s +rate 897kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1230kbit +delay 50ms +loss 0.0008 +wait 1s +rate 102kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1070kbit +delay 50ms +loss 0.0008 +wait 1s +rate 250kbit +delay 50ms +loss 0.0008 +wait 1s +rate 495kbit +delay 50ms +loss 0.0008 +wait 1s +rate 837kbit +delay 50ms +loss 0.0008 +wait 1s +rate 337kbit +delay 50ms +loss 0.0008 +wait 1s +rate 395kbit +delay 50ms +loss 0.0008 +wait 1s +rate 720kbit +delay 50ms +loss 0.0008 +wait 1s +rate 422kbit +delay 50ms +loss 0.0008 +wait 1s +rate 412kbit +delay 50ms +loss 0.0008 +wait 1s +rate 380kbit +delay 50ms +loss 0.0008 +wait 1s +rate 600kbit +delay 50ms +loss 0.0008 +wait 1s +rate 472kbit +delay 50ms +loss 0.0008 +wait 1s +rate 412kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1085kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1247kbit +delay 50ms +loss 0.0008 +wait 1s +rate 927kbit +delay 50ms +loss 0.0008 +wait 1s +rate 897kbit +delay 50ms +loss 0.0008 +wait 1s +rate 145kbit +delay 50ms +loss 0.0008 +wait 1s +rate 455kbit +delay 50ms +loss 0.0008 +wait 1s +rate 272kbit +delay 50ms +loss 0.0008 +wait 1s +rate 82kbit +delay 50ms +loss 0.0008 +wait 1s +rate 462kbit +delay 50ms +loss 0.0008 +wait 1s +rate 675kbit +delay 50ms +loss 0.0008 +wait 1s +rate 890kbit +delay 50ms +loss 0.0008 +wait 1s +rate 877kbit +delay 50ms +loss 0.0008 +wait 1s +rate 235kbit +delay 50ms +loss 0.0008 +wait 1s +rate 230kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1327kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1162kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1062kbit +delay 50ms +loss 0.0008 +wait 1s +rate 647kbit +delay 50ms +loss 0.0008 +wait 1s +rate 322kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1120kbit +delay 50ms +loss 0.0008 +wait 1s +rate 65kbit +delay 50ms +loss 0.0008 +wait 1s +rate 92kbit +delay 50ms +loss 0.0008 +wait 1s +rate 320kbit +delay 50ms +loss 0.0008 +wait 1s +rate 982kbit +delay 50ms +loss 0.0008 +wait 1s +rate 932kbit +delay 50ms +loss 0.0008 +wait 1s +rate 1167kbit +delay 50ms +loss 0.08% +wait 1s +rate 702kbit +delay 50ms +loss 0.08% +wait 1s +rate 480kbit +delay 50ms +loss 0.08% +wait 1s +rate 155kbit +delay 50ms +loss 0.08% +wait 1s +rate 450kbit +delay 50ms +loss 0.08% +wait 1s +rate 640kbit +delay 50ms +loss 0.08% +wait 1s +rate 247kbit +delay 50ms +loss 0.08% +wait 1s +rate 405kbit +delay 50ms +loss 0.08% +wait 1s +rate 915kbit +delay 50ms +loss 0.08% +wait 1s +rate 1012kbit +delay 50ms +loss 0.08% +wait 1s +rate 772kbit +delay 50ms +loss 0.08% +wait 1s +rate 107kbit +delay 50ms +loss 0.08% +wait 1s +rate 525kbit +delay 50ms +loss 0.08% +wait 1s +rate 552kbit +delay 50ms +loss 0.08% +wait 1s +rate 720kbit +delay 50ms +loss 0.08% +wait 1s +rate 325kbit +delay 50ms +loss 0.08% +wait 1s +rate 95kbit +delay 50ms +loss 0.08% +wait 1s +rate 662kbit +delay 50ms +loss 0.08% +wait 1s +rate 190kbit +delay 50ms +loss 0.08% +wait 1s +rate 365kbit +delay 50ms +loss 0.08% +wait 1s +rate 915kbit +delay 50ms +loss 0.08% +wait 1s +rate 512kbit +delay 50ms +loss 0.08% +wait 1s +rate 1015kbit +delay 50ms +loss 0.08% +wait 1s +rate 960kbit +delay 50ms +loss 0.08% +wait 1s +rate 550kbit +delay 50ms +loss 0.08% +wait 1s +rate 320kbit +delay 50ms +loss 0.08% +wait 1s +rate 897kbit +delay 50ms +loss 0.08% +wait 1s +rate 892kbit +delay 50ms +loss 0.08% +wait 1s +rate 737kbit +delay 50ms +loss 0.08% +wait 1s +rate 177kbit +delay 50ms +loss 0.08% +wait 1s +rate 640kbit +delay 50ms +loss 0.08% +wait 1s +rate 12kbit +delay 50ms +loss 0.08% +wait 1s +rate 517kbit +delay 50ms +loss 0.08% +wait 1s +rate 1287kbit +delay 50ms +loss 0.08% +wait 1s +rate 150kbit +delay 50ms +loss 0.08% +wait 1s +rate 780kbit +delay 50ms +loss 0.08% +wait 1s +rate 287kbit +delay 50ms +loss 0.08% +wait 1s +rate 685kbit +delay 50ms +loss 0.08% +wait 1s +rate 1150kbit +delay 50ms +loss 0.08% +wait 1s +rate 572kbit +delay 50ms +loss 0.08% +wait 1s +rate 745kbit +delay 50ms +loss 0.08% +wait 1s +rate 677kbit +delay 50ms +loss 0.08% +wait 1s +rate 65kbit +delay 50ms +loss 0.08% +wait 1s +rate 512kbit +delay 50ms +loss 0.08% +wait 1s +rate 152kbit +delay 50ms +loss 0.08% +wait 1s +rate 1035kbit +delay 50ms +loss 0.08% +wait 1s +rate 1062kbit +delay 50ms +loss 0.08% +wait 1s +rate 697kbit +delay 50ms +loss 0.08% +wait 1s +rate 640kbit +delay 50ms +loss 0.08% +wait 1s +rate 110kbit +delay 50ms +loss 0.08% +wait 1s +rate 485kbit +delay 50ms +loss 0.08% +wait 1s +rate 7kbit +delay 50ms +loss 0.08% +wait 1s +rate 1285kbit +delay 50ms +loss 0.08% +wait 1s +rate 402kbit +delay 50ms +loss 0.08% +wait 1s +rate 877kbit +delay 50ms +loss 0.08% +wait 1s +rate 490kbit +delay 50ms +loss 0.08% +wait 1s +rate 1257kbit +delay 50ms +loss 0.08% +wait 1s +rate 85kbit +delay 50ms +loss 0.08% +wait 1s +rate 912kbit +delay 50ms +loss 0.08% +wait 1s +rate 687kbit +delay 50ms +loss 0.08% +wait 1s +rate 687kbit +delay 50ms +loss 0.08% +wait 1s +rate 340kbit +delay 50ms +loss 0.08% +wait 1s +rate 725kbit +delay 50ms +loss 0.08% +wait 1s +rate 895kbit +delay 50ms +loss 0.08% +wait 1s +rate 375kbit +delay 50ms +loss 0.08% +wait 1s +rate 1150kbit +delay 50ms +loss 0.08% +wait 1s +rate 122kbit +delay 50ms +loss 0.08% +wait 1s +rate 37kbit +delay 50ms +loss 0.08% +wait 1s +rate 435kbit +delay 50ms +loss 0.08% +wait 1s +rate 62kbit +delay 50ms +loss 0.08% +wait 1s +rate 785kbit +delay 50ms +loss 0.08% +wait 1s +rate 662kbit +delay 50ms +loss 0.08% +wait 1s +rate 145kbit +delay 50ms +loss 0.08% +wait 1s +rate 1192kbit +delay 50ms +loss 0.08% +wait 1s +rate 1007kbit +delay 50ms +loss 0.08% +wait 1s +rate 790kbit +delay 50ms +loss 0.08% +wait 1s +rate 602kbit +delay 50ms +loss 0.08% +wait 1s +rate 820kbit +delay 50ms +loss 0.08% +wait 1s +rate 565kbit +delay 50ms +loss 0.08% +wait 1s +rate 132kbit +delay 50ms +loss 0.08% +wait 1s +rate 780kbit +delay 50ms +loss 0.08% +wait 1s +rate 150kbit +delay 50ms +loss 0.08% +wait 1s +rate 92kbit +delay 50ms +loss 0.08% +wait 1s +rate 220kbit +delay 50ms +loss 0.08% +wait 1s +rate 1022kbit +delay 50ms +loss 0.08% +wait 1s +rate 842kbit +delay 50ms +loss 0.08% +wait 1s +rate 382kbit +delay 50ms +loss 0.08% +wait 1s +rate 1070kbit +delay 50ms +loss 0.08% +wait 1s +rate 445kbit +delay 50ms +loss 0.08% +wait 1s +rate 80kbit +delay 50ms +loss 0.08% +wait 1s +rate 820kbit +delay 50ms +loss 0.08% +wait 1s +rate 567kbit +delay 50ms +loss 0.08% +wait 1s +rate 20kbit +delay 50ms +loss 0.08% +wait 1s +rate 110kbit +delay 50ms +loss 0.08% +wait 1s +rate 872kbit +delay 50ms +loss 0.08% +wait 1s +rate 645kbit +delay 50ms +loss 0.08% +wait 1s +rate 2kbit +delay 50ms +loss 0.08% +wait 1s +rate 97kbit +delay 50ms +loss 0.08% +wait 1s +rate 1155kbit +delay 50ms +loss 0.08% +wait 1s +rate 735kbit +delay 50ms +loss 0.08% +wait 1s +rate 375kbit +delay 50ms +loss 0.08% +wait 1s +rate 247kbit +delay 50ms +loss 0.08% +wait 1s +rate 420kbit +delay 50ms +loss 0.08% +wait 1s +rate 962kbit +delay 50ms +loss 0.08% +wait 1s +rate 627kbit +delay 50ms +loss 0.08% +wait 1s +rate 647kbit +delay 50ms +loss 0.08% +wait 1s +rate 1170kbit +delay 50ms +loss 0.08% +wait 1s +rate 627kbit +delay 50ms +loss 0.08% +wait 1s +rate 270kbit +delay 50ms +loss 0.08% +wait 1s +rate 132kbit +delay 50ms +loss 0.08% +wait 1s +rate 780kbit +delay 50ms +loss 0.08% +wait 1s +rate 1222kbit +delay 50ms +loss 0.08% +wait 1s +rate 652kbit +delay 50ms +loss 0.08% +wait 1s +rate 665kbit +delay 50ms +loss 0.08% +wait 1s +rate 775kbit +delay 50ms +loss 0.08% +wait 1s +rate 1145kbit +delay 50ms +loss 0.08% +wait 1s +rate 320kbit +delay 50ms +loss 0.08% +wait 1s +rate 692kbit +delay 50ms +loss 0.08% +wait 1s +rate 22kbit +delay 50ms +loss 0.08% +wait 1s +rate 652kbit +delay 50ms +loss 0.08% +wait 1s +rate 392kbit +delay 50ms +loss 0.08% +wait 1s +rate 957kbit +delay 50ms +loss 0.08% +wait 1s +rate 1095kbit +delay 50ms +loss 0.08% +wait 1s +rate 1002kbit +delay 50ms +loss 0.08% +wait 1s +rate 932kbit +delay 50ms +loss 0.08% +wait 1s +rate 65kbit +delay 50ms +loss 0.08% +wait 1s +rate 335kbit +delay 50ms +loss 0.08% +wait 1s +rate 787kbit +delay 50ms +loss 0.08% +wait 1s +rate 1192kbit +delay 50ms +loss 0.08% +wait 1s +rate 455kbit +delay 50ms +loss 0.08% +wait 1s +rate 260kbit +delay 50ms +loss 0.08% +wait 1s +rate 25kbit +delay 50ms +loss 0.08% +wait 1s +rate 872kbit +delay 50ms +loss 0.08% +wait 1s +rate 550kbit +delay 50ms +loss 0.08% +wait 1s +rate 882kbit +delay 50ms +loss 0.08% +wait 1s +rate 652kbit +delay 50ms +loss 0.08% +wait 1s +rate 27kbit +delay 50ms +loss 0.08% +wait 1s +rate 477kbit +delay 50ms +loss 0.08% +wait 1s +rate 560kbit +delay 50ms +loss 0.08% +wait 1s +rate 907kbit +delay 50ms +loss 0.08% +wait 1s +rate 342kbit +delay 50ms +loss 0.08% +wait 1s +rate 872kbit +delay 50ms +loss 0.08% +wait 1s +rate 155kbit +delay 50ms +loss 0.08% +wait 1s +rate 260kbit +delay 50ms +loss 0.08% +wait 1s +rate 352kbit +delay 50ms +loss 0.08% +wait 1s +rate 80kbit +delay 50ms +loss 0.08% +wait 1s +rate 847kbit +delay 50ms +loss 0.08% +wait 1s +rate 15kbit +delay 50ms +loss 0.08% +wait 1s +rate 747kbit +delay 50ms +loss 0.08% +wait 1s +rate 972kbit +delay 50ms +loss 0.08% +wait 1s +rate 437kbit +delay 50ms +loss 0.08% +wait 1s +rate 730kbit +delay 50ms +loss 0.08% +wait 1s +rate 195kbit +delay 50ms +loss 0.08% +wait 1s +rate 240kbit +delay 50ms +loss 0.08% +wait 1s +rate 702kbit +delay 50ms +loss 0.08% +wait 1s +rate 475kbit +delay 50ms +loss 0.08% +wait 1s +rate 412kbit +delay 50ms +loss 0.08% +wait 1s +rate 545kbit +delay 50ms +loss 0.08% +wait 1s +rate 700kbit +delay 50ms +loss 0.08% +wait 1s +rate 637kbit +delay 50ms +loss 0.08% +wait 1s +rate 377kbit +delay 50ms +loss 0.08% +wait 1s +rate 950kbit +delay 50ms +loss 0.08% +wait 1s +rate 547kbit +delay 50ms +loss 0.08% +wait 1s +rate 712kbit +delay 50ms +loss 0.08% +wait 1s +rate 480kbit +delay 50ms +loss 0.08% +wait 1s +rate 57kbit +delay 50ms +loss 0.08% +wait 1s +rate 502kbit +delay 50ms +loss 0.08% +wait 1s +rate 302kbit +delay 50ms +loss 0.08% +wait 1s +rate 740kbit +delay 50ms +loss 0.08% +wait 1s +rate 470kbit +delay 50ms +loss 0.08% +wait 1s +rate 115kbit +delay 50ms +loss 0.08% +wait 1s +rate 1017kbit +delay 50ms +loss 0.08% +wait 1s +rate 1205kbit +delay 50ms +loss 0.08% +wait 1s +rate 12kbit +delay 50ms +loss 0.08% +wait 1s +rate 200kbit +delay 50ms +loss 0.08% +wait 1s +rate 655kbit +delay 50ms +loss 0.08% +wait 1s +rate 82kbit +delay 50ms +loss 0.08% +wait 1s +rate 772kbit +delay 50ms +loss 0.08% +wait 1s +rate 770kbit +delay 50ms +loss 0.08% +wait 1s +rate 1035kbit +delay 50ms +loss 0.08% +wait 1s +rate 967kbit +delay 50ms +loss 0.08% +wait 1s +rate 747kbit +delay 50ms +loss 0.08% +wait 1s +rate 440kbit +delay 50ms +loss 0.08% +wait 1s +rate 102kbit +delay 50ms +loss 0.08% +wait 1s +rate 992kbit +delay 50ms +loss 0.08% +wait 1s +rate 817kbit +delay 50ms +loss 0.08% +wait 1s +rate 602kbit +delay 50ms +loss 0.08% +wait 1s +rate 470kbit +delay 50ms +loss 0.08% +wait 1s +rate 570kbit +delay 50ms +loss 0.08% +wait 1s +rate 622kbit +delay 50ms +loss 0.08% +wait 1s +rate 1047kbit +delay 50ms +loss 0.08% +wait 1s +rate 1185kbit +delay 50ms +loss 0.08% +wait 1s +rate 390kbit +delay 50ms +loss 0.08% +wait 1s +rate 57kbit +delay 50ms +loss 0.08% +wait 1s +rate 1072kbit +delay 50ms +loss 0.08% +wait 1s +rate 377kbit +delay 50ms +loss 0.08% +wait 1s +rate 1120kbit +delay 50ms +loss 0.08% +wait 1s +rate 642kbit +delay 50ms +loss 0.08% +wait 1s +rate 657kbit +delay 50ms +loss 0.08% +wait 1s +rate 1100kbit +delay 50ms +loss 0.08% +wait 1s +rate 460kbit +delay 50ms +loss 0.08% +wait 1s +rate 910kbit +delay 50ms +loss 0.08% +wait 1s +rate 77kbit +delay 50ms +loss 0.08% +wait 1s +rate 900kbit +delay 50ms +loss 0.08% +wait 1s +rate 240kbit +delay 50ms +loss 0.08% +wait 1s +rate 652kbit +delay 50ms +loss 0.08% +wait 1s +rate 497kbit +delay 50ms +loss 0.08% +wait 1s +rate 480kbit +delay 50ms +loss 0.08% +wait 1s +rate 567kbit +delay 50ms +loss 0.08% +wait 1s +rate 995kbit +delay 50ms +loss 0.08% +wait 1s +rate 912kbit +delay 50ms +loss 0.08% +wait 1s +rate 665kbit +delay 50ms +loss 0.08% +wait 1s +rate 915kbit +delay 50ms +loss 0.08% +wait 1s +rate 445kbit +delay 50ms +loss 0.08% +wait 1s +rate 150kbit +delay 50ms +loss 0.08% +wait 1s +rate 330kbit +delay 50ms +loss 0.08% +wait 1s +rate 382kbit +delay 50ms +loss 0.08% +wait 1s +rate 2kbit +delay 50ms +loss 0.08% +wait 1s +rate 252kbit +delay 50ms +loss 0.08% +wait 1s +rate 440kbit +delay 50ms +loss 0.08% +wait 1s +rate 262kbit +delay 50ms +loss 0.08% +wait 1s +rate 85kbit +delay 50ms +loss 0.08% +wait 1s +rate 925kbit +delay 50ms +loss 0.08% +wait 1s +rate 205kbit +delay 50ms +loss 0.08% +wait 1s +rate 937kbit +delay 50ms +loss 0.08% +wait 1s +rate 585kbit +delay 50ms +loss 0.08% +wait 1s +rate 1182kbit +delay 50ms +loss 0.08% +wait 1s +rate 467kbit +delay 50ms +loss 0.08% +wait 1s +rate 972kbit +delay 50ms +loss 0.08% +wait 1s +rate 697kbit +delay 50ms +loss 0.08% +wait 1s +rate 1090kbit +delay 50ms +loss 0.08% +wait 1s +rate 902kbit +delay 50ms +loss 0.08% +wait 1s +rate 322kbit +delay 50ms +loss 0.08% +wait 1s +rate 22kbit +delay 50ms +loss 0.08% +wait 1s +rate 1015kbit +delay 50ms +loss 0.08% +wait 1s +rate 1170kbit +delay 50ms +loss 0.08% +wait 1s +rate 167kbit +delay 50ms +loss 0.08% +wait 1s +rate 30kbit +delay 50ms +loss 0.08% +wait 1s +rate 57kbit +delay 50ms +loss 0.08% +wait 1s +rate 350kbit +delay 50ms +loss 0.08% +wait 1s +rate 912kbit +delay 50ms +loss 0.08% +wait 1s +rate 592kbit +delay 50ms +loss 0.08% +wait 1s +rate 1102kbit +delay 50ms +loss 0.08% +wait 1s +rate 75kbit +delay 50ms +loss 0.08% +wait 1s +rate 335kbit +delay 50ms +loss 0.08% +wait 1s +rate 540kbit +delay 50ms +loss 0.08% +wait 1s +rate 142kbit +delay 50ms +loss 0.08% +wait 1s +rate 605kbit +delay 50ms +loss 0.08% +wait 1s +rate 1075kbit +delay 50ms +loss 0.08% +wait 1s +rate 665kbit +delay 50ms +loss 0.08% +wait 1s +rate 877kbit +delay 50ms +loss 0.08% +wait 1s +rate 390kbit +delay 50ms +loss 0.08% +wait 1s +rate 42kbit +delay 50ms +loss 0.08% +wait 1s +rate 360kbit +delay 50ms +loss 0.08% +wait 1s +rate 237kbit +delay 50ms +loss 0.08% +wait 1s +rate 600kbit +delay 50ms +loss 0.08% +wait 1s +rate 450kbit +delay 50ms +loss 0.08% +wait 1s +rate 237kbit +delay 50ms +loss 0.08% +wait 1s +rate 102kbit +delay 50ms +loss 0.08% +wait 1s +rate 250kbit +delay 50ms +loss 0.08% +wait 1s +rate 22kbit +delay 50ms +loss 0.08% +wait 1s +rate 1040kbit +delay 50ms +loss 0.08% +wait 1s +rate 892kbit +delay 50ms +loss 0.08% +wait 1s +rate 750kbit +delay 50ms +loss 0.08% +wait 1s +rate 140kbit +delay 50ms +loss 0.08% +wait 1s +rate 490kbit +delay 50ms +loss 0.08% +wait 1s +rate 710kbit +delay 50ms +loss 0.08% +wait 1s +rate 887kbit +delay 50ms +loss 0.08% +wait 1s +rate 1072kbit +delay 50ms +loss 0.08% +wait 1s +rate 30kbit +delay 50ms +loss 0.08% +wait 1s +rate 85kbit +delay 50ms +loss 0.08% +wait 1s +rate 1105kbit +delay 50ms +loss 0.08% +wait 1s +rate 1260kbit +delay 50ms +loss 0.08% +wait 1s +rate 1282kbit +delay 50ms +loss 0.08% +wait 1s +rate 212kbit +delay 50ms +loss 0.08% +wait 1s +rate 310kbit +delay 50ms +loss 0.08% +wait 1s +rate 810kbit +delay 50ms +loss 0.08% +wait 1s +rate 102kbit +delay 50ms +loss 0.08% +wait 1s +rate 695kbit +delay 50ms +loss 0.08% +wait 1s +rate 822kbit +delay 50ms +loss 0.08% +wait 1s +rate 795kbit +delay 50ms +loss 0.08% +wait 1s +rate 1112kbit +delay 50ms +loss 0.08% +wait 1s +rate 255kbit +delay 50ms +loss 0.08% +wait 1s +rate 705kbit +delay 50ms +loss 0.08% +wait 1s +rate 437kbit +delay 50ms +loss 0.08% +wait 1s +rate 292kbit +delay 50ms +loss 0.08% +wait 1s +rate 175kbit +delay 50ms +loss 0.08% +wait 1s +rate 507kbit +delay 50ms +loss 0.08% +wait 1s +rate 900kbit +delay 50ms +loss 0.08% +wait 1s +rate 200kbit +delay 50ms +loss 0.08% +wait 1s +rate 452kbit +delay 50ms +loss 0.08% +wait 1s +rate 517kbit +delay 50ms +loss 0.08% +wait 1s +rate 422kbit +delay 50ms +loss 0.08% +wait 1s +rate 275kbit +delay 50ms +loss 0.08% +wait 1s +rate 92kbit +delay 50ms +loss 0.08% +wait 1s +rate 100kbit +delay 50ms +loss 0.08% +wait 1s +rate 1120kbit +delay 50ms +loss 0.08% +wait 1s +rate 562kbit +delay 50ms +loss 0.08% +wait 1s +rate 315kbit +delay 50ms +loss 0.08% +wait 1s +rate 20kbit +delay 50ms +loss 0.08% +wait 1s \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/bandwidth_scale.ods b/repos/moq-rs/tc_profiles/bandwidth_scale.ods new file mode 100644 index 0000000..796ec18 Binary files /dev/null and b/repos/moq-rs/tc_profiles/bandwidth_scale.ods differ diff --git a/repos/moq-rs/tc_profiles/cascade_profile b/repos/moq-rs/tc_profiles/cascade_profile new file mode 100644 index 0000000..9654649 --- /dev/null +++ b/repos/moq-rs/tc_profiles/cascade_profile @@ -0,0 +1,10 @@ +rate 1200kbit +wait 5s +rate 800kbit +wait 5s +rate 400kbit +wait 5s +rate 800kbit +wait 5s +rate 1200kbit +wait 5s diff --git a/repos/moq-rs/tc_profiles/cascade_profile_x0.05 b/repos/moq-rs/tc_profiles/cascade_profile_x0.05 new file mode 100644 index 0000000..905d960 --- /dev/null +++ b/repos/moq-rs/tc_profiles/cascade_profile_x0.05 @@ -0,0 +1,10 @@ +rate 60kbit +wait 30s +rate 40kbit +wait 30s +rate 20kbit +wait 30s +rate 40kbit +wait 30s +rate 60kbit +wait 30s diff --git a/repos/moq-rs/tc_profiles/cascade_profile_x0.25 b/repos/moq-rs/tc_profiles/cascade_profile_x0.25 new file mode 100644 index 0000000..91f97d4 --- /dev/null +++ b/repos/moq-rs/tc_profiles/cascade_profile_x0.25 @@ -0,0 +1,10 @@ +rate 300kbit +wait 5s +rate 200kbit +wait 5s +rate 100kbit +wait 5s +rate 200kbit +wait 5s +rate 300kbit +wait 5s diff --git a/repos/moq-rs/tc_profiles/cascade_profile_x1.25 b/repos/moq-rs/tc_profiles/cascade_profile_x1.25 new file mode 100644 index 0000000..2590240 --- /dev/null +++ b/repos/moq-rs/tc_profiles/cascade_profile_x1.25 @@ -0,0 +1,10 @@ +rate 1500kbit +wait 5s +rate 1000kbit +wait 5s +rate 500kbit +wait 5s +rate 1000kbit +wait 5s +rate 1500kbit +wait 5s \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/cascade_profile_x3 b/repos/moq-rs/tc_profiles/cascade_profile_x3 new file mode 100644 index 0000000..e5b19eb --- /dev/null +++ b/repos/moq-rs/tc_profiles/cascade_profile_x3 @@ -0,0 +1,10 @@ +rate 3600kbit +wait 30s +rate 2400kbit +wait 30s +rate 1200kbit +wait 30s +rate 2400kbit +wait 30s +rate 3600kbit +wait 30s \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/cascade_profile_x4 b/repos/moq-rs/tc_profiles/cascade_profile_x4 new file mode 100644 index 0000000..e16b826 --- /dev/null +++ b/repos/moq-rs/tc_profiles/cascade_profile_x4 @@ -0,0 +1,10 @@ +rate 4800kbit +wait 30s +rate 3200kbit +wait 30s +rate 1600kbit +wait 30s +rate 3200kbit +wait 30s +rate 4800kbit +wait 30s \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/cascade_profile_x4.5 b/repos/moq-rs/tc_profiles/cascade_profile_x4.5 new file mode 100644 index 0000000..29a97d1 --- /dev/null +++ b/repos/moq-rs/tc_profiles/cascade_profile_x4.5 @@ -0,0 +1,10 @@ +rate 5400kbit +wait 30s +rate 3600kbit +wait 30s +rate 1800kbit +wait 30s +rate 3600kbit +wait 30s +rate 5400kbit +wait 30s \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/lte_profile b/repos/moq-rs/tc_profiles/lte_profile new file mode 100644 index 0000000..da40ebe --- /dev/null +++ b/repos/moq-rs/tc_profiles/lte_profile @@ -0,0 +1,1200 @@ +rate 693kbit +wait 1s +rate 1208kbit +wait 1s +rate 1496kbit +wait 1s +rate 891kbit +wait 1s +rate 2624kbit +wait 1s +rate 1370kbit +wait 1s +rate 1537kbit +wait 1s +rate 1566kbit +wait 1s +rate 4046kbit +wait 1s +rate 2590kbit +wait 1s +rate 2813kbit +wait 1s +rate 3539kbit +wait 1s +rate 2033kbit +wait 1s +rate 1014kbit +wait 1s +rate 1875kbit +wait 1s +rate 3163kbit +wait 1s +rate 1362kbit +wait 1s +rate 1787kbit +wait 1s +rate 2566kbit +wait 1s +rate 3392kbit +wait 1s +rate 2758kbit +wait 1s +rate 5188kbit +wait 1s +rate 5106kbit +wait 1s +rate 6575kbit +wait 1s +rate 6557kbit +wait 1s +rate 4684kbit +wait 1s +rate 1737kbit +wait 1s +rate 1148kbit +wait 1s +rate 5168kbit +wait 1s +rate 6224kbit +wait 1s +rate 5358kbit +wait 1s +rate 4200kbit +wait 1s +rate 1563kbit +wait 1s +rate 4428kbit +wait 1s +rate 2541kbit +wait 1s +rate 300kbit +wait 1s +rate 2532kbit +wait 1s +rate 1977kbit +wait 1s +rate 887kbit +wait 1s +rate 1015kbit +wait 1s +rate 1281kbit +wait 1s +rate 850kbit +wait 1s +rate 991kbit +wait 1s +rate 417kbit +wait 1s +rate 503kbit +wait 1s +rate 613kbit +wait 1s +rate 1088kbit +wait 1s +rate 3107kbit +wait 1s +rate 4133kbit +wait 1s +rate 3768kbit +wait 1s +rate 2970kbit +wait 1s +rate 3578kbit +wait 1s +rate 2480kbit +wait 1s +rate 2330kbit +wait 1s +rate 2501kbit +wait 1s +rate 2425kbit +wait 1s +rate 2619kbit +wait 1s +rate 4849kbit +wait 1s +rate 4248kbit +wait 1s +rate 4604kbit +wait 1s +rate 4882kbit +wait 1s +rate 4669kbit +wait 1s +rate 3506kbit +wait 1s +rate 1589kbit +wait 1s +rate 7086kbit +wait 1s +rate 7569kbit +wait 1s +rate 4642kbit +wait 1s +rate 3961kbit +wait 1s +rate 3505kbit +wait 1s +rate 2316kbit +wait 1s +rate 3957kbit +wait 1s +rate 5290kbit +wait 1s +rate 4554kbit +wait 1s +rate 4830kbit +wait 1s +rate 3942kbit +wait 1s +rate 4253kbit +wait 1s +rate 3917kbit +wait 1s +rate 3481kbit +wait 1s +rate 6457kbit +wait 1s +rate 5634kbit +wait 1s +rate 4860kbit +wait 1s +rate 4906kbit +wait 1s +rate 4073kbit +wait 1s +rate 5197kbit +wait 1s +rate 1839kbit +wait 1s +rate 6822kbit +wait 1s +rate 6068kbit +wait 1s +rate 3313kbit +wait 1s +rate 2943kbit +wait 1s +rate 3648kbit +wait 1s +rate 3085kbit +wait 1s +rate 2550kbit +wait 1s +rate 2596kbit +wait 1s +rate 2393kbit +wait 1s +rate 2015kbit +wait 1s +rate 3069kbit +wait 1s +rate 2737kbit +wait 1s +rate 856kbit +wait 1s +rate 1264kbit +wait 1s +rate 1827kbit +wait 1s +rate 3503kbit +wait 1s +rate 2497kbit +wait 1s +rate 2601kbit +wait 1s +rate 4287kbit +wait 1s +rate 4130kbit +wait 1s +rate 2219kbit +wait 1s +rate 3460kbit +wait 1s +rate 4906kbit +wait 1s +rate 5746kbit +wait 1s +rate 5863kbit +wait 1s +rate 5786kbit +wait 1s +rate 6777kbit +wait 1s +rate 3238kbit +wait 1s +rate 4151kbit +wait 1s +rate 6081kbit +wait 1s +rate 4198kbit +wait 1s +rate 5497kbit +wait 1s +rate 3597kbit +wait 1s +rate 5485kbit +wait 1s +rate 4777kbit +wait 1s +rate 4625kbit +wait 1s +rate 4541kbit +wait 1s +rate 3194kbit +wait 1s +rate 5389kbit +wait 1s +rate 4849kbit +wait 1s +rate 5170kbit +wait 1s +rate 4772kbit +wait 1s +rate 3363kbit +wait 1s +rate 4982kbit +wait 1s +rate 4671kbit +wait 1s +rate 6679kbit +wait 1s +rate 4481kbit +wait 1s +rate 4420kbit +wait 1s +rate 3727kbit +wait 1s +rate 5800kbit +wait 1s +rate 5666kbit +wait 1s +rate 4044kbit +wait 1s +rate 4984kbit +wait 1s +rate 3895kbit +wait 1s +rate 5075kbit +wait 1s +rate 2355kbit +wait 1s +rate 2439kbit +wait 1s +rate 1608kbit +wait 1s +rate 800kbit +wait 1s +rate 800kbit +wait 1s +rate 800kbit +wait 1s +rate 800kbit +wait 1s +rate 3601kbit +wait 1s +rate 664kbit +wait 1s +rate 764kbit +wait 1s +rate 781kbit +wait 1s +rate 2390kbit +wait 1s +rate 544kbit +wait 1s +rate 899kbit +wait 1s +rate 2247kbit +wait 1s +rate 2837kbit +wait 1s +rate 1689kbit +wait 1s +rate 2106kbit +wait 1s +rate 3321kbit +wait 1s +rate 1401kbit +wait 1s +rate 340kbit +wait 1s +rate 329kbit +wait 1s +rate 300kbit +wait 1s +rate 300kbit +wait 1s +rate 303kbit +wait 1s +rate 389kbit +wait 1s +rate 300kbit +wait 1s +rate 300kbit +wait 1s +rate 300kbit +wait 1s +rate 920kbit +wait 1s +rate 300kbit +wait 1s +rate 300kbit +wait 1s +rate 300kbit +wait 1s +rate 300kbit +wait 1s +rate 300kbit +wait 1s +rate 300kbit +wait 1s +rate 300kbit +wait 1s +rate 300kbit +wait 1s +rate 5225kbit +wait 1s +rate 3820kbit +wait 1s +rate 4770kbit +wait 1s +rate 3614kbit +wait 1s +rate 4837kbit +wait 1s +rate 4931kbit +wait 1s +rate 4023kbit +wait 1s +rate 4234kbit +wait 1s +rate 4303kbit +wait 1s +rate 2143kbit +wait 1s +rate 2180kbit +wait 1s +rate 3072kbit +wait 1s +rate 828kbit +wait 1s +rate 2177kbit +wait 1s +rate 1892kbit +wait 1s +rate 855kbit +wait 1s +rate 2338kbit +wait 1s +rate 1755kbit +wait 1s +rate 2313kbit +wait 1s +rate 1523kbit +wait 1s +rate 428kbit +wait 1s +rate 300kbit +wait 1s +rate 728kbit +wait 1s +rate 328kbit +wait 1s +rate 3903kbit +wait 1s +rate 2499kbit +wait 1s +rate 3864kbit +wait 1s +rate 3351kbit +wait 1s +rate 4325kbit +wait 1s +rate 3951kbit +wait 1s +rate 1876kbit +wait 1s +rate 1582kbit +wait 1s +rate 1624kbit +wait 1s +rate 865kbit +wait 1s +rate 477kbit +wait 1s +rate 477kbit +wait 1s +rate 477kbit +wait 1s +rate 477kbit +wait 1s +rate 692kbit +wait 1s +rate 5396kbit +wait 1s +rate 3864kbit +wait 1s +rate 5317kbit +wait 1s +rate 3542kbit +wait 1s +rate 6376kbit +wait 1s +rate 3918kbit +wait 1s +rate 4299kbit +wait 1s +rate 4002kbit +wait 1s +rate 4674kbit +wait 1s +rate 5747kbit +wait 1s +rate 2925kbit +wait 1s +rate 1498kbit +wait 1s +rate 4018kbit +wait 1s +rate 4206kbit +wait 1s +rate 3494kbit +wait 1s +rate 2168kbit +wait 1s +rate 4291kbit +wait 1s +rate 3843kbit +wait 1s +rate 3536kbit +wait 1s +rate 4461kbit +wait 1s +rate 6009kbit +wait 1s +rate 6853kbit +wait 1s +rate 3979kbit +wait 1s +rate 5464kbit +wait 1s +rate 5033kbit +wait 1s +rate 3843kbit +wait 1s +rate 3049kbit +wait 1s +rate 3162kbit +wait 1s +rate 3252kbit +wait 1s +rate 2864kbit +wait 1s +rate 2961kbit +wait 1s +rate 2469kbit +wait 1s +rate 2033kbit +wait 1s +rate 2072kbit +wait 1s +rate 872kbit +wait 1s +rate 2401kbit +wait 1s +rate 2480kbit +wait 1s +rate 4526kbit +wait 1s +rate 3205kbit +wait 1s +rate 2562kbit +wait 1s +rate 2833kbit +wait 1s +rate 1915kbit +wait 1s +rate 1817kbit +wait 1s +rate 845kbit +wait 1s +rate 300kbit +wait 1s +rate 300kbit +wait 1s +rate 335kbit +wait 1s +rate 3067kbit +wait 1s +rate 3067kbit +wait 1s +rate 300kbit +wait 1s +rate 3406kbit +wait 1s +rate 820kbit +wait 1s +rate 488kbit +wait 1s +rate 2334kbit +wait 1s +rate 1132kbit +wait 1s +rate 675kbit +wait 1s +rate 884kbit +wait 1s +rate 3045kbit +wait 1s +rate 2895kbit +wait 1s +rate 791kbit +wait 1s +rate 616kbit +wait 1s +rate 338kbit +wait 1s +rate 419kbit +wait 1s +rate 578kbit +wait 1s +rate 815kbit +wait 1s +rate 721kbit +wait 1s +rate 598kbit +wait 1s +rate 900kbit +wait 1s +rate 1274kbit +wait 1s +rate 874kbit +wait 1s +rate 1402kbit +wait 1s +rate 1704kbit +wait 1s +rate 2162kbit +wait 1s +rate 1570kbit +wait 1s +rate 2219kbit +wait 1s +rate 3420kbit +wait 1s +rate 3915kbit +wait 1s +rate 4575kbit +wait 1s +rate 3874kbit +wait 1s +rate 2638kbit +wait 1s +rate 1388kbit +wait 1s +rate 1608kbit +wait 1s +rate 2773kbit +wait 1s +rate 1242kbit +wait 1s +rate 1224kbit +wait 1s +rate 2073kbit +wait 1s +rate 3226kbit +wait 1s +rate 2437kbit +wait 1s +rate 3308kbit +wait 1s +rate 1962kbit +wait 1s +rate 771kbit +wait 1s +rate 405kbit +wait 1s +rate 876kbit +wait 1s +rate 896kbit +wait 1s +rate 1325kbit +wait 1s +rate 1590kbit +wait 1s +rate 827kbit +wait 1s +rate 1341kbit +wait 1s +rate 1440kbit +wait 1s +rate 1440kbit +wait 1s +rate 300kbit +wait 1s +rate 3453kbit +wait 1s +rate 4905kbit +wait 1s +rate 3695kbit +wait 1s +rate 2203kbit +wait 1s +rate 3264kbit +wait 1s +rate 2917kbit +wait 1s +rate 2811kbit +wait 1s +rate 5188kbit +wait 1s +rate 4855kbit +wait 1s +rate 2909kbit +wait 1s +rate 1943kbit +wait 1s +rate 3245kbit +wait 1s +rate 1516kbit +wait 1s +rate 2183kbit +wait 1s +rate 1323kbit +wait 1s +rate 2253kbit +wait 1s +rate 2235kbit +wait 1s +rate 1916kbit +wait 1s +rate 1655kbit +wait 1s +rate 2656kbit +wait 1s +rate 4314kbit +wait 1s +rate 3214kbit +wait 1s +rate 3963kbit +wait 1s +rate 2939kbit +wait 1s +rate 5132kbit +wait 1s +rate 2330kbit +wait 1s +rate 1555kbit +wait 1s +rate 2687kbit +wait 1s +rate 4240kbit +wait 1s +rate 2678kbit +wait 1s +rate 5395kbit +wait 1s +rate 4863kbit +wait 1s +rate 4190kbit +wait 1s +rate 4484kbit +wait 1s +rate 3808kbit +wait 1s +rate 3144kbit +wait 1s +rate 4501kbit +wait 1s +rate 3478kbit +wait 1s +rate 4416kbit +wait 1s +rate 3105kbit +wait 1s +rate 4026kbit +wait 1s +rate 4619kbit +wait 1s +rate 2955kbit +wait 1s +rate 4802kbit +wait 1s +rate 4049kbit +wait 1s +rate 5244kbit +wait 1s +rate 7086kbit +wait 1s +rate 4491kbit +wait 1s +rate 4090kbit +wait 1s +rate 7144kbit +wait 1s +rate 5474kbit +wait 1s +rate 4374kbit +wait 1s +rate 3601kbit +wait 1s +rate 5039kbit +wait 1s +rate 3976kbit +wait 1s +rate 3772kbit +wait 1s +rate 3879kbit +wait 1s +rate 3638kbit +wait 1s +rate 4023kbit +wait 1s +rate 3676kbit +wait 1s +rate 3375kbit +wait 1s +rate 2653kbit +wait 1s +rate 1756kbit +wait 1s +rate 1916kbit +wait 1s +rate 815kbit +wait 1s +rate 815kbit +wait 1s +rate 1135kbit +wait 1s +rate 300kbit +wait 1s +rate 300kbit +wait 1s +rate 3085kbit +wait 1s +rate 1817kbit +wait 1s +rate 2724kbit +wait 1s +rate 1987kbit +wait 1s +rate 876kbit +wait 1s +rate 2378kbit +wait 1s +rate 1988kbit +wait 1s +rate 2383kbit +wait 1s +rate 2346kbit +wait 1s +rate 2023kbit +wait 1s +rate 3505kbit +wait 1s +rate 3578kbit +wait 1s +rate 1953kbit +wait 1s +rate 1908kbit +wait 1s +rate 2694kbit +wait 1s +rate 2398kbit +wait 1s +rate 2373kbit +wait 1s +rate 2580kbit +wait 1s +rate 2824kbit +wait 1s +rate 2074kbit +wait 1s +rate 2613kbit +wait 1s +rate 2254kbit +wait 1s +rate 2529kbit +wait 1s +rate 1510kbit +wait 1s +rate 973kbit +wait 1s +rate 520kbit +wait 1s +rate 776kbit +wait 1s +rate 301kbit +wait 1s +rate 300kbit +wait 1s +rate 508kbit +wait 1s +rate 576kbit +wait 1s +rate 1275kbit +wait 1s +rate 1876kbit +wait 1s +rate 2138kbit +wait 1s +rate 1433kbit +wait 1s +rate 1224kbit +wait 1s +rate 960kbit +wait 1s +rate 2422kbit +wait 1s +rate 1827kbit +wait 1s +rate 2433kbit +wait 1s +rate 2945kbit +wait 1s +rate 3360kbit +wait 1s +rate 2849kbit +wait 1s +rate 2056kbit +wait 1s +rate 2611kbit +wait 1s +rate 2274kbit +wait 1s +rate 2012kbit +wait 1s +rate 1998kbit +wait 1s +rate 2647kbit +wait 1s +rate 2172kbit +wait 1s +rate 2575kbit +wait 1s +rate 2398kbit +wait 1s +rate 3442kbit +wait 1s +rate 2633kbit +wait 1s +rate 1069kbit +wait 1s +rate 1212kbit +wait 1s +rate 1472kbit +wait 1s +rate 300kbit +wait 1s +rate 2170kbit +wait 1s +rate 300kbit +wait 1s +rate 1591kbit +wait 1s +rate 2069kbit +wait 1s +rate 1733kbit +wait 1s +rate 2254kbit +wait 1s +rate 2167kbit +wait 1s +rate 2264kbit +wait 1s +rate 1343kbit +wait 1s +rate 3402kbit +wait 1s +rate 2365kbit +wait 1s +rate 1125kbit +wait 1s +rate 7008kbit +wait 1s +rate 2684kbit +wait 1s +rate 3804kbit +wait 1s +rate 2670kbit +wait 1s +rate 4154kbit +wait 1s +rate 3311kbit +wait 1s +rate 4237kbit +wait 1s +rate 4055kbit +wait 1s +rate 4182kbit +wait 1s +rate 3899kbit +wait 1s +rate 4096kbit +wait 1s +rate 3607kbit +wait 1s +rate 4687kbit +wait 1s +rate 4165kbit +wait 1s +rate 3630kbit +wait 1s +rate 4959kbit +wait 1s +rate 4680kbit +wait 1s +rate 3237kbit +wait 1s +rate 5280kbit +wait 1s +rate 3764kbit +wait 1s +rate 3776kbit +wait 1s +rate 2280kbit +wait 1s +rate 4496kbit +wait 1s +rate 3791kbit +wait 1s +rate 3994kbit +wait 1s +rate 5692kbit +wait 1s +rate 5422kbit +wait 1s +rate 5629kbit +wait 1s +rate 3578kbit +wait 1s +rate 4140kbit +wait 1s +rate 2809kbit +wait 1s +rate 5159kbit +wait 1s +rate 3990kbit +wait 1s +rate 4904kbit +wait 1s +rate 3882kbit +wait 1s +rate 3190kbit +wait 1s +rate 6746kbit +wait 1s +rate 6785kbit +wait 1s +rate 6605kbit +wait 1s +rate 5648kbit +wait 1s +rate 5935kbit +wait 1s +rate 3636kbit +wait 1s +rate 6320kbit +wait 1s +rate 5037kbit +wait 1s +rate 4868kbit +wait 1s +rate 5456kbit +wait 1s +rate 3720kbit +wait 1s +rate 4511kbit +wait 1s +rate 5512kbit +wait 1s +rate 693kbit +wait 1s +rate 1208kbit +wait 1s +rate 1496kbit +wait 1s +rate 891kbit +wait 1s +rate 2624kbit +wait 1s +rate 1370kbit +wait 1s +rate 1537kbit +wait 1s +rate 1566kbit +wait 1s +rate 4046kbit +wait 1s +rate 2590kbit +wait 1s +rate 2813kbit +wait 1s +rate 3539kbit +wait 1s +rate 2033kbit +wait 1s +rate 1014kbit +wait 1s +rate 1875kbit +wait 1s +rate 3163kbit +wait 1s +rate 1362kbit +wait 1s +rate 1787kbit +wait 1s +rate 2566kbit +wait 1s +rate 3392kbit +wait 1s +rate 2758kbit +wait 1s +rate 5188kbit +wait 1s +rate 5106kbit +wait 1s +rate 6575kbit +wait 1s +rate 6557kbit +wait 1s +rate 4684kbit +wait 1s +rate 1737kbit +wait 1s +rate 1148kbit +wait 1s +rate 5168kbit +wait 1s +rate 6224kbit +wait 1s +rate 5358kbit +wait 1s +rate 4200kbit +wait 1s +rate 1563kbit +wait 1s +rate 4428kbit +wait 1s +rate 2541kbit +wait 1s +rate 300kbit +wait 1s +rate 2532kbit +wait 1s +rate 1977kbit +wait 1s +rate 887kbit +wait 1s +rate 1015kbit +wait 1s +rate 1281kbit +wait 1s +rate 850kbit +wait 1s +rate 991kbit +wait 1s +rate 417kbit +wait 1s +rate 503kbit +wait 1s +rate 613kbit +wait 1s +rate 1088kbit +wait 1s +rate 3107kbit +wait 1s +rate 4133kbit +wait 1s +rate 3768kbit +wait 1s +rate 2970kbit +wait 1s +rate 3578kbit +wait 1s +rate 2480kbit +wait 1s +rate 2330kbit +wait 1s +rate 2501kbit +wait 1s +rate 2425kbit +wait 1s +rate 2619kbit +wait 1s +rate 4849kbit +wait 1s +rate 4248kbit +wait 1s +rate 4604kbit +wait 1s +rate 4882kbit +wait 1s +rate 4669kbit +wait 1s +rate 3506kbit +wait 1s +rate 1589kbit +wait 1s +rate 7086kbit +wait 1s +rate 7569kbit +wait 1s +rate 4642kbit +wait 1s +rate 3961kbit +wait 1s +rate 3505kbit +wait 1s +rate 2316kbit +wait 1s +rate 3957kbit +wait 1s +rate 5290kbit +wait 1s +rate 4554kbit +wait 1s +rate 4830kbit +wait 1s +rate 3942kbit +wait 1s +rate 4253kbit +wait 1s +rate 3917kbit +wait 1s +rate 3481kbit +wait 1s +rate 6457kbit +wait 1s +rate 5634kbit +wait 1s +rate 4860kbit +wait 1s +rate 4906kbit +wait 1s +rate 4073kbit +wait 1s +rate 5197kbit +wait 1s +rate 1839kbit +wait 1s +rate 6822kbit +wait 1s +rate 6068kbit +wait 1s +rate 3313kbit +wait 1s +rate 2943kbit +wait 1s +rate 3648kbit +wait 1s +rate 3085kbit +wait 1s +rate 2550kbit +wait 1s +rate 2596kbit +wait 1s +rate 2393kbit +wait 1s \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/lte_profile_x0.25 b/repos/moq-rs/tc_profiles/lte_profile_x0.25 new file mode 100644 index 0000000..8803e9a --- /dev/null +++ b/repos/moq-rs/tc_profiles/lte_profile_x0.25 @@ -0,0 +1,1200 @@ +rate 173kbit +wait 1s +rate 302kbit +wait 1s +rate 374kbit +wait 1s +rate 222kbit +wait 1s +rate 656kbit +wait 1s +rate 342kbit +wait 1s +rate 384kbit +wait 1s +rate 391kbit +wait 1s +rate 1011kbit +wait 1s +rate 647kbit +wait 1s +rate 703kbit +wait 1s +rate 884kbit +wait 1s +rate 508kbit +wait 1s +rate 253kbit +wait 1s +rate 468kbit +wait 1s +rate 790kbit +wait 1s +rate 340kbit +wait 1s +rate 446kbit +wait 1s +rate 641kbit +wait 1s +rate 848kbit +wait 1s +rate 689kbit +wait 1s +rate 1297kbit +wait 1s +rate 1276kbit +wait 1s +rate 1643kbit +wait 1s +rate 1639kbit +wait 1s +rate 1171kbit +wait 1s +rate 434kbit +wait 1s +rate 287kbit +wait 1s +rate 1292kbit +wait 1s +rate 1556kbit +wait 1s +rate 1339kbit +wait 1s +rate 1050kbit +wait 1s +rate 390kbit +wait 1s +rate 1107kbit +wait 1s +rate 635kbit +wait 1s +rate 75kbit +wait 1s +rate 633kbit +wait 1s +rate 494kbit +wait 1s +rate 221kbit +wait 1s +rate 253kbit +wait 1s +rate 320kbit +wait 1s +rate 212kbit +wait 1s +rate 247kbit +wait 1s +rate 104kbit +wait 1s +rate 125kbit +wait 1s +rate 153kbit +wait 1s +rate 272kbit +wait 1s +rate 776kbit +wait 1s +rate 1033kbit +wait 1s +rate 942kbit +wait 1s +rate 742kbit +wait 1s +rate 894kbit +wait 1s +rate 620kbit +wait 1s +rate 582kbit +wait 1s +rate 625kbit +wait 1s +rate 606kbit +wait 1s +rate 654kbit +wait 1s +rate 1212kbit +wait 1s +rate 1062kbit +wait 1s +rate 1151kbit +wait 1s +rate 1220kbit +wait 1s +rate 1167kbit +wait 1s +rate 876kbit +wait 1s +rate 397kbit +wait 1s +rate 1771kbit +wait 1s +rate 1892kbit +wait 1s +rate 1160kbit +wait 1s +rate 990kbit +wait 1s +rate 876kbit +wait 1s +rate 579kbit +wait 1s +rate 989kbit +wait 1s +rate 1322kbit +wait 1s +rate 1138kbit +wait 1s +rate 1207kbit +wait 1s +rate 985kbit +wait 1s +rate 1063kbit +wait 1s +rate 979kbit +wait 1s +rate 870kbit +wait 1s +rate 1614kbit +wait 1s +rate 1408kbit +wait 1s +rate 1215kbit +wait 1s +rate 1226kbit +wait 1s +rate 1018kbit +wait 1s +rate 1299kbit +wait 1s +rate 459kbit +wait 1s +rate 1705kbit +wait 1s +rate 1517kbit +wait 1s +rate 828kbit +wait 1s +rate 735kbit +wait 1s +rate 912kbit +wait 1s +rate 771kbit +wait 1s +rate 637kbit +wait 1s +rate 649kbit +wait 1s +rate 598kbit +wait 1s +rate 503kbit +wait 1s +rate 767kbit +wait 1s +rate 684kbit +wait 1s +rate 214kbit +wait 1s +rate 316kbit +wait 1s +rate 456kbit +wait 1s +rate 875kbit +wait 1s +rate 624kbit +wait 1s +rate 650kbit +wait 1s +rate 1071kbit +wait 1s +rate 1032kbit +wait 1s +rate 554kbit +wait 1s +rate 865kbit +wait 1s +rate 1226kbit +wait 1s +rate 1436kbit +wait 1s +rate 1465kbit +wait 1s +rate 1446kbit +wait 1s +rate 1694kbit +wait 1s +rate 809kbit +wait 1s +rate 1037kbit +wait 1s +rate 1520kbit +wait 1s +rate 1049kbit +wait 1s +rate 1374kbit +wait 1s +rate 899kbit +wait 1s +rate 1371kbit +wait 1s +rate 1194kbit +wait 1s +rate 1156kbit +wait 1s +rate 1135kbit +wait 1s +rate 798kbit +wait 1s +rate 1347kbit +wait 1s +rate 1212kbit +wait 1s +rate 1292kbit +wait 1s +rate 1193kbit +wait 1s +rate 840kbit +wait 1s +rate 1245kbit +wait 1s +rate 1167kbit +wait 1s +rate 1669kbit +wait 1s +rate 1120kbit +wait 1s +rate 1105kbit +wait 1s +rate 931kbit +wait 1s +rate 1450kbit +wait 1s +rate 1416kbit +wait 1s +rate 1011kbit +wait 1s +rate 1246kbit +wait 1s +rate 973kbit +wait 1s +rate 1268kbit +wait 1s +rate 588kbit +wait 1s +rate 609kbit +wait 1s +rate 402kbit +wait 1s +rate 200kbit +wait 1s +rate 200kbit +wait 1s +rate 200kbit +wait 1s +rate 200kbit +wait 1s +rate 900kbit +wait 1s +rate 166kbit +wait 1s +rate 191kbit +wait 1s +rate 195kbit +wait 1s +rate 597kbit +wait 1s +rate 136kbit +wait 1s +rate 224kbit +wait 1s +rate 561kbit +wait 1s +rate 709kbit +wait 1s +rate 422kbit +wait 1s +rate 526kbit +wait 1s +rate 830kbit +wait 1s +rate 350kbit +wait 1s +rate 85kbit +wait 1s +rate 82kbit +wait 1s +rate 75kbit +wait 1s +rate 75kbit +wait 1s +rate 75kbit +wait 1s +rate 97kbit +wait 1s +rate 75kbit +wait 1s +rate 75kbit +wait 1s +rate 75kbit +wait 1s +rate 230kbit +wait 1s +rate 75kbit +wait 1s +rate 75kbit +wait 1s +rate 75kbit +wait 1s +rate 75kbit +wait 1s +rate 75kbit +wait 1s +rate 75kbit +wait 1s +rate 75kbit +wait 1s +rate 75kbit +wait 1s +rate 1306kbit +wait 1s +rate 955kbit +wait 1s +rate 1192kbit +wait 1s +rate 903kbit +wait 1s +rate 1209kbit +wait 1s +rate 1232kbit +wait 1s +rate 1005kbit +wait 1s +rate 1058kbit +wait 1s +rate 1075kbit +wait 1s +rate 535kbit +wait 1s +rate 545kbit +wait 1s +rate 768kbit +wait 1s +rate 207kbit +wait 1s +rate 544kbit +wait 1s +rate 473kbit +wait 1s +rate 213kbit +wait 1s +rate 584kbit +wait 1s +rate 438kbit +wait 1s +rate 578kbit +wait 1s +rate 380kbit +wait 1s +rate 107kbit +wait 1s +rate 75kbit +wait 1s +rate 182kbit +wait 1s +rate 82kbit +wait 1s +rate 975kbit +wait 1s +rate 624kbit +wait 1s +rate 966kbit +wait 1s +rate 837kbit +wait 1s +rate 1081kbit +wait 1s +rate 987kbit +wait 1s +rate 469kbit +wait 1s +rate 395kbit +wait 1s +rate 406kbit +wait 1s +rate 216kbit +wait 1s +rate 119kbit +wait 1s +rate 119kbit +wait 1s +rate 119kbit +wait 1s +rate 119kbit +wait 1s +rate 173kbit +wait 1s +rate 1349kbit +wait 1s +rate 966kbit +wait 1s +rate 1329kbit +wait 1s +rate 885kbit +wait 1s +rate 1594kbit +wait 1s +rate 979kbit +wait 1s +rate 1074kbit +wait 1s +rate 1000kbit +wait 1s +rate 1168kbit +wait 1s +rate 1436kbit +wait 1s +rate 731kbit +wait 1s +rate 374kbit +wait 1s +rate 1004kbit +wait 1s +rate 1051kbit +wait 1s +rate 873kbit +wait 1s +rate 542kbit +wait 1s +rate 1072kbit +wait 1s +rate 960kbit +wait 1s +rate 884kbit +wait 1s +rate 1115kbit +wait 1s +rate 1502kbit +wait 1s +rate 1713kbit +wait 1s +rate 994kbit +wait 1s +rate 1366kbit +wait 1s +rate 1258kbit +wait 1s +rate 960kbit +wait 1s +rate 762kbit +wait 1s +rate 790kbit +wait 1s +rate 813kbit +wait 1s +rate 716kbit +wait 1s +rate 740kbit +wait 1s +rate 617kbit +wait 1s +rate 508kbit +wait 1s +rate 518kbit +wait 1s +rate 218kbit +wait 1s +rate 600kbit +wait 1s +rate 620kbit +wait 1s +rate 1131kbit +wait 1s +rate 801kbit +wait 1s +rate 640kbit +wait 1s +rate 708kbit +wait 1s +rate 478kbit +wait 1s +rate 454kbit +wait 1s +rate 211kbit +wait 1s +rate 75kbit +wait 1s +rate 75kbit +wait 1s +rate 83kbit +wait 1s +rate 766kbit +wait 1s +rate 766kbit +wait 1s +rate 75kbit +wait 1s +rate 851kbit +wait 1s +rate 205kbit +wait 1s +rate 122kbit +wait 1s +rate 583kbit +wait 1s +rate 283kbit +wait 1s +rate 168kbit +wait 1s +rate 221kbit +wait 1s +rate 761kbit +wait 1s +rate 723kbit +wait 1s +rate 197kbit +wait 1s +rate 154kbit +wait 1s +rate 84kbit +wait 1s +rate 104kbit +wait 1s +rate 144kbit +wait 1s +rate 203kbit +wait 1s +rate 180kbit +wait 1s +rate 149kbit +wait 1s +rate 225kbit +wait 1s +rate 318kbit +wait 1s +rate 218kbit +wait 1s +rate 350kbit +wait 1s +rate 426kbit +wait 1s +rate 540kbit +wait 1s +rate 392kbit +wait 1s +rate 554kbit +wait 1s +rate 855kbit +wait 1s +rate 978kbit +wait 1s +rate 1143kbit +wait 1s +rate 968kbit +wait 1s +rate 659kbit +wait 1s +rate 347kbit +wait 1s +rate 402kbit +wait 1s +rate 693kbit +wait 1s +rate 310kbit +wait 1s +rate 306kbit +wait 1s +rate 518kbit +wait 1s +rate 806kbit +wait 1s +rate 609kbit +wait 1s +rate 827kbit +wait 1s +rate 490kbit +wait 1s +rate 192kbit +wait 1s +rate 101kbit +wait 1s +rate 219kbit +wait 1s +rate 224kbit +wait 1s +rate 331kbit +wait 1s +rate 397kbit +wait 1s +rate 206kbit +wait 1s +rate 335kbit +wait 1s +rate 360kbit +wait 1s +rate 360kbit +wait 1s +rate 75kbit +wait 1s +rate 863kbit +wait 1s +rate 1226kbit +wait 1s +rate 923kbit +wait 1s +rate 550kbit +wait 1s +rate 816kbit +wait 1s +rate 729kbit +wait 1s +rate 702kbit +wait 1s +rate 1297kbit +wait 1s +rate 1213kbit +wait 1s +rate 727kbit +wait 1s +rate 485kbit +wait 1s +rate 811kbit +wait 1s +rate 379kbit +wait 1s +rate 545kbit +wait 1s +rate 330kbit +wait 1s +rate 563kbit +wait 1s +rate 558kbit +wait 1s +rate 479kbit +wait 1s +rate 413kbit +wait 1s +rate 664kbit +wait 1s +rate 1078kbit +wait 1s +rate 803kbit +wait 1s +rate 990kbit +wait 1s +rate 734kbit +wait 1s +rate 1283kbit +wait 1s +rate 582kbit +wait 1s +rate 388kbit +wait 1s +rate 671kbit +wait 1s +rate 1060kbit +wait 1s +rate 669kbit +wait 1s +rate 1348kbit +wait 1s +rate 1215kbit +wait 1s +rate 1047kbit +wait 1s +rate 1121kbit +wait 1s +rate 952kbit +wait 1s +rate 786kbit +wait 1s +rate 1125kbit +wait 1s +rate 869kbit +wait 1s +rate 1104kbit +wait 1s +rate 776kbit +wait 1s +rate 1006kbit +wait 1s +rate 1154kbit +wait 1s +rate 738kbit +wait 1s +rate 1200kbit +wait 1s +rate 1012kbit +wait 1s +rate 1311kbit +wait 1s +rate 1771kbit +wait 1s +rate 1122kbit +wait 1s +rate 1022kbit +wait 1s +rate 1786kbit +wait 1s +rate 1368kbit +wait 1s +rate 1093kbit +wait 1s +rate 900kbit +wait 1s +rate 1259kbit +wait 1s +rate 994kbit +wait 1s +rate 943kbit +wait 1s +rate 969kbit +wait 1s +rate 909kbit +wait 1s +rate 1005kbit +wait 1s +rate 919kbit +wait 1s +rate 843kbit +wait 1s +rate 663kbit +wait 1s +rate 439kbit +wait 1s +rate 479kbit +wait 1s +rate 203kbit +wait 1s +rate 203kbit +wait 1s +rate 283kbit +wait 1s +rate 75kbit +wait 1s +rate 75kbit +wait 1s +rate 771kbit +wait 1s +rate 454kbit +wait 1s +rate 681kbit +wait 1s +rate 496kbit +wait 1s +rate 219kbit +wait 1s +rate 594kbit +wait 1s +rate 497kbit +wait 1s +rate 595kbit +wait 1s +rate 586kbit +wait 1s +rate 505kbit +wait 1s +rate 876kbit +wait 1s +rate 894kbit +wait 1s +rate 488kbit +wait 1s +rate 477kbit +wait 1s +rate 673kbit +wait 1s +rate 599kbit +wait 1s +rate 593kbit +wait 1s +rate 645kbit +wait 1s +rate 706kbit +wait 1s +rate 518kbit +wait 1s +rate 653kbit +wait 1s +rate 563kbit +wait 1s +rate 632kbit +wait 1s +rate 377kbit +wait 1s +rate 243kbit +wait 1s +rate 130kbit +wait 1s +rate 194kbit +wait 1s +rate 75kbit +wait 1s +rate 75kbit +wait 1s +rate 127kbit +wait 1s +rate 144kbit +wait 1s +rate 318kbit +wait 1s +rate 469kbit +wait 1s +rate 534kbit +wait 1s +rate 358kbit +wait 1s +rate 306kbit +wait 1s +rate 240kbit +wait 1s +rate 605kbit +wait 1s +rate 456kbit +wait 1s +rate 608kbit +wait 1s +rate 736kbit +wait 1s +rate 840kbit +wait 1s +rate 712kbit +wait 1s +rate 514kbit +wait 1s +rate 652kbit +wait 1s +rate 568kbit +wait 1s +rate 503kbit +wait 1s +rate 499kbit +wait 1s +rate 661kbit +wait 1s +rate 543kbit +wait 1s +rate 643kbit +wait 1s +rate 599kbit +wait 1s +rate 860kbit +wait 1s +rate 658kbit +wait 1s +rate 267kbit +wait 1s +rate 303kbit +wait 1s +rate 368kbit +wait 1s +rate 75kbit +wait 1s +rate 542kbit +wait 1s +rate 75kbit +wait 1s +rate 397kbit +wait 1s +rate 517kbit +wait 1s +rate 433kbit +wait 1s +rate 563kbit +wait 1s +rate 541kbit +wait 1s +rate 566kbit +wait 1s +rate 335kbit +wait 1s +rate 850kbit +wait 1s +rate 591kbit +wait 1s +rate 281kbit +wait 1s +rate 1752kbit +wait 1s +rate 671kbit +wait 1s +rate 951kbit +wait 1s +rate 667kbit +wait 1s +rate 1038kbit +wait 1s +rate 827kbit +wait 1s +rate 1059kbit +wait 1s +rate 1013kbit +wait 1s +rate 1045kbit +wait 1s +rate 974kbit +wait 1s +rate 1024kbit +wait 1s +rate 901kbit +wait 1s +rate 1171kbit +wait 1s +rate 1041kbit +wait 1s +rate 907kbit +wait 1s +rate 1239kbit +wait 1s +rate 1170kbit +wait 1s +rate 809kbit +wait 1s +rate 1320kbit +wait 1s +rate 941kbit +wait 1s +rate 944kbit +wait 1s +rate 570kbit +wait 1s +rate 1124kbit +wait 1s +rate 947kbit +wait 1s +rate 998kbit +wait 1s +rate 1423kbit +wait 1s +rate 1355kbit +wait 1s +rate 1407kbit +wait 1s +rate 894kbit +wait 1s +rate 1035kbit +wait 1s +rate 702kbit +wait 1s +rate 1289kbit +wait 1s +rate 997kbit +wait 1s +rate 1226kbit +wait 1s +rate 970kbit +wait 1s +rate 797kbit +wait 1s +rate 1686kbit +wait 1s +rate 1696kbit +wait 1s +rate 1651kbit +wait 1s +rate 1412kbit +wait 1s +rate 1483kbit +wait 1s +rate 909kbit +wait 1s +rate 1580kbit +wait 1s +rate 1259kbit +wait 1s +rate 1217kbit +wait 1s +rate 1364kbit +wait 1s +rate 930kbit +wait 1s +rate 1127kbit +wait 1s +rate 1378kbit +wait 1s +rate 173kbit +wait 1s +rate 302kbit +wait 1s +rate 374kbit +wait 1s +rate 222kbit +wait 1s +rate 656kbit +wait 1s +rate 342kbit +wait 1s +rate 384kbit +wait 1s +rate 391kbit +wait 1s +rate 1011kbit +wait 1s +rate 647kbit +wait 1s +rate 703kbit +wait 1s +rate 884kbit +wait 1s +rate 508kbit +wait 1s +rate 253kbit +wait 1s +rate 468kbit +wait 1s +rate 790kbit +wait 1s +rate 340kbit +wait 1s +rate 446kbit +wait 1s +rate 641kbit +wait 1s +rate 848kbit +wait 1s +rate 689kbit +wait 1s +rate 1297kbit +wait 1s +rate 1276kbit +wait 1s +rate 1643kbit +wait 1s +rate 1639kbit +wait 1s +rate 1171kbit +wait 1s +rate 434kbit +wait 1s +rate 287kbit +wait 1s +rate 1292kbit +wait 1s +rate 1556kbit +wait 1s +rate 1339kbit +wait 1s +rate 1050kbit +wait 1s +rate 390kbit +wait 1s +rate 1107kbit +wait 1s +rate 635kbit +wait 1s +rate 75kbit +wait 1s +rate 633kbit +wait 1s +rate 494kbit +wait 1s +rate 221kbit +wait 1s +rate 253kbit +wait 1s +rate 320kbit +wait 1s +rate 212kbit +wait 1s +rate 247kbit +wait 1s +rate 104kbit +wait 1s +rate 125kbit +wait 1s +rate 153kbit +wait 1s +rate 272kbit +wait 1s +rate 776kbit +wait 1s +rate 1033kbit +wait 1s +rate 942kbit +wait 1s +rate 742kbit +wait 1s +rate 894kbit +wait 1s +rate 620kbit +wait 1s +rate 582kbit +wait 1s +rate 625kbit +wait 1s +rate 606kbit +wait 1s +rate 654kbit +wait 1s +rate 1212kbit +wait 1s +rate 1062kbit +wait 1s +rate 1151kbit +wait 1s +rate 1220kbit +wait 1s +rate 1167kbit +wait 1s +rate 876kbit +wait 1s +rate 397kbit +wait 1s +rate 1771kbit +wait 1s +rate 1892kbit +wait 1s +rate 1160kbit +wait 1s +rate 990kbit +wait 1s +rate 876kbit +wait 1s +rate 579kbit +wait 1s +rate 989kbit +wait 1s +rate 1322kbit +wait 1s +rate 1138kbit +wait 1s +rate 1207kbit +wait 1s +rate 985kbit +wait 1s +rate 1063kbit +wait 1s +rate 979kbit +wait 1s +rate 870kbit +wait 1s +rate 1614kbit +wait 1s +rate 1408kbit +wait 1s +rate 1215kbit +wait 1s +rate 1226kbit +wait 1s +rate 1018kbit +wait 1s +rate 1299kbit +wait 1s +rate 459kbit +wait 1s +rate 1705kbit +wait 1s +rate 1517kbit +wait 1s +rate 828kbit +wait 1s +rate 735kbit +wait 1s +rate 912kbit +wait 1s +rate 771kbit +wait 1s +rate 637kbit +wait 1s +rate 649kbit +wait 1s +rate 598kbit +wait 1s diff --git a/repos/moq-rs/tc_profiles/lte_profile_x3 b/repos/moq-rs/tc_profiles/lte_profile_x3 new file mode 100644 index 0000000..18510a0 --- /dev/null +++ b/repos/moq-rs/tc_profiles/lte_profile_x3 @@ -0,0 +1,1200 @@ +rate 2079kbit +wait 1s +rate 3624kbit +wait 1s +rate 4488kbit +wait 1s +rate 2673kbit +wait 1s +rate 7872kbit +wait 1s +rate 4110kbit +wait 1s +rate 4611kbit +wait 1s +rate 4698kbit +wait 1s +rate 12138kbit +wait 1s +rate 7770kbit +wait 1s +rate 8439kbit +wait 1s +rate 10617kbit +wait 1s +rate 6099kbit +wait 1s +rate 3042kbit +wait 1s +rate 5625kbit +wait 1s +rate 9489kbit +wait 1s +rate 4086kbit +wait 1s +rate 5361kbit +wait 1s +rate 7698kbit +wait 1s +rate 10176kbit +wait 1s +rate 8274kbit +wait 1s +rate 15564kbit +wait 1s +rate 15318kbit +wait 1s +rate 19725kbit +wait 1s +rate 19671kbit +wait 1s +rate 14052kbit +wait 1s +rate 5211kbit +wait 1s +rate 3444kbit +wait 1s +rate 15504kbit +wait 1s +rate 18672kbit +wait 1s +rate 16074kbit +wait 1s +rate 12600kbit +wait 1s +rate 4689kbit +wait 1s +rate 13284kbit +wait 1s +rate 7623kbit +wait 1s +rate 900kbit +wait 1s +rate 7596kbit +wait 1s +rate 5931kbit +wait 1s +rate 2661kbit +wait 1s +rate 3045kbit +wait 1s +rate 3843kbit +wait 1s +rate 2550kbit +wait 1s +rate 2973kbit +wait 1s +rate 1251kbit +wait 1s +rate 1509kbit +wait 1s +rate 1839kbit +wait 1s +rate 3264kbit +wait 1s +rate 9321kbit +wait 1s +rate 12399kbit +wait 1s +rate 11304kbit +wait 1s +rate 8910kbit +wait 1s +rate 10734kbit +wait 1s +rate 7440kbit +wait 1s +rate 6990kbit +wait 1s +rate 7503kbit +wait 1s +rate 7275kbit +wait 1s +rate 7857kbit +wait 1s +rate 14547kbit +wait 1s +rate 12744kbit +wait 1s +rate 13812kbit +wait 1s +rate 14646kbit +wait 1s +rate 14007kbit +wait 1s +rate 10518kbit +wait 1s +rate 4767kbit +wait 1s +rate 21258kbit +wait 1s +rate 22707kbit +wait 1s +rate 13926kbit +wait 1s +rate 11883kbit +wait 1s +rate 10515kbit +wait 1s +rate 6948kbit +wait 1s +rate 11871kbit +wait 1s +rate 15870kbit +wait 1s +rate 13662kbit +wait 1s +rate 14490kbit +wait 1s +rate 11826kbit +wait 1s +rate 12759kbit +wait 1s +rate 11751kbit +wait 1s +rate 10443kbit +wait 1s +rate 19371kbit +wait 1s +rate 16902kbit +wait 1s +rate 14580kbit +wait 1s +rate 14718kbit +wait 1s +rate 12219kbit +wait 1s +rate 15591kbit +wait 1s +rate 5517kbit +wait 1s +rate 20466kbit +wait 1s +rate 18204kbit +wait 1s +rate 9939kbit +wait 1s +rate 8829kbit +wait 1s +rate 10944kbit +wait 1s +rate 9255kbit +wait 1s +rate 7650kbit +wait 1s +rate 7788kbit +wait 1s +rate 7179kbit +wait 1s +rate 6045kbit +wait 1s +rate 9207kbit +wait 1s +rate 8211kbit +wait 1s +rate 2568kbit +wait 1s +rate 3792kbit +wait 1s +rate 5481kbit +wait 1s +rate 10509kbit +wait 1s +rate 7491kbit +wait 1s +rate 7803kbit +wait 1s +rate 12861kbit +wait 1s +rate 12390kbit +wait 1s +rate 6657kbit +wait 1s +rate 10380kbit +wait 1s +rate 14718kbit +wait 1s +rate 17238kbit +wait 1s +rate 17589kbit +wait 1s +rate 17358kbit +wait 1s +rate 20331kbit +wait 1s +rate 9714kbit +wait 1s +rate 12453kbit +wait 1s +rate 18243kbit +wait 1s +rate 12594kbit +wait 1s +rate 16491kbit +wait 1s +rate 10791kbit +wait 1s +rate 16455kbit +wait 1s +rate 14331kbit +wait 1s +rate 13875kbit +wait 1s +rate 13623kbit +wait 1s +rate 9582kbit +wait 1s +rate 16167kbit +wait 1s +rate 14547kbit +wait 1s +rate 15510kbit +wait 1s +rate 14316kbit +wait 1s +rate 10089kbit +wait 1s +rate 14946kbit +wait 1s +rate 14013kbit +wait 1s +rate 20037kbit +wait 1s +rate 13443kbit +wait 1s +rate 13260kbit +wait 1s +rate 11181kbit +wait 1s +rate 17400kbit +wait 1s +rate 16998kbit +wait 1s +rate 12132kbit +wait 1s +rate 14952kbit +wait 1s +rate 11685kbit +wait 1s +rate 15225kbit +wait 1s +rate 7065kbit +wait 1s +rate 7317kbit +wait 1s +rate 4824kbit +wait 1s +rate 2400kbit +wait 1s +rate 2400kbit +wait 1s +rate 2400kbit +wait 1s +rate 2400kbit +wait 1s +rate 10803kbit +wait 1s +rate 1992kbit +wait 1s +rate 2292kbit +wait 1s +rate 2343kbit +wait 1s +rate 7170kbit +wait 1s +rate 1632kbit +wait 1s +rate 2697kbit +wait 1s +rate 6741kbit +wait 1s +rate 8511kbit +wait 1s +rate 5067kbit +wait 1s +rate 6318kbit +wait 1s +rate 9963kbit +wait 1s +rate 4203kbit +wait 1s +rate 1020kbit +wait 1s +rate 987kbit +wait 1s +rate 900kbit +wait 1s +rate 900kbit +wait 1s +rate 909kbit +wait 1s +rate 1167kbit +wait 1s +rate 900kbit +wait 1s +rate 900kbit +wait 1s +rate 900kbit +wait 1s +rate 2760kbit +wait 1s +rate 900kbit +wait 1s +rate 900kbit +wait 1s +rate 900kbit +wait 1s +rate 900kbit +wait 1s +rate 900kbit +wait 1s +rate 900kbit +wait 1s +rate 900kbit +wait 1s +rate 900kbit +wait 1s +rate 15675kbit +wait 1s +rate 11460kbit +wait 1s +rate 14310kbit +wait 1s +rate 10842kbit +wait 1s +rate 14511kbit +wait 1s +rate 14793kbit +wait 1s +rate 12069kbit +wait 1s +rate 12702kbit +wait 1s +rate 12909kbit +wait 1s +rate 6429kbit +wait 1s +rate 6540kbit +wait 1s +rate 9216kbit +wait 1s +rate 2484kbit +wait 1s +rate 6531kbit +wait 1s +rate 5676kbit +wait 1s +rate 2565kbit +wait 1s +rate 7014kbit +wait 1s +rate 5265kbit +wait 1s +rate 6939kbit +wait 1s +rate 4569kbit +wait 1s +rate 1284kbit +wait 1s +rate 900kbit +wait 1s +rate 2184kbit +wait 1s +rate 984kbit +wait 1s +rate 11709kbit +wait 1s +rate 7497kbit +wait 1s +rate 11592kbit +wait 1s +rate 10053kbit +wait 1s +rate 12975kbit +wait 1s +rate 11853kbit +wait 1s +rate 5628kbit +wait 1s +rate 4746kbit +wait 1s +rate 4872kbit +wait 1s +rate 2595kbit +wait 1s +rate 1431kbit +wait 1s +rate 1431kbit +wait 1s +rate 1431kbit +wait 1s +rate 1431kbit +wait 1s +rate 2076kbit +wait 1s +rate 16188kbit +wait 1s +rate 11592kbit +wait 1s +rate 15951kbit +wait 1s +rate 10626kbit +wait 1s +rate 19128kbit +wait 1s +rate 11754kbit +wait 1s +rate 12897kbit +wait 1s +rate 12006kbit +wait 1s +rate 14022kbit +wait 1s +rate 17241kbit +wait 1s +rate 8775kbit +wait 1s +rate 4494kbit +wait 1s +rate 12054kbit +wait 1s +rate 12618kbit +wait 1s +rate 10482kbit +wait 1s +rate 6504kbit +wait 1s +rate 12873kbit +wait 1s +rate 11529kbit +wait 1s +rate 10608kbit +wait 1s +rate 13383kbit +wait 1s +rate 18027kbit +wait 1s +rate 20559kbit +wait 1s +rate 11937kbit +wait 1s +rate 16392kbit +wait 1s +rate 15099kbit +wait 1s +rate 11529kbit +wait 1s +rate 9147kbit +wait 1s +rate 9486kbit +wait 1s +rate 9756kbit +wait 1s +rate 8592kbit +wait 1s +rate 8883kbit +wait 1s +rate 7407kbit +wait 1s +rate 6099kbit +wait 1s +rate 6216kbit +wait 1s +rate 2616kbit +wait 1s +rate 7203kbit +wait 1s +rate 7440kbit +wait 1s +rate 13578kbit +wait 1s +rate 9615kbit +wait 1s +rate 7686kbit +wait 1s +rate 8499kbit +wait 1s +rate 5745kbit +wait 1s +rate 5451kbit +wait 1s +rate 2535kbit +wait 1s +rate 900kbit +wait 1s +rate 900kbit +wait 1s +rate 1005kbit +wait 1s +rate 9201kbit +wait 1s +rate 9201kbit +wait 1s +rate 900kbit +wait 1s +rate 10218kbit +wait 1s +rate 2460kbit +wait 1s +rate 1464kbit +wait 1s +rate 7002kbit +wait 1s +rate 3396kbit +wait 1s +rate 2025kbit +wait 1s +rate 2652kbit +wait 1s +rate 9135kbit +wait 1s +rate 8685kbit +wait 1s +rate 2373kbit +wait 1s +rate 1848kbit +wait 1s +rate 1014kbit +wait 1s +rate 1257kbit +wait 1s +rate 1734kbit +wait 1s +rate 2445kbit +wait 1s +rate 2163kbit +wait 1s +rate 1794kbit +wait 1s +rate 2700kbit +wait 1s +rate 3822kbit +wait 1s +rate 2622kbit +wait 1s +rate 4206kbit +wait 1s +rate 5112kbit +wait 1s +rate 6486kbit +wait 1s +rate 4710kbit +wait 1s +rate 6657kbit +wait 1s +rate 10260kbit +wait 1s +rate 11745kbit +wait 1s +rate 13725kbit +wait 1s +rate 11622kbit +wait 1s +rate 7914kbit +wait 1s +rate 4164kbit +wait 1s +rate 4824kbit +wait 1s +rate 8319kbit +wait 1s +rate 3726kbit +wait 1s +rate 3672kbit +wait 1s +rate 6219kbit +wait 1s +rate 9678kbit +wait 1s +rate 7311kbit +wait 1s +rate 9924kbit +wait 1s +rate 5886kbit +wait 1s +rate 2313kbit +wait 1s +rate 1215kbit +wait 1s +rate 2628kbit +wait 1s +rate 2688kbit +wait 1s +rate 3975kbit +wait 1s +rate 4770kbit +wait 1s +rate 2481kbit +wait 1s +rate 4023kbit +wait 1s +rate 4320kbit +wait 1s +rate 4320kbit +wait 1s +rate 900kbit +wait 1s +rate 10359kbit +wait 1s +rate 14715kbit +wait 1s +rate 11085kbit +wait 1s +rate 6609kbit +wait 1s +rate 9792kbit +wait 1s +rate 8751kbit +wait 1s +rate 8433kbit +wait 1s +rate 15564kbit +wait 1s +rate 14565kbit +wait 1s +rate 8727kbit +wait 1s +rate 5829kbit +wait 1s +rate 9735kbit +wait 1s +rate 4548kbit +wait 1s +rate 6549kbit +wait 1s +rate 3969kbit +wait 1s +rate 6759kbit +wait 1s +rate 6705kbit +wait 1s +rate 5748kbit +wait 1s +rate 4965kbit +wait 1s +rate 7968kbit +wait 1s +rate 12942kbit +wait 1s +rate 9642kbit +wait 1s +rate 11889kbit +wait 1s +rate 8817kbit +wait 1s +rate 15396kbit +wait 1s +rate 6990kbit +wait 1s +rate 4665kbit +wait 1s +rate 8061kbit +wait 1s +rate 12720kbit +wait 1s +rate 8034kbit +wait 1s +rate 16185kbit +wait 1s +rate 14589kbit +wait 1s +rate 12570kbit +wait 1s +rate 13452kbit +wait 1s +rate 11424kbit +wait 1s +rate 9432kbit +wait 1s +rate 13503kbit +wait 1s +rate 10434kbit +wait 1s +rate 13248kbit +wait 1s +rate 9315kbit +wait 1s +rate 12078kbit +wait 1s +rate 13857kbit +wait 1s +rate 8865kbit +wait 1s +rate 14406kbit +wait 1s +rate 12147kbit +wait 1s +rate 15732kbit +wait 1s +rate 21258kbit +wait 1s +rate 13473kbit +wait 1s +rate 12270kbit +wait 1s +rate 21432kbit +wait 1s +rate 16422kbit +wait 1s +rate 13122kbit +wait 1s +rate 10803kbit +wait 1s +rate 15117kbit +wait 1s +rate 11928kbit +wait 1s +rate 11316kbit +wait 1s +rate 11637kbit +wait 1s +rate 10914kbit +wait 1s +rate 12069kbit +wait 1s +rate 11028kbit +wait 1s +rate 10125kbit +wait 1s +rate 7959kbit +wait 1s +rate 5268kbit +wait 1s +rate 5748kbit +wait 1s +rate 2445kbit +wait 1s +rate 2445kbit +wait 1s +rate 3405kbit +wait 1s +rate 900kbit +wait 1s +rate 900kbit +wait 1s +rate 9255kbit +wait 1s +rate 5451kbit +wait 1s +rate 8172kbit +wait 1s +rate 5961kbit +wait 1s +rate 2628kbit +wait 1s +rate 7134kbit +wait 1s +rate 5964kbit +wait 1s +rate 7149kbit +wait 1s +rate 7038kbit +wait 1s +rate 6069kbit +wait 1s +rate 10515kbit +wait 1s +rate 10734kbit +wait 1s +rate 5859kbit +wait 1s +rate 5724kbit +wait 1s +rate 8082kbit +wait 1s +rate 7194kbit +wait 1s +rate 7119kbit +wait 1s +rate 7740kbit +wait 1s +rate 8472kbit +wait 1s +rate 6222kbit +wait 1s +rate 7839kbit +wait 1s +rate 6762kbit +wait 1s +rate 7587kbit +wait 1s +rate 4530kbit +wait 1s +rate 2919kbit +wait 1s +rate 1560kbit +wait 1s +rate 2328kbit +wait 1s +rate 903kbit +wait 1s +rate 900kbit +wait 1s +rate 1524kbit +wait 1s +rate 1728kbit +wait 1s +rate 3825kbit +wait 1s +rate 5628kbit +wait 1s +rate 6414kbit +wait 1s +rate 4299kbit +wait 1s +rate 3672kbit +wait 1s +rate 2880kbit +wait 1s +rate 7266kbit +wait 1s +rate 5481kbit +wait 1s +rate 7299kbit +wait 1s +rate 8835kbit +wait 1s +rate 10080kbit +wait 1s +rate 8547kbit +wait 1s +rate 6168kbit +wait 1s +rate 7833kbit +wait 1s +rate 6822kbit +wait 1s +rate 6036kbit +wait 1s +rate 5994kbit +wait 1s +rate 7941kbit +wait 1s +rate 6516kbit +wait 1s +rate 7725kbit +wait 1s +rate 7194kbit +wait 1s +rate 10326kbit +wait 1s +rate 7899kbit +wait 1s +rate 3207kbit +wait 1s +rate 3636kbit +wait 1s +rate 4416kbit +wait 1s +rate 900kbit +wait 1s +rate 6510kbit +wait 1s +rate 900kbit +wait 1s +rate 4773kbit +wait 1s +rate 6207kbit +wait 1s +rate 5199kbit +wait 1s +rate 6762kbit +wait 1s +rate 6501kbit +wait 1s +rate 6792kbit +wait 1s +rate 4029kbit +wait 1s +rate 10206kbit +wait 1s +rate 7095kbit +wait 1s +rate 3375kbit +wait 1s +rate 21024kbit +wait 1s +rate 8052kbit +wait 1s +rate 11412kbit +wait 1s +rate 8010kbit +wait 1s +rate 12462kbit +wait 1s +rate 9933kbit +wait 1s +rate 12711kbit +wait 1s +rate 12165kbit +wait 1s +rate 12546kbit +wait 1s +rate 11697kbit +wait 1s +rate 12288kbit +wait 1s +rate 10821kbit +wait 1s +rate 14061kbit +wait 1s +rate 12495kbit +wait 1s +rate 10890kbit +wait 1s +rate 14877kbit +wait 1s +rate 14040kbit +wait 1s +rate 9711kbit +wait 1s +rate 15840kbit +wait 1s +rate 11292kbit +wait 1s +rate 11328kbit +wait 1s +rate 6840kbit +wait 1s +rate 13488kbit +wait 1s +rate 11373kbit +wait 1s +rate 11982kbit +wait 1s +rate 17076kbit +wait 1s +rate 16266kbit +wait 1s +rate 16887kbit +wait 1s +rate 10734kbit +wait 1s +rate 12420kbit +wait 1s +rate 8427kbit +wait 1s +rate 15477kbit +wait 1s +rate 11970kbit +wait 1s +rate 14712kbit +wait 1s +rate 11646kbit +wait 1s +rate 9570kbit +wait 1s +rate 20238kbit +wait 1s +rate 20355kbit +wait 1s +rate 19815kbit +wait 1s +rate 16944kbit +wait 1s +rate 17805kbit +wait 1s +rate 10908kbit +wait 1s +rate 18960kbit +wait 1s +rate 15111kbit +wait 1s +rate 14604kbit +wait 1s +rate 16368kbit +wait 1s +rate 11160kbit +wait 1s +rate 13533kbit +wait 1s +rate 16536kbit +wait 1s +rate 2079kbit +wait 1s +rate 3624kbit +wait 1s +rate 4488kbit +wait 1s +rate 2673kbit +wait 1s +rate 7872kbit +wait 1s +rate 4110kbit +wait 1s +rate 4611kbit +wait 1s +rate 4698kbit +wait 1s +rate 12138kbit +wait 1s +rate 7770kbit +wait 1s +rate 8439kbit +wait 1s +rate 10617kbit +wait 1s +rate 6099kbit +wait 1s +rate 3042kbit +wait 1s +rate 5625kbit +wait 1s +rate 9489kbit +wait 1s +rate 4086kbit +wait 1s +rate 5361kbit +wait 1s +rate 7698kbit +wait 1s +rate 10176kbit +wait 1s +rate 8274kbit +wait 1s +rate 15564kbit +wait 1s +rate 15318kbit +wait 1s +rate 19725kbit +wait 1s +rate 19671kbit +wait 1s +rate 14052kbit +wait 1s +rate 5211kbit +wait 1s +rate 3444kbit +wait 1s +rate 15504kbit +wait 1s +rate 18672kbit +wait 1s +rate 16074kbit +wait 1s +rate 12600kbit +wait 1s +rate 4689kbit +wait 1s +rate 13284kbit +wait 1s +rate 7623kbit +wait 1s +rate 900kbit +wait 1s +rate 7596kbit +wait 1s +rate 5931kbit +wait 1s +rate 2661kbit +wait 1s +rate 3045kbit +wait 1s +rate 3843kbit +wait 1s +rate 2550kbit +wait 1s +rate 2973kbit +wait 1s +rate 1251kbit +wait 1s +rate 1509kbit +wait 1s +rate 1839kbit +wait 1s +rate 3264kbit +wait 1s +rate 9321kbit +wait 1s +rate 12399kbit +wait 1s +rate 11304kbit +wait 1s +rate 8910kbit +wait 1s +rate 10734kbit +wait 1s +rate 7440kbit +wait 1s +rate 6990kbit +wait 1s +rate 7503kbit +wait 1s +rate 7275kbit +wait 1s +rate 7857kbit +wait 1s +rate 14547kbit +wait 1s +rate 12744kbit +wait 1s +rate 13812kbit +wait 1s +rate 14646kbit +wait 1s +rate 14007kbit +wait 1s +rate 10518kbit +wait 1s +rate 4767kbit +wait 1s +rate 21258kbit +wait 1s +rate 22707kbit +wait 1s +rate 13926kbit +wait 1s +rate 11883kbit +wait 1s +rate 10515kbit +wait 1s +rate 6948kbit +wait 1s +rate 11871kbit +wait 1s +rate 15870kbit +wait 1s +rate 13662kbit +wait 1s +rate 14490kbit +wait 1s +rate 11826kbit +wait 1s +rate 12759kbit +wait 1s +rate 11751kbit +wait 1s +rate 10443kbit +wait 1s +rate 19371kbit +wait 1s +rate 16902kbit +wait 1s +rate 14580kbit +wait 1s +rate 14718kbit +wait 1s +rate 12219kbit +wait 1s +rate 15591kbit +wait 1s +rate 5517kbit +wait 1s +rate 20466kbit +wait 1s +rate 18204kbit +wait 1s +rate 9939kbit +wait 1s +rate 8829kbit +wait 1s +rate 10944kbit +wait 1s +rate 9255kbit +wait 1s +rate 7650kbit +wait 1s +rate 7788kbit +wait 1s +rate 7179kbit +wait 1s \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/lte_profile_x4 b/repos/moq-rs/tc_profiles/lte_profile_x4 new file mode 100644 index 0000000..3ab27fc --- /dev/null +++ b/repos/moq-rs/tc_profiles/lte_profile_x4 @@ -0,0 +1,1200 @@ +rate 2772kbit +wait 1s +rate 4832kbit +wait 1s +rate 5984kbit +wait 1s +rate 3564kbit +wait 1s +rate 10496kbit +wait 1s +rate 5480kbit +wait 1s +rate 6148kbit +wait 1s +rate 6264kbit +wait 1s +rate 16184kbit +wait 1s +rate 10360kbit +wait 1s +rate 11252kbit +wait 1s +rate 14156kbit +wait 1s +rate 8132kbit +wait 1s +rate 4056kbit +wait 1s +rate 7500kbit +wait 1s +rate 12652kbit +wait 1s +rate 5448kbit +wait 1s +rate 7148kbit +wait 1s +rate 10264kbit +wait 1s +rate 13568kbit +wait 1s +rate 11032kbit +wait 1s +rate 20752kbit +wait 1s +rate 20424kbit +wait 1s +rate 26300kbit +wait 1s +rate 26228kbit +wait 1s +rate 18736kbit +wait 1s +rate 6948kbit +wait 1s +rate 4592kbit +wait 1s +rate 20672kbit +wait 1s +rate 24896kbit +wait 1s +rate 21432kbit +wait 1s +rate 16800kbit +wait 1s +rate 6252kbit +wait 1s +rate 17712kbit +wait 1s +rate 10164kbit +wait 1s +rate 1200kbit +wait 1s +rate 10128kbit +wait 1s +rate 7908kbit +wait 1s +rate 3548kbit +wait 1s +rate 4060kbit +wait 1s +rate 5124kbit +wait 1s +rate 3400kbit +wait 1s +rate 3964kbit +wait 1s +rate 1668kbit +wait 1s +rate 2012kbit +wait 1s +rate 2452kbit +wait 1s +rate 4352kbit +wait 1s +rate 12428kbit +wait 1s +rate 16532kbit +wait 1s +rate 15072kbit +wait 1s +rate 11880kbit +wait 1s +rate 14312kbit +wait 1s +rate 9920kbit +wait 1s +rate 9320kbit +wait 1s +rate 10004kbit +wait 1s +rate 9700kbit +wait 1s +rate 10476kbit +wait 1s +rate 19396kbit +wait 1s +rate 16992kbit +wait 1s +rate 18416kbit +wait 1s +rate 19528kbit +wait 1s +rate 18676kbit +wait 1s +rate 14024kbit +wait 1s +rate 6356kbit +wait 1s +rate 28344kbit +wait 1s +rate 30276kbit +wait 1s +rate 18568kbit +wait 1s +rate 15844kbit +wait 1s +rate 14020kbit +wait 1s +rate 9264kbit +wait 1s +rate 15828kbit +wait 1s +rate 21160kbit +wait 1s +rate 18216kbit +wait 1s +rate 19320kbit +wait 1s +rate 15768kbit +wait 1s +rate 17012kbit +wait 1s +rate 15668kbit +wait 1s +rate 13924kbit +wait 1s +rate 25828kbit +wait 1s +rate 22536kbit +wait 1s +rate 19440kbit +wait 1s +rate 19624kbit +wait 1s +rate 16292kbit +wait 1s +rate 20788kbit +wait 1s +rate 7356kbit +wait 1s +rate 27288kbit +wait 1s +rate 24272kbit +wait 1s +rate 13252kbit +wait 1s +rate 11772kbit +wait 1s +rate 14592kbit +wait 1s +rate 12340kbit +wait 1s +rate 10200kbit +wait 1s +rate 10384kbit +wait 1s +rate 9572kbit +wait 1s +rate 8060kbit +wait 1s +rate 12276kbit +wait 1s +rate 10948kbit +wait 1s +rate 3424kbit +wait 1s +rate 5056kbit +wait 1s +rate 7308kbit +wait 1s +rate 14012kbit +wait 1s +rate 9988kbit +wait 1s +rate 10404kbit +wait 1s +rate 17148kbit +wait 1s +rate 16520kbit +wait 1s +rate 8876kbit +wait 1s +rate 13840kbit +wait 1s +rate 19624kbit +wait 1s +rate 22984kbit +wait 1s +rate 23452kbit +wait 1s +rate 23144kbit +wait 1s +rate 27108kbit +wait 1s +rate 12952kbit +wait 1s +rate 16604kbit +wait 1s +rate 24324kbit +wait 1s +rate 16792kbit +wait 1s +rate 21988kbit +wait 1s +rate 14388kbit +wait 1s +rate 21940kbit +wait 1s +rate 19108kbit +wait 1s +rate 18500kbit +wait 1s +rate 18164kbit +wait 1s +rate 12776kbit +wait 1s +rate 21556kbit +wait 1s +rate 19396kbit +wait 1s +rate 20680kbit +wait 1s +rate 19088kbit +wait 1s +rate 13452kbit +wait 1s +rate 19928kbit +wait 1s +rate 18684kbit +wait 1s +rate 26716kbit +wait 1s +rate 17924kbit +wait 1s +rate 17680kbit +wait 1s +rate 14908kbit +wait 1s +rate 23200kbit +wait 1s +rate 22664kbit +wait 1s +rate 16176kbit +wait 1s +rate 19936kbit +wait 1s +rate 15580kbit +wait 1s +rate 20300kbit +wait 1s +rate 9420kbit +wait 1s +rate 9756kbit +wait 1s +rate 6432kbit +wait 1s +rate 3200kbit +wait 1s +rate 3200kbit +wait 1s +rate 3200kbit +wait 1s +rate 3200kbit +wait 1s +rate 14404kbit +wait 1s +rate 2656kbit +wait 1s +rate 3056kbit +wait 1s +rate 3124kbit +wait 1s +rate 9560kbit +wait 1s +rate 2176kbit +wait 1s +rate 3596kbit +wait 1s +rate 8988kbit +wait 1s +rate 11348kbit +wait 1s +rate 6756kbit +wait 1s +rate 8424kbit +wait 1s +rate 13284kbit +wait 1s +rate 5604kbit +wait 1s +rate 1360kbit +wait 1s +rate 1316kbit +wait 1s +rate 1200kbit +wait 1s +rate 1200kbit +wait 1s +rate 1212kbit +wait 1s +rate 1556kbit +wait 1s +rate 1200kbit +wait 1s +rate 1200kbit +wait 1s +rate 1200kbit +wait 1s +rate 3680kbit +wait 1s +rate 1200kbit +wait 1s +rate 1200kbit +wait 1s +rate 1200kbit +wait 1s +rate 1200kbit +wait 1s +rate 1200kbit +wait 1s +rate 1200kbit +wait 1s +rate 1200kbit +wait 1s +rate 1200kbit +wait 1s +rate 20900kbit +wait 1s +rate 15280kbit +wait 1s +rate 19080kbit +wait 1s +rate 14456kbit +wait 1s +rate 19348kbit +wait 1s +rate 19724kbit +wait 1s +rate 16092kbit +wait 1s +rate 16936kbit +wait 1s +rate 17212kbit +wait 1s +rate 8572kbit +wait 1s +rate 8720kbit +wait 1s +rate 12288kbit +wait 1s +rate 3312kbit +wait 1s +rate 8708kbit +wait 1s +rate 7568kbit +wait 1s +rate 3420kbit +wait 1s +rate 9352kbit +wait 1s +rate 7020kbit +wait 1s +rate 9252kbit +wait 1s +rate 6092kbit +wait 1s +rate 1712kbit +wait 1s +rate 1200kbit +wait 1s +rate 2912kbit +wait 1s +rate 1312kbit +wait 1s +rate 15612kbit +wait 1s +rate 9996kbit +wait 1s +rate 15456kbit +wait 1s +rate 13404kbit +wait 1s +rate 17300kbit +wait 1s +rate 15804kbit +wait 1s +rate 7504kbit +wait 1s +rate 6328kbit +wait 1s +rate 6496kbit +wait 1s +rate 3460kbit +wait 1s +rate 1908kbit +wait 1s +rate 1908kbit +wait 1s +rate 1908kbit +wait 1s +rate 1908kbit +wait 1s +rate 2768kbit +wait 1s +rate 21584kbit +wait 1s +rate 15456kbit +wait 1s +rate 21268kbit +wait 1s +rate 14168kbit +wait 1s +rate 25504kbit +wait 1s +rate 15672kbit +wait 1s +rate 17196kbit +wait 1s +rate 16008kbit +wait 1s +rate 18696kbit +wait 1s +rate 22988kbit +wait 1s +rate 11700kbit +wait 1s +rate 5992kbit +wait 1s +rate 16072kbit +wait 1s +rate 16824kbit +wait 1s +rate 13976kbit +wait 1s +rate 8672kbit +wait 1s +rate 17164kbit +wait 1s +rate 15372kbit +wait 1s +rate 14144kbit +wait 1s +rate 17844kbit +wait 1s +rate 24036kbit +wait 1s +rate 27412kbit +wait 1s +rate 15916kbit +wait 1s +rate 21856kbit +wait 1s +rate 20132kbit +wait 1s +rate 15372kbit +wait 1s +rate 12196kbit +wait 1s +rate 12648kbit +wait 1s +rate 13008kbit +wait 1s +rate 11456kbit +wait 1s +rate 11844kbit +wait 1s +rate 9876kbit +wait 1s +rate 8132kbit +wait 1s +rate 8288kbit +wait 1s +rate 3488kbit +wait 1s +rate 9604kbit +wait 1s +rate 9920kbit +wait 1s +rate 18104kbit +wait 1s +rate 12820kbit +wait 1s +rate 10248kbit +wait 1s +rate 11332kbit +wait 1s +rate 7660kbit +wait 1s +rate 7268kbit +wait 1s +rate 3380kbit +wait 1s +rate 1200kbit +wait 1s +rate 1200kbit +wait 1s +rate 1340kbit +wait 1s +rate 12268kbit +wait 1s +rate 12268kbit +wait 1s +rate 1200kbit +wait 1s +rate 13624kbit +wait 1s +rate 3280kbit +wait 1s +rate 1952kbit +wait 1s +rate 9336kbit +wait 1s +rate 4528kbit +wait 1s +rate 2700kbit +wait 1s +rate 3536kbit +wait 1s +rate 12180kbit +wait 1s +rate 11580kbit +wait 1s +rate 3164kbit +wait 1s +rate 2464kbit +wait 1s +rate 1352kbit +wait 1s +rate 1676kbit +wait 1s +rate 2312kbit +wait 1s +rate 3260kbit +wait 1s +rate 2884kbit +wait 1s +rate 2392kbit +wait 1s +rate 3600kbit +wait 1s +rate 5096kbit +wait 1s +rate 3496kbit +wait 1s +rate 5608kbit +wait 1s +rate 6816kbit +wait 1s +rate 8648kbit +wait 1s +rate 6280kbit +wait 1s +rate 8876kbit +wait 1s +rate 13680kbit +wait 1s +rate 15660kbit +wait 1s +rate 18300kbit +wait 1s +rate 15496kbit +wait 1s +rate 10552kbit +wait 1s +rate 5552kbit +wait 1s +rate 6432kbit +wait 1s +rate 11092kbit +wait 1s +rate 4968kbit +wait 1s +rate 4896kbit +wait 1s +rate 8292kbit +wait 1s +rate 12904kbit +wait 1s +rate 9748kbit +wait 1s +rate 13232kbit +wait 1s +rate 7848kbit +wait 1s +rate 3084kbit +wait 1s +rate 1620kbit +wait 1s +rate 3504kbit +wait 1s +rate 3584kbit +wait 1s +rate 5300kbit +wait 1s +rate 6360kbit +wait 1s +rate 3308kbit +wait 1s +rate 5364kbit +wait 1s +rate 5760kbit +wait 1s +rate 5760kbit +wait 1s +rate 1200kbit +wait 1s +rate 13812kbit +wait 1s +rate 19620kbit +wait 1s +rate 14780kbit +wait 1s +rate 8812kbit +wait 1s +rate 13056kbit +wait 1s +rate 11668kbit +wait 1s +rate 11244kbit +wait 1s +rate 20752kbit +wait 1s +rate 19420kbit +wait 1s +rate 11636kbit +wait 1s +rate 7772kbit +wait 1s +rate 12980kbit +wait 1s +rate 6064kbit +wait 1s +rate 8732kbit +wait 1s +rate 5292kbit +wait 1s +rate 9012kbit +wait 1s +rate 8940kbit +wait 1s +rate 7664kbit +wait 1s +rate 6620kbit +wait 1s +rate 10624kbit +wait 1s +rate 17256kbit +wait 1s +rate 12856kbit +wait 1s +rate 15852kbit +wait 1s +rate 11756kbit +wait 1s +rate 20528kbit +wait 1s +rate 9320kbit +wait 1s +rate 6220kbit +wait 1s +rate 10748kbit +wait 1s +rate 16960kbit +wait 1s +rate 10712kbit +wait 1s +rate 21580kbit +wait 1s +rate 19452kbit +wait 1s +rate 16760kbit +wait 1s +rate 17936kbit +wait 1s +rate 15232kbit +wait 1s +rate 12576kbit +wait 1s +rate 18004kbit +wait 1s +rate 13912kbit +wait 1s +rate 17664kbit +wait 1s +rate 12420kbit +wait 1s +rate 16104kbit +wait 1s +rate 18476kbit +wait 1s +rate 11820kbit +wait 1s +rate 19208kbit +wait 1s +rate 16196kbit +wait 1s +rate 20976kbit +wait 1s +rate 28344kbit +wait 1s +rate 17964kbit +wait 1s +rate 16360kbit +wait 1s +rate 28576kbit +wait 1s +rate 21896kbit +wait 1s +rate 17496kbit +wait 1s +rate 14404kbit +wait 1s +rate 20156kbit +wait 1s +rate 15904kbit +wait 1s +rate 15088kbit +wait 1s +rate 15516kbit +wait 1s +rate 14552kbit +wait 1s +rate 16092kbit +wait 1s +rate 14704kbit +wait 1s +rate 13500kbit +wait 1s +rate 10612kbit +wait 1s +rate 7024kbit +wait 1s +rate 7664kbit +wait 1s +rate 3260kbit +wait 1s +rate 3260kbit +wait 1s +rate 4540kbit +wait 1s +rate 1200kbit +wait 1s +rate 1200kbit +wait 1s +rate 12340kbit +wait 1s +rate 7268kbit +wait 1s +rate 10896kbit +wait 1s +rate 7948kbit +wait 1s +rate 3504kbit +wait 1s +rate 9512kbit +wait 1s +rate 7952kbit +wait 1s +rate 9532kbit +wait 1s +rate 9384kbit +wait 1s +rate 8092kbit +wait 1s +rate 14020kbit +wait 1s +rate 14312kbit +wait 1s +rate 7812kbit +wait 1s +rate 7632kbit +wait 1s +rate 10776kbit +wait 1s +rate 9592kbit +wait 1s +rate 9492kbit +wait 1s +rate 10320kbit +wait 1s +rate 11296kbit +wait 1s +rate 8296kbit +wait 1s +rate 10452kbit +wait 1s +rate 9016kbit +wait 1s +rate 10116kbit +wait 1s +rate 6040kbit +wait 1s +rate 3892kbit +wait 1s +rate 2080kbit +wait 1s +rate 3104kbit +wait 1s +rate 1204kbit +wait 1s +rate 1200kbit +wait 1s +rate 2032kbit +wait 1s +rate 2304kbit +wait 1s +rate 5100kbit +wait 1s +rate 7504kbit +wait 1s +rate 8552kbit +wait 1s +rate 5732kbit +wait 1s +rate 4896kbit +wait 1s +rate 3840kbit +wait 1s +rate 9688kbit +wait 1s +rate 7308kbit +wait 1s +rate 9732kbit +wait 1s +rate 11780kbit +wait 1s +rate 13440kbit +wait 1s +rate 11396kbit +wait 1s +rate 8224kbit +wait 1s +rate 10444kbit +wait 1s +rate 9096kbit +wait 1s +rate 8048kbit +wait 1s +rate 7992kbit +wait 1s +rate 10588kbit +wait 1s +rate 8688kbit +wait 1s +rate 10300kbit +wait 1s +rate 9592kbit +wait 1s +rate 13768kbit +wait 1s +rate 10532kbit +wait 1s +rate 4276kbit +wait 1s +rate 4848kbit +wait 1s +rate 5888kbit +wait 1s +rate 1200kbit +wait 1s +rate 8680kbit +wait 1s +rate 1200kbit +wait 1s +rate 6364kbit +wait 1s +rate 8276kbit +wait 1s +rate 6932kbit +wait 1s +rate 9016kbit +wait 1s +rate 8668kbit +wait 1s +rate 9056kbit +wait 1s +rate 5372kbit +wait 1s +rate 13608kbit +wait 1s +rate 9460kbit +wait 1s +rate 4500kbit +wait 1s +rate 28032kbit +wait 1s +rate 10736kbit +wait 1s +rate 15216kbit +wait 1s +rate 10680kbit +wait 1s +rate 16616kbit +wait 1s +rate 13244kbit +wait 1s +rate 16948kbit +wait 1s +rate 16220kbit +wait 1s +rate 16728kbit +wait 1s +rate 15596kbit +wait 1s +rate 16384kbit +wait 1s +rate 14428kbit +wait 1s +rate 18748kbit +wait 1s +rate 16660kbit +wait 1s +rate 14520kbit +wait 1s +rate 19836kbit +wait 1s +rate 18720kbit +wait 1s +rate 12948kbit +wait 1s +rate 21120kbit +wait 1s +rate 15056kbit +wait 1s +rate 15104kbit +wait 1s +rate 9120kbit +wait 1s +rate 17984kbit +wait 1s +rate 15164kbit +wait 1s +rate 15976kbit +wait 1s +rate 22768kbit +wait 1s +rate 21688kbit +wait 1s +rate 22516kbit +wait 1s +rate 14312kbit +wait 1s +rate 16560kbit +wait 1s +rate 11236kbit +wait 1s +rate 20636kbit +wait 1s +rate 15960kbit +wait 1s +rate 19616kbit +wait 1s +rate 15528kbit +wait 1s +rate 12760kbit +wait 1s +rate 26984kbit +wait 1s +rate 27140kbit +wait 1s +rate 26420kbit +wait 1s +rate 22592kbit +wait 1s +rate 23740kbit +wait 1s +rate 14544kbit +wait 1s +rate 25280kbit +wait 1s +rate 20148kbit +wait 1s +rate 19472kbit +wait 1s +rate 21824kbit +wait 1s +rate 14880kbit +wait 1s +rate 18044kbit +wait 1s +rate 22048kbit +wait 1s +rate 2772kbit +wait 1s +rate 4832kbit +wait 1s +rate 5984kbit +wait 1s +rate 3564kbit +wait 1s +rate 10496kbit +wait 1s +rate 5480kbit +wait 1s +rate 6148kbit +wait 1s +rate 6264kbit +wait 1s +rate 16184kbit +wait 1s +rate 10360kbit +wait 1s +rate 11252kbit +wait 1s +rate 14156kbit +wait 1s +rate 8132kbit +wait 1s +rate 4056kbit +wait 1s +rate 7500kbit +wait 1s +rate 12652kbit +wait 1s +rate 5448kbit +wait 1s +rate 7148kbit +wait 1s +rate 10264kbit +wait 1s +rate 13568kbit +wait 1s +rate 11032kbit +wait 1s +rate 20752kbit +wait 1s +rate 20424kbit +wait 1s +rate 26300kbit +wait 1s +rate 26228kbit +wait 1s +rate 18736kbit +wait 1s +rate 6948kbit +wait 1s +rate 4592kbit +wait 1s +rate 20672kbit +wait 1s +rate 24896kbit +wait 1s +rate 21432kbit +wait 1s +rate 16800kbit +wait 1s +rate 6252kbit +wait 1s +rate 17712kbit +wait 1s +rate 10164kbit +wait 1s +rate 1200kbit +wait 1s +rate 10128kbit +wait 1s +rate 7908kbit +wait 1s +rate 3548kbit +wait 1s +rate 4060kbit +wait 1s +rate 5124kbit +wait 1s +rate 3400kbit +wait 1s +rate 3964kbit +wait 1s +rate 1668kbit +wait 1s +rate 2012kbit +wait 1s +rate 2452kbit +wait 1s +rate 4352kbit +wait 1s +rate 12428kbit +wait 1s +rate 16532kbit +wait 1s +rate 15072kbit +wait 1s +rate 11880kbit +wait 1s +rate 14312kbit +wait 1s +rate 9920kbit +wait 1s +rate 9320kbit +wait 1s +rate 10004kbit +wait 1s +rate 9700kbit +wait 1s +rate 10476kbit +wait 1s +rate 19396kbit +wait 1s +rate 16992kbit +wait 1s +rate 18416kbit +wait 1s +rate 19528kbit +wait 1s +rate 18676kbit +wait 1s +rate 14024kbit +wait 1s +rate 6356kbit +wait 1s +rate 28344kbit +wait 1s +rate 30276kbit +wait 1s +rate 18568kbit +wait 1s +rate 15844kbit +wait 1s +rate 14020kbit +wait 1s +rate 9264kbit +wait 1s +rate 15828kbit +wait 1s +rate 21160kbit +wait 1s +rate 18216kbit +wait 1s +rate 19320kbit +wait 1s +rate 15768kbit +wait 1s +rate 17012kbit +wait 1s +rate 15668kbit +wait 1s +rate 13924kbit +wait 1s +rate 25828kbit +wait 1s +rate 22536kbit +wait 1s +rate 19440kbit +wait 1s +rate 19624kbit +wait 1s +rate 16292kbit +wait 1s +rate 20788kbit +wait 1s +rate 7356kbit +wait 1s +rate 27288kbit +wait 1s +rate 24272kbit +wait 1s +rate 13252kbit +wait 1s +rate 11772kbit +wait 1s +rate 14592kbit +wait 1s +rate 12340kbit +wait 1s +rate 10200kbit +wait 1s +rate 10384kbit +wait 1s +rate 9572kbit +wait 1s \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/tc_clear.sh b/repos/moq-rs/tc_profiles/tc_clear.sh new file mode 100644 index 0000000..99be55a --- /dev/null +++ b/repos/moq-rs/tc_profiles/tc_clear.sh @@ -0,0 +1,15 @@ +TC='/sbin/tc' +INTERFACE_1=$1 + +if [ -z $INTERFACE_1 ]; then + echo "interface has to be specified" + exit 1; +fi + +killall tc_policy.sh 1>/dev/null 2>&1 +killall sleep 1>/dev/null 2>&1 +killall tc 1>/dev/null 2>&1 + +$TC qdisc del dev $INTERFACE_1 root handle 1:0 1>/dev/null 2>&1 +$TC qdisc del dev $INTERFACE_1 root 1>/dev/null 2>&1 +$TC qdisc del dev lo root 1>/dev/null 2>&1 diff --git a/repos/moq-rs/tc_profiles/tc_limit.sh b/repos/moq-rs/tc_profiles/tc_limit.sh new file mode 100644 index 0000000..7e7990d --- /dev/null +++ b/repos/moq-rs/tc_profiles/tc_limit.sh @@ -0,0 +1 @@ +sudo tc qdisc add dev enp0s31f6 root tbf rate 1500kbit burst 16kbit latency 10ms \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/tc_netem.sh b/repos/moq-rs/tc_profiles/tc_netem.sh new file mode 100644 index 0000000..985bf85 --- /dev/null +++ b/repos/moq-rs/tc_profiles/tc_netem.sh @@ -0,0 +1,8 @@ +INTERFACE=$1 +DELAY_MS=40 +RATE_MBIT=$2 +BUF_PKTS=33 +BDP_BYTES=$(echo "($DELAY_MS/1000.0)*($RATE_MBIT*1000000.0/8.0)" | bc -q -l) +BDP_PKTS=$(echo "$BDP_BYTES/1500" | bc -q) +LIMIT_PKTS=$(echo "$BDP_PKTS+$BUF_PKTS" | bc -q) +tc qdisc replace dev $INTERFACE root netem delay ${DELAY_MS}ms rate ${RATE_MBIT}Mbit limit ${LIMIT_PKTS} diff --git a/repos/moq-rs/tc_profiles/tc_policy.sh b/repos/moq-rs/tc_profiles/tc_policy.sh new file mode 100644 index 0000000..8f412eb --- /dev/null +++ b/repos/moq-rs/tc_profiles/tc_policy.sh @@ -0,0 +1,108 @@ +#!/bin/bash +TC="/sbin/tc" +PORT_1=8443 +INTERFACE_1=enp0s31f6 #$1 +FILE_1=zafer_profile #$2 + +#PORT=443 +#INTERFACE_2=$INTERFACE_1 +#FILE_2=$FILE_1 + + +if [ -z $INTERFACE_1 ]; then + echo "interface has to be specified" + exit 1; +fi + +if [ -z $FILE_1 ]; then + echo "policy file name has to be specified" + exit 1; +fi + +parsePolicyFile () { + device=$1 + filename=$2 + classId=$3 + childClassId=$4 + if [ -z "$filename" ] || [ -z "$classId" ];then + echo "filename and classid paramters required" + else + latestLoss="0%"; + latestDelay="0ms"; + while read -r line; do + if [[ $line == \#* ]];then + continue; + else + keys=($line) + comm=${keys[0]} + value=${keys[1]} + case $comm in + rate) + echo "setting rate on $device $classId $value" + burst=`awk "BEGIN {print $value/800*1000}"` + $TC class change dev $device parent 1: classid 1:$classId htb rate $value burst ${burst} cburst ${burst} + $TC class change dev $device parent 1: classid 1:$childClassId htb rate $value burst ${burst} cburst ${burst} + #$TC qdisc del dev $device root 1>/dev/null 2>&1 + #$TC qdisc change dev $device root tbf rate $value burst $burst latency 1ms + ;; + loss) + latestLoss=$value; + echo "setting loss on $device $classId $value" + $TC qdisc change dev $device parent 1:$classId netem loss $latestLoss delay $latestDelay + ;; + delay) + latestDelay=$value; + echo "setting delay on $device $classId $value" + $TC qdisc change dev $device parent 1:$classId netem loss $latestLoss delay $latestDelay + ;; + wait) + echo "waiting for $device $value seconds" + sleep $value + ;; + esac + fi + done < "$filename" + fi +} + +policyLoop () { + device=$1 + filename=$2 + classId=$3 + childClassId=$4 + while true; do + parsePolicyFile $device $filename $classId $childClassId + done +} + +currentIfNo=1 +while [[ -v INTERFACE_$currentIfNo ]]; do + interface=INTERFACE_$currentIfNo + interface="${!interface}" + $TC qdisc del dev $interface root 1>/dev/null 2>&1 + $TC qdisc add dev $interface root handle 1: htb default 10 + ((currentIfNo++)) +done + +currentIfNo=1 +while [[ -v PORT_$currentIfNo ]]; do + interface=INTERFACE_$currentIfNo + interface="${!interface}" + port=PORT_$currentIfNo + port="${!port}" + file=FILE_$currentIfNo + file="${!file}" + + childIfNo=${currentIfNo}0 + $TC class add dev $interface parent 1: classid 1:$currentIfNo htb rate 1024Mbps + $TC class add dev $interface parent 1:$currentIfNo classid 1:$childIfNo htb rate 1024Mbps + $TC qdisc add dev $interface parent 1:$childIfNo handle 10: sfq perturb 10 + $TC filter add dev $interface parent 1:0 protocol ip prio 1 u32 match ip sport $port 0xffff flowid 1:$childIfNo + policyLoop $interface $file $currentIfNo $childIfNo & + ((currentIfNo++)) +done + +wait + + #$TC qdisc del dev $device root 1>/dev/null 2>&1 + #$TC qdisc add dev $device root tbf rate $value burst $burst latency 1ms diff --git a/repos/moq-rs/tc_profiles/tc_start.sh b/repos/moq-rs/tc_profiles/tc_start.sh new file mode 100644 index 0000000..c527bdb --- /dev/null +++ b/repos/moq-rs/tc_profiles/tc_start.sh @@ -0,0 +1,6 @@ +#!/bin/bash +INTERFACE_1="en0" +PROFILE="lte_profile" + +sudo bash "tc_clear.sh" $INTERFACE_1 +sudo bash "tc_policy.sh" $INTERFACE_1 $PROFILE diff --git a/repos/moq-rs/tc_profiles/twitch_profile b/repos/moq-rs/tc_profiles/twitch_profile new file mode 100644 index 0000000..b7e3531 --- /dev/null +++ b/repos/moq-rs/tc_profiles/twitch_profile @@ -0,0 +1,76 @@ +rate 440kbit +wait 10s +rate 370kbit +wait 5s +rate 370kbit +wait 5s +rate 2598kbit +wait 5s +rate 2022kbit +wait 5s +rate 2022kbit +wait 5s +rate 2367kbit +wait 5s +rate 2367kbit +wait 5s +rate 1831kbit +wait 5s +rate 1831kbit +wait 5s +rate 1541kbit +wait 5s +rate 1541kbit +wait 5s +rate 2158kbit +wait 5s +rate 2158kbit +wait 5s +rate 295kbit +wait 5s +rate 295kbit +wait 5s +rate 2544kbit +wait 5s +rate 1780kbit +wait 5s +rate 1780kbit +wait 5s +rate 1330kbit +wait 5s +rate 1330kbit +wait 5s +rate 300kbit +wait 5s +rate 300kbit +wait 5s +rate 346kbit +wait 5s +rate 346kbit +wait 5s +rate 346kbit +wait 5s +rate 346kbit +wait 5s +rate 457kbit +wait 5s +rate 457kbit +wait 5s +rate 306kbit +wait 5s +rate 306kbit +wait 5s +rate 1806kbit +wait 5s +rate 1806kbit +wait 5s +rate 2379kbit +wait 5s +rate 2379kbit +wait 5s +rate 1066kbit +wait 5s +rate 2260kbit +wait 5s +rate 2260kbit +wait 5s diff --git a/repos/moq-rs/tc_profiles/twitch_profile_x0.25 b/repos/moq-rs/tc_profiles/twitch_profile_x0.25 new file mode 100644 index 0000000..b57b350 --- /dev/null +++ b/repos/moq-rs/tc_profiles/twitch_profile_x0.25 @@ -0,0 +1,78 @@ +rate 505kbit +wait 5s +rate 110kbit +wait 5s +rate 92kbit +wait 5s +rate 92kbit +wait 5s +rate 649kbit +wait 5s +rate 505kbit +wait 5s +rate 505kbit +wait 5s +rate 591kbit +wait 5s +rate 591kbit +wait 5s +rate 457kbit +wait 5s +rate 457kbit +wait 5s +rate 385kbit +wait 5s +rate 385kbit +wait 5s +rate 539kbit +wait 5s +rate 539kbit +wait 5s +rate 73kbit +wait 5s +rate 73kbit +wait 5s +rate 636kbit +wait 5s +rate 445kbit +wait 5s +rate 445kbit +wait 5s +rate 332kbit +wait 5s +rate 332kbit +wait 5s +rate 75kbit +wait 5s +rate 75kbit +wait 5s +rate 86kbit +wait 5s +rate 86kbit +wait 5s +rate 86kbit +wait 5s +rate 86kbit +wait 5s +rate 114kbit +wait 5s +rate 114kbit +wait 5s +rate 76kbit +wait 5s +rate 76kbit +wait 5s +rate 451kbit +wait 5s +rate 451kbit +wait 5s +rate 594kbit +wait 5s +rate 594kbit +wait 5s +rate 266kbit +wait 5s +rate 565kbit +wait 5s +rate 565kbit +wait 5s diff --git a/repos/moq-rs/tc_profiles/twitch_profile_x3 b/repos/moq-rs/tc_profiles/twitch_profile_x3 new file mode 100644 index 0000000..f9677c4 --- /dev/null +++ b/repos/moq-rs/tc_profiles/twitch_profile_x3 @@ -0,0 +1,76 @@ +rate 1320kbit +wait 5s +rate 1110kbit +wait 5s +rate 1110kbit +wait 5s +rate 7794kbit +wait 5s +rate 6066kbit +wait 5s +rate 6066kbit +wait 5s +rate 7101kbit +wait 5s +rate 7101kbit +wait 5s +rate 5493kbit +wait 5s +rate 5493kbit +wait 5s +rate 4623kbit +wait 5s +rate 4623kbit +wait 5s +rate 6474kbit +wait 5s +rate 6474kbit +wait 5s +rate 885kbit +wait 5s +rate 885kbit +wait 5s +rate 7632kbit +wait 5s +rate 5340kbit +wait 5s +rate 5340kbit +wait 5s +rate 3990kbit +wait 5s +rate 3990kbit +wait 5s +rate 900kbit +wait 5s +rate 900kbit +wait 5s +rate 1038kbit +wait 5s +rate 1038kbit +wait 5s +rate 1038kbit +wait 5s +rate 1038kbit +wait 5s +rate 1371kbit +wait 5s +rate 1371kbit +wait 5s +rate 918kbit +wait 5s +rate 918kbit +wait 5s +rate 5418kbit +wait 5s +rate 5418kbit +wait 5s +rate 7137kbit +wait 5s +rate 7137kbit +wait 5s +rate 3198kbit +wait 5s +rate 6780kbit +wait 5s +rate 6780kbit +wait 5s \ No newline at end of file diff --git a/repos/moq-rs/tc_profiles/twitch_profile_x4 b/repos/moq-rs/tc_profiles/twitch_profile_x4 new file mode 100644 index 0000000..09f4cb9 --- /dev/null +++ b/repos/moq-rs/tc_profiles/twitch_profile_x4 @@ -0,0 +1,76 @@ +rate 1760kbit +wait 5s +rate 1480kbit +wait 5s +rate 1480kbit +wait 5s +rate 10392kbit +wait 5s +rate 8088kbit +wait 5s +rate 8088kbit +wait 5s +rate 9468kbit +wait 5s +rate 9468kbit +wait 5s +rate 7324kbit +wait 5s +rate 7324kbit +wait 5s +rate 6164kbit +wait 5s +rate 6164kbit +wait 5s +rate 8632kbit +wait 5s +rate 8632kbit +wait 5s +rate 1180kbit +wait 5s +rate 1180kbit +wait 5s +rate 10176kbit +wait 5s +rate 7120kbit +wait 5s +rate 7120kbit +wait 5s +rate 5320kbit +wait 5s +rate 5320kbit +wait 5s +rate 1200kbit +wait 5s +rate 1200kbit +wait 5s +rate 1384kbit +wait 5s +rate 1384kbit +wait 5s +rate 1384kbit +wait 5s +rate 1384kbit +wait 5s +rate 1828kbit +wait 5s +rate 1828kbit +wait 5s +rate 1224kbit +wait 5s +rate 1224kbit +wait 5s +rate 7224kbit +wait 5s +rate 7224kbit +wait 5s +rate 9516kbit +wait 5s +rate 9516kbit +wait 5s +rate 4264kbit +wait 5s +rate 9040kbit +wait 5s +rate 9040kbit +wait 5s \ No newline at end of file diff --git a/repos/moq-rs/third_party/mp4-rust/.github/workflows/rust.yml b/repos/moq-rs/third_party/mp4-rust/.github/workflows/rust.yml new file mode 100644 index 0000000..e2d357d --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/.github/workflows/rust.yml @@ -0,0 +1,50 @@ +name: Rust + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install rust toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt, clippy + + - name: Setup rust smart caching + uses: Swatinem/rust-cache@v1.3.0 + + - name: Cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --no-deps -- -D warnings + + - name: Cargo build + uses: actions-rs/cargo@v1 + with: + command: build + + - name: Cargo test + uses: actions-rs/cargo@v1 + with: + command: test diff --git a/repos/moq-rs/third_party/mp4-rust/.gitignore b/repos/moq-rs/third_party/mp4-rust/.gitignore new file mode 100644 index 0000000..3f946ee --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/.gitignore @@ -0,0 +1,9 @@ +/Cargo.lock +/target +**/*.rs.bk +*.exe +*.pdb +*.mp4 +.idea/ +.vscode/ +!tests/samples/*.mp4 diff --git a/repos/moq-rs/third_party/mp4-rust/Cargo.toml b/repos/moq-rs/third_party/mp4-rust/Cargo.toml new file mode 100644 index 0000000..ec35a0d --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "mp4" +version = "0.14.0" +authors = ["Alf "] +edition = "2018" +description = "MP4 reader and writer library in Rust." +documentation = "https://docs.rs/mp4" +readme = "README.md" +homepage = "https://github.com/alfg/mp4-rust" +repository = "https://github.com/alfg/mp4-rust" +keywords = ["mp4", "iso-mp4", "isobmff", "video", "multimedia"] +license = "MIT" +include = ["src", "benches", "Cargo.toml", "README", "LICENSE"] + +[dependencies] +thiserror = "^1.0" +byteorder = "1" +bytes = "1.1.0" +num-rational = { version = "0.4.0", features = ["serde"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[dev-dependencies] +criterion = "0.3" + +[[bench]] +name = "bench_main" +harness = false diff --git a/repos/moq-rs/third_party/mp4-rust/LICENSE b/repos/moq-rs/third_party/mp4-rust/LICENSE new file mode 100644 index 0000000..c0309cd --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Alfred Gutierrez + +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. \ No newline at end of file diff --git a/repos/moq-rs/third_party/mp4-rust/README.md b/repos/moq-rs/third_party/mp4-rust/README.md new file mode 100644 index 0000000..91778ec --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/README.md @@ -0,0 +1,151 @@ +# mp4 +> MP4 Reader and Writer in Rust 🦀 + +`mp4` is a Rust library to read and write ISO-MP4 files. This package contains MPEG-4 specifications defined in parts: +* [ISO/IEC 14496-12](https://en.wikipedia.org/wiki/ISO/IEC_base_media_file_format) - ISO Base Media File Format (QuickTime, MPEG-4, etc) +* [ISO/IEC 14496-14](https://en.wikipedia.org/wiki/MPEG-4_Part_14) - MP4 file format +* ISO/IEC 14496-17 - Streaming text format + +https://crates.io/crates/mp4 + +[![Crates.io](https://img.shields.io/crates/v/mp4)](https://crates.io/crates/mp4) +[![Crates.io](https://img.shields.io/crates/d/mp4)](https://crates.io/crates/mp4) +[![Docs](https://img.shields.io/badge/docs-online-5023dd.svg?style=flat-square)](https://docs.rs/mp4) +[![Rust](https://github.com/alfg/mp4-rust/workflows/Rust/badge.svg)](https://github.com/alfg/mp4-rust/actions) + +#### Example +```rust +use std::fs::File; +use std::io::{BufReader}; +use mp4::{Result}; + +fn main() -> Result<()> { + let f = File::open("tests/samples/minimal.mp4").unwrap(); + let size = f.metadata()?.len(); + let reader = BufReader::new(f); + + let mp4 = mp4::Mp4Reader::read_header(reader, size)?; + + // Print boxes. + println!("major brand: {}", mp4.ftyp.major_brand); + println!("timescale: {}", mp4.moov.mvhd.timescale); + + // Use available methods. + println!("size: {}", mp4.size()); + + let mut compatible_brands = String::new(); + for brand in mp4.compatible_brands().iter() { + compatible_brands.push_str(&brand.to_string()); + compatible_brands.push_str(","); + } + println!("compatible brands: {}", compatible_brands); + println!("duration: {:?}", mp4.duration()); + + // Track info. + for track in mp4.tracks().values() { + println!( + "track: #{}({}) {} : {}", + track.track_id(), + track.language(), + track.track_type()?, + track.box_type()?, + ); + } + Ok(()) +} +``` + +See [examples/](examples/) for more examples. + +#### Install +``` +cargo add mp4 +``` +or add to your `Cargo.toml`: +```toml +mp4 = "0.14.0" +``` + +#### Documentation +* https://docs.rs/mp4/ + +## Development + +#### Requirements +* [Rust](https://www.rust-lang.org/) + +#### Build +``` +cargo build +``` + +#### Lint and Format +``` +cargo clippy --fix +cargo fmt --all +``` + +#### Run Examples +* `mp4info` +``` +cargo run --example mp4info +``` + +* `mp4dump` +``` +cargo run --example mp4dump +``` + +#### Run Tests +``` +cargo test +``` + +With print statement output. +``` +cargo test -- --nocapture +``` + +#### Run Cargo fmt +Run fmt to catch formatting errors. + +``` +rustup component add rustfmt +cargo fmt --all -- --check +``` + +#### Run Clippy +Run Clippy tests to catch common lints and mistakes. + +``` +rustup component add clippy +cargo clippy --no-deps -- -D warnings +``` + +#### Run Benchmark Tests +``` +cargo bench +``` + +View HTML report at `target/criterion/report/index.html` + +#### Generate Docs +``` +cargo docs +``` + +View at `target/doc/mp4/index.html` + +## Web Assembly +See the [mp4-inspector](https://github.com/alfg/mp4-inspector) project as a reference for using this library in Javascript via Web Assembly. + +## Related Projects +* https://github.com/mozilla/mp4parse-rust +* https://github.com/pcwalton/rust-media +* https://github.com/alfg/mp4 + +## License +MIT + +[docs]: https://docs.rs/mp4 +[docs-badge]: https://img.shields.io/badge/docs-online-5023dd.svg?style=flat-square diff --git a/repos/moq-rs/third_party/mp4-rust/benches/bench_main.rs b/repos/moq-rs/third_party/mp4-rust/benches/bench_main.rs new file mode 100644 index 0000000..99e1ab5 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/benches/bench_main.rs @@ -0,0 +1,26 @@ +use criterion::BenchmarkId; +use criterion::{criterion_group, criterion_main, Criterion}; + +use std::fs::File; + +fn read_mp4(filename: &str) -> u64 { + let f = File::open(filename).unwrap(); + let m = mp4::read_mp4(f).unwrap(); + + m.size() +} + +fn criterion_benchmark(c: &mut Criterion) { + let filename = "tests/samples/minimal.mp4"; + + c.bench_with_input( + BenchmarkId::new("input_example", filename), + &filename, + |b, &s| { + b.iter(|| read_mp4(s)); + }, + ); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/repos/moq-rs/third_party/mp4-rust/examples/mp4copy.rs b/repos/moq-rs/third_party/mp4-rust/examples/mp4copy.rs new file mode 100644 index 0000000..98d1ba8 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/examples/mp4copy.rs @@ -0,0 +1,93 @@ +use std::env; +use std::fs::File; +use std::io::prelude::*; +use std::io::{self, BufReader, BufWriter}; +use std::path::Path; + +use mp4::{ + AacConfig, AvcConfig, HevcConfig, MediaConfig, MediaType, Mp4Config, Result, TrackConfig, + TtxtConfig, Vp9Config, +}; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() < 3 { + println!("Usage: mp4copy "); + std::process::exit(1); + } + + if let Err(err) = copy(&args[1], &args[2]) { + let _ = writeln!(io::stderr(), "{}", err); + } +} + +fn copy>(src_filename: &P, dst_filename: &P) -> Result<()> { + let src_file = File::open(src_filename)?; + let size = src_file.metadata()?.len(); + let reader = BufReader::new(src_file); + + let dst_file = File::create(dst_filename)?; + let writer = BufWriter::new(dst_file); + + let mut mp4_reader = mp4::Mp4Reader::read_header(reader, size)?; + let mut mp4_writer = mp4::Mp4Writer::write_start( + writer, + &Mp4Config { + major_brand: *mp4_reader.major_brand(), + minor_version: mp4_reader.minor_version(), + compatible_brands: mp4_reader.compatible_brands().to_vec(), + timescale: mp4_reader.timescale(), + }, + )?; + + // TODO interleaving + for track in mp4_reader.tracks().values() { + let media_conf = match track.media_type()? { + MediaType::H264 => MediaConfig::AvcConfig(AvcConfig { + width: track.width(), + height: track.height(), + seq_param_set: track.sequence_parameter_set()?.to_vec(), + pic_param_set: track.picture_parameter_set()?.to_vec(), + }), + MediaType::H265 => MediaConfig::HevcConfig(HevcConfig { + width: track.width(), + height: track.height(), + }), + MediaType::VP9 => MediaConfig::Vp9Config(Vp9Config { + width: track.width(), + height: track.height(), + }), + MediaType::AAC => MediaConfig::AacConfig(AacConfig { + bitrate: track.bitrate(), + profile: track.audio_profile()?, + freq_index: track.sample_freq_index()?, + chan_conf: track.channel_config()?, + }), + MediaType::TTXT => MediaConfig::TtxtConfig(TtxtConfig {}), + }; + + let track_conf = TrackConfig { + track_type: track.track_type()?, + timescale: track.timescale(), + language: track.language().to_string(), + media_conf, + }; + + mp4_writer.add_track(&track_conf)?; + } + + for track_id in mp4_reader.tracks().keys().copied().collect::>() { + let sample_count = mp4_reader.sample_count(track_id)?; + for sample_idx in 0..sample_count { + let sample_id = sample_idx + 1; + let sample = mp4_reader.read_sample(track_id, sample_id)?.unwrap(); + mp4_writer.write_sample(track_id, &sample)?; + // println!("copy {}:({})", sample_id, sample); + } + } + + mp4_writer.write_end()?; + + Ok(()) +} diff --git a/repos/moq-rs/third_party/mp4-rust/examples/mp4dump.rs b/repos/moq-rs/third_party/mp4-rust/examples/mp4dump.rs new file mode 100644 index 0000000..6a97d9a --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/examples/mp4dump.rs @@ -0,0 +1,142 @@ +use std::env; +use std::fs::File; +use std::io::prelude::*; +use std::io::{self, BufReader}; +use std::path::Path; + +use mp4::{Mp4Box, Result}; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() < 2 { + println!("Usage: mp4dump "); + std::process::exit(1); + } + + if let Err(err) = dump(&args[1]) { + let _ = writeln!(io::stderr(), "{}", err); + } +} + +fn dump>(filename: &P) -> Result<()> { + let f = File::open(filename)?; + let boxes = get_boxes(f)?; + + // print out boxes + for b in boxes.iter() { + println!("[{}] size={} {}", b.name, b.size, b.summary); + } + + Ok(()) +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Box { + name: String, + size: u64, + summary: String, + indent: u32, +} + +fn get_boxes(file: File) -> Result> { + let size = file.metadata()?.len(); + let reader = BufReader::new(file); + let mp4 = mp4::Mp4Reader::read_header(reader, size)?; + + // collect known boxes + let mut boxes = vec![ + build_box(&mp4.ftyp), + build_box(&mp4.moov), + build_box(&mp4.moov.mvhd), + ]; + + if let Some(ref mvex) = &mp4.moov.mvex { + boxes.push(build_box(mvex)); + if let Some(mehd) = &mvex.mehd { + boxes.push(build_box(mehd)); + } + boxes.push(build_box(&mvex.trex)); + } + + // trak. + for track in mp4.tracks().values() { + boxes.push(build_box(&track.trak)); + boxes.push(build_box(&track.trak.tkhd)); + if let Some(ref edts) = track.trak.edts { + boxes.push(build_box(edts)); + if let Some(ref elst) = edts.elst { + boxes.push(build_box(elst)); + } + } + + // trak.mdia + let mdia = &track.trak.mdia; + boxes.push(build_box(mdia)); + boxes.push(build_box(&mdia.mdhd)); + boxes.push(build_box(&mdia.hdlr)); + boxes.push(build_box(&track.trak.mdia.minf)); + + // trak.mdia.minf + let minf = &track.trak.mdia.minf; + if let Some(ref vmhd) = &minf.vmhd { + boxes.push(build_box(vmhd)); + } + if let Some(ref smhd) = &minf.smhd { + boxes.push(build_box(smhd)); + } + + // trak.mdia.minf.stbl + let stbl = &track.trak.mdia.minf.stbl; + boxes.push(build_box(stbl)); + boxes.push(build_box(&stbl.stsd)); + if let Some(ref avc1) = &stbl.stsd.avc1 { + boxes.push(build_box(avc1)); + } + if let Some(ref hev1) = &stbl.stsd.hev1 { + boxes.push(build_box(hev1)); + } + if let Some(ref mp4a) = &stbl.stsd.mp4a { + boxes.push(build_box(mp4a)); + } + boxes.push(build_box(&stbl.stts)); + if let Some(ref ctts) = &stbl.ctts { + boxes.push(build_box(ctts)); + } + if let Some(ref stss) = &stbl.stss { + boxes.push(build_box(stss)); + } + boxes.push(build_box(&stbl.stsc)); + boxes.push(build_box(&stbl.stsz)); + if let Some(ref stco) = &stbl.stco { + boxes.push(build_box(stco)); + } + if let Some(ref co64) = &stbl.co64 { + boxes.push(build_box(co64)); + } + } + + // If fragmented, add moof boxes. + for moof in mp4.moofs.iter() { + boxes.push(build_box(moof)); + boxes.push(build_box(&moof.mfhd)); + for traf in moof.trafs.iter() { + boxes.push(build_box(traf)); + boxes.push(build_box(&traf.tfhd)); + if let Some(ref trun) = &traf.trun { + boxes.push(build_box(trun)); + } + } + } + + Ok(boxes) +} + +fn build_box(m: &M) -> Box { + Box { + name: m.box_type().to_string(), + size: m.box_size(), + summary: m.summary().unwrap(), + indent: 0, + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/examples/mp4info.rs b/repos/moq-rs/third_party/mp4-rust/examples/mp4info.rs new file mode 100644 index 0000000..00de8ce --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/examples/mp4info.rs @@ -0,0 +1,143 @@ +use std::env; +use std::fs::File; +use std::io::prelude::*; +use std::io::{self, BufReader}; +use std::path::Path; + +use mp4::{Error, Mp4Track, Result, TrackType}; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() < 2 { + println!("Usage: mp4info "); + std::process::exit(1); + } + + if let Err(err) = info(&args[1]) { + let _ = writeln!(io::stderr(), "{}", err); + } +} + +fn info>(filename: &P) -> Result<()> { + let f = File::open(filename)?; + let size = f.metadata()?.len(); + let reader = BufReader::new(f); + + let mp4 = mp4::Mp4Reader::read_header(reader, size)?; + + println!("File:"); + println!(" file size: {}", mp4.size()); + println!(" major_brand: {}", mp4.major_brand()); + let mut compatible_brands = String::new(); + for brand in mp4.compatible_brands().iter() { + compatible_brands.push_str(&brand.to_string()); + compatible_brands.push(' '); + } + println!(" compatible_brands: {}\n", compatible_brands); + + println!("Movie:"); + println!(" version: {}", mp4.moov.mvhd.version); + println!( + " creation time: {}", + creation_time(mp4.moov.mvhd.creation_time) + ); + println!(" duration: {:?}", mp4.duration()); + println!(" fragments: {:?}", mp4.is_fragmented()); + println!(" timescale: {:?}\n", mp4.timescale()); + + println!("Found {} Tracks", mp4.tracks().len()); + for track in mp4.tracks().values() { + let media_info = match track.track_type()? { + TrackType::Video => video_info(track), + TrackType::Audio => audio_info(track), + TrackType::Subtitle => subtitle_info(track), + }; + + println!( + " Track: #{}({}) {}: {}", + track.track_id(), + track.language(), + track.track_type()?, + media_info.unwrap_or_else(|e| e.to_string()) + ); + } + Ok(()) +} + +fn video_info(track: &Mp4Track) -> Result { + if track.trak.mdia.minf.stbl.stsd.avc1.is_some() { + Ok(format!( + "{} ({}) ({:?}), {}x{}, {} kb/s, {:.2} fps", + track.media_type()?, + track.video_profile()?, + track.box_type()?, + track.width(), + track.height(), + track.bitrate() / 1000, + track.frame_rate() + )) + } else { + Ok(format!( + "{} ({:?}), {}x{}, {} kb/s, {:.2} fps", + track.media_type()?, + track.box_type()?, + track.width(), + track.height(), + track.bitrate() / 1000, + track.frame_rate() + )) + } +} + +fn audio_info(track: &Mp4Track) -> Result { + if let Some(ref mp4a) = track.trak.mdia.minf.stbl.stsd.mp4a { + if mp4a.esds.is_some() { + let profile = match track.audio_profile() { + Ok(val) => val.to_string(), + _ => "-".to_string(), + }; + + let channel_config = match track.channel_config() { + Ok(val) => val.to_string(), + _ => "-".to_string(), + }; + + Ok(format!( + "{} ({}) ({:?}), {} Hz, {}, {} kb/s", + track.media_type()?, + profile, + track.box_type()?, + track.sample_freq_index()?.freq(), + channel_config, + track.bitrate() / 1000 + )) + } else { + Ok(format!( + "{} ({:?}), {} kb/s", + track.media_type()?, + track.box_type()?, + track.bitrate() / 1000 + )) + } + } else { + Err(Error::InvalidData("mp4a box not found")) + } +} + +fn subtitle_info(track: &Mp4Track) -> Result { + if track.trak.mdia.minf.stbl.stsd.tx3g.is_some() { + Ok(format!("{} ({:?})", track.media_type()?, track.box_type()?,)) + } else { + Err(Error::InvalidData("tx3g box not found")) + } +} + +fn creation_time(creation_time: u64) -> u64 { + // convert from MP4 epoch (1904-01-01) to Unix epoch (1970-01-01) + if creation_time >= 2082844800 { + creation_time - 2082844800 + } else { + creation_time + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/examples/mp4sample.rs b/repos/moq-rs/third_party/mp4-rust/examples/mp4sample.rs new file mode 100644 index 0000000..6495daf --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/examples/mp4sample.rs @@ -0,0 +1,50 @@ +use std::env; +use std::fs::File; +use std::io::prelude::*; +use std::io::{self, BufReader}; +use std::path::Path; + +use mp4::Result; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() < 2 { + println!("Usage: mp4sample "); + std::process::exit(1); + } + + if let Err(err) = samples(&args[1]) { + let _ = writeln!(io::stderr(), "{}", err); + } +} + +fn samples>(filename: &P) -> Result<()> { + let f = File::open(filename)?; + let size = f.metadata()?.len(); + let reader = BufReader::new(f); + + let mut mp4 = mp4::Mp4Reader::read_header(reader, size)?; + + for track_id in mp4.tracks().keys().copied().collect::>() { + let sample_count = mp4.sample_count(track_id).unwrap(); + + for sample_idx in 0..sample_count { + let sample_id = sample_idx + 1; + let sample = mp4.read_sample(track_id, sample_id); + + if let Some(ref samp) = sample.unwrap() { + println!( + "[{}] start_time={} duration={} rendering_offset={} size={} is_sync={}", + sample_id, + samp.start_time, + samp.duration, + samp.rendering_offset, + samp.bytes.len(), + samp.is_sync, + ); + } + } + } + Ok(()) +} diff --git a/repos/moq-rs/third_party/mp4-rust/examples/mp4writer.rs b/repos/moq-rs/third_party/mp4-rust/examples/mp4writer.rs new file mode 100644 index 0000000..0ab515b --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/examples/mp4writer.rs @@ -0,0 +1,24 @@ +use mp4::{Mp4Config, Mp4Writer}; +use std::io::Cursor; + +fn main() -> mp4::Result<()> { + let config = Mp4Config { + major_brand: str::parse("isom").unwrap(), + minor_version: 512, + compatible_brands: vec![ + str::parse("isom").unwrap(), + str::parse("iso2").unwrap(), + str::parse("avc1").unwrap(), + str::parse("mp41").unwrap(), + ], + timescale: 1000, + }; + + let data = Cursor::new(Vec::::new()); + let mut writer = Mp4Writer::write_start(data, &config)?; + writer.write_end()?; + + let data: Vec = writer.into_writer().into_inner(); + println!("{:?}", data); + Ok(()) +} diff --git a/repos/moq-rs/third_party/mp4-rust/examples/mpeg_aac_decoder/.gitignore b/repos/moq-rs/third_party/mp4-rust/examples/mpeg_aac_decoder/.gitignore new file mode 100644 index 0000000..1b72444 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/examples/mpeg_aac_decoder/.gitignore @@ -0,0 +1,2 @@ +/Cargo.lock +/target diff --git a/repos/moq-rs/third_party/mp4-rust/examples/mpeg_aac_decoder/Cargo.toml b/repos/moq-rs/third_party/mp4-rust/examples/mpeg_aac_decoder/Cargo.toml new file mode 100644 index 0000000..97426c7 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/examples/mpeg_aac_decoder/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mpeg_aac_decoder" +version = "0.1.0" +edition = "2018" + +[dependencies] +mp4 = "0.8.1" +fdk-aac = "0.4.0" +rodio = { version = "0.13.0", default-features = false } diff --git a/repos/moq-rs/third_party/mp4-rust/examples/mpeg_aac_decoder/audio_aac.m4a b/repos/moq-rs/third_party/mp4-rust/examples/mpeg_aac_decoder/audio_aac.m4a new file mode 100644 index 0000000..5c3f57c Binary files /dev/null and b/repos/moq-rs/third_party/mp4-rust/examples/mpeg_aac_decoder/audio_aac.m4a differ diff --git a/repos/moq-rs/third_party/mp4-rust/examples/mpeg_aac_decoder/src/main.rs b/repos/moq-rs/third_party/mp4-rust/examples/mpeg_aac_decoder/src/main.rs new file mode 100644 index 0000000..ee35f30 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/examples/mpeg_aac_decoder/src/main.rs @@ -0,0 +1,237 @@ +use fdk_aac::dec::{Decoder, DecoderError, Transport}; +use rodio::{OutputStream, Sink, Source}; +use std::fs::File; +use std::io::{BufReader, Read, Seek}; +use std::ops::Range; +use std::time::Duration; + +fn main() { + let path = "audio_aac.m4a"; + let file = File::open(path).expect("Error opening file"); + + let metadata = file.metadata().expect("Error getting file metadata"); + let size = metadata.len(); + let buf = BufReader::new(file); + + let decoder = MpegAacDecoder::new(buf, size).expect("Error creating decoder"); + + let output_stream = OutputStream::try_default(); + let (_stream, handle) = output_stream.expect("Error creating output stream"); + let sink = Sink::try_new(&handle).expect("Error creating sink"); + + sink.append(decoder); + sink.play(); + sink.set_volume(0.5); + sink.sleep_until_end(); +} + +pub struct MpegAacDecoder +where + R: Read + Seek, +{ + mp4_reader: mp4::Mp4Reader, + decoder: Decoder, + current_pcm_index: usize, + current_pcm: Vec, + track_id: u32, + position: u32, +} + +impl MpegAacDecoder +where + R: Read + Seek, +{ + pub fn new(reader: R, size: u64) -> Result, &'static str> { + let decoder = Decoder::new(Transport::Adts); + let mp4 = mp4::Mp4Reader::read_header(reader, size).or(Err("Error reading MPEG header"))?; + let mut track_id: Option = None; + { + for track in mp4.tracks().iter() { + let media_type = track.media_type().or(Err("Error getting media type"))?; + match media_type { + mp4::MediaType::AAC => { + track_id = Some(track.track_id()); + break; + } + _ => {} + } + } + } + match track_id { + Some(track_id) => { + return Ok(MpegAacDecoder { + mp4_reader: mp4, + decoder: decoder, + current_pcm_index: 0, + current_pcm: Vec::new(), + track_id: track_id, + position: 1, + }); + } + None => { + return Err("No aac track found"); + } + } + } +} + +impl Iterator for MpegAacDecoder +where + R: Read + Seek, +{ + type Item = i16; + fn next(&mut self) -> Option { + if self.current_pcm_index == self.current_pcm.len() { + let mut pcm = vec![0; 8192]; + let result = match self.decoder.decode_frame(&mut self.current_pcm) { + Err(DecoderError::NOT_ENOUGH_BITS) => { + let sample_result = self.mp4_reader.read_sample(self.track_id, self.position); + let sample = sample_result.expect("Error reading sample")?; + let tracks = self.mp4_reader.tracks(); + let track = tracks.get(self.track_id as usize - 1).expect("No track ID"); + let adts_header = construct_adts_header(track, &sample).expect("ADTS bytes"); + let adts_bytes = mp4::Bytes::copy_from_slice(&adts_header); + let bytes = [adts_bytes, sample.bytes].concat(); + self.position += 1; + let _bytes_read = match self.decoder.fill(&bytes) { + Ok(bytes_read) => bytes_read, + Err(_) => return None, + }; + self.decoder.decode_frame(&mut pcm) + } + val => val, + }; + if let Err(err) = result { + println!("DecoderError: {}", err); + return None; + } + let decoded_fram_size = self.decoder.decoded_frame_size(); + if decoded_fram_size < pcm.len() { + let _ = pcm.split_off(decoded_fram_size); + } + self.current_pcm = pcm; + self.current_pcm_index = 0; + } + let value = self.current_pcm[self.current_pcm_index]; + self.current_pcm_index += 1; + return Some(value); + } +} + +impl Source for MpegAacDecoder +where + R: Read + Seek, +{ + fn current_frame_len(&self) -> Option { + let frame_size: usize = self.decoder.decoded_frame_size(); + Some(frame_size) + } + fn channels(&self) -> u16 { + let num_channels: i32 = self.decoder.stream_info().numChannels; + num_channels as _ + } + fn sample_rate(&self) -> u32 { + let sample_rate: i32 = self.decoder.stream_info().sampleRate; + sample_rate as _ + } + fn total_duration(&self) -> Option { + return None; + } +} + +fn get_bits(byte: u16, range: Range) -> u16 { + let shaved_left = byte << range.start - 1; + let moved_back = shaved_left >> range.start - 1; + let shave_right = moved_back >> 16 - range.end; + return shave_right; +} + +fn get_bits_u8(byte: u8, range: Range) -> u8 { + let shaved_left = byte << range.start - 1; + let moved_back = shaved_left >> range.start - 1; + let shave_right = moved_back >> 8 - range.end; + return shave_right; +} + +pub fn construct_adts_header(track: &mp4::Mp4Track, sample: &mp4::Mp4Sample) -> Option> { + // B: Only support 0 (MPEG-4) + // D: Only support 1 (without CRC) + // byte7 and byte9 not included without CRC + let adts_header_length = 7; + + // AAAA_AAAA + let byte0 = 0b1111_1111; + + // AAAA_BCCD + let byte1 = 0b1111_0001; + + // EEFF_FFGH + let mut byte2 = 0b0000_0000; + let object_type = match track.audio_profile() { + Ok(mp4::AudioObjectType::AacMain) => 1, + Ok(mp4::AudioObjectType::AacLowComplexity) => 2, + Ok(mp4::AudioObjectType::AacScalableSampleRate) => 3, + Ok(mp4::AudioObjectType::AacLongTermPrediction) => 4, + Err(_) => return None, + }; + let adts_object_type = object_type - 1; + byte2 = (byte2 << 2) | adts_object_type; // EE + + let sample_freq_index = match track.sample_freq_index() { + Ok(mp4::SampleFreqIndex::Freq96000) => 0, + Ok(mp4::SampleFreqIndex::Freq88200) => 1, + Ok(mp4::SampleFreqIndex::Freq64000) => 2, + Ok(mp4::SampleFreqIndex::Freq48000) => 3, + Ok(mp4::SampleFreqIndex::Freq44100) => 4, + Ok(mp4::SampleFreqIndex::Freq32000) => 5, + Ok(mp4::SampleFreqIndex::Freq24000) => 6, + Ok(mp4::SampleFreqIndex::Freq22050) => 7, + Ok(mp4::SampleFreqIndex::Freq16000) => 8, + Ok(mp4::SampleFreqIndex::Freq12000) => 9, + Ok(mp4::SampleFreqIndex::Freq11025) => 10, + Ok(mp4::SampleFreqIndex::Freq8000) => 11, + Ok(mp4::SampleFreqIndex::Freq7350) => 12, + // 13-14 = reserved + // 15 = explicit frequency (forbidden in adts) + Err(_) => return None, + }; + byte2 = (byte2 << 4) | sample_freq_index; // FFFF + byte2 = (byte2 << 1) | 0b1; // G + + let channel_config = match track.channel_config() { + // 0 = for when channel config is sent via an inband PCE + Ok(mp4::ChannelConfig::Mono) => 1, + Ok(mp4::ChannelConfig::Stereo) => 2, + Ok(mp4::ChannelConfig::Three) => 3, + Ok(mp4::ChannelConfig::Four) => 4, + Ok(mp4::ChannelConfig::Five) => 5, + Ok(mp4::ChannelConfig::FiveOne) => 6, + Ok(mp4::ChannelConfig::SevenOne) => 7, + // 8-15 = reserved + Err(_) => return None, + }; + byte2 = (byte2 << 1) | get_bits_u8(channel_config, 6..6); // H + + // HHIJ_KLMM + let mut byte3 = 0b0000_0000; + byte3 = (byte3 << 2) | get_bits_u8(channel_config, 7..8); // HH + byte3 = (byte3 << 4) | 0b1111; // IJKL + + let frame_length = adts_header_length + sample.bytes.len() as u16; + byte3 = (byte3 << 2) | get_bits(frame_length, 3..5) as u8; // MM + + // MMMM_MMMM + let byte4 = get_bits(frame_length, 6..13) as u8; + + // MMMO_OOOO + let mut byte5 = 0b0000_0000; + byte5 = (byte5 << 3) | get_bits(frame_length, 14..16) as u8; + byte5 = (byte5 << 5) | 0b11111; // OOOOO + + // OOOO_OOPP + let mut byte6 = 0b0000_0000; + byte6 = (byte6 << 6) | 0b111111; // OOOOOO + byte6 = (byte6 << 2) | 0b00; // PP + + return Some(vec![byte0, byte1, byte2, byte3, byte4, byte5, byte6]); +} diff --git a/repos/moq-rs/third_party/mp4-rust/examples/simple.rs b/repos/moq-rs/third_party/mp4-rust/examples/simple.rs new file mode 100644 index 0000000..0dabedd --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/examples/simple.rs @@ -0,0 +1,27 @@ +use std::env; +use std::fs::File; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() < 2 { + println!("Usage: simple "); + std::process::exit(1); + } + + let filename = &args[1]; + let f = File::open(filename).unwrap(); + let mp4 = mp4::read_mp4(f).unwrap(); + + println!("Major Brand: {}", mp4.major_brand()); + + for track in mp4.tracks().values() { + println!( + "Track: #{}({}) {} {}", + track.track_id(), + track.language(), + track.track_type().unwrap(), + track.box_type().unwrap(), + ); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/error.rs b/repos/moq-rs/third_party/mp4-rust/src/error.rs new file mode 100644 index 0000000..11690f0 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/error.rs @@ -0,0 +1,29 @@ +use thiserror::Error; + +use crate::mp4box::BoxType; + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + IoError(#[from] std::io::Error), + #[error("{0}")] + InvalidData(&'static str), + #[error("{0} not found")] + BoxNotFound(BoxType), + #[error("{0} and {1} not found")] + Box2NotFound(BoxType, BoxType), + #[error("trak[{0}] not found")] + TrakNotFound(u32), + #[error("trak[{0}].{1} not found")] + BoxInTrakNotFound(u32, BoxType), + #[error("traf[{0}].{1} not found")] + BoxInTrafNotFound(u32, BoxType), + #[error("trak[{0}].stbl.{1} not found")] + BoxInStblNotFound(u32, BoxType), + #[error("trak[{0}].stbl.{1}.entry[{2}] not found")] + EntryInStblNotFound(u32, BoxType, u32), + #[error("traf[{0}].trun.{1}.entry[{2}] not found")] + EntryInTrunNotFound(u32, BoxType, u32), + #[error("{0} version {1} is not supported")] + UnsupportedBoxVersion(BoxType, u8), +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/lib.rs b/repos/moq-rs/third_party/mp4-rust/src/lib.rs new file mode 100644 index 0000000..92319e1 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/lib.rs @@ -0,0 +1,96 @@ +//! `mp4` is a Rust library to read and write ISO-MP4 files. +//! +//! This package contains MPEG-4 specifications defined in parts: +//! * ISO/IEC 14496-12 - ISO Base Media File Format (QuickTime, MPEG-4, etc) +//! * ISO/IEC 14496-14 - MP4 file format +//! * ISO/IEC 14496-17 - Streaming text format +//! +//! See: [mp4box] for supported MP4 atoms. +//! +//! ### Example +//! +//! ``` +//! use std::fs::File; +//! use std::io::{BufReader}; +//! use mp4::{Result}; +//! +//! fn main() -> Result<()> { +//! let f = File::open("tests/samples/minimal.mp4").unwrap(); +//! let size = f.metadata()?.len(); +//! let reader = BufReader::new(f); +//! +//! let mp4 = mp4::Mp4Reader::read_header(reader, size)?; +//! +//! // Print boxes. +//! println!("major brand: {}", mp4.ftyp.major_brand); +//! println!("timescale: {}", mp4.moov.mvhd.timescale); +//! +//! // Use available methods. +//! println!("size: {}", mp4.size()); +//! +//! let mut compatible_brands = String::new(); +//! for brand in mp4.compatible_brands().iter() { +//! compatible_brands.push_str(&brand.to_string()); +//! compatible_brands.push_str(","); +//! } +//! println!("compatible brands: {}", compatible_brands); +//! println!("duration: {:?}", mp4.duration()); +//! +//! // Track info. +//! for track in mp4.tracks().values() { +//! println!( +//! "track: #{}({}) {} : {}", +//! track.track_id(), +//! track.language(), +//! track.track_type()?, +//! track.box_type()?, +//! ); +//! } +//! Ok(()) +//! } +//! ``` +//! +//! See [examples] for more examples. +//! +//! # Installation +//! +//! Add the following to your `Cargo.toml` file: +//! +//! ```toml +//! [dependencies] +//! mp4 = "0.7.0" +//! ``` +//! +//! [mp4box]: https://github.com/alfg/mp4-rust/blob/master/src/mp4box/mod.rs +//! [examples]: https://github.com/alfg/mp4-rust/tree/master/examples +#![doc(html_root_url = "https://docs.rs/mp4/*")] + +use std::fs::File; +use std::io::BufReader; + +mod error; +pub use error::Error; + +pub type Result = std::result::Result; + +mod types; +pub use types::*; + +mod mp4box; +pub use mp4box::*; + +mod track; +pub use track::{Mp4Track, TrackConfig}; + +mod reader; +pub use reader::Mp4Reader; + +mod writer; +pub use writer::{Mp4Config, Mp4Writer}; + +pub fn read_mp4(f: File) -> Result>> { + let size = f.metadata()?.len(); + let reader = BufReader::new(f); + let mp4 = reader::Mp4Reader::read_header(reader, size)?; + Ok(mp4) +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/avc1.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/avc1.rs new file mode 100644 index 0000000..f386f9a --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/avc1.rs @@ -0,0 +1,354 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Avc1Box { + pub data_reference_index: u16, + pub width: u16, + pub height: u16, + + #[serde(with = "value_u32")] + pub horizresolution: FixedPointU16, + + #[serde(with = "value_u32")] + pub vertresolution: FixedPointU16, + pub frame_count: u16, + pub depth: u16, + pub avcc: AvcCBox, +} + +impl Default for Avc1Box { + fn default() -> Self { + Avc1Box { + data_reference_index: 0, + width: 0, + height: 0, + horizresolution: FixedPointU16::new(0x48), + vertresolution: FixedPointU16::new(0x48), + frame_count: 1, + depth: 0x0018, + avcc: AvcCBox::default(), + } + } +} + +impl Avc1Box { + pub fn new(config: &AvcConfig) -> Self { + Avc1Box { + data_reference_index: 1, + width: config.width, + height: config.height, + horizresolution: FixedPointU16::new(0x48), + vertresolution: FixedPointU16::new(0x48), + frame_count: 1, + depth: 0x0018, + avcc: AvcCBox::new(&config.seq_param_set, &config.pic_param_set), + } + } + + pub fn get_type(&self) -> BoxType { + BoxType::Avc1Box + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + 8 + 70 + self.avcc.box_size() + } +} + +impl Mp4Box for Avc1Box { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!( + "data_reference_index={} width={} height={} frame_count={}", + self.data_reference_index, self.width, self.height, self.frame_count + ); + Ok(s) + } +} + +impl ReadBox<&mut R> for Avc1Box { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + reader.read_u32::()?; // reserved + reader.read_u16::()?; // reserved + let data_reference_index = reader.read_u16::()?; + + reader.read_u32::()?; // pre-defined, reserved + reader.read_u64::()?; // pre-defined + reader.read_u32::()?; // pre-defined + let width = reader.read_u16::()?; + let height = reader.read_u16::()?; + let horizresolution = FixedPointU16::new_raw(reader.read_u32::()?); + let vertresolution = FixedPointU16::new_raw(reader.read_u32::()?); + reader.read_u32::()?; // reserved + let frame_count = reader.read_u16::()?; + skip_bytes(reader, 32)?; // compressorname + let depth = reader.read_u16::()?; + reader.read_i16::()?; // pre-defined + + let end = start + size; + loop { + let current = reader.stream_position()?; + if current >= end { + return Err(Error::InvalidData("avcc not found")); + } + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "avc1 box contains a box with a larger size than it", + )); + } + if name == BoxType::AvcCBox { + let avcc = AvcCBox::read_box(reader, s)?; + + skip_bytes_to(reader, start + size)?; + + return Ok(Avc1Box { + data_reference_index, + width, + height, + horizresolution, + vertresolution, + frame_count, + depth, + avcc, + }); + } else { + skip_bytes_to(reader, current + s)?; + } + } + } +} + +impl WriteBox<&mut W> for Avc1Box { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + writer.write_u32::(0)?; // reserved + writer.write_u16::(0)?; // reserved + writer.write_u16::(self.data_reference_index)?; + + writer.write_u32::(0)?; // pre-defined, reserved + writer.write_u64::(0)?; // pre-defined + writer.write_u32::(0)?; // pre-defined + writer.write_u16::(self.width)?; + writer.write_u16::(self.height)?; + writer.write_u32::(self.horizresolution.raw_value())?; + writer.write_u32::(self.vertresolution.raw_value())?; + writer.write_u32::(0)?; // reserved + writer.write_u16::(self.frame_count)?; + // skip compressorname + write_zeros(writer, 32)?; + writer.write_u16::(self.depth)?; + writer.write_i16::(-1)?; // pre-defined + + self.avcc.write_box(writer)?; + + Ok(size) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct AvcCBox { + pub configuration_version: u8, + pub avc_profile_indication: u8, + pub profile_compatibility: u8, + pub avc_level_indication: u8, + pub length_size_minus_one: u8, + pub sequence_parameter_sets: Vec, + pub picture_parameter_sets: Vec, +} + +impl AvcCBox { + pub fn new(sps: &[u8], pps: &[u8]) -> Self { + Self { + configuration_version: 1, + avc_profile_indication: sps[1], + profile_compatibility: sps[2], + avc_level_indication: sps[3], + length_size_minus_one: 0xff, // length_size = 4 + sequence_parameter_sets: vec![NalUnit::from(sps)], + picture_parameter_sets: vec![NalUnit::from(pps)], + } + } +} + +impl Mp4Box for AvcCBox { + fn box_type(&self) -> BoxType { + BoxType::AvcCBox + } + + fn box_size(&self) -> u64 { + let mut size = HEADER_SIZE + 7; + for sps in self.sequence_parameter_sets.iter() { + size += sps.size() as u64; + } + for pps in self.picture_parameter_sets.iter() { + size += pps.size() as u64; + } + size + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("avc_profile_indication={}", self.avc_profile_indication); + Ok(s) + } +} + +impl ReadBox<&mut R> for AvcCBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let configuration_version = reader.read_u8()?; + let avc_profile_indication = reader.read_u8()?; + let profile_compatibility = reader.read_u8()?; + let avc_level_indication = reader.read_u8()?; + let length_size_minus_one = reader.read_u8()? & 0x3; + let num_of_spss = reader.read_u8()? & 0x1F; + let mut sequence_parameter_sets = Vec::with_capacity(num_of_spss as usize); + for _ in 0..num_of_spss { + let nal_unit = NalUnit::read(reader)?; + sequence_parameter_sets.push(nal_unit); + } + let num_of_ppss = reader.read_u8()?; + let mut picture_parameter_sets = Vec::with_capacity(num_of_ppss as usize); + for _ in 0..num_of_ppss { + let nal_unit = NalUnit::read(reader)?; + picture_parameter_sets.push(nal_unit); + } + + skip_bytes_to(reader, start + size)?; + + Ok(AvcCBox { + configuration_version, + avc_profile_indication, + profile_compatibility, + avc_level_indication, + length_size_minus_one, + sequence_parameter_sets, + picture_parameter_sets, + }) + } +} + +impl WriteBox<&mut W> for AvcCBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + writer.write_u8(self.configuration_version)?; + writer.write_u8(self.avc_profile_indication)?; + writer.write_u8(self.profile_compatibility)?; + writer.write_u8(self.avc_level_indication)?; + writer.write_u8(self.length_size_minus_one | 0xFC)?; + writer.write_u8(self.sequence_parameter_sets.len() as u8 | 0xE0)?; + for sps in self.sequence_parameter_sets.iter() { + sps.write(writer)?; + } + writer.write_u8(self.picture_parameter_sets.len() as u8)?; + for pps in self.picture_parameter_sets.iter() { + pps.write(writer)?; + } + Ok(size) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct NalUnit { + pub bytes: Vec, +} + +impl From<&[u8]> for NalUnit { + fn from(bytes: &[u8]) -> Self { + Self { + bytes: bytes.to_vec(), + } + } +} + +impl NalUnit { + fn size(&self) -> usize { + 2 + self.bytes.len() + } + + fn read(reader: &mut R) -> Result { + let length = reader.read_u16::()? as usize; + let mut bytes = vec![0u8; length]; + reader.read_exact(&mut bytes)?; + Ok(NalUnit { bytes }) + } + + fn write(&self, writer: &mut W) -> Result { + writer.write_u16::(self.bytes.len() as u16)?; + writer.write_all(&self.bytes)?; + Ok(self.size() as u64) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_avc1() { + let src_box = Avc1Box { + data_reference_index: 1, + width: 320, + height: 240, + horizresolution: FixedPointU16::new(0x48), + vertresolution: FixedPointU16::new(0x48), + frame_count: 1, + depth: 24, + avcc: AvcCBox { + configuration_version: 1, + avc_profile_indication: 100, + profile_compatibility: 0, + avc_level_indication: 13, + length_size_minus_one: 3, + sequence_parameter_sets: vec![NalUnit { + bytes: vec![ + 0x67, 0x64, 0x00, 0x0D, 0xAC, 0xD9, 0x41, 0x41, 0xFA, 0x10, 0x00, 0x00, + 0x03, 0x00, 0x10, 0x00, 0x00, 0x03, 0x03, 0x20, 0xF1, 0x42, 0x99, 0x60, + ], + }], + picture_parameter_sets: vec![NalUnit { + bytes: vec![0x68, 0xEB, 0xE3, 0xCB, 0x22, 0xC0], + }], + }, + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::Avc1Box); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = Avc1Box::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/co64.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/co64.rs new file mode 100644 index 0000000..978137e --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/co64.rs @@ -0,0 +1,123 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; +use std::mem::size_of; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct Co64Box { + pub version: u8, + pub flags: u32, + + #[serde(skip_serializing)] + pub entries: Vec, +} + +impl Co64Box { + pub fn get_type(&self) -> BoxType { + BoxType::Co64Box + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + 4 + (8 * self.entries.len() as u64) + } +} + +impl Mp4Box for Co64Box { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("entries_count={}", self.entries.len()); + Ok(s) + } +} + +impl ReadBox<&mut R> for Co64Box { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let header_size = HEADER_SIZE + HEADER_EXT_SIZE; + let other_size = size_of::(); // entry_count + let entry_size = size_of::(); // chunk_offset + let entry_count = reader.read_u32::()?; + if u64::from(entry_count) + > size + .saturating_sub(header_size) + .saturating_sub(other_size as u64) + / entry_size as u64 + { + return Err(Error::InvalidData( + "co64 entry_count indicates more entries than could fit in the box", + )); + } + let mut entries = Vec::with_capacity(entry_count as usize); + for _i in 0..entry_count { + let chunk_offset = reader.read_u64::()?; + entries.push(chunk_offset); + } + + skip_bytes_to(reader, start + size)?; + + Ok(Co64Box { + version, + flags, + entries, + }) + } +} + +impl WriteBox<&mut W> for Co64Box { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u32::(self.entries.len() as u32)?; + for chunk_offset in self.entries.iter() { + writer.write_u64::(*chunk_offset)?; + } + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_co64() { + let src_box = Co64Box { + version: 0, + flags: 0, + entries: vec![267, 1970, 2535, 2803, 11843, 22223, 33584], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::Co64Box); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = Co64Box::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/ctts.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/ctts.rs new file mode 100644 index 0000000..673e8c9 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/ctts.rs @@ -0,0 +1,143 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; +use std::mem::size_of; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct CttsBox { + pub version: u8, + pub flags: u32, + + #[serde(skip_serializing)] + pub entries: Vec, +} + +impl CttsBox { + pub fn get_type(&self) -> BoxType { + BoxType::CttsBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + 4 + (8 * self.entries.len() as u64) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct CttsEntry { + pub sample_count: u32, + pub sample_offset: i32, +} + +impl Mp4Box for CttsBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("entries_count={}", self.entries.len()); + Ok(s) + } +} + +impl ReadBox<&mut R> for CttsBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let header_size = HEADER_SIZE + HEADER_EXT_SIZE; + let entry_count = reader.read_u32::()?; + let entry_size = size_of::() + size_of::(); // sample_count + sample_offset + // (sample_offset might be a u32, but the size is the same.) + let other_size = size_of::(); // entry_count + if u64::from(entry_count) + > size + .saturating_sub(header_size) + .saturating_sub(other_size as u64) + / entry_size as u64 + { + return Err(Error::InvalidData( + "ctts entry_count indicates more entries than could fit in the box", + )); + } + let mut entries = Vec::with_capacity(entry_count as usize); + for _ in 0..entry_count { + let entry = CttsEntry { + sample_count: reader.read_u32::()?, + sample_offset: reader.read_i32::()?, + }; + entries.push(entry); + } + + skip_bytes_to(reader, start + size)?; + + Ok(CttsBox { + version, + flags, + entries, + }) + } +} + +impl WriteBox<&mut W> for CttsBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u32::(self.entries.len() as u32)?; + for entry in self.entries.iter() { + writer.write_u32::(entry.sample_count)?; + writer.write_i32::(entry.sample_offset)?; + } + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_ctts() { + let src_box = CttsBox { + version: 0, + flags: 0, + entries: vec![ + CttsEntry { + sample_count: 1, + sample_offset: 200, + }, + CttsEntry { + sample_count: 2, + sample_offset: -100, + }, + ], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::CttsBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = CttsBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/data.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/data.rs new file mode 100644 index 0000000..19b5c77 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/data.rs @@ -0,0 +1,118 @@ +use std::{ + convert::TryFrom, + io::{Read, Seek}, +}; + +use serde::Serialize; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct DataBox { + pub data: Vec, + pub data_type: DataType, +} + +impl DataBox { + pub fn get_type(&self) -> BoxType { + BoxType::DataBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE; + size += 4; // data_type + size += 4; // reserved + size += self.data.len() as u64; + size + } +} + +impl Mp4Box for DataBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("type={:?} len={}", self.data_type, self.data.len()); + Ok(s) + } +} + +impl ReadBox<&mut R> for DataBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let data_type = DataType::try_from(reader.read_u32::()?)?; + + reader.read_u32::()?; // reserved = 0 + + let current = reader.stream_position()?; + let mut data = vec![0u8; (start + size - current) as usize]; + reader.read_exact(&mut data)?; + + Ok(DataBox { data, data_type }) + } +} + +impl WriteBox<&mut W> for DataBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + writer.write_u32::(self.data_type.clone() as u32)?; + writer.write_u32::(0)?; // reserved = 0 + writer.write_all(&self.data)?; + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_data() { + let src_box = DataBox { + data_type: DataType::Text, + data: b"test_data".to_vec(), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::DataBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = DataBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_data_empty() { + let src_box = DataBox::default(); + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::DataBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = DataBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/dinf.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/dinf.rs new file mode 100644 index 0000000..e365e4a --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/dinf.rs @@ -0,0 +1,302 @@ +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct DinfBox { + dref: DrefBox, +} + +impl DinfBox { + pub fn get_type(&self) -> BoxType { + BoxType::DinfBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + self.dref.box_size() + } +} + +impl Mp4Box for DinfBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = String::new(); + Ok(s) + } +} + +impl ReadBox<&mut R> for DinfBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut dref = None; + + let mut current = reader.stream_position()?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "dinf box contains a box with a larger size than it", + )); + } + + match name { + BoxType::DrefBox => { + dref = Some(DrefBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.stream_position()?; + } + + if dref.is_none() { + return Err(Error::BoxNotFound(BoxType::DrefBox)); + } + + skip_bytes_to(reader, start + size)?; + + Ok(DinfBox { + dref: dref.unwrap(), + }) + } +} + +impl WriteBox<&mut W> for DinfBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + self.dref.write_box(writer)?; + Ok(size) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct DrefBox { + pub version: u8, + pub flags: u32, + + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, +} + +impl Default for DrefBox { + fn default() -> Self { + DrefBox { + version: 0, + flags: 0, + url: Some(UrlBox::default()), + } + } +} + +impl DrefBox { + pub fn get_type(&self) -> BoxType { + BoxType::DrefBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE + HEADER_EXT_SIZE + 4; + if let Some(ref url) = self.url { + size += url.box_size(); + } + size + } +} + +impl Mp4Box for DrefBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = String::new(); + Ok(s) + } +} + +impl ReadBox<&mut R> for DrefBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut current = reader.stream_position()?; + + let (version, flags) = read_box_header_ext(reader)?; + let end = start + size; + + let mut url = None; + + let entry_count = reader.read_u32::()?; + for _i in 0..entry_count { + if current >= end { + break; + } + + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "dinf box contains a box with a larger size than it", + )); + } + + match name { + BoxType::UrlBox => { + url = Some(UrlBox::read_box(reader, s)?); + } + _ => { + skip_box(reader, s)?; + } + } + + current = reader.stream_position()?; + } + + skip_bytes_to(reader, start + size)?; + + Ok(DrefBox { + version, + flags, + url, + }) + } +} + +impl WriteBox<&mut W> for DrefBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u32::(1)?; + + if let Some(ref url) = self.url { + url.write_box(writer)?; + } + + Ok(size) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct UrlBox { + pub version: u8, + pub flags: u32, + pub location: String, +} + +impl Default for UrlBox { + fn default() -> Self { + UrlBox { + version: 0, + flags: 1, + location: String::default(), + } + } +} + +impl UrlBox { + pub fn get_type(&self) -> BoxType { + BoxType::UrlBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE + HEADER_EXT_SIZE; + + if !self.location.is_empty() { + size += self.location.bytes().len() as u64 + 1; + } + + size + } +} + +impl Mp4Box for UrlBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("location={}", self.location); + Ok(s) + } +} + +impl ReadBox<&mut R> for UrlBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let buf_size = size + .checked_sub(HEADER_SIZE + HEADER_EXT_SIZE) + .ok_or(Error::InvalidData("url size too small"))?; + + let mut buf = vec![0u8; buf_size as usize]; + reader.read_exact(&mut buf)?; + if let Some(end) = buf.iter().position(|&b| b == b'\0') { + buf.truncate(end); + } + let location = String::from_utf8(buf).unwrap_or_default(); + + skip_bytes_to(reader, start + size)?; + + Ok(UrlBox { + version, + flags, + location, + }) + } +} + +impl WriteBox<&mut W> for UrlBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + if !self.location.is_empty() { + writer.write_all(self.location.as_bytes())?; + writer.write_u8(0)?; + } + + Ok(size) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/edts.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/edts.rs new file mode 100644 index 0000000..9077bb1 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/edts.rs @@ -0,0 +1,85 @@ +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::elst::ElstBox; +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct EdtsBox { + pub elst: Option, +} + +impl EdtsBox { + pub(crate) fn new() -> EdtsBox { + Default::default() + } + + pub fn get_type(&self) -> BoxType { + BoxType::EdtsBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE; + if let Some(ref elst) = self.elst { + size += elst.box_size(); + } + size + } +} + +impl Mp4Box for EdtsBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = String::new(); + Ok(s) + } +} + +impl ReadBox<&mut R> for EdtsBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut edts = EdtsBox::new(); + + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "edts box contains a box with a larger size than it", + )); + } + + if let BoxType::ElstBox = name { + let elst = ElstBox::read_box(reader, s)?; + edts.elst = Some(elst); + } + + skip_bytes_to(reader, start + size)?; + + Ok(edts) + } +} + +impl WriteBox<&mut W> for EdtsBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + if let Some(ref elst) = self.elst { + elst.write_box(writer)?; + } + + Ok(size) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/elst.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/elst.rs new file mode 100644 index 0000000..297fb63 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/elst.rs @@ -0,0 +1,201 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; +use std::mem::size_of; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct ElstBox { + pub version: u8, + pub flags: u32, + + #[serde(skip_serializing)] + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct ElstEntry { + pub segment_duration: u64, + pub media_time: u64, + pub media_rate: u16, + pub media_rate_fraction: u16, +} + +impl ElstBox { + pub fn get_type(&self) -> BoxType { + BoxType::ElstBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE + HEADER_EXT_SIZE + 4; + if self.version == 1 { + size += self.entries.len() as u64 * 20; + } else if self.version == 0 { + size += self.entries.len() as u64 * 12; + } + size + } +} + +impl Mp4Box for ElstBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("elst_entries={}", self.entries.len()); + Ok(s) + } +} + +impl ReadBox<&mut R> for ElstBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let header_size = HEADER_SIZE + HEADER_EXT_SIZE; + let entry_count = reader.read_u32::()?; + let other_size = size_of::(); // entry_count + let entry_size = { + let mut entry_size = 0; + entry_size += if version == 1 { + size_of::() + size_of::() // segment_duration + media_time + } else { + size_of::() + size_of::() // segment_duration + media_time + }; + entry_size += size_of::() + size_of::(); // media_rate_integer + media_rate_fraction + entry_size + }; + if u64::from(entry_count) + > size + .saturating_sub(header_size) + .saturating_sub(other_size as u64) + / entry_size as u64 + { + return Err(Error::InvalidData( + "elst entry_count indicates more entries than could fit in the box", + )); + } + let mut entries = Vec::with_capacity(entry_count as usize); + for _ in 0..entry_count { + let (segment_duration, media_time) = if version == 1 { + ( + reader.read_u64::()?, + reader.read_u64::()?, + ) + } else { + ( + reader.read_u32::()? as u64, + reader.read_u32::()? as u64, + ) + }; + + let entry = ElstEntry { + segment_duration, + media_time, + media_rate: reader.read_u16::()?, + media_rate_fraction: reader.read_u16::()?, + }; + entries.push(entry); + } + + skip_bytes_to(reader, start + size)?; + + Ok(ElstBox { + version, + flags, + entries, + }) + } +} + +impl WriteBox<&mut W> for ElstBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u32::(self.entries.len() as u32)?; + for entry in self.entries.iter() { + if self.version == 1 { + writer.write_u64::(entry.segment_duration)?; + writer.write_u64::(entry.media_time)?; + } else { + writer.write_u32::(entry.segment_duration as u32)?; + writer.write_u32::(entry.media_time as u32)?; + } + writer.write_u16::(entry.media_rate)?; + writer.write_u16::(entry.media_rate_fraction)?; + } + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_elst32() { + let src_box = ElstBox { + version: 0, + flags: 0, + entries: vec![ElstEntry { + segment_duration: 634634, + media_time: 0, + media_rate: 1, + media_rate_fraction: 0, + }], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::ElstBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = ElstBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_elst64() { + let src_box = ElstBox { + version: 1, + flags: 0, + entries: vec![ElstEntry { + segment_duration: 634634, + media_time: 0, + media_rate: 1, + media_rate_fraction: 0, + }], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::ElstBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = ElstBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/emsg.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/emsg.rs new file mode 100644 index 0000000..f68ba3a --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/emsg.rs @@ -0,0 +1,242 @@ +use std::ffi::CStr; +use std::io::{Read, Seek, Write}; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct EmsgBox { + pub version: u8, + pub flags: u32, + pub timescale: u32, + pub presentation_time: Option, + pub presentation_time_delta: Option, + pub event_duration: u32, + pub id: u32, + pub scheme_id_uri: String, + pub value: String, + pub message_data: Vec, +} + +impl EmsgBox { + fn size_without_message(version: u8, scheme_id_uri: &str, value: &str) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + + 4 + // id + Self::time_size(version) + + (scheme_id_uri.len() + 1) as u64 + + (value.len() as u64 + 1) + } + + fn time_size(version: u8) -> u64 { + match version { + 0 => 12, + 1 => 16, + _ => panic!("version must be 0 or 1"), + } + } +} + +impl Mp4Box for EmsgBox { + fn box_type(&self) -> BoxType { + BoxType::EmsgBox + } + + fn box_size(&self) -> u64 { + Self::size_without_message(self.version, &self.scheme_id_uri, &self.value) + + self.message_data.len() as u64 + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("id={} value={}", self.id, self.value); + Ok(s) + } +} + +impl ReadBox<&mut R> for EmsgBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + let (version, flags) = read_box_header_ext(reader)?; + + let ( + timescale, + presentation_time, + presentation_time_delta, + event_duration, + id, + scheme_id_uri, + value, + ) = match version { + 0 => { + let scheme_id_uri = read_null_terminated_utf8_string(reader)?; + let value = read_null_terminated_utf8_string(reader)?; + ( + reader.read_u32::()?, + None, + Some(reader.read_u32::()?), + reader.read_u32::()?, + reader.read_u32::()?, + scheme_id_uri, + value, + ) + } + 1 => ( + reader.read_u32::()?, + Some(reader.read_u64::()?), + None, + reader.read_u32::()?, + reader.read_u32::()?, + read_null_terminated_utf8_string(reader)?, + read_null_terminated_utf8_string(reader)?, + ), + _ => return Err(Error::InvalidData("version must be 0 or 1")), + }; + + let message_size = size - Self::size_without_message(version, &scheme_id_uri, &value); + let mut message_data = Vec::with_capacity(message_size as usize); + for _ in 0..message_size { + message_data.push(reader.read_u8()?); + } + + skip_bytes_to(reader, start + size)?; + + Ok(EmsgBox { + version, + flags, + timescale, + presentation_time, + presentation_time_delta, + event_duration, + id, + scheme_id_uri, + value, + message_data, + }) + } +} + +impl WriteBox<&mut W> for EmsgBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + match self.version { + 0 => { + write_null_terminated_str(writer, &self.scheme_id_uri)?; + write_null_terminated_str(writer, &self.value)?; + writer.write_u32::(self.timescale)?; + writer.write_u32::(self.presentation_time_delta.unwrap())?; + writer.write_u32::(self.event_duration)?; + writer.write_u32::(self.id)?; + } + 1 => { + writer.write_u32::(self.timescale)?; + writer.write_u64::(self.presentation_time.unwrap())?; + writer.write_u32::(self.event_duration)?; + writer.write_u32::(self.id)?; + write_null_terminated_str(writer, &self.scheme_id_uri)?; + write_null_terminated_str(writer, &self.value)?; + } + _ => return Err(Error::InvalidData("version must be 0 or 1")), + } + + for &byte in &self.message_data { + writer.write_u8(byte)?; + } + + Ok(size) + } +} + +fn read_null_terminated_utf8_string(reader: &mut R) -> Result { + let mut bytes = Vec::new(); + loop { + let byte = reader.read_u8()?; + bytes.push(byte); + if byte == 0 { + break; + } + } + if let Ok(str) = unsafe { CStr::from_bytes_with_nul_unchecked(&bytes) }.to_str() { + Ok(str.to_string()) + } else { + Err(Error::InvalidData("invalid utf8")) + } +} + +fn write_null_terminated_str(writer: &mut W, string: &str) -> Result<()> { + for byte in string.bytes() { + writer.write_u8(byte)?; + } + writer.write_u8(0)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use crate::mp4box::BoxHeader; + + use super::*; + + #[test] + fn test_emsg_version0() { + let src_box = EmsgBox { + version: 0, + flags: 0, + timescale: 48000, + presentation_time: None, + presentation_time_delta: Some(100), + event_duration: 200, + id: 8, + scheme_id_uri: String::from("foo"), + value: String::from("foo"), + message_data: vec![1, 2, 3], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::EmsgBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = EmsgBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_emsg_version1() { + let src_box = EmsgBox { + version: 1, + flags: 0, + timescale: 48000, + presentation_time: Some(50000), + presentation_time_delta: None, + event_duration: 200, + id: 8, + scheme_id_uri: String::from("foo"), + value: String::from("foo"), + message_data: vec![3, 2, 1], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::EmsgBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = EmsgBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/ftyp.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/ftyp.rs new file mode 100644 index 0000000..789cd4e --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/ftyp.rs @@ -0,0 +1,123 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct FtypBox { + pub major_brand: FourCC, + pub minor_version: u32, + pub compatible_brands: Vec, +} + +impl FtypBox { + pub fn get_type(&self) -> BoxType { + BoxType::FtypBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + 8 + (4 * self.compatible_brands.len() as u64) + } +} + +impl Mp4Box for FtypBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let mut compatible_brands = Vec::new(); + for brand in self.compatible_brands.iter() { + compatible_brands.push(brand.to_string()); + } + let s = format!( + "major_brand={} minor_version={} compatible_brands={}", + self.major_brand, + self.minor_version, + compatible_brands.join("-") + ); + Ok(s) + } +} + +impl ReadBox<&mut R> for FtypBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + if size < 16 || size % 4 != 0 { + return Err(Error::InvalidData("ftyp size too small or not aligned")); + } + let brand_count = (size - 16) / 4; // header + major + minor + let major = reader.read_u32::()?; + let minor = reader.read_u32::()?; + + let mut brands = Vec::new(); + for _ in 0..brand_count { + let b = reader.read_u32::()?; + brands.push(From::from(b)); + } + + skip_bytes_to(reader, start + size)?; + + Ok(FtypBox { + major_brand: From::from(major), + minor_version: minor, + compatible_brands: brands, + }) + } +} + +impl WriteBox<&mut W> for FtypBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + writer.write_u32::((&self.major_brand).into())?; + writer.write_u32::(self.minor_version)?; + for b in self.compatible_brands.iter() { + writer.write_u32::(b.into())?; + } + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_ftyp() { + let src_box = FtypBox { + major_brand: str::parse("isom").unwrap(), + minor_version: 0, + compatible_brands: vec![ + str::parse("isom").unwrap(), + str::parse("iso2").unwrap(), + str::parse("avc1").unwrap(), + str::parse("mp41").unwrap(), + ], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::FtypBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = FtypBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/hdlr.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/hdlr.rs new file mode 100644 index 0000000..b9d86a9 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/hdlr.rs @@ -0,0 +1,173 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct HdlrBox { + pub version: u8, + pub flags: u32, + pub handler_type: FourCC, + pub name: String, +} + +impl HdlrBox { + pub fn get_type(&self) -> BoxType { + BoxType::HdlrBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + 20 + self.name.len() as u64 + 1 + } +} + +impl Mp4Box for HdlrBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("handler_type={} name={}", self.handler_type, self.name); + Ok(s) + } +} + +impl ReadBox<&mut R> for HdlrBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + reader.read_u32::()?; // pre-defined + let handler = reader.read_u32::()?; + + skip_bytes(reader, 12)?; // reserved + + let buf_size = size + .checked_sub(HEADER_SIZE + HEADER_EXT_SIZE + 20) + .ok_or(Error::InvalidData("hdlr size too small"))?; + + let mut buf = vec![0u8; buf_size as usize]; + reader.read_exact(&mut buf)?; + if let Some(end) = buf.iter().position(|&b| b == b'\0') { + buf.truncate(end); + } + let handler_string = String::from_utf8(buf).unwrap_or_default(); + + skip_bytes_to(reader, start + size)?; + + Ok(HdlrBox { + version, + flags, + handler_type: From::from(handler), + name: handler_string, + }) + } +} + +impl WriteBox<&mut W> for HdlrBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u32::(0)?; // pre-defined + writer.write_u32::((&self.handler_type).into())?; + + // 12 bytes reserved + for _ in 0..3 { + writer.write_u32::(0)?; + } + + writer.write_all(self.name.as_bytes())?; + writer.write_u8(0)?; + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_hdlr() { + let src_box = HdlrBox { + version: 0, + flags: 0, + handler_type: str::parse::("vide").unwrap(), + name: String::from("VideoHandler"), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::HdlrBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = HdlrBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_hdlr_empty() { + let src_box = HdlrBox { + version: 0, + flags: 0, + handler_type: str::parse::("vide").unwrap(), + name: String::new(), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::HdlrBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = HdlrBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_hdlr_extra() { + let real_src_box = HdlrBox { + version: 0, + flags: 0, + handler_type: str::parse::("vide").unwrap(), + name: String::from("Good"), + }; + let src_box = HdlrBox { + version: 0, + flags: 0, + handler_type: str::parse::("vide").unwrap(), + name: String::from_utf8(b"Good\0Bad".to_vec()).unwrap(), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::HdlrBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = HdlrBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(real_src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/hev1.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/hev1.rs new file mode 100644 index 0000000..3070fb8 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/hev1.rs @@ -0,0 +1,395 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Hev1Box { + pub data_reference_index: u16, + pub width: u16, + pub height: u16, + + #[serde(with = "value_u32")] + pub horizresolution: FixedPointU16, + + #[serde(with = "value_u32")] + pub vertresolution: FixedPointU16, + pub frame_count: u16, + pub depth: u16, + pub hvcc: HvcCBox, +} + +impl Default for Hev1Box { + fn default() -> Self { + Hev1Box { + data_reference_index: 0, + width: 0, + height: 0, + horizresolution: FixedPointU16::new(0x48), + vertresolution: FixedPointU16::new(0x48), + frame_count: 1, + depth: 0x0018, + hvcc: HvcCBox::default(), + } + } +} + +impl Hev1Box { + pub fn new(config: &HevcConfig) -> Self { + Hev1Box { + data_reference_index: 1, + width: config.width, + height: config.height, + horizresolution: FixedPointU16::new(0x48), + vertresolution: FixedPointU16::new(0x48), + frame_count: 1, + depth: 0x0018, + hvcc: HvcCBox::new(), + } + } + + pub fn get_type(&self) -> BoxType { + BoxType::Hev1Box + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + 8 + 70 + self.hvcc.box_size() + } +} + +impl Mp4Box for Hev1Box { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!( + "data_reference_index={} width={} height={} frame_count={}", + self.data_reference_index, self.width, self.height, self.frame_count + ); + Ok(s) + } +} + +impl ReadBox<&mut R> for Hev1Box { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + reader.read_u32::()?; // reserved + reader.read_u16::()?; // reserved + let data_reference_index = reader.read_u16::()?; + + reader.read_u32::()?; // pre-defined, reserved + reader.read_u64::()?; // pre-defined + reader.read_u32::()?; // pre-defined + let width = reader.read_u16::()?; + let height = reader.read_u16::()?; + let horizresolution = FixedPointU16::new_raw(reader.read_u32::()?); + let vertresolution = FixedPointU16::new_raw(reader.read_u32::()?); + reader.read_u32::()?; // reserved + let frame_count = reader.read_u16::()?; + skip_bytes(reader, 32)?; // compressorname + let depth = reader.read_u16::()?; + reader.read_i16::()?; // pre-defined + + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "hev1 box contains a box with a larger size than it", + )); + } + if name == BoxType::HvcCBox { + let hvcc = HvcCBox::read_box(reader, s)?; + + skip_bytes_to(reader, start + size)?; + + Ok(Hev1Box { + data_reference_index, + width, + height, + horizresolution, + vertresolution, + frame_count, + depth, + hvcc, + }) + } else { + Err(Error::InvalidData("hvcc not found")) + } + } +} + +impl WriteBox<&mut W> for Hev1Box { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + writer.write_u32::(0)?; // reserved + writer.write_u16::(0)?; // reserved + writer.write_u16::(self.data_reference_index)?; + + writer.write_u32::(0)?; // pre-defined, reserved + writer.write_u64::(0)?; // pre-defined + writer.write_u32::(0)?; // pre-defined + writer.write_u16::(self.width)?; + writer.write_u16::(self.height)?; + writer.write_u32::(self.horizresolution.raw_value())?; + writer.write_u32::(self.vertresolution.raw_value())?; + writer.write_u32::(0)?; // reserved + writer.write_u16::(self.frame_count)?; + // skip compressorname + write_zeros(writer, 32)?; + writer.write_u16::(self.depth)?; + writer.write_i16::(-1)?; // pre-defined + + self.hvcc.write_box(writer)?; + + Ok(size) + } +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)] +pub struct HvcCBox { + pub configuration_version: u8, + pub general_profile_space: u8, + pub general_tier_flag: bool, + pub general_profile_idc: u8, + pub general_profile_compatibility_flags: u32, + pub general_constraint_indicator_flag: u64, + pub general_level_idc: u8, + pub min_spatial_segmentation_idc: u16, + pub parallelism_type: u8, + pub chroma_format_idc: u8, + pub bit_depth_luma_minus8: u8, + pub bit_depth_chroma_minus8: u8, + pub avg_frame_rate: u16, + pub constant_frame_rate: u8, + pub num_temporal_layers: u8, + pub temporal_id_nested: bool, + pub length_size_minus_one: u8, + pub arrays: Vec, +} + +impl HvcCBox { + pub fn new() -> Self { + Self { + configuration_version: 1, + ..Default::default() + } + } +} + +impl Mp4Box for HvcCBox { + fn box_type(&self) -> BoxType { + BoxType::HvcCBox + } + + fn box_size(&self) -> u64 { + HEADER_SIZE + + 23 + + self + .arrays + .iter() + .map(|a| 3 + a.nalus.iter().map(|x| 2 + x.data.len() as u64).sum::()) + .sum::() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + Ok(format!("configuration_version={} general_profile_space={} general_tier_flag={} general_profile_idc={} general_profile_compatibility_flags={} general_constraint_indicator_flag={} general_level_idc={} min_spatial_segmentation_idc={} parallelism_type={} chroma_format_idc={} bit_depth_luma_minus8={} bit_depth_chroma_minus8={} avg_frame_rate={} constant_frame_rate={} num_temporal_layers={} temporal_id_nested={} length_size_minus_one={}", + self.configuration_version, + self.general_profile_space, + self.general_tier_flag, + self.general_profile_idc, + self.general_profile_compatibility_flags, + self.general_constraint_indicator_flag, + self.general_level_idc, + self.min_spatial_segmentation_idc, + self.parallelism_type, + self.chroma_format_idc, + self.bit_depth_luma_minus8, + self.bit_depth_chroma_minus8, + self.avg_frame_rate, + self.constant_frame_rate, + self.num_temporal_layers, + self.temporal_id_nested, + self.length_size_minus_one + )) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct HvcCArrayNalu { + pub size: u16, + pub data: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct HvcCArray { + pub completeness: bool, + pub nal_unit_type: u8, + pub nalus: Vec, +} + +impl ReadBox<&mut R> for HvcCBox { + fn read_box(reader: &mut R, _size: u64) -> Result { + let configuration_version = reader.read_u8()?; + let params = reader.read_u8()?; + let general_profile_space = params & 0b11000000 >> 6; + let general_tier_flag = (params & 0b00100000 >> 5) > 0; + let general_profile_idc = params & 0b00011111; + + let general_profile_compatibility_flags = reader.read_u32::()?; + let general_constraint_indicator_flag = reader.read_u48::()?; + let general_level_idc = reader.read_u8()?; + let min_spatial_segmentation_idc = reader.read_u16::()? & 0x0FFF; + let parallelism_type = reader.read_u8()? & 0b11; + let chroma_format_idc = reader.read_u8()? & 0b11; + let bit_depth_luma_minus8 = reader.read_u8()? & 0b111; + let bit_depth_chroma_minus8 = reader.read_u8()? & 0b111; + let avg_frame_rate = reader.read_u16::()?; + + let params = reader.read_u8()?; + let constant_frame_rate = params & 0b11000000 >> 6; + let num_temporal_layers = params & 0b00111000 >> 3; + let temporal_id_nested = (params & 0b00000100 >> 2) > 0; + let length_size_minus_one = params & 0b000011; + + let num_of_arrays = reader.read_u8()?; + + let mut arrays = Vec::with_capacity(num_of_arrays as _); + for _ in 0..num_of_arrays { + let params = reader.read_u8()?; + let num_nalus = reader.read_u16::()?; + let mut nalus = Vec::with_capacity(num_nalus as usize); + + for _ in 0..num_nalus { + let size = reader.read_u16::()?; + let mut data = vec![0; size as usize]; + + reader.read_exact(&mut data)?; + + nalus.push(HvcCArrayNalu { size, data }) + } + + arrays.push(HvcCArray { + completeness: (params & 0b10000000) > 0, + nal_unit_type: params & 0b111111, + nalus, + }); + } + + Ok(HvcCBox { + configuration_version, + general_profile_space, + general_tier_flag, + general_profile_idc, + general_profile_compatibility_flags, + general_constraint_indicator_flag, + general_level_idc, + min_spatial_segmentation_idc, + parallelism_type, + chroma_format_idc, + bit_depth_luma_minus8, + bit_depth_chroma_minus8, + avg_frame_rate, + constant_frame_rate, + num_temporal_layers, + temporal_id_nested, + length_size_minus_one, + arrays, + }) + } +} + +impl WriteBox<&mut W> for HvcCBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + writer.write_u8(self.configuration_version)?; + let general_profile_space = (self.general_profile_space & 0b11) << 6; + let general_tier_flag = u8::from(self.general_tier_flag) << 5; + let general_profile_idc = self.general_profile_idc & 0b11111; + + writer.write_u8(general_profile_space | general_tier_flag | general_profile_idc)?; + writer.write_u32::(self.general_profile_compatibility_flags)?; + writer.write_u48::(self.general_constraint_indicator_flag)?; + writer.write_u8(self.general_level_idc)?; + + writer.write_u16::(self.min_spatial_segmentation_idc & 0x0FFF)?; + writer.write_u8(self.parallelism_type & 0b11)?; + writer.write_u8(self.chroma_format_idc & 0b11)?; + writer.write_u8(self.bit_depth_luma_minus8 & 0b111)?; + writer.write_u8(self.bit_depth_chroma_minus8 & 0b111)?; + writer.write_u16::(self.avg_frame_rate)?; + + let constant_frame_rate = (self.constant_frame_rate & 0b11) << 6; + let num_temporal_layers = (self.num_temporal_layers & 0b111) << 3; + let temporal_id_nested = u8::from(self.temporal_id_nested) << 2; + let length_size_minus_one = self.length_size_minus_one & 0b11; + writer.write_u8( + constant_frame_rate | num_temporal_layers | temporal_id_nested | length_size_minus_one, + )?; + writer.write_u8(self.arrays.len() as u8)?; + for arr in &self.arrays { + writer.write_u8((arr.nal_unit_type & 0b111111) | u8::from(arr.completeness) << 7)?; + writer.write_u16::(arr.nalus.len() as _)?; + + for nalu in &arr.nalus { + writer.write_u16::(nalu.size)?; + writer.write_all(&nalu.data)?; + } + } + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_hev1() { + let src_box = Hev1Box { + data_reference_index: 1, + width: 320, + height: 240, + horizresolution: FixedPointU16::new(0x48), + vertresolution: FixedPointU16::new(0x48), + frame_count: 1, + depth: 24, + hvcc: HvcCBox { + configuration_version: 1, + ..Default::default() + }, + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::Hev1Box); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = Hev1Box::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/ilst.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/ilst.rs new file mode 100644 index 0000000..d0292a3 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/ilst.rs @@ -0,0 +1,253 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::io::{Read, Seek}; + +use byteorder::ByteOrder; +use serde::Serialize; + +use crate::mp4box::data::DataBox; +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct IlstBox { + pub items: HashMap, +} + +impl IlstBox { + pub fn get_type(&self) -> BoxType { + BoxType::IlstBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE; + for item in self.items.values() { + size += item.get_size(); + } + size + } +} + +impl Mp4Box for IlstBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("item_count={}", self.items.len()); + Ok(s) + } +} + +impl ReadBox<&mut R> for IlstBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut items = HashMap::new(); + + let mut current = reader.stream_position()?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "ilst box contains a box with a larger size than it", + )); + } + + match name { + BoxType::NameBox => { + items.insert(MetadataKey::Title, IlstItemBox::read_box(reader, s)?); + } + BoxType::DayBox => { + items.insert(MetadataKey::Year, IlstItemBox::read_box(reader, s)?); + } + BoxType::CovrBox => { + items.insert(MetadataKey::Poster, IlstItemBox::read_box(reader, s)?); + } + BoxType::DescBox => { + items.insert(MetadataKey::Summary, IlstItemBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.stream_position()?; + } + + skip_bytes_to(reader, start + size)?; + + Ok(IlstBox { items }) + } +} + +impl WriteBox<&mut W> for IlstBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + for (key, value) in &self.items { + let name = match key { + MetadataKey::Title => BoxType::NameBox, + MetadataKey::Year => BoxType::DayBox, + MetadataKey::Poster => BoxType::CovrBox, + MetadataKey::Summary => BoxType::DescBox, + }; + BoxHeader::new(name, value.get_size()).write(writer)?; + value.data.write_box(writer)?; + } + Ok(size) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct IlstItemBox { + pub data: DataBox, +} + +impl IlstItemBox { + fn get_size(&self) -> u64 { + HEADER_SIZE + self.data.box_size() + } +} + +impl ReadBox<&mut R> for IlstItemBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut data = None; + + let mut current = reader.stream_position()?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "ilst item box contains a box with a larger size than it", + )); + } + + match name { + BoxType::DataBox => { + data = Some(DataBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.stream_position()?; + } + + if data.is_none() { + return Err(Error::BoxNotFound(BoxType::DataBox)); + } + + skip_bytes_to(reader, start + size)?; + + Ok(IlstItemBox { + data: data.unwrap(), + }) + } +} + +impl<'a> Metadata<'a> for IlstBox { + fn title(&self) -> Option> { + self.items.get(&MetadataKey::Title).map(item_to_str) + } + + fn year(&self) -> Option { + self.items.get(&MetadataKey::Year).and_then(item_to_u32) + } + + fn poster(&self) -> Option<&[u8]> { + self.items.get(&MetadataKey::Poster).map(item_to_bytes) + } + + fn summary(&self) -> Option> { + self.items.get(&MetadataKey::Summary).map(item_to_str) + } +} + +fn item_to_bytes(item: &IlstItemBox) -> &[u8] { + &item.data.data +} + +fn item_to_str(item: &IlstItemBox) -> Cow { + String::from_utf8_lossy(&item.data.data) +} + +fn item_to_u32(item: &IlstItemBox) -> Option { + match item.data.data_type { + DataType::Binary if item.data.data.len() == 4 => Some(BigEndian::read_u32(&item.data.data)), + DataType::Text => String::from_utf8_lossy(&item.data.data).parse::().ok(), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_ilst() { + let src_year = IlstItemBox { + data: DataBox { + data_type: DataType::Text, + data: b"test_year".to_vec(), + }, + }; + let src_box = IlstBox { + items: [ + (MetadataKey::Title, IlstItemBox::default()), + (MetadataKey::Year, src_year), + (MetadataKey::Poster, IlstItemBox::default()), + (MetadataKey::Summary, IlstItemBox::default()), + ] + .into(), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::IlstBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = IlstBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_ilst_empty() { + let src_box = IlstBox::default(); + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::IlstBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = IlstBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/mdhd.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mdhd.rs new file mode 100644 index 0000000..31c65a8 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mdhd.rs @@ -0,0 +1,231 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::char::{decode_utf16, REPLACEMENT_CHARACTER}; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct MdhdBox { + pub version: u8, + pub flags: u32, + pub creation_time: u64, + pub modification_time: u64, + pub timescale: u32, + pub duration: u64, + pub language: String, +} + +impl MdhdBox { + pub fn get_type(&self) -> BoxType { + BoxType::MdhdBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE + HEADER_EXT_SIZE; + + if self.version == 1 { + size += 28; + } else if self.version == 0 { + size += 16; + } + size += 4; + size + } +} + +impl Default for MdhdBox { + fn default() -> Self { + MdhdBox { + version: 0, + flags: 0, + creation_time: 0, + modification_time: 0, + timescale: 1000, + duration: 0, + language: String::from("und"), + } + } +} + +impl Mp4Box for MdhdBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!( + "creation_time={} timescale={} duration={} language={}", + self.creation_time, self.timescale, self.duration, self.language + ); + Ok(s) + } +} + +impl ReadBox<&mut R> for MdhdBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let (creation_time, modification_time, timescale, duration) = if version == 1 { + ( + reader.read_u64::()?, + reader.read_u64::()?, + reader.read_u32::()?, + reader.read_u64::()?, + ) + } else if version == 0 { + ( + reader.read_u32::()? as u64, + reader.read_u32::()? as u64, + reader.read_u32::()?, + reader.read_u32::()? as u64, + ) + } else { + return Err(Error::InvalidData("version must be 0 or 1")); + }; + let language_code = reader.read_u16::()?; + let language = language_string(language_code); + + skip_bytes_to(reader, start + size)?; + + Ok(MdhdBox { + version, + flags, + creation_time, + modification_time, + timescale, + duration, + language, + }) + } +} + +impl WriteBox<&mut W> for MdhdBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + if self.version == 1 { + writer.write_u64::(self.creation_time)?; + writer.write_u64::(self.modification_time)?; + writer.write_u32::(self.timescale)?; + writer.write_u64::(self.duration)?; + } else if self.version == 0 { + writer.write_u32::(self.creation_time as u32)?; + writer.write_u32::(self.modification_time as u32)?; + writer.write_u32::(self.timescale)?; + writer.write_u32::(self.duration as u32)?; + } else { + return Err(Error::InvalidData("version must be 0 or 1")); + } + + let language_code = language_code(&self.language); + writer.write_u16::(language_code)?; + writer.write_u16::(0)?; // pre-defined + + Ok(size) + } +} + +fn language_string(language: u16) -> String { + let mut lang: [u16; 3] = [0; 3]; + + lang[0] = ((language >> 10) & 0x1F) + 0x60; + lang[1] = ((language >> 5) & 0x1F) + 0x60; + lang[2] = ((language) & 0x1F) + 0x60; + + // Decode utf-16 encoded bytes into a string. + let lang_str = decode_utf16(lang.iter().cloned()) + .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER)) + .collect::(); + + lang_str +} + +fn language_code(language: &str) -> u16 { + let mut lang = language.encode_utf16(); + let mut code = (lang.next().unwrap_or(0) & 0x1F) << 10; + code += (lang.next().unwrap_or(0) & 0x1F) << 5; + code += lang.next().unwrap_or(0) & 0x1F; + code +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + fn test_language_code(lang: &str) { + let code = language_code(lang); + let lang2 = language_string(code); + assert_eq!(lang, lang2); + } + + #[test] + fn test_language_codes() { + test_language_code("und"); + test_language_code("eng"); + test_language_code("kor"); + } + + #[test] + fn test_mdhd32() { + let src_box = MdhdBox { + version: 0, + flags: 0, + creation_time: 100, + modification_time: 200, + timescale: 48000, + duration: 30439936, + language: String::from("und"), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MdhdBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = MdhdBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_mdhd64() { + let src_box = MdhdBox { + version: 0, + flags: 0, + creation_time: 100, + modification_time: 200, + timescale: 48000, + duration: 30439936, + language: String::from("eng"), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MdhdBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = MdhdBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/mdia.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mdia.rs new file mode 100644 index 0000000..423bf72 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mdia.rs @@ -0,0 +1,113 @@ +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; +use crate::mp4box::{hdlr::HdlrBox, mdhd::MdhdBox, minf::MinfBox}; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct MdiaBox { + pub mdhd: MdhdBox, + pub hdlr: HdlrBox, + pub minf: MinfBox, +} + +impl MdiaBox { + pub fn get_type(&self) -> BoxType { + BoxType::MdiaBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + self.mdhd.box_size() + self.hdlr.box_size() + self.minf.box_size() + } +} + +impl Mp4Box for MdiaBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = String::new(); + Ok(s) + } +} + +impl ReadBox<&mut R> for MdiaBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut mdhd = None; + let mut hdlr = None; + let mut minf = None; + + let mut current = reader.stream_position()?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "mdia box contains a box with a larger size than it", + )); + } + + match name { + BoxType::MdhdBox => { + mdhd = Some(MdhdBox::read_box(reader, s)?); + } + BoxType::HdlrBox => { + hdlr = Some(HdlrBox::read_box(reader, s)?); + } + BoxType::MinfBox => { + minf = Some(MinfBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.stream_position()?; + } + + if mdhd.is_none() { + return Err(Error::BoxNotFound(BoxType::MdhdBox)); + } + if hdlr.is_none() { + return Err(Error::BoxNotFound(BoxType::HdlrBox)); + } + if minf.is_none() { + return Err(Error::BoxNotFound(BoxType::MinfBox)); + } + + skip_bytes_to(reader, start + size)?; + + Ok(MdiaBox { + mdhd: mdhd.unwrap(), + hdlr: hdlr.unwrap(), + minf: minf.unwrap(), + }) + } +} + +impl WriteBox<&mut W> for MdiaBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + self.mdhd.write_box(writer)?; + self.hdlr.write_box(writer)?; + self.minf.write_box(writer)?; + + Ok(size) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/mehd.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mehd.rs new file mode 100644 index 0000000..63c0246 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mehd.rs @@ -0,0 +1,137 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)] +pub struct MehdBox { + pub version: u8, + pub flags: u32, + pub fragment_duration: u64, +} + +impl MehdBox { + pub fn get_type(&self) -> BoxType { + BoxType::MehdBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE + HEADER_EXT_SIZE; + + if self.version == 1 { + size += 8; + } else if self.version == 0 { + size += 4; + } + size + } +} + +impl Mp4Box for MehdBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("fragment_duration={}", self.fragment_duration); + Ok(s) + } +} + +impl ReadBox<&mut R> for MehdBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let fragment_duration = if version == 1 { + reader.read_u64::()? + } else if version == 0 { + reader.read_u32::()? as u64 + } else { + return Err(Error::InvalidData("version must be 0 or 1")); + }; + skip_bytes_to(reader, start + size)?; + + Ok(MehdBox { + version, + flags, + fragment_duration, + }) + } +} + +impl WriteBox<&mut W> for MehdBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + if self.version == 1 { + writer.write_u64::(self.fragment_duration)?; + } else if self.version == 0 { + writer.write_u32::(self.fragment_duration as u32)?; + } else { + return Err(Error::InvalidData("version must be 0 or 1")); + } + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_mehd32() { + let src_box = MehdBox { + version: 0, + flags: 0, + fragment_duration: 32, + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MehdBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = MehdBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_mehd64() { + let src_box = MehdBox { + version: 0, + flags: 0, + fragment_duration: 30439936, + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MehdBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = MehdBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/meta.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/meta.rs new file mode 100644 index 0000000..56ca816 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/meta.rs @@ -0,0 +1,312 @@ +use std::io::{Read, Seek}; + +use serde::Serialize; + +use crate::mp4box::hdlr::HdlrBox; +use crate::mp4box::ilst::IlstBox; +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(tag = "hdlr")] +#[serde(rename_all = "lowercase")] +pub enum MetaBox { + Mdir { + #[serde(skip_serializing_if = "Option::is_none")] + ilst: Option, + }, + + #[serde(skip)] + Unknown { + #[serde(skip)] + hdlr: HdlrBox, + + #[serde(skip)] + data: Vec<(BoxType, Vec)>, + }, +} + +const MDIR: FourCC = FourCC { value: *b"mdir" }; + +impl MetaBox { + pub fn get_type(&self) -> BoxType { + BoxType::MetaBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE + HEADER_EXT_SIZE; + match self { + Self::Mdir { ilst } => { + size += HdlrBox::default().box_size(); + if let Some(ilst) = ilst { + size += ilst.box_size(); + } + } + Self::Unknown { hdlr, data } => { + size += hdlr.box_size() + + data + .iter() + .map(|(_, data)| data.len() as u64 + HEADER_SIZE) + .sum::() + } + } + size + } +} + +impl Mp4Box for MetaBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = match self { + Self::Mdir { .. } => "hdlr=ilst".to_string(), + Self::Unknown { hdlr, data } => { + format!("hdlr={} data_len={}", hdlr.handler_type, data.len()) + } + }; + Ok(s) + } +} + +impl Default for MetaBox { + fn default() -> Self { + Self::Unknown { + hdlr: Default::default(), + data: Default::default(), + } + } +} + +impl ReadBox<&mut R> for MetaBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let extended_header = reader.read_u32::()?; + if extended_header != 0 { + // ISO mp4 requires this header (version & flags) to be 0. Some + // files skip the extended header and directly start the hdlr box. + let possible_hdlr = BoxType::from(reader.read_u32::()?); + if possible_hdlr == BoxType::HdlrBox { + // This file skipped the extended header! Go back to start. + reader.seek(SeekFrom::Current(-8))?; + } else { + // Looks like we actually have a bad version number or flags. + let v = (extended_header >> 24) as u8; + return Err(Error::UnsupportedBoxVersion(BoxType::MetaBox, v)); + } + } + + let mut current = reader.stream_position()?; + let end = start + size; + + let content_start = current; + + // find the hdlr box + let mut hdlr = None; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + + match name { + BoxType::HdlrBox => { + hdlr = Some(HdlrBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.stream_position()?; + } + + let Some(hdlr) = hdlr else { + return Err(Error::BoxNotFound(BoxType::HdlrBox)); + }; + + // rewind and handle the other boxes + reader.seek(SeekFrom::Start(content_start))?; + current = reader.stream_position()?; + + let mut ilst = None; + + match hdlr.handler_type { + MDIR => { + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + + match name { + BoxType::IlstBox => { + ilst = Some(IlstBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.stream_position()?; + } + + Ok(MetaBox::Mdir { ilst }) + } + _ => { + let mut data = Vec::new(); + + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + + match name { + BoxType::HdlrBox => { + skip_box(reader, s)?; + } + _ => { + let mut box_data = vec![0; (s - HEADER_SIZE) as usize]; + reader.read_exact(&mut box_data)?; + + data.push((name, box_data)); + } + } + + current = reader.stream_position()?; + } + + Ok(MetaBox::Unknown { hdlr, data }) + } + } + } +} + +impl WriteBox<&mut W> for MetaBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, 0, 0)?; + + let hdlr = match self { + Self::Mdir { .. } => HdlrBox { + handler_type: MDIR, + ..Default::default() + }, + Self::Unknown { hdlr, .. } => hdlr.clone(), + }; + hdlr.write_box(writer)?; + + match self { + Self::Mdir { ilst } => { + if let Some(ilst) = ilst { + ilst.write_box(writer)?; + } + } + Self::Unknown { data, .. } => { + for (box_type, data) in data { + BoxHeader::new(*box_type, data.len() as u64 + HEADER_SIZE).write(writer)?; + writer.write_all(data)?; + } + } + } + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_meta_mdir_empty() { + let src_box = MetaBox::Mdir { ilst: None }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MetaBox); + assert_eq!(header.size, src_box.box_size()); + + let dst_box = MetaBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(dst_box, src_box); + } + + #[test] + fn test_meta_mdir() { + let src_box = MetaBox::Mdir { + ilst: Some(IlstBox::default()), + }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MetaBox); + assert_eq!(header.size, src_box.box_size()); + + let dst_box = MetaBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(dst_box, src_box); + } + + #[test] + fn test_meta_hdrl_non_first() { + let data = b"\x00\x00\x00\x7fmeta\x00\x00\x00\x00\x00\x00\x00Qilst\x00\x00\x00I\xa9too\x00\x00\x00Adata\x00\x00\x00\x01\x00\x00\x00\x00TMPGEnc Video Mastering Works 7 Version 7.0.15.17\x00\x00\x00\"hdlr\x00\x00\x00\x00\x00\x00\x00\x00mdirappl\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + let mut reader = Cursor::new(data); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MetaBox); + + let meta_box = MetaBox::read_box(&mut reader, header.size).unwrap(); + + // this contains \xa9too box in the ilst + // it designates the tool that created the file, but is not yet supported by this crate + assert_eq!( + meta_box, + MetaBox::Mdir { + ilst: Some(IlstBox::default()) + } + ); + } + + #[test] + fn test_meta_unknown() { + let src_hdlr = HdlrBox { + handler_type: FourCC::from(*b"test"), + ..Default::default() + }; + let src_data = (BoxType::UnknownBox(0x42494241), b"123".to_vec()); + let src_box = MetaBox::Unknown { + hdlr: src_hdlr, + data: vec![src_data], + }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MetaBox); + assert_eq!(header.size, src_box.box_size()); + + let dst_box = MetaBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(dst_box, src_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/mfhd.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mfhd.rs new file mode 100644 index 0000000..7bc2f72 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mfhd.rs @@ -0,0 +1,107 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct MfhdBox { + pub version: u8, + pub flags: u32, + pub sequence_number: u32, +} + +impl Default for MfhdBox { + fn default() -> Self { + MfhdBox { + version: 0, + flags: 0, + sequence_number: 1, + } + } +} + +impl MfhdBox { + pub fn get_type(&self) -> BoxType { + BoxType::MfhdBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + 4 + } +} + +impl Mp4Box for MfhdBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("sequence_number={}", self.sequence_number); + Ok(s) + } +} + +impl ReadBox<&mut R> for MfhdBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + let sequence_number = reader.read_u32::()?; + + skip_bytes_to(reader, start + size)?; + + Ok(MfhdBox { + version, + flags, + sequence_number, + }) + } +} + +impl WriteBox<&mut W> for MfhdBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + writer.write_u32::(self.sequence_number)?; + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_mfhd() { + let src_box = MfhdBox { + version: 0, + flags: 0, + sequence_number: 1, + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MfhdBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = MfhdBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/minf.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/minf.rs new file mode 100644 index 0000000..5ea853b --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/minf.rs @@ -0,0 +1,134 @@ +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; +use crate::mp4box::{dinf::DinfBox, smhd::SmhdBox, stbl::StblBox, vmhd::VmhdBox}; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct MinfBox { + #[serde(skip_serializing_if = "Option::is_none")] + pub vmhd: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub smhd: Option, + + pub dinf: DinfBox, + pub stbl: StblBox, +} + +impl MinfBox { + pub fn get_type(&self) -> BoxType { + BoxType::MinfBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE; + if let Some(ref vmhd) = self.vmhd { + size += vmhd.box_size(); + } + if let Some(ref smhd) = self.smhd { + size += smhd.box_size(); + } + size += self.dinf.box_size(); + size += self.stbl.box_size(); + size + } +} + +impl Mp4Box for MinfBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = String::new(); + Ok(s) + } +} + +impl ReadBox<&mut R> for MinfBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut vmhd = None; + let mut smhd = None; + let mut dinf = None; + let mut stbl = None; + + let mut current = reader.stream_position()?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "minf box contains a box with a larger size than it", + )); + } + + match name { + BoxType::VmhdBox => { + vmhd = Some(VmhdBox::read_box(reader, s)?); + } + BoxType::SmhdBox => { + smhd = Some(SmhdBox::read_box(reader, s)?); + } + BoxType::DinfBox => { + dinf = Some(DinfBox::read_box(reader, s)?); + } + BoxType::StblBox => { + stbl = Some(StblBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.stream_position()?; + } + + if dinf.is_none() { + return Err(Error::BoxNotFound(BoxType::DinfBox)); + } + if stbl.is_none() { + return Err(Error::BoxNotFound(BoxType::StblBox)); + } + + skip_bytes_to(reader, start + size)?; + + Ok(MinfBox { + vmhd, + smhd, + dinf: dinf.unwrap(), + stbl: stbl.unwrap(), + }) + } +} + +impl WriteBox<&mut W> for MinfBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + if let Some(ref vmhd) = self.vmhd { + vmhd.write_box(writer)?; + } + if let Some(ref smhd) = self.smhd { + smhd.write_box(writer)?; + } + self.dinf.write_box(writer)?; + self.stbl.write_box(writer)?; + + Ok(size) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/mod.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mod.rs new file mode 100644 index 0000000..56b828d --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mod.rs @@ -0,0 +1,438 @@ +//! All ISO-MP4 boxes (atoms) and operations. +//! +//! * [ISO/IEC 14496-12](https://en.wikipedia.org/wiki/MPEG-4_Part_14) - ISO Base Media File Format (QuickTime, MPEG-4, etc) +//! * [ISO/IEC 14496-14](https://en.wikipedia.org/wiki/MPEG-4_Part_14) - MP4 file format +//! * ISO/IEC 14496-17 - Streaming text format +//! * [ISO 23009-1](https://www.iso.org/standard/79329.html) -Dynamic adaptive streaming over HTTP (DASH) +//! +//! http://developer.apple.com/documentation/QuickTime/QTFF/index.html +//! http://www.adobe.com/devnet/video/articles/mp4_movie_atom.html +//! http://mp4ra.org/#/atoms +//! +//! Supported Atoms: +//! ftyp +//! moov +//! mvhd +//! udta +//! meta +//! ilst +//! data +//! trak +//! tkhd +//! mdia +//! mdhd +//! hdlr +//! minf +//! stbl +//! stsd +//! avc1 +//! hev1 +//! mp4a +//! tx3g +//! stts +//! stsc +//! stsz +//! stss +//! stco +//! co64 +//! ctts +//! dinf +//! dref +//! smhd +//! vmhd +//! edts +//! elst +//! mvex +//! mehd +//! trex +//! emsg +//! prft +//! moof +//! mfhd +//! traf +//! tfhd +//! tfdt +//! trun +//! mdat +//! free +//! + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use std::convert::TryInto; +use std::io::{Read, Seek, SeekFrom, Write}; + +use crate::*; + +pub(crate) mod avc1; +pub(crate) mod co64; +pub(crate) mod ctts; +pub(crate) mod data; +pub(crate) mod dinf; +pub(crate) mod edts; +pub(crate) mod elst; +pub(crate) mod emsg; +pub(crate) mod ftyp; +pub(crate) mod hdlr; +pub(crate) mod hev1; +pub(crate) mod ilst; +pub(crate) mod mdhd; +pub(crate) mod mdia; +pub(crate) mod mehd; +pub(crate) mod meta; +pub(crate) mod mfhd; +pub(crate) mod minf; +pub(crate) mod prft; +pub(crate) mod moof; +pub(crate) mod moov; +pub(crate) mod mp4a; +pub(crate) mod mvex; +pub(crate) mod mvhd; +pub(crate) mod smhd; +pub(crate) mod stbl; +pub(crate) mod stco; +pub(crate) mod stsc; +pub(crate) mod stsd; +pub(crate) mod stss; +pub(crate) mod stsz; +pub(crate) mod stts; +pub(crate) mod tfdt; +pub(crate) mod tfhd; +pub(crate) mod tkhd; +pub(crate) mod traf; +pub(crate) mod trak; +pub(crate) mod trex; +pub(crate) mod trun; +pub(crate) mod tx3g; +pub(crate) mod udta; +pub(crate) mod vmhd; +pub(crate) mod vp09; +pub(crate) mod vpcc; + +pub use avc1::Avc1Box; +pub use co64::Co64Box; +pub use ctts::CttsBox; +pub use data::DataBox; +pub use dinf::DinfBox; +pub use edts::EdtsBox; +pub use elst::ElstBox; +pub use emsg::EmsgBox; +pub use ftyp::FtypBox; +pub use hdlr::HdlrBox; +pub use hev1::Hev1Box; +pub use ilst::IlstBox; +pub use mdhd::MdhdBox; +pub use mdia::MdiaBox; +pub use mehd::MehdBox; +pub use meta::MetaBox; +pub use mfhd::MfhdBox; +pub use minf::MinfBox; +pub use prft::PrftBox; +pub use moof::MoofBox; +pub use moov::MoovBox; +pub use mp4a::Mp4aBox; +pub use mvex::MvexBox; +pub use mvhd::MvhdBox; +pub use smhd::SmhdBox; +pub use stbl::StblBox; +pub use stco::StcoBox; +pub use stsc::StscBox; +pub use stsd::StsdBox; +pub use stss::StssBox; +pub use stsz::StszBox; +pub use stts::SttsBox; +pub use tfdt::TfdtBox; +pub use tfhd::TfhdBox; +pub use tkhd::TkhdBox; +pub use traf::TrafBox; +pub use trak::TrakBox; +pub use trex::TrexBox; +pub use trun::TrunBox; +pub use tx3g::Tx3gBox; +pub use udta::UdtaBox; +pub use vmhd::VmhdBox; +pub use vp09::Vp09Box; +pub use vpcc::VpccBox; + +pub const HEADER_SIZE: u64 = 8; +// const HEADER_LARGE_SIZE: u64 = 16; +pub const HEADER_EXT_SIZE: u64 = 4; + +macro_rules! boxtype { + ($( $name:ident => $value:expr ),*) => { + #[derive(Clone, Copy, PartialEq, Eq)] + pub enum BoxType { + $( $name, )* + UnknownBox(u32), + } + + impl From for BoxType { + fn from(t: u32) -> BoxType { + match t { + $( $value => BoxType::$name, )* + _ => BoxType::UnknownBox(t), + } + } + } + + impl From for u32 { + fn from(b: BoxType) -> u32 { + match b { + $( BoxType::$name => $value, )* + BoxType::UnknownBox(t) => t, + } + } + } + } +} + +boxtype! { + FtypBox => 0x66747970, + MvhdBox => 0x6d766864, + MfhdBox => 0x6d666864, + FreeBox => 0x66726565, + MdatBox => 0x6d646174, + MoovBox => 0x6d6f6f76, + MvexBox => 0x6d766578, + MehdBox => 0x6d656864, + TrexBox => 0x74726578, + EmsgBox => 0x656d7367, + PrftBox => 0x70726674, + MoofBox => 0x6d6f6f66, + TkhdBox => 0x746b6864, + TfhdBox => 0x74666864, + TfdtBox => 0x74666474, + EdtsBox => 0x65647473, + MdiaBox => 0x6d646961, + ElstBox => 0x656c7374, + MdhdBox => 0x6d646864, + HdlrBox => 0x68646c72, + MinfBox => 0x6d696e66, + VmhdBox => 0x766d6864, + StblBox => 0x7374626c, + StsdBox => 0x73747364, + SttsBox => 0x73747473, + CttsBox => 0x63747473, + StssBox => 0x73747373, + StscBox => 0x73747363, + StszBox => 0x7374737A, + StcoBox => 0x7374636F, + Co64Box => 0x636F3634, + TrakBox => 0x7472616b, + TrafBox => 0x74726166, + TrunBox => 0x7472756E, + UdtaBox => 0x75647461, + MetaBox => 0x6d657461, + DinfBox => 0x64696e66, + DrefBox => 0x64726566, + UrlBox => 0x75726C20, + SmhdBox => 0x736d6864, + Avc1Box => 0x61766331, + AvcCBox => 0x61766343, + Hev1Box => 0x68657631, + HvcCBox => 0x68766343, + Mp4aBox => 0x6d703461, + EsdsBox => 0x65736473, + Tx3gBox => 0x74783367, + VpccBox => 0x76706343, + Vp09Box => 0x76703039, + DataBox => 0x64617461, + IlstBox => 0x696c7374, + NameBox => 0xa96e616d, + DayBox => 0xa9646179, + CovrBox => 0x636f7672, + DescBox => 0x64657363, + WideBox => 0x77696465, + WaveBox => 0x77617665 +} + +pub trait Mp4Box: Sized { + fn box_type(&self) -> BoxType; + fn box_size(&self) -> u64; + fn to_json(&self) -> Result; + fn summary(&self) -> Result; +} + +pub trait ReadBox: Sized { + fn read_box(_: T, size: u64) -> Result; +} + +pub trait WriteBox: Sized { + fn write_box(&self, _: T) -> Result; +} + +#[derive(Debug, Clone, Copy)] +pub struct BoxHeader { + pub name: BoxType, + pub size: u64, +} + +impl BoxHeader { + pub fn new(name: BoxType, size: u64) -> Self { + Self { name, size } + } + + // TODO: if size is 0, then this box is the last one in the file + pub fn read(reader: &mut R) -> Result { + // Create and read to buf. + let mut buf = [0u8; 8]; // 8 bytes for box header. + reader.read_exact(&mut buf)?; + + // Get size. + let s = buf[0..4].try_into().unwrap(); + let size = u32::from_be_bytes(s); + + // Get box type string. + let t = buf[4..8].try_into().unwrap(); + let typ = u32::from_be_bytes(t); + + // Get largesize if size is 1 + if size == 1 { + reader.read_exact(&mut buf)?; + let largesize = u64::from_be_bytes(buf); + + Ok(BoxHeader { + name: BoxType::from(typ), + + // Subtract the length of the serialized largesize, as callers assume `size - HEADER_SIZE` is the length + // of the box data. Disallow `largesize < 16`, or else a largesize of 8 will result in a BoxHeader::size + // of 0, incorrectly indicating that the box data extends to the end of the stream. + size: match largesize { + 0 => 0, + 1..=15 => return Err(Error::InvalidData("64-bit box size too small")), + 16..=u64::MAX => largesize - 8, + }, + }) + } else { + Ok(BoxHeader { + name: BoxType::from(typ), + size: size as u64, + }) + } + } + + pub fn write(&self, writer: &mut W) -> Result { + if self.size > u32::MAX as u64 { + writer.write_u32::(1)?; + writer.write_u32::(self.name.into())?; + writer.write_u64::(self.size)?; + Ok(16) + } else { + writer.write_u32::(self.size as u32)?; + writer.write_u32::(self.name.into())?; + Ok(8) + } + } +} + +pub fn read_box_header_ext(reader: &mut R) -> Result<(u8, u32)> { + let version = reader.read_u8()?; + let flags = reader.read_u24::()?; + Ok((version, flags)) +} + +pub fn write_box_header_ext(w: &mut W, v: u8, f: u32) -> Result { + w.write_u8(v)?; + w.write_u24::(f)?; + Ok(4) +} + +pub fn box_start(seeker: &mut R) -> Result { + Ok(seeker.stream_position()? - HEADER_SIZE) +} + +pub fn skip_bytes(seeker: &mut S, size: u64) -> Result<()> { + seeker.seek(SeekFrom::Current(size as i64))?; + Ok(()) +} + +pub fn skip_bytes_to(seeker: &mut S, pos: u64) -> Result<()> { + seeker.seek(SeekFrom::Start(pos))?; + Ok(()) +} + +pub fn skip_box(seeker: &mut S, size: u64) -> Result<()> { + let start = box_start(seeker)?; + skip_bytes_to(seeker, start + size)?; + Ok(()) +} + +pub fn write_zeros(writer: &mut W, size: u64) -> Result<()> { + for _ in 0..size { + writer.write_u8(0)?; + } + Ok(()) +} + +mod value_u32 { + use crate::types::FixedPointU16; + use serde::{self, Serializer}; + + pub fn serialize(fixed: &FixedPointU16, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u16(fixed.value()) + } +} + +mod value_i16 { + use crate::types::FixedPointI8; + use serde::{self, Serializer}; + + pub fn serialize(fixed: &FixedPointI8, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_i8(fixed.value()) + } +} + +mod value_u8 { + use crate::types::FixedPointU8; + use serde::{self, Serializer}; + + pub fn serialize(fixed: &FixedPointU8, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u8(fixed.value()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fourcc() { + let ftyp_fcc = 0x66747970; + let ftyp_value = FourCC::from(ftyp_fcc); + assert_eq!(&ftyp_value.value[..], b"ftyp"); + let ftyp_fcc2: u32 = ftyp_value.into(); + assert_eq!(ftyp_fcc, ftyp_fcc2); + } + + #[test] + fn test_largesize_too_small() { + let error = BoxHeader::read(&mut &[0, 0, 0, 1, 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 7][..]); + assert!(matches!(error, Err(Error::InvalidData(_)))); + } + + #[test] + fn test_zero_largesize() { + let error = BoxHeader::read(&mut &[0, 0, 0, 1, 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 8][..]); + assert!(matches!(error, Err(Error::InvalidData(_)))); + } + + #[test] + fn test_nonzero_largesize_too_small() { + let error = BoxHeader::read(&mut &[0, 0, 0, 1, 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 15][..]); + assert!(matches!(error, Err(Error::InvalidData(_)))); + } + + #[test] + fn test_valid_largesize() { + let header = BoxHeader::read(&mut &[0, 0, 0, 1, 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 16][..]); + assert!(matches!(header, Ok(BoxHeader { size: 8, .. }))); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/moof.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/moof.rs new file mode 100644 index 0000000..20c3565 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/moof.rs @@ -0,0 +1,107 @@ +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; +use crate::mp4box::{mfhd::MfhdBox, traf::TrafBox}; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct MoofBox { + pub mfhd: MfhdBox, + + #[serde(rename = "traf")] + pub trafs: Vec, +} + +impl MoofBox { + pub fn get_type(&self) -> BoxType { + BoxType::MoofBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE + self.mfhd.box_size(); + for traf in self.trafs.iter() { + size += traf.box_size(); + } + size + } +} + +impl Mp4Box for MoofBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("trafs={}", self.trafs.len()); + Ok(s) + } +} + +impl ReadBox<&mut R> for MoofBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut mfhd = None; + let mut trafs = Vec::new(); + + let mut current = reader.stream_position()?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "moof box contains a box with a larger size than it", + )); + } + + match name { + BoxType::MfhdBox => { + mfhd = Some(MfhdBox::read_box(reader, s)?); + } + BoxType::TrafBox => { + let traf = TrafBox::read_box(reader, s)?; + trafs.push(traf); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + current = reader.stream_position()?; + } + + if mfhd.is_none() { + return Err(Error::BoxNotFound(BoxType::MfhdBox)); + } + + skip_bytes_to(reader, start + size)?; + + Ok(MoofBox { + mfhd: mfhd.unwrap(), + trafs, + }) + } +} + +impl WriteBox<&mut W> for MoofBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + self.mfhd.write_box(writer)?; + for traf in self.trafs.iter() { + traf.write_box(writer)?; + } + Ok(0) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/moov.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/moov.rs new file mode 100644 index 0000000..ac19381 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/moov.rs @@ -0,0 +1,192 @@ +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::meta::MetaBox; +use crate::mp4box::*; +use crate::mp4box::{mvex::MvexBox, mvhd::MvhdBox, trak::TrakBox, udta::UdtaBox}; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct MoovBox { + pub mvhd: MvhdBox, + + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub mvex: Option, + + #[serde(rename = "trak")] + pub traks: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + pub udta: Option, +} + +impl MoovBox { + pub fn get_type(&self) -> BoxType { + BoxType::MoovBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE + self.mvhd.box_size(); + for trak in self.traks.iter() { + size += trak.box_size(); + } + if let Some(meta) = &self.meta { + size += meta.box_size(); + } + if let Some(udta) = &self.udta { + size += udta.box_size(); + } + size + } +} + +impl Mp4Box for MoovBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("traks={}", self.traks.len()); + Ok(s) + } +} + +impl ReadBox<&mut R> for MoovBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut mvhd = None; + let mut meta = None; + let mut udta = None; + let mut mvex = None; + let mut traks = Vec::new(); + + let mut current = reader.stream_position()?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "moov box contains a box with a larger size than it", + )); + } + + match name { + BoxType::MvhdBox => { + mvhd = Some(MvhdBox::read_box(reader, s)?); + } + BoxType::MetaBox => { + meta = Some(MetaBox::read_box(reader, s)?); + } + BoxType::MvexBox => { + mvex = Some(MvexBox::read_box(reader, s)?); + } + BoxType::TrakBox => { + let trak = TrakBox::read_box(reader, s)?; + traks.push(trak); + } + BoxType::UdtaBox => { + udta = Some(UdtaBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.stream_position()?; + } + + if mvhd.is_none() { + return Err(Error::BoxNotFound(BoxType::MvhdBox)); + } + + skip_bytes_to(reader, start + size)?; + + Ok(MoovBox { + mvhd: mvhd.unwrap(), + meta, + udta, + mvex, + traks, + }) + } +} + +impl WriteBox<&mut W> for MoovBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + self.mvhd.write_box(writer)?; + for trak in self.traks.iter() { + trak.write_box(writer)?; + } + if let Some(meta) = &self.meta { + meta.write_box(writer)?; + } + if let Some(udta) = &self.udta { + udta.write_box(writer)?; + } + Ok(0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_moov() { + let src_box = MoovBox { + mvhd: MvhdBox::default(), + mvex: None, // XXX mvex is not written currently + traks: vec![], + meta: Some(MetaBox::default()), + udta: Some(UdtaBox::default()), + }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MoovBox); + assert_eq!(header.size, src_box.box_size()); + + let dst_box = MoovBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(dst_box, src_box); + } + + #[test] + fn test_moov_empty() { + let src_box = MoovBox::default(); + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MoovBox); + assert_eq!(header.size, src_box.box_size()); + + let dst_box = MoovBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(dst_box, src_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/mp4a.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mp4a.rs new file mode 100644 index 0000000..a80c6c4 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mp4a.rs @@ -0,0 +1,683 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Mp4aBox { + pub data_reference_index: u16, + pub channelcount: u16, + pub samplesize: u16, + + #[serde(with = "value_u32")] + pub samplerate: FixedPointU16, + pub esds: Option, +} + +impl Default for Mp4aBox { + fn default() -> Self { + Self { + data_reference_index: 0, + channelcount: 2, + samplesize: 16, + samplerate: FixedPointU16::new(48000), + esds: Some(EsdsBox::default()), + } + } +} + +impl Mp4aBox { + pub fn new(config: &AacConfig) -> Self { + Self { + data_reference_index: 1, + channelcount: config.chan_conf as u16, + samplesize: 16, + samplerate: FixedPointU16::new(config.freq_index.freq() as u16), + esds: Some(EsdsBox::new(config)), + } + } + + pub fn get_type(&self) -> BoxType { + BoxType::Mp4aBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE + 8 + 20; + if let Some(ref esds) = self.esds { + size += esds.box_size(); + } + size + } +} + +impl Mp4Box for Mp4aBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!( + "channel_count={} sample_size={} sample_rate={}", + self.channelcount, + self.samplesize, + self.samplerate.value() + ); + Ok(s) + } +} + +impl ReadBox<&mut R> for Mp4aBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + reader.read_u32::()?; // reserved + reader.read_u16::()?; // reserved + let data_reference_index = reader.read_u16::()?; + let version = reader.read_u16::()?; + reader.read_u16::()?; // reserved + reader.read_u32::()?; // reserved + let channelcount = reader.read_u16::()?; + let samplesize = reader.read_u16::()?; + reader.read_u32::()?; // pre-defined, reserved + let samplerate = FixedPointU16::new_raw(reader.read_u32::()?); + + if version == 1 { + // Skip QTFF + reader.read_u64::()?; + reader.read_u64::()?; + } + + // Find esds in mp4a or wave + let mut esds = None; + let end = start + size; + loop { + let current = reader.stream_position()?; + if current >= end { + break; + } + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "mp4a box contains a box with a larger size than it", + )); + } + if name == BoxType::EsdsBox { + esds = Some(EsdsBox::read_box(reader, s)?); + break; + } else if name == BoxType::WaveBox { + // Typically contains frma, mp4a, esds, and a terminator atom + } else { + // Skip boxes + let skip_to = current + s; + skip_bytes_to(reader, skip_to)?; + } + } + + skip_bytes_to(reader, end)?; + + Ok(Mp4aBox { + data_reference_index, + channelcount, + samplesize, + samplerate, + esds, + }) + } +} + +impl WriteBox<&mut W> for Mp4aBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + writer.write_u32::(0)?; // reserved + writer.write_u16::(0)?; // reserved + writer.write_u16::(self.data_reference_index)?; + + writer.write_u64::(0)?; // reserved + writer.write_u16::(self.channelcount)?; + writer.write_u16::(self.samplesize)?; + writer.write_u32::(0)?; // reserved + writer.write_u32::(self.samplerate.raw_value())?; + + if let Some(ref esds) = self.esds { + esds.write_box(writer)?; + } + + Ok(size) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct EsdsBox { + pub version: u8, + pub flags: u32, + pub es_desc: ESDescriptor, +} + +impl EsdsBox { + pub fn new(config: &AacConfig) -> Self { + Self { + version: 0, + flags: 0, + es_desc: ESDescriptor::new(config), + } + } +} + +impl Mp4Box for EsdsBox { + fn box_type(&self) -> BoxType { + BoxType::EsdsBox + } + + fn box_size(&self) -> u64 { + HEADER_SIZE + + HEADER_EXT_SIZE + + 1 + + size_of_length(ESDescriptor::desc_size()) as u64 + + ESDescriptor::desc_size() as u64 + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + Ok(String::new()) + } +} + +impl ReadBox<&mut R> for EsdsBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let mut es_desc = None; + + let mut current = reader.stream_position()?; + let end = start + size; + while current < end { + let (desc_tag, desc_size) = read_desc(reader)?; + match desc_tag { + 0x03 => { + es_desc = Some(ESDescriptor::read_desc(reader, desc_size)?); + } + _ => break, + } + current = reader.stream_position()?; + } + + if es_desc.is_none() { + return Err(Error::InvalidData("ESDescriptor not found")); + } + + skip_bytes_to(reader, start + size)?; + + Ok(EsdsBox { + version, + flags, + es_desc: es_desc.unwrap(), + }) + } +} + +impl WriteBox<&mut W> for EsdsBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + self.es_desc.write_desc(writer)?; + + Ok(size) + } +} + +trait Descriptor: Sized { + fn desc_tag() -> u8; + fn desc_size() -> u32; +} + +trait ReadDesc: Sized { + fn read_desc(_: T, size: u32) -> Result; +} + +trait WriteDesc: Sized { + fn write_desc(&self, _: T) -> Result; +} + +fn read_desc(reader: &mut R) -> Result<(u8, u32)> { + let tag = reader.read_u8()?; + + let mut size: u32 = 0; + for _ in 0..4 { + let b = reader.read_u8()?; + size = (size << 7) | (b & 0x7F) as u32; + if b & 0x80 == 0 { + break; + } + } + + Ok((tag, size)) +} + +fn size_of_length(size: u32) -> u32 { + match size { + 0x0..=0x7F => 1, + 0x80..=0x3FFF => 2, + 0x4000..=0x1FFFFF => 3, + _ => 4, + } +} + +fn write_desc(writer: &mut W, tag: u8, size: u32) -> Result { + writer.write_u8(tag)?; + + if size as u64 > std::u32::MAX as u64 { + return Err(Error::InvalidData("invalid descriptor length range")); + } + + let nbytes = size_of_length(size); + + for i in 0..nbytes { + let mut b = (size >> ((nbytes - i - 1) * 7)) as u8 & 0x7F; + if i < nbytes - 1 { + b |= 0x80; + } + writer.write_u8(b)?; + } + + Ok(1 + nbytes as u64) +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct ESDescriptor { + pub es_id: u16, + + pub dec_config: DecoderConfigDescriptor, + pub sl_config: SLConfigDescriptor, +} + +impl ESDescriptor { + pub fn new(config: &AacConfig) -> Self { + Self { + es_id: 1, + dec_config: DecoderConfigDescriptor::new(config), + sl_config: SLConfigDescriptor::new(), + } + } +} + +impl Descriptor for ESDescriptor { + fn desc_tag() -> u8 { + 0x03 + } + + fn desc_size() -> u32 { + 3 + 1 + + size_of_length(DecoderConfigDescriptor::desc_size()) + + DecoderConfigDescriptor::desc_size() + + 1 + + size_of_length(SLConfigDescriptor::desc_size()) + + SLConfigDescriptor::desc_size() + } +} + +impl ReadDesc<&mut R> for ESDescriptor { + fn read_desc(reader: &mut R, size: u32) -> Result { + let start = reader.stream_position()?; + + let es_id = reader.read_u16::()?; + reader.read_u8()?; // XXX flags must be 0 + + let mut dec_config = None; + let mut sl_config = None; + + let mut current = reader.stream_position()?; + let end = start + size as u64; + while current < end { + let (desc_tag, desc_size) = read_desc(reader)?; + match desc_tag { + 0x04 => { + dec_config = Some(DecoderConfigDescriptor::read_desc(reader, desc_size)?); + } + 0x06 => { + sl_config = Some(SLConfigDescriptor::read_desc(reader, desc_size)?); + } + _ => { + skip_bytes(reader, desc_size as u64)?; + } + } + current = reader.stream_position()?; + } + + Ok(ESDescriptor { + es_id, + dec_config: dec_config.unwrap_or_default(), + sl_config: sl_config.unwrap_or_default(), + }) + } +} + +impl WriteDesc<&mut W> for ESDescriptor { + fn write_desc(&self, writer: &mut W) -> Result { + let size = Self::desc_size(); + write_desc(writer, Self::desc_tag(), size)?; + + writer.write_u16::(self.es_id)?; + writer.write_u8(0)?; + + self.dec_config.write_desc(writer)?; + self.sl_config.write_desc(writer)?; + + Ok(size) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct DecoderConfigDescriptor { + pub object_type_indication: u8, + pub stream_type: u8, + pub up_stream: u8, + pub buffer_size_db: u32, + pub max_bitrate: u32, + pub avg_bitrate: u32, + + pub dec_specific: DecoderSpecificDescriptor, +} + +impl DecoderConfigDescriptor { + pub fn new(config: &AacConfig) -> Self { + Self { + object_type_indication: 0x40, // XXX AAC + stream_type: 0x05, // XXX Audio + up_stream: 0, + buffer_size_db: 0, + max_bitrate: config.bitrate, // XXX + avg_bitrate: config.bitrate, + dec_specific: DecoderSpecificDescriptor::new(config), + } + } +} + +impl Descriptor for DecoderConfigDescriptor { + fn desc_tag() -> u8 { + 0x04 + } + + fn desc_size() -> u32 { + 13 + 1 + + size_of_length(DecoderSpecificDescriptor::desc_size()) + + DecoderSpecificDescriptor::desc_size() + } +} + +impl ReadDesc<&mut R> for DecoderConfigDescriptor { + fn read_desc(reader: &mut R, size: u32) -> Result { + let start = reader.stream_position()?; + + let object_type_indication = reader.read_u8()?; + let byte_a = reader.read_u8()?; + let stream_type = (byte_a & 0xFC) >> 2; + let up_stream = byte_a & 0x02; + let buffer_size_db = reader.read_u24::()?; + let max_bitrate = reader.read_u32::()?; + let avg_bitrate = reader.read_u32::()?; + + let mut dec_specific = None; + + let mut current = reader.stream_position()?; + let end = start + size as u64; + while current < end { + let (desc_tag, desc_size) = read_desc(reader)?; + match desc_tag { + 0x05 => { + dec_specific = Some(DecoderSpecificDescriptor::read_desc(reader, desc_size)?); + } + _ => { + skip_bytes(reader, desc_size as u64)?; + } + } + current = reader.stream_position()?; + } + + Ok(DecoderConfigDescriptor { + object_type_indication, + stream_type, + up_stream, + buffer_size_db, + max_bitrate, + avg_bitrate, + dec_specific: dec_specific.unwrap_or_default(), + }) + } +} + +impl WriteDesc<&mut W> for DecoderConfigDescriptor { + fn write_desc(&self, writer: &mut W) -> Result { + let size = Self::desc_size(); + write_desc(writer, Self::desc_tag(), size)?; + + writer.write_u8(self.object_type_indication)?; + writer.write_u8((self.stream_type << 2) + (self.up_stream & 0x02) + 1)?; // 1 reserved + writer.write_u24::(self.buffer_size_db)?; + writer.write_u32::(self.max_bitrate)?; + writer.write_u32::(self.avg_bitrate)?; + + self.dec_specific.write_desc(writer)?; + + Ok(size) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct DecoderSpecificDescriptor { + pub profile: u8, + pub freq_index: u8, + pub chan_conf: u8, +} + +impl DecoderSpecificDescriptor { + pub fn new(config: &AacConfig) -> Self { + Self { + profile: config.profile as u8, + freq_index: config.freq_index as u8, + chan_conf: config.chan_conf as u8, + } + } +} + +impl Descriptor for DecoderSpecificDescriptor { + fn desc_tag() -> u8 { + 0x05 + } + + fn desc_size() -> u32 { + 2 + } +} + +fn get_audio_object_type(byte_a: u8, byte_b: u8) -> u8 { + let mut profile = byte_a >> 3; + if profile == 31 { + profile = 32 + ((byte_a & 7) | (byte_b >> 5)); + } + + profile +} + +fn get_chan_conf( + reader: &mut R, + byte_b: u8, + freq_index: u8, + extended_profile: bool, +) -> Result { + let chan_conf; + if freq_index == 15 { + // Skip the 24 bit sample rate + let sample_rate = reader.read_u24::()?; + chan_conf = ((sample_rate >> 4) & 0x0F) as u8; + } else if extended_profile { + let byte_c = reader.read_u8()?; + chan_conf = (byte_b & 1) | (byte_c & 0xE0); + } else { + chan_conf = (byte_b >> 3) & 0x0F; + } + + Ok(chan_conf) +} + +impl ReadDesc<&mut R> for DecoderSpecificDescriptor { + fn read_desc(reader: &mut R, _size: u32) -> Result { + let byte_a = reader.read_u8()?; + let byte_b = reader.read_u8()?; + let profile = get_audio_object_type(byte_a, byte_b); + let freq_index; + let chan_conf; + if profile > 31 { + freq_index = (byte_b >> 1) & 0x0F; + chan_conf = get_chan_conf(reader, byte_b, freq_index, true)?; + } else { + freq_index = ((byte_a & 0x07) << 1) + (byte_b >> 7); + chan_conf = get_chan_conf(reader, byte_b, freq_index, false)?; + } + + Ok(DecoderSpecificDescriptor { + profile, + freq_index, + chan_conf, + }) + } +} + +impl WriteDesc<&mut W> for DecoderSpecificDescriptor { + fn write_desc(&self, writer: &mut W) -> Result { + let size = Self::desc_size(); + write_desc(writer, Self::desc_tag(), size)?; + + writer.write_u8((self.profile << 3) + (self.freq_index >> 1))?; + writer.write_u8((self.freq_index << 7) + (self.chan_conf << 3))?; + + Ok(size) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct SLConfigDescriptor {} + +impl SLConfigDescriptor { + pub fn new() -> Self { + SLConfigDescriptor {} + } +} + +impl Descriptor for SLConfigDescriptor { + fn desc_tag() -> u8 { + 0x06 + } + + fn desc_size() -> u32 { + 1 + } +} + +impl ReadDesc<&mut R> for SLConfigDescriptor { + fn read_desc(reader: &mut R, _size: u32) -> Result { + reader.read_u8()?; // pre-defined + + Ok(SLConfigDescriptor {}) + } +} + +impl WriteDesc<&mut W> for SLConfigDescriptor { + fn write_desc(&self, writer: &mut W) -> Result { + let size = Self::desc_size(); + write_desc(writer, Self::desc_tag(), size)?; + + writer.write_u8(2)?; // pre-defined + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_mp4a() { + let src_box = Mp4aBox { + data_reference_index: 1, + channelcount: 2, + samplesize: 16, + samplerate: FixedPointU16::new(48000), + esds: Some(EsdsBox { + version: 0, + flags: 0, + es_desc: ESDescriptor { + es_id: 2, + dec_config: DecoderConfigDescriptor { + object_type_indication: 0x40, + stream_type: 0x05, + up_stream: 0, + buffer_size_db: 0, + max_bitrate: 67695, + avg_bitrate: 67695, + dec_specific: DecoderSpecificDescriptor { + profile: 2, + freq_index: 3, + chan_conf: 1, + }, + }, + sl_config: SLConfigDescriptor::default(), + }, + }), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::Mp4aBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = Mp4aBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_mp4a_no_esds() { + let src_box = Mp4aBox { + data_reference_index: 1, + channelcount: 2, + samplesize: 16, + samplerate: FixedPointU16::new(48000), + esds: None, + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::Mp4aBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = Mp4aBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/mvex.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mvex.rs new file mode 100644 index 0000000..8be683b --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mvex.rs @@ -0,0 +1,102 @@ +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; +use crate::mp4box::{mehd::MehdBox, trex::TrexBox}; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct MvexBox { + pub mehd: Option, + pub trex: TrexBox, +} + +impl MvexBox { + pub fn get_type(&self) -> BoxType { + BoxType::MdiaBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + self.mehd.as_ref().map(|x| x.box_size()).unwrap_or(0) + self.trex.box_size() + } +} + +impl Mp4Box for MvexBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = String::new(); + Ok(s) + } +} + +impl ReadBox<&mut R> for MvexBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut mehd = None; + let mut trex = None; + + let mut current = reader.stream_position()?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "mvex box contains a box with a larger size than it", + )); + } + + match name { + BoxType::MehdBox => { + mehd = Some(MehdBox::read_box(reader, s)?); + } + BoxType::TrexBox => { + trex = Some(TrexBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.stream_position()?; + } + + if trex.is_none() { + return Err(Error::BoxNotFound(BoxType::TrexBox)); + } + + skip_bytes_to(reader, start + size)?; + + Ok(MvexBox { + mehd, + trex: trex.unwrap(), + }) + } +} + +impl WriteBox<&mut W> for MvexBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + if let Some(mehd) = &self.mehd { + mehd.write_box(writer)?; + } + self.trex.write_box(writer)?; + + Ok(size) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/mvhd.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mvhd.rs new file mode 100644 index 0000000..462a29b --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/mvhd.rs @@ -0,0 +1,257 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct MvhdBox { + pub version: u8, + pub flags: u32, + pub creation_time: u64, + pub modification_time: u64, + pub timescale: u32, + pub duration: u64, + + #[serde(with = "value_u32")] + pub rate: FixedPointU16, + #[serde(with = "value_u8")] + pub volume: FixedPointU8, + + pub matrix: tkhd::Matrix, + + pub next_track_id: u32, +} + +impl MvhdBox { + pub fn get_type(&self) -> BoxType { + BoxType::MvhdBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE + HEADER_EXT_SIZE; + if self.version == 1 { + size += 28; + } else if self.version == 0 { + size += 16; + } + size += 80; + size + } +} + +impl Default for MvhdBox { + fn default() -> Self { + MvhdBox { + version: 0, + flags: 0, + creation_time: 0, + modification_time: 0, + timescale: 1000, + duration: 0, + rate: FixedPointU16::new(1), + matrix: tkhd::Matrix::default(), + volume: FixedPointU8::new(1), + next_track_id: 1, + } + } +} + +impl Mp4Box for MvhdBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!( + "creation_time={} timescale={} duration={} rate={} volume={}, matrix={}, next_track_id={}", + self.creation_time, + self.timescale, + self.duration, + self.rate.value(), + self.volume.value(), + self.matrix, + self.next_track_id + ); + Ok(s) + } +} + +impl ReadBox<&mut R> for MvhdBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let (creation_time, modification_time, timescale, duration) = if version == 1 { + ( + reader.read_u64::()?, + reader.read_u64::()?, + reader.read_u32::()?, + reader.read_u64::()?, + ) + } else if version == 0 { + ( + reader.read_u32::()? as u64, + reader.read_u32::()? as u64, + reader.read_u32::()?, + reader.read_u32::()? as u64, + ) + } else { + return Err(Error::InvalidData("version must be 0 or 1")); + }; + let rate = FixedPointU16::new_raw(reader.read_u32::()?); + + let volume = FixedPointU8::new_raw(reader.read_u16::()?); + + reader.read_u16::()?; // reserved = 0 + + reader.read_u64::()?; // reserved = 0 + + let matrix = tkhd::Matrix { + a: reader.read_i32::()?, + b: reader.read_i32::()?, + u: reader.read_i32::()?, + c: reader.read_i32::()?, + d: reader.read_i32::()?, + v: reader.read_i32::()?, + x: reader.read_i32::()?, + y: reader.read_i32::()?, + w: reader.read_i32::()?, + }; + + skip_bytes(reader, 24)?; // pre_defined = 0 + + let next_track_id = reader.read_u32::()?; + + skip_bytes_to(reader, start + size)?; + + Ok(MvhdBox { + version, + flags, + creation_time, + modification_time, + timescale, + duration, + rate, + volume, + matrix, + next_track_id, + }) + } +} + +impl WriteBox<&mut W> for MvhdBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + if self.version == 1 { + writer.write_u64::(self.creation_time)?; + writer.write_u64::(self.modification_time)?; + writer.write_u32::(self.timescale)?; + writer.write_u64::(self.duration)?; + } else if self.version == 0 { + writer.write_u32::(self.creation_time as u32)?; + writer.write_u32::(self.modification_time as u32)?; + writer.write_u32::(self.timescale)?; + writer.write_u32::(self.duration as u32)?; + } else { + return Err(Error::InvalidData("version must be 0 or 1")); + } + writer.write_u32::(self.rate.raw_value())?; + + writer.write_u16::(self.volume.raw_value())?; + + writer.write_u16::(0)?; // reserved = 0 + + writer.write_u64::(0)?; // reserved = 0 + + writer.write_i32::(self.matrix.a)?; + writer.write_i32::(self.matrix.b)?; + writer.write_i32::(self.matrix.u)?; + writer.write_i32::(self.matrix.c)?; + writer.write_i32::(self.matrix.d)?; + writer.write_i32::(self.matrix.v)?; + writer.write_i32::(self.matrix.x)?; + writer.write_i32::(self.matrix.y)?; + writer.write_i32::(self.matrix.w)?; + + write_zeros(writer, 24)?; // pre_defined = 0 + + writer.write_u32::(self.next_track_id)?; + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_mvhd32() { + let src_box = MvhdBox { + version: 0, + flags: 0, + creation_time: 100, + modification_time: 200, + timescale: 1000, + duration: 634634, + rate: FixedPointU16::new(1), + volume: FixedPointU8::new(1), + matrix: tkhd::Matrix::default(), + next_track_id: 1, + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MvhdBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = MvhdBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_mvhd64() { + let src_box = MvhdBox { + version: 1, + flags: 0, + creation_time: 100, + modification_time: 200, + timescale: 1000, + duration: 634634, + rate: FixedPointU16::new(1), + volume: FixedPointU8::new(1), + matrix: tkhd::Matrix::default(), + next_track_id: 1, + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::MvhdBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = MvhdBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/prft.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/prft.rs new file mode 100644 index 0000000..7e840fc --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/prft.rs @@ -0,0 +1,111 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct PrftBox { + pub version: u8, + pub flags: u32, + pub reference_track_id: u32, + pub ntp_timestamp: u64, + pub media_time: u64, +} + +impl PrftBox { + pub fn get_type(&self) -> BoxType { + BoxType::PrftBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE + HEADER_EXT_SIZE; + + // Add 4 bytes for reference_track_id + size += 4; + + // Add 8 bytes for ntp_timestamp + size += 8; + + // Decide the size of media_time + if self.version == 1 { + size += 8; + } else if self.version == 0 { + size += 4; + } + size + } +} + +impl Mp4Box for PrftBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!( + "reference_track_id={} ntp_timestamp={} media_time={}", + self.reference_track_id, self.ntp_timestamp, self.media_time + ); + Ok(s) + } +} + +impl ReadBox<&mut R> for PrftBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let reference_track_id = reader.read_u32::()?; + let ntp_timestamp = reader.read_u64::()?; + + let media_time = if version == 1 { + reader.read_u64::()? + } else if version == 0 { + reader.read_u32::()? as u64 + } else { + return Err(Error::InvalidData("version must be 0 or 1")); + }; + + skip_bytes_to(reader, start + size)?; + + Ok(PrftBox { + version, + flags, + reference_track_id, + ntp_timestamp, + media_time, + }) + } +} + +impl WriteBox<&mut W> for PrftBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u32::(self.reference_track_id)?; + writer.write_u64::(self.ntp_timestamp)?; + + if self.version == 1 { + writer.write_u64::(self.media_time)?; + } else if self.version == 0 { + writer.write_u32::(self.media_time as u32)?; + } else { + return Err(Error::InvalidData("version must be 0 or 1")); + } + + Ok(size) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/smhd.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/smhd.rs new file mode 100644 index 0000000..cab7e4b --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/smhd.rs @@ -0,0 +1,112 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct SmhdBox { + pub version: u8, + pub flags: u32, + + #[serde(with = "value_i16")] + pub balance: FixedPointI8, +} + +impl SmhdBox { + pub fn get_type(&self) -> BoxType { + BoxType::SmhdBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + 4 + } +} + +impl Default for SmhdBox { + fn default() -> Self { + SmhdBox { + version: 0, + flags: 0, + balance: FixedPointI8::new_raw(0), + } + } +} + +impl Mp4Box for SmhdBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("balance={}", self.balance.value()); + Ok(s) + } +} + +impl ReadBox<&mut R> for SmhdBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let balance = FixedPointI8::new_raw(reader.read_i16::()?); + + skip_bytes_to(reader, start + size)?; + + Ok(SmhdBox { + version, + flags, + balance, + }) + } +} + +impl WriteBox<&mut W> for SmhdBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_i16::(self.balance.raw_value())?; + writer.write_u16::(0)?; // reserved + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_smhd() { + let src_box = SmhdBox { + version: 0, + flags: 0, + balance: FixedPointI8::new_raw(-1), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::SmhdBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = SmhdBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/stbl.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/stbl.rs new file mode 100644 index 0000000..ef8433b --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/stbl.rs @@ -0,0 +1,189 @@ +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; +use crate::mp4box::{ + co64::Co64Box, ctts::CttsBox, stco::StcoBox, stsc::StscBox, stsd::StsdBox, stss::StssBox, + stsz::StszBox, stts::SttsBox, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct StblBox { + pub stsd: StsdBox, + pub stts: SttsBox, + + #[serde(skip_serializing_if = "Option::is_none")] + pub ctts: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub stss: Option, + pub stsc: StscBox, + pub stsz: StszBox, + + #[serde(skip_serializing_if = "Option::is_none")] + pub stco: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub co64: Option, +} + +impl StblBox { + pub fn get_type(&self) -> BoxType { + BoxType::StblBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE; + size += self.stsd.box_size(); + size += self.stts.box_size(); + if let Some(ref ctts) = self.ctts { + size += ctts.box_size(); + } + if let Some(ref stss) = self.stss { + size += stss.box_size(); + } + size += self.stsc.box_size(); + size += self.stsz.box_size(); + if let Some(ref stco) = self.stco { + size += stco.box_size(); + } + if let Some(ref co64) = self.co64 { + size += co64.box_size(); + } + size + } +} + +impl Mp4Box for StblBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = String::new(); + Ok(s) + } +} + +impl ReadBox<&mut R> for StblBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut stsd = None; + let mut stts = None; + let mut ctts = None; + let mut stss = None; + let mut stsc = None; + let mut stsz = None; + let mut stco = None; + let mut co64 = None; + + let mut current = reader.stream_position()?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "stbl box contains a box with a larger size than it", + )); + } + + match name { + BoxType::StsdBox => { + stsd = Some(StsdBox::read_box(reader, s)?); + } + BoxType::SttsBox => { + stts = Some(SttsBox::read_box(reader, s)?); + } + BoxType::CttsBox => { + ctts = Some(CttsBox::read_box(reader, s)?); + } + BoxType::StssBox => { + stss = Some(StssBox::read_box(reader, s)?); + } + BoxType::StscBox => { + stsc = Some(StscBox::read_box(reader, s)?); + } + BoxType::StszBox => { + stsz = Some(StszBox::read_box(reader, s)?); + } + BoxType::StcoBox => { + stco = Some(StcoBox::read_box(reader, s)?); + } + BoxType::Co64Box => { + co64 = Some(Co64Box::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + current = reader.stream_position()?; + } + + if stsd.is_none() { + return Err(Error::BoxNotFound(BoxType::StsdBox)); + } + if stts.is_none() { + return Err(Error::BoxNotFound(BoxType::SttsBox)); + } + if stsc.is_none() { + return Err(Error::BoxNotFound(BoxType::StscBox)); + } + if stsz.is_none() { + return Err(Error::BoxNotFound(BoxType::StszBox)); + } + if stco.is_none() && co64.is_none() { + return Err(Error::Box2NotFound(BoxType::StcoBox, BoxType::Co64Box)); + } + + skip_bytes_to(reader, start + size)?; + + Ok(StblBox { + stsd: stsd.unwrap(), + stts: stts.unwrap(), + ctts, + stss, + stsc: stsc.unwrap(), + stsz: stsz.unwrap(), + stco, + co64, + }) + } +} + +impl WriteBox<&mut W> for StblBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + self.stsd.write_box(writer)?; + self.stts.write_box(writer)?; + if let Some(ref ctts) = self.ctts { + ctts.write_box(writer)?; + } + if let Some(ref stss) = self.stss { + stss.write_box(writer)?; + } + self.stsc.write_box(writer)?; + self.stsz.write_box(writer)?; + if let Some(ref stco) = self.stco { + stco.write_box(writer)?; + } + if let Some(ref co64) = self.co64 { + co64.write_box(writer)?; + } + + Ok(size) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/stco.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/stco.rs new file mode 100644 index 0000000..a00da8f --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/stco.rs @@ -0,0 +1,141 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; +use std::mem::size_of; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct StcoBox { + pub version: u8, + pub flags: u32, + + #[serde(skip_serializing)] + pub entries: Vec, +} + +impl StcoBox { + pub fn get_type(&self) -> BoxType { + BoxType::StcoBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + 4 + (4 * self.entries.len() as u64) + } +} + +impl Mp4Box for StcoBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("entries={}", self.entries.len()); + Ok(s) + } +} + +impl ReadBox<&mut R> for StcoBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let header_size = HEADER_SIZE + HEADER_EXT_SIZE; + let other_size = size_of::(); // entry_count + let entry_size = size_of::(); // chunk_offset + let entry_count = reader.read_u32::()?; + if u64::from(entry_count) + > size + .saturating_sub(header_size) + .saturating_sub(other_size as u64) + / entry_size as u64 + { + return Err(Error::InvalidData( + "stco entry_count indicates more entries than could fit in the box", + )); + } + let mut entries = Vec::with_capacity(entry_count as usize); + for _i in 0..entry_count { + let chunk_offset = reader.read_u32::()?; + entries.push(chunk_offset); + } + + skip_bytes_to(reader, start + size)?; + + Ok(StcoBox { + version, + flags, + entries, + }) + } +} + +impl WriteBox<&mut W> for StcoBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u32::(self.entries.len() as u32)?; + for chunk_offset in self.entries.iter() { + writer.write_u32::(*chunk_offset)?; + } + + Ok(size) + } +} + +impl std::convert::TryFrom<&co64::Co64Box> for StcoBox { + type Error = std::num::TryFromIntError; + + fn try_from(co64: &co64::Co64Box) -> std::result::Result { + let entries = co64 + .entries + .iter() + .copied() + .map(u32::try_from) + .collect::, _>>()?; + Ok(Self { + version: 0, + flags: 0, + entries, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_stco() { + let src_box = StcoBox { + version: 0, + flags: 0, + entries: vec![267, 1970, 2535, 2803, 11843, 22223, 33584], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::StcoBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = StcoBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/stsc.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/stsc.rs new file mode 100644 index 0000000..a2b034b --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/stsc.rs @@ -0,0 +1,171 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; +use std::mem::size_of; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct StscBox { + pub version: u8, + pub flags: u32, + + #[serde(skip_serializing)] + pub entries: Vec, +} + +impl StscBox { + pub fn get_type(&self) -> BoxType { + BoxType::StscBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + 4 + (12 * self.entries.len() as u64) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct StscEntry { + pub first_chunk: u32, + pub samples_per_chunk: u32, + pub sample_description_index: u32, + pub first_sample: u32, +} + +impl Mp4Box for StscBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("entries={}", self.entries.len()); + Ok(s) + } +} + +impl ReadBox<&mut R> for StscBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let header_size = HEADER_SIZE + HEADER_EXT_SIZE; + let other_size = size_of::(); // entry_count + let entry_size = size_of::() + size_of::() + size_of::(); // first_chunk + samples_per_chunk + sample_description_index + let entry_count = reader.read_u32::()?; + if u64::from(entry_count) + > size + .saturating_sub(header_size) + .saturating_sub(other_size as u64) + / entry_size as u64 + { + return Err(Error::InvalidData( + "stsc entry_count indicates more entries than could fit in the box", + )); + } + let mut entries = Vec::with_capacity(entry_count as usize); + for _ in 0..entry_count { + let entry = StscEntry { + first_chunk: reader.read_u32::()?, + samples_per_chunk: reader.read_u32::()?, + sample_description_index: reader.read_u32::()?, + first_sample: 0, + }; + entries.push(entry); + } + + let mut sample_id = 1; + for i in 0..entry_count { + let (first_chunk, samples_per_chunk) = { + let entry = entries.get_mut(i as usize).unwrap(); + entry.first_sample = sample_id; + (entry.first_chunk, entry.samples_per_chunk) + }; + if i < entry_count - 1 { + let next_entry = entries.get(i as usize + 1).unwrap(); + sample_id = next_entry + .first_chunk + .checked_sub(first_chunk) + .and_then(|n| n.checked_mul(samples_per_chunk)) + .and_then(|n| n.checked_add(sample_id)) + .ok_or(Error::InvalidData( + "attempt to calculate stsc sample_id with overflow", + ))?; + } + } + + skip_bytes_to(reader, start + size)?; + + Ok(StscBox { + version, + flags, + entries, + }) + } +} + +impl WriteBox<&mut W> for StscBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u32::(self.entries.len() as u32)?; + for entry in self.entries.iter() { + writer.write_u32::(entry.first_chunk)?; + writer.write_u32::(entry.samples_per_chunk)?; + writer.write_u32::(entry.sample_description_index)?; + } + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_stsc() { + let src_box = StscBox { + version: 0, + flags: 0, + entries: vec![ + StscEntry { + first_chunk: 1, + samples_per_chunk: 1, + sample_description_index: 1, + first_sample: 1, + }, + StscEntry { + first_chunk: 19026, + samples_per_chunk: 14, + sample_description_index: 1, + first_sample: 19026, + }, + ], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::StscBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = StscBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/stsd.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/stsd.rs new file mode 100644 index 0000000..af947c6 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/stsd.rs @@ -0,0 +1,150 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::vp09::Vp09Box; +use crate::mp4box::*; +use crate::mp4box::{avc1::Avc1Box, hev1::Hev1Box, mp4a::Mp4aBox, tx3g::Tx3gBox}; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct StsdBox { + pub version: u8, + pub flags: u32, + + #[serde(skip_serializing_if = "Option::is_none")] + pub avc1: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub hev1: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub vp09: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub mp4a: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub tx3g: Option, +} + +impl StsdBox { + pub fn get_type(&self) -> BoxType { + BoxType::StsdBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE + HEADER_EXT_SIZE + 4; + if let Some(ref avc1) = self.avc1 { + size += avc1.box_size(); + } else if let Some(ref hev1) = self.hev1 { + size += hev1.box_size(); + } else if let Some(ref vp09) = self.vp09 { + size += vp09.box_size(); + } else if let Some(ref mp4a) = self.mp4a { + size += mp4a.box_size(); + } else if let Some(ref tx3g) = self.tx3g { + size += tx3g.box_size(); + } + size + } +} + +impl Mp4Box for StsdBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = String::new(); + Ok(s) + } +} + +impl ReadBox<&mut R> for StsdBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + reader.read_u32::()?; // XXX entry_count + + let mut avc1 = None; + let mut hev1 = None; + let mut vp09 = None; + let mut mp4a = None; + let mut tx3g = None; + + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "stsd box contains a box with a larger size than it", + )); + } + + match name { + BoxType::Avc1Box => { + avc1 = Some(Avc1Box::read_box(reader, s)?); + } + BoxType::Hev1Box => { + hev1 = Some(Hev1Box::read_box(reader, s)?); + } + BoxType::Vp09Box => { + vp09 = Some(Vp09Box::read_box(reader, s)?); + } + BoxType::Mp4aBox => { + mp4a = Some(Mp4aBox::read_box(reader, s)?); + } + BoxType::Tx3gBox => { + tx3g = Some(Tx3gBox::read_box(reader, s)?); + } + _ => {} + } + + skip_bytes_to(reader, start + size)?; + + Ok(StsdBox { + version, + flags, + avc1, + hev1, + vp09, + mp4a, + tx3g, + }) + } +} + +impl WriteBox<&mut W> for StsdBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u32::(1)?; // entry_count + + if let Some(ref avc1) = self.avc1 { + avc1.write_box(writer)?; + } else if let Some(ref hev1) = self.hev1 { + hev1.write_box(writer)?; + } else if let Some(ref vp09) = self.vp09 { + vp09.write_box(writer)?; + } else if let Some(ref mp4a) = self.mp4a { + mp4a.write_box(writer)?; + } else if let Some(ref tx3g) = self.tx3g { + tx3g.write_box(writer)?; + } + + Ok(size) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/stss.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/stss.rs new file mode 100644 index 0000000..dd9e552 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/stss.rs @@ -0,0 +1,123 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; +use std::mem::size_of; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct StssBox { + pub version: u8, + pub flags: u32, + + #[serde(skip_serializing)] + pub entries: Vec, +} + +impl StssBox { + pub fn get_type(&self) -> BoxType { + BoxType::StssBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + 4 + (4 * self.entries.len() as u64) + } +} + +impl Mp4Box for StssBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("entries={}", self.entries.len()); + Ok(s) + } +} + +impl ReadBox<&mut R> for StssBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let header_size = HEADER_SIZE + HEADER_EXT_SIZE; + let other_size = size_of::(); // entry_count + let entry_size = size_of::(); // sample_number + let entry_count = reader.read_u32::()?; + if u64::from(entry_count) + > size + .saturating_sub(header_size) + .saturating_sub(other_size as u64) + / entry_size as u64 + { + return Err(Error::InvalidData( + "stss entry_count indicates more entries than could fit in the box", + )); + } + let mut entries = Vec::with_capacity(entry_count as usize); + for _i in 0..entry_count { + let sample_number = reader.read_u32::()?; + entries.push(sample_number); + } + + skip_bytes_to(reader, start + size)?; + + Ok(StssBox { + version, + flags, + entries, + }) + } +} + +impl WriteBox<&mut W> for StssBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u32::(self.entries.len() as u32)?; + for sample_number in self.entries.iter() { + writer.write_u32::(*sample_number)?; + } + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_stss() { + let src_box = StssBox { + version: 0, + flags: 0, + entries: vec![1, 61, 121, 181, 241, 301, 361, 421, 481], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::StssBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = StssBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/stsz.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/stsz.rs new file mode 100644 index 0000000..b07e765 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/stsz.rs @@ -0,0 +1,170 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; +use std::mem::size_of; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct StszBox { + pub version: u8, + pub flags: u32, + pub sample_size: u32, + pub sample_count: u32, + + #[serde(skip_serializing)] + pub sample_sizes: Vec, +} + +impl StszBox { + pub fn get_type(&self) -> BoxType { + BoxType::StszBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + 8 + (4 * self.sample_sizes.len() as u64) + } +} + +impl Mp4Box for StszBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!( + "sample_size={} sample_count={} sample_sizes={}", + self.sample_size, + self.sample_count, + self.sample_sizes.len() + ); + Ok(s) + } +} + +impl ReadBox<&mut R> for StszBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let header_size = HEADER_SIZE + HEADER_EXT_SIZE; + let other_size = size_of::() + size_of::(); // sample_size + sample_count + let sample_size = reader.read_u32::()?; + let stsz_item_size = if sample_size == 0 { + size_of::() // entry_size + } else { + 0 + }; + let sample_count = reader.read_u32::()?; + let mut sample_sizes = Vec::new(); + if sample_size == 0 { + if u64::from(sample_count) + > size + .saturating_sub(header_size) + .saturating_sub(other_size as u64) + / stsz_item_size as u64 + { + return Err(Error::InvalidData( + "stsz sample_count indicates more values than could fit in the box", + )); + } + sample_sizes.reserve(sample_count as usize); + for _ in 0..sample_count { + let sample_number = reader.read_u32::()?; + sample_sizes.push(sample_number); + } + } + + skip_bytes_to(reader, start + size)?; + + Ok(StszBox { + version, + flags, + sample_size, + sample_count, + sample_sizes, + }) + } +} + +impl WriteBox<&mut W> for StszBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u32::(self.sample_size)?; + writer.write_u32::(self.sample_count)?; + if self.sample_size == 0 { + if self.sample_count != self.sample_sizes.len() as u32 { + return Err(Error::InvalidData("sample count out of sync")); + } + for sample_number in self.sample_sizes.iter() { + writer.write_u32::(*sample_number)?; + } + } + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_stsz_same_size() { + let src_box = StszBox { + version: 0, + flags: 0, + sample_size: 1165, + sample_count: 12, + sample_sizes: vec![], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::StszBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = StszBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_stsz_many_sizes() { + let src_box = StszBox { + version: 0, + flags: 0, + sample_size: 0, + sample_count: 9, + sample_sizes: vec![1165, 11, 11, 8545, 10126, 10866, 9643, 9351, 7730], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::StszBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = StszBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/stts.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/stts.rs new file mode 100644 index 0000000..82de6c5 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/stts.rs @@ -0,0 +1,142 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; +use std::mem::size_of; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct SttsBox { + pub version: u8, + pub flags: u32, + + #[serde(skip_serializing)] + pub entries: Vec, +} + +impl SttsBox { + pub fn get_type(&self) -> BoxType { + BoxType::SttsBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + 4 + (8 * self.entries.len() as u64) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct SttsEntry { + pub sample_count: u32, + pub sample_delta: u32, +} + +impl Mp4Box for SttsBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("entries={}", self.entries.len()); + Ok(s) + } +} + +impl ReadBox<&mut R> for SttsBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let header_size = HEADER_SIZE + HEADER_EXT_SIZE; + let other_size = size_of::(); // entry_count + let entry_size = size_of::() + size_of::(); // sample_count + sample_delta + let entry_count = reader.read_u32::()?; + if u64::from(entry_count) + > size + .saturating_sub(header_size) + .saturating_sub(other_size as u64) + / entry_size as u64 + { + return Err(Error::InvalidData( + "stts entry_count indicates more entries than could fit in the box", + )); + } + let mut entries = Vec::with_capacity(entry_count as usize); + for _i in 0..entry_count { + let entry = SttsEntry { + sample_count: reader.read_u32::()?, + sample_delta: reader.read_u32::()?, + }; + entries.push(entry); + } + + skip_bytes_to(reader, start + size)?; + + Ok(SttsBox { + version, + flags, + entries, + }) + } +} + +impl WriteBox<&mut W> for SttsBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u32::(self.entries.len() as u32)?; + for entry in self.entries.iter() { + writer.write_u32::(entry.sample_count)?; + writer.write_u32::(entry.sample_delta)?; + } + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_stts() { + let src_box = SttsBox { + version: 0, + flags: 0, + entries: vec![ + SttsEntry { + sample_count: 29726, + sample_delta: 1024, + }, + SttsEntry { + sample_count: 1, + sample_delta: 512, + }, + ], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::SttsBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = SttsBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/tfdt.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/tfdt.rs new file mode 100644 index 0000000..ef92889 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/tfdt.rs @@ -0,0 +1,137 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct TfdtBox { + pub version: u8, + pub flags: u32, + pub base_media_decode_time: u64, +} + +impl TfdtBox { + pub fn get_type(&self) -> BoxType { + BoxType::TfdtBox + } + + pub fn get_size(&self) -> u64 { + let mut sum = HEADER_SIZE + HEADER_EXT_SIZE; + if self.version == 1 { + sum += 8; + } else { + sum += 4; + } + sum + } +} + +impl Mp4Box for TfdtBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("base_media_decode_time={}", self.base_media_decode_time); + Ok(s) + } +} + +impl ReadBox<&mut R> for TfdtBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let base_media_decode_time = if version == 1 { + reader.read_u64::()? + } else if version == 0 { + reader.read_u32::()? as u64 + } else { + return Err(Error::InvalidData("version must be 0 or 1")); + }; + + skip_bytes_to(reader, start + size)?; + + Ok(TfdtBox { + version, + flags, + base_media_decode_time, + }) + } +} + +impl WriteBox<&mut W> for TfdtBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + if self.version == 1 { + writer.write_u64::(self.base_media_decode_time)?; + } else if self.version == 0 { + writer.write_u32::(self.base_media_decode_time as u32)?; + } else { + return Err(Error::InvalidData("version must be 0 or 1")); + } + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_tfdt32() { + let src_box = TfdtBox { + version: 0, + flags: 0, + base_media_decode_time: 0, + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::TfdtBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = TfdtBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_tfdt64() { + let src_box = TfdtBox { + version: 1, + flags: 0, + base_media_decode_time: 0, + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::TfdtBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = TfdtBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/tfhd.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/tfhd.rs new file mode 100644 index 0000000..5b529e6 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/tfhd.rs @@ -0,0 +1,203 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)] +pub struct TfhdBox { + pub version: u8, + pub flags: u32, + pub track_id: u32, + pub base_data_offset: Option, + pub sample_description_index: Option, + pub default_sample_duration: Option, + pub default_sample_size: Option, + pub default_sample_flags: Option, +} + +impl TfhdBox { + pub const FLAG_BASE_DATA_OFFSET: u32 = 0x01; + pub const FLAG_SAMPLE_DESCRIPTION_INDEX: u32 = 0x02; + pub const FLAG_DEFAULT_SAMPLE_DURATION: u32 = 0x08; + pub const FLAG_DEFAULT_SAMPLE_SIZE: u32 = 0x10; + pub const FLAG_DEFAULT_SAMPLE_FLAGS: u32 = 0x20; + pub const FLAG_DURATION_IS_EMPTY: u32 = 0x10000; + pub const FLAG_DEFAULT_BASE_IS_MOOF: u32 = 0x20000; + + pub fn get_type(&self) -> BoxType { + BoxType::TfhdBox + } + + pub fn get_size(&self) -> u64 { + let mut sum = HEADER_SIZE + HEADER_EXT_SIZE + 4; + if TfhdBox::FLAG_BASE_DATA_OFFSET & self.flags > 0 { + sum += 8; + } + if TfhdBox::FLAG_SAMPLE_DESCRIPTION_INDEX & self.flags > 0 { + sum += 4; + } + if TfhdBox::FLAG_DEFAULT_SAMPLE_DURATION & self.flags > 0 { + sum += 4; + } + if TfhdBox::FLAG_DEFAULT_SAMPLE_SIZE & self.flags > 0 { + sum += 4; + } + if TfhdBox::FLAG_DEFAULT_SAMPLE_FLAGS & self.flags > 0 { + sum += 4; + } + sum + } +} + +impl Mp4Box for TfhdBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("track_id={}", self.track_id); + Ok(s) + } +} + +impl ReadBox<&mut R> for TfhdBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + let track_id = reader.read_u32::()?; + let base_data_offset = if TfhdBox::FLAG_BASE_DATA_OFFSET & flags > 0 { + Some(reader.read_u64::()?) + } else { + None + }; + let sample_description_index = if TfhdBox::FLAG_SAMPLE_DESCRIPTION_INDEX & flags > 0 { + Some(reader.read_u32::()?) + } else { + None + }; + let default_sample_duration = if TfhdBox::FLAG_DEFAULT_SAMPLE_DURATION & flags > 0 { + Some(reader.read_u32::()?) + } else { + None + }; + let default_sample_size = if TfhdBox::FLAG_DEFAULT_SAMPLE_SIZE & flags > 0 { + Some(reader.read_u32::()?) + } else { + None + }; + let default_sample_flags = if TfhdBox::FLAG_DEFAULT_SAMPLE_FLAGS & flags > 0 { + Some(reader.read_u32::()?) + } else { + None + }; + + skip_bytes_to(reader, start + size)?; + + Ok(TfhdBox { + version, + flags, + track_id, + base_data_offset, + sample_description_index, + default_sample_duration, + default_sample_size, + default_sample_flags, + }) + } +} + +impl WriteBox<&mut W> for TfhdBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + writer.write_u32::(self.track_id)?; + if let Some(base_data_offset) = self.base_data_offset { + writer.write_u64::(base_data_offset)?; + } + if let Some(sample_description_index) = self.sample_description_index { + writer.write_u32::(sample_description_index)?; + } + if let Some(default_sample_duration) = self.default_sample_duration { + writer.write_u32::(default_sample_duration)?; + } + if let Some(default_sample_size) = self.default_sample_size { + writer.write_u32::(default_sample_size)?; + } + if let Some(default_sample_flags) = self.default_sample_flags { + writer.write_u32::(default_sample_flags)?; + } + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_tfhd() { + let src_box = TfhdBox { + version: 0, + flags: 0, + track_id: 1, + base_data_offset: None, + sample_description_index: None, + default_sample_duration: None, + default_sample_size: None, + default_sample_flags: None, + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::TfhdBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = TfhdBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_tfhd_with_flags() { + let src_box = TfhdBox { + version: 0, + flags: TfhdBox::FLAG_SAMPLE_DESCRIPTION_INDEX + | TfhdBox::FLAG_DEFAULT_SAMPLE_DURATION + | TfhdBox::FLAG_DEFAULT_SAMPLE_FLAGS, + track_id: 1, + base_data_offset: None, + sample_description_index: Some(1), + default_sample_duration: Some(512), + default_sample_size: None, + default_sample_flags: Some(0x1010000), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::TfhdBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = TfhdBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/tkhd.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/tkhd.rs new file mode 100644 index 0000000..d7bcfbe --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/tkhd.rs @@ -0,0 +1,323 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +pub enum TrackFlag { + TrackEnabled = 0x000001, + // TrackInMovie = 0x000002, + // TrackInPreview = 0x000004, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct TkhdBox { + pub version: u8, + pub flags: u32, + pub creation_time: u64, + pub modification_time: u64, + pub track_id: u32, + pub duration: u64, + pub layer: u16, + pub alternate_group: u16, + + #[serde(with = "value_u8")] + pub volume: FixedPointU8, + pub matrix: Matrix, + + #[serde(with = "value_u32")] + pub width: FixedPointU16, + + #[serde(with = "value_u32")] + pub height: FixedPointU16, +} + +impl Default for TkhdBox { + fn default() -> Self { + TkhdBox { + version: 0, + flags: TrackFlag::TrackEnabled as u32, + creation_time: 0, + modification_time: 0, + track_id: 0, + duration: 0, + layer: 0, + alternate_group: 0, + volume: FixedPointU8::new(1), + matrix: Matrix::default(), + width: FixedPointU16::new(0), + height: FixedPointU16::new(0), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Matrix { + pub a: i32, + pub b: i32, + pub u: i32, + pub c: i32, + pub d: i32, + pub v: i32, + pub x: i32, + pub y: i32, + pub w: i32, +} + +impl std::fmt::Display for Matrix { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:#x} {:#x} {:#x} {:#x} {:#x} {:#x} {:#x} {:#x} {:#x}", + self.a, self.b, self.u, self.c, self.d, self.v, self.x, self.y, self.w + ) + } +} + +impl Default for Matrix { + fn default() -> Self { + Self { + // unity matrix according to ISO/IEC 14496-12:2005(E) + a: 0x00010000, + b: 0, + u: 0, + c: 0, + d: 0x00010000, + v: 0, + x: 0, + y: 0, + w: 0x40000000, + } + } +} + +impl TkhdBox { + pub fn get_type(&self) -> BoxType { + BoxType::TkhdBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE + HEADER_EXT_SIZE; + if self.version == 1 { + size += 32; + } else if self.version == 0 { + size += 20; + } + size += 60; + size + } + + pub fn set_width(&mut self, width: u16) { + self.width = FixedPointU16::new(width); + } + + pub fn set_height(&mut self, height: u16) { + self.height = FixedPointU16::new(height); + } +} + +impl Mp4Box for TkhdBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!( + "creation_time={} track_id={} duration={} layer={} volume={} matrix={} width={} height={}", + self.creation_time, + self.track_id, + self.duration, + self.layer, + self.volume.value(), + self.matrix, + self.width.value(), + self.height.value() + ); + Ok(s) + } +} + +impl ReadBox<&mut R> for TkhdBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let (creation_time, modification_time, track_id, _, duration) = if version == 1 { + ( + reader.read_u64::()?, + reader.read_u64::()?, + reader.read_u32::()?, + reader.read_u32::()?, + reader.read_u64::()?, + ) + } else if version == 0 { + ( + reader.read_u32::()? as u64, + reader.read_u32::()? as u64, + reader.read_u32::()?, + reader.read_u32::()?, + reader.read_u32::()? as u64, + ) + } else { + return Err(Error::InvalidData("version must be 0 or 1")); + }; + reader.read_u64::()?; // reserved + let layer = reader.read_u16::()?; + let alternate_group = reader.read_u16::()?; + let volume = FixedPointU8::new_raw(reader.read_u16::()?); + + reader.read_u16::()?; // reserved + let matrix = Matrix { + a: reader.read_i32::()?, + b: reader.read_i32::()?, + u: reader.read_i32::()?, + c: reader.read_i32::()?, + d: reader.read_i32::()?, + v: reader.read_i32::()?, + x: reader.read_i32::()?, + y: reader.read_i32::()?, + w: reader.read_i32::()?, + }; + + let width = FixedPointU16::new_raw(reader.read_u32::()?); + let height = FixedPointU16::new_raw(reader.read_u32::()?); + + skip_bytes_to(reader, start + size)?; + + Ok(TkhdBox { + version, + flags, + creation_time, + modification_time, + track_id, + duration, + layer, + alternate_group, + volume, + matrix, + width, + height, + }) + } +} + +impl WriteBox<&mut W> for TkhdBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + if self.version == 1 { + writer.write_u64::(self.creation_time)?; + writer.write_u64::(self.modification_time)?; + writer.write_u32::(self.track_id)?; + writer.write_u32::(0)?; // reserved + writer.write_u64::(self.duration)?; + } else if self.version == 0 { + writer.write_u32::(self.creation_time as u32)?; + writer.write_u32::(self.modification_time as u32)?; + writer.write_u32::(self.track_id)?; + writer.write_u32::(0)?; // reserved + writer.write_u32::(self.duration as u32)?; + } else { + return Err(Error::InvalidData("version must be 0 or 1")); + } + + writer.write_u64::(0)?; // reserved + writer.write_u16::(self.layer)?; + writer.write_u16::(self.alternate_group)?; + writer.write_u16::(self.volume.raw_value())?; + + writer.write_u16::(0)?; // reserved + + writer.write_i32::(self.matrix.a)?; + writer.write_i32::(self.matrix.b)?; + writer.write_i32::(self.matrix.u)?; + writer.write_i32::(self.matrix.c)?; + writer.write_i32::(self.matrix.d)?; + writer.write_i32::(self.matrix.v)?; + writer.write_i32::(self.matrix.x)?; + writer.write_i32::(self.matrix.y)?; + writer.write_i32::(self.matrix.w)?; + + writer.write_u32::(self.width.raw_value())?; + writer.write_u32::(self.height.raw_value())?; + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_tkhd32() { + let src_box = TkhdBox { + version: 0, + flags: TrackFlag::TrackEnabled as u32, + creation_time: 100, + modification_time: 200, + track_id: 1, + duration: 634634, + layer: 0, + alternate_group: 0, + volume: FixedPointU8::new(1), + matrix: Matrix::default(), + width: FixedPointU16::new(512), + height: FixedPointU16::new(288), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::TkhdBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = TkhdBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_tkhd64() { + let src_box = TkhdBox { + version: 1, + flags: TrackFlag::TrackEnabled as u32, + creation_time: 100, + modification_time: 200, + track_id: 1, + duration: 634634, + layer: 0, + alternate_group: 0, + volume: FixedPointU8::new(1), + matrix: Matrix::default(), + width: FixedPointU16::new(512), + height: FixedPointU16::new(288), + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::TkhdBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = TkhdBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/traf.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/traf.rs new file mode 100644 index 0000000..d53d713 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/traf.rs @@ -0,0 +1,119 @@ +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; +use crate::mp4box::{tfdt::TfdtBox, tfhd::TfhdBox, trun::TrunBox}; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct TrafBox { + pub tfhd: TfhdBox, + pub tfdt: Option, + pub trun: Option, +} + +impl TrafBox { + pub fn get_type(&self) -> BoxType { + BoxType::TrafBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE; + size += self.tfhd.box_size(); + if let Some(ref tfdt) = self.tfdt { + size += tfdt.box_size(); + } + if let Some(ref trun) = self.trun { + size += trun.box_size(); + } + size + } +} + +impl Mp4Box for TrafBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = String::new(); + Ok(s) + } +} + +impl ReadBox<&mut R> for TrafBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut tfhd = None; + let mut tfdt = None; + let mut trun = None; + + let mut current = reader.stream_position()?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "traf box contains a box with a larger size than it", + )); + } + + match name { + BoxType::TfhdBox => { + tfhd = Some(TfhdBox::read_box(reader, s)?); + } + BoxType::TfdtBox => { + tfdt = Some(TfdtBox::read_box(reader, s)?); + } + BoxType::TrunBox => { + trun = Some(TrunBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.stream_position()?; + } + + if tfhd.is_none() { + return Err(Error::BoxNotFound(BoxType::TfhdBox)); + } + + skip_bytes_to(reader, start + size)?; + + Ok(TrafBox { + tfhd: tfhd.unwrap(), + tfdt, + trun, + }) + } +} + +impl WriteBox<&mut W> for TrafBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + self.tfhd.write_box(writer)?; + if let Some(ref tfdt) = self.tfdt { + tfdt.write_box(writer)?; + } + if let Some(ref trun) = self.trun { + trun.write_box(writer)?; + } + + Ok(size) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/trak.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/trak.rs new file mode 100644 index 0000000..e8ae760 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/trak.rs @@ -0,0 +1,130 @@ +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::meta::MetaBox; +use crate::mp4box::*; +use crate::mp4box::{edts::EdtsBox, mdia::MdiaBox, tkhd::TkhdBox}; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct TrakBox { + pub tkhd: TkhdBox, + + #[serde(skip_serializing_if = "Option::is_none")] + pub edts: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, + + pub mdia: MdiaBox, +} + +impl TrakBox { + pub fn get_type(&self) -> BoxType { + BoxType::TrakBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE; + size += self.tkhd.box_size(); + if let Some(ref edts) = self.edts { + size += edts.box_size(); + } + size += self.mdia.box_size(); + size + } +} + +impl Mp4Box for TrakBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = String::new(); + Ok(s) + } +} + +impl ReadBox<&mut R> for TrakBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut tkhd = None; + let mut edts = None; + let mut meta = None; + let mut mdia = None; + + let mut current = reader.stream_position()?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "trak box contains a box with a larger size than it", + )); + } + + match name { + BoxType::TkhdBox => { + tkhd = Some(TkhdBox::read_box(reader, s)?); + } + BoxType::EdtsBox => { + edts = Some(EdtsBox::read_box(reader, s)?); + } + BoxType::MetaBox => { + meta = Some(MetaBox::read_box(reader, s)?); + } + BoxType::MdiaBox => { + mdia = Some(MdiaBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.stream_position()?; + } + + if tkhd.is_none() { + return Err(Error::BoxNotFound(BoxType::TkhdBox)); + } + if mdia.is_none() { + return Err(Error::BoxNotFound(BoxType::MdiaBox)); + } + + skip_bytes_to(reader, start + size)?; + + Ok(TrakBox { + tkhd: tkhd.unwrap(), + edts, + meta, + mdia: mdia.unwrap(), + }) + } +} + +impl WriteBox<&mut W> for TrakBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + self.tkhd.write_box(writer)?; + if let Some(ref edts) = self.edts { + edts.write_box(writer)?; + } + self.mdia.write_box(writer)?; + + Ok(size) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/trex.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/trex.rs new file mode 100644 index 0000000..2694fd6 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/trex.rs @@ -0,0 +1,122 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct TrexBox { + pub version: u8, + pub flags: u32, + pub track_id: u32, + pub default_sample_description_index: u32, + pub default_sample_duration: u32, + pub default_sample_size: u32, + pub default_sample_flags: u32, +} + +impl TrexBox { + pub fn get_type(&self) -> BoxType { + BoxType::TrexBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + 20 + } +} + +impl Mp4Box for TrexBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!( + "track_id={} default_sample_duration={}", + self.track_id, self.default_sample_duration + ); + Ok(s) + } +} + +impl ReadBox<&mut R> for TrexBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let track_id = reader.read_u32::()?; + let default_sample_description_index = reader.read_u32::()?; + let default_sample_duration = reader.read_u32::()?; + let default_sample_size = reader.read_u32::()?; + let default_sample_flags = reader.read_u32::()?; + + skip_bytes_to(reader, start + size)?; + + Ok(TrexBox { + version, + flags, + track_id, + default_sample_description_index, + default_sample_duration, + default_sample_size, + default_sample_flags, + }) + } +} + +impl WriteBox<&mut W> for TrexBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u32::(self.track_id)?; + writer.write_u32::(self.default_sample_description_index)?; + writer.write_u32::(self.default_sample_duration)?; + writer.write_u32::(self.default_sample_size)?; + writer.write_u32::(self.default_sample_flags)?; + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_trex() { + let src_box = TrexBox { + version: 0, + flags: 0, + track_id: 1, + default_sample_description_index: 1, + default_sample_duration: 1000, + default_sample_size: 0, + default_sample_flags: 65536, + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::TrexBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = TrexBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/trun.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/trun.rs new file mode 100644 index 0000000..efbb2b0 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/trun.rs @@ -0,0 +1,270 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; +use std::mem::size_of; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct TrunBox { + pub version: u8, + pub flags: u32, + pub sample_count: u32, + pub data_offset: Option, + pub first_sample_flags: Option, + + #[serde(skip_serializing)] + pub sample_durations: Vec, + #[serde(skip_serializing)] + pub sample_sizes: Vec, + #[serde(skip_serializing)] + pub sample_flags: Vec, + #[serde(skip_serializing)] + pub sample_cts: Vec, +} + +impl TrunBox { + pub const FLAG_DATA_OFFSET: u32 = 0x01; + pub const FLAG_FIRST_SAMPLE_FLAGS: u32 = 0x04; + pub const FLAG_SAMPLE_DURATION: u32 = 0x100; + pub const FLAG_SAMPLE_SIZE: u32 = 0x200; + pub const FLAG_SAMPLE_FLAGS: u32 = 0x400; + pub const FLAG_SAMPLE_CTS: u32 = 0x800; + + pub fn get_type(&self) -> BoxType { + BoxType::TrunBox + } + + pub fn get_size(&self) -> u64 { + let mut sum = HEADER_SIZE + HEADER_EXT_SIZE + 4; + if TrunBox::FLAG_DATA_OFFSET & self.flags > 0 { + sum += 4; + } + if TrunBox::FLAG_FIRST_SAMPLE_FLAGS & self.flags > 0 { + sum += 4; + } + if TrunBox::FLAG_SAMPLE_DURATION & self.flags > 0 { + sum += 4 * self.sample_count as u64; + } + if TrunBox::FLAG_SAMPLE_SIZE & self.flags > 0 { + sum += 4 * self.sample_count as u64; + } + if TrunBox::FLAG_SAMPLE_FLAGS & self.flags > 0 { + sum += 4 * self.sample_count as u64; + } + if TrunBox::FLAG_SAMPLE_CTS & self.flags > 0 { + sum += 4 * self.sample_count as u64; + } + sum + } +} + +impl Mp4Box for TrunBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("sample_size={}", self.sample_count); + Ok(s) + } +} + +impl ReadBox<&mut R> for TrunBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let header_size = HEADER_SIZE + HEADER_EXT_SIZE; + let other_size = size_of::() // sample_count + + if TrunBox::FLAG_DATA_OFFSET & flags > 0 { size_of::() } else { 0 } // data_offset + + if TrunBox::FLAG_FIRST_SAMPLE_FLAGS & flags > 0 { size_of::() } else { 0 }; // first_sample_flags + let sample_size = if TrunBox::FLAG_SAMPLE_DURATION & flags > 0 { size_of::() } else { 0 } // sample_duration + + if TrunBox::FLAG_SAMPLE_SIZE & flags > 0 { size_of::() } else { 0 } // sample_size + + if TrunBox::FLAG_SAMPLE_FLAGS & flags > 0 { size_of::() } else { 0 } // sample_flags + + if TrunBox::FLAG_SAMPLE_CTS & flags > 0 { size_of::() } else { 0 }; // sample_composition_time_offset + + let sample_count = reader.read_u32::()?; + + let data_offset = if TrunBox::FLAG_DATA_OFFSET & flags > 0 { + Some(reader.read_i32::()?) + } else { + None + }; + + let first_sample_flags = if TrunBox::FLAG_FIRST_SAMPLE_FLAGS & flags > 0 { + Some(reader.read_u32::()?) + } else { + None + }; + + let mut sample_durations = Vec::new(); + let mut sample_sizes = Vec::new(); + let mut sample_flags = Vec::new(); + let mut sample_cts = Vec::new(); + if u64::from(sample_count) * sample_size as u64 + > size + .saturating_sub(header_size) + .saturating_sub(other_size as u64) + { + return Err(Error::InvalidData( + "trun sample_count indicates more values than could fit in the box", + )); + } + if TrunBox::FLAG_SAMPLE_DURATION & flags > 0 { + sample_durations.reserve(sample_count as usize); + } + if TrunBox::FLAG_SAMPLE_SIZE & flags > 0 { + sample_sizes.reserve(sample_count as usize); + } + if TrunBox::FLAG_SAMPLE_FLAGS & flags > 0 { + sample_flags.reserve(sample_count as usize); + } + if TrunBox::FLAG_SAMPLE_CTS & flags > 0 { + sample_cts.reserve(sample_count as usize); + } + + for _ in 0..sample_count { + if TrunBox::FLAG_SAMPLE_DURATION & flags > 0 { + let duration = reader.read_u32::()?; + sample_durations.push(duration); + } + + if TrunBox::FLAG_SAMPLE_SIZE & flags > 0 { + let sample_size = reader.read_u32::()?; + sample_sizes.push(sample_size); + } + + if TrunBox::FLAG_SAMPLE_FLAGS & flags > 0 { + let sample_flag = reader.read_u32::()?; + sample_flags.push(sample_flag); + } + + if TrunBox::FLAG_SAMPLE_CTS & flags > 0 { + let cts = reader.read_u32::()?; + sample_cts.push(cts); + } + } + + skip_bytes_to(reader, start + size)?; + + Ok(TrunBox { + version, + flags, + sample_count, + data_offset, + first_sample_flags, + sample_durations, + sample_sizes, + sample_flags, + sample_cts, + }) + } +} + +impl WriteBox<&mut W> for TrunBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u32::(self.sample_count)?; + if let Some(v) = self.data_offset { + writer.write_i32::(v)?; + } + if let Some(v) = self.first_sample_flags { + writer.write_u32::(v)?; + } + if self.sample_count != self.sample_sizes.len() as u32 { + return Err(Error::InvalidData("sample count out of sync")); + } + for i in 0..self.sample_count as usize { + if TrunBox::FLAG_SAMPLE_DURATION & self.flags > 0 { + writer.write_u32::(self.sample_durations[i])?; + } + if TrunBox::FLAG_SAMPLE_SIZE & self.flags > 0 { + writer.write_u32::(self.sample_sizes[i])?; + } + if TrunBox::FLAG_SAMPLE_FLAGS & self.flags > 0 { + writer.write_u32::(self.sample_flags[i])?; + } + if TrunBox::FLAG_SAMPLE_CTS & self.flags > 0 { + writer.write_u32::(self.sample_cts[i])?; + } + } + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_trun_same_size() { + let src_box = TrunBox { + version: 0, + flags: 0, + data_offset: None, + sample_count: 0, + sample_sizes: vec![], + sample_flags: vec![], + first_sample_flags: None, + sample_durations: vec![], + sample_cts: vec![], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::TrunBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = TrunBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } + + #[test] + fn test_trun_many_sizes() { + let src_box = TrunBox { + version: 0, + flags: TrunBox::FLAG_SAMPLE_DURATION + | TrunBox::FLAG_SAMPLE_SIZE + | TrunBox::FLAG_SAMPLE_FLAGS + | TrunBox::FLAG_SAMPLE_CTS, + data_offset: None, + sample_count: 9, + sample_sizes: vec![1165, 11, 11, 8545, 10126, 10866, 9643, 9351, 7730], + sample_flags: vec![1165, 11, 11, 8545, 10126, 10866, 9643, 9351, 7730], + first_sample_flags: None, + sample_durations: vec![1165, 11, 11, 8545, 10126, 10866, 9643, 9351, 7730], + sample_cts: vec![1165, 11, 11, 8545, 10126, 10866, 9643, 9351, 7730], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::TrunBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = TrunBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/tx3g.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/tx3g.rs new file mode 100644 index 0000000..d696315 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/tx3g.rs @@ -0,0 +1,189 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Tx3gBox { + pub data_reference_index: u16, + pub display_flags: u32, + pub horizontal_justification: i8, + pub vertical_justification: i8, + pub bg_color_rgba: RgbaColor, + pub box_record: [i16; 4], + pub style_record: [u8; 12], +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct RgbaColor { + pub red: u8, + pub green: u8, + pub blue: u8, + pub alpha: u8, +} + +impl Default for Tx3gBox { + fn default() -> Self { + Tx3gBox { + data_reference_index: 0, + display_flags: 0, + horizontal_justification: 1, + vertical_justification: -1, + bg_color_rgba: RgbaColor { + red: 0, + green: 0, + blue: 0, + alpha: 255, + }, + box_record: [0, 0, 0, 0], + style_record: [0, 0, 0, 0, 0, 1, 0, 16, 255, 255, 255, 255], + } + } +} + +impl Tx3gBox { + pub fn get_type(&self) -> BoxType { + BoxType::Tx3gBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + 6 + 32 + } +} + +impl Mp4Box for Tx3gBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!("data_reference_index={} horizontal_justification={} vertical_justification={} rgba={}{}{}{}", + self.data_reference_index, self.horizontal_justification, + self.vertical_justification, self.bg_color_rgba.red, + self.bg_color_rgba.green, self.bg_color_rgba.blue, self.bg_color_rgba.alpha); + Ok(s) + } +} + +impl ReadBox<&mut R> for Tx3gBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + reader.read_u32::()?; // reserved + reader.read_u16::()?; // reserved + let data_reference_index = reader.read_u16::()?; + + let display_flags = reader.read_u32::()?; + let horizontal_justification = reader.read_i8()?; + let vertical_justification = reader.read_i8()?; + let bg_color_rgba = RgbaColor { + red: reader.read_u8()?, + green: reader.read_u8()?, + blue: reader.read_u8()?, + alpha: reader.read_u8()?, + }; + let box_record: [i16; 4] = [ + reader.read_i16::()?, + reader.read_i16::()?, + reader.read_i16::()?, + reader.read_i16::()?, + ]; + let style_record: [u8; 12] = [ + reader.read_u8()?, + reader.read_u8()?, + reader.read_u8()?, + reader.read_u8()?, + reader.read_u8()?, + reader.read_u8()?, + reader.read_u8()?, + reader.read_u8()?, + reader.read_u8()?, + reader.read_u8()?, + reader.read_u8()?, + reader.read_u8()?, + ]; + + skip_bytes_to(reader, start + size)?; + + Ok(Tx3gBox { + data_reference_index, + display_flags, + horizontal_justification, + vertical_justification, + bg_color_rgba, + box_record, + style_record, + }) + } +} + +impl WriteBox<&mut W> for Tx3gBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + writer.write_u32::(0)?; // reserved + writer.write_u16::(0)?; // reserved + writer.write_u16::(self.data_reference_index)?; + writer.write_u32::(self.display_flags)?; + writer.write_i8(self.horizontal_justification)?; + writer.write_i8(self.vertical_justification)?; + writer.write_u8(self.bg_color_rgba.red)?; + writer.write_u8(self.bg_color_rgba.green)?; + writer.write_u8(self.bg_color_rgba.blue)?; + writer.write_u8(self.bg_color_rgba.alpha)?; + for n in 0..4 { + writer.write_i16::(self.box_record[n])?; + } + for n in 0..12 { + writer.write_u8(self.style_record[n])?; + } + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_tx3g() { + let src_box = Tx3gBox { + data_reference_index: 1, + display_flags: 0, + horizontal_justification: 1, + vertical_justification: -1, + bg_color_rgba: RgbaColor { + red: 0, + green: 0, + blue: 0, + alpha: 255, + }, + box_record: [0, 0, 0, 0], + style_record: [0, 0, 0, 0, 0, 1, 0, 16, 255, 255, 255, 255], + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::Tx3gBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = Tx3gBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/udta.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/udta.rs new file mode 100644 index 0000000..9daec17 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/udta.rs @@ -0,0 +1,136 @@ +use std::io::{Read, Seek}; + +use serde::Serialize; + +use crate::mp4box::meta::MetaBox; +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct UdtaBox { + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +impl UdtaBox { + pub fn get_type(&self) -> BoxType { + BoxType::UdtaBox + } + + pub fn get_size(&self) -> u64 { + let mut size = HEADER_SIZE; + if let Some(meta) = &self.meta { + size += meta.box_size(); + } + size + } +} + +impl Mp4Box for UdtaBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + Ok(String::new()) + } +} + +impl ReadBox<&mut R> for UdtaBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let mut meta = None; + + let mut current = reader.stream_position()?; + let end = start + size; + while current < end { + // Get box header. + let header = BoxHeader::read(reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "udta box contains a box with a larger size than it", + )); + } + + match name { + BoxType::MetaBox => { + meta = Some(MetaBox::read_box(reader, s)?); + } + _ => { + // XXX warn!() + skip_box(reader, s)?; + } + } + + current = reader.stream_position()?; + } + + skip_bytes_to(reader, start + size)?; + + Ok(UdtaBox { meta }) + } +} + +impl WriteBox<&mut W> for UdtaBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + if let Some(meta) = &self.meta { + meta.write_box(writer)?; + } + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_udta_empty() { + let src_box = UdtaBox { meta: None }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::UdtaBox); + assert_eq!(header.size, src_box.box_size()); + + let dst_box = UdtaBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(dst_box, src_box); + } + + #[test] + fn test_udta() { + let src_box = UdtaBox { + meta: Some(MetaBox::default()), + }; + + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::UdtaBox); + assert_eq!(header.size, src_box.box_size()); + + let dst_box = UdtaBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(dst_box, src_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/vmhd.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/vmhd.rs new file mode 100644 index 0000000..31f24b2 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/vmhd.rs @@ -0,0 +1,124 @@ +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use serde::Serialize; +use std::io::{Read, Seek, Write}; + +use crate::mp4box::*; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct VmhdBox { + pub version: u8, + pub flags: u32, + pub graphics_mode: u16, + pub op_color: RgbColor, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct RgbColor { + pub red: u16, + pub green: u16, + pub blue: u16, +} + +impl VmhdBox { + pub fn get_type(&self) -> BoxType { + BoxType::VmhdBox + } + + pub fn get_size(&self) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + 8 + } +} + +impl Mp4Box for VmhdBox { + fn box_type(&self) -> BoxType { + self.get_type() + } + + fn box_size(&self) -> u64 { + self.get_size() + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + let s = format!( + "graphics_mode={} op_color={}{}{}", + self.graphics_mode, self.op_color.red, self.op_color.green, self.op_color.blue + ); + Ok(s) + } +} + +impl ReadBox<&mut R> for VmhdBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + + let (version, flags) = read_box_header_ext(reader)?; + + let graphics_mode = reader.read_u16::()?; + let op_color = RgbColor { + red: reader.read_u16::()?, + green: reader.read_u16::()?, + blue: reader.read_u16::()?, + }; + + skip_bytes_to(reader, start + size)?; + + Ok(VmhdBox { + version, + flags, + graphics_mode, + op_color, + }) + } +} + +impl WriteBox<&mut W> for VmhdBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u16::(self.graphics_mode)?; + writer.write_u16::(self.op_color.red)?; + writer.write_u16::(self.op_color.green)?; + writer.write_u16::(self.op_color.blue)?; + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_vmhd() { + let src_box = VmhdBox { + version: 0, + flags: 1, + graphics_mode: 0, + op_color: RgbColor { + red: 0, + green: 0, + blue: 0, + }, + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::VmhdBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = VmhdBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/vp09.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/vp09.rs new file mode 100644 index 0000000..0f88dd1 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/vp09.rs @@ -0,0 +1,205 @@ +use crate::mp4box::vpcc::VpccBox; +use crate::mp4box::*; +use crate::Mp4Box; +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct Vp09Box { + pub version: u8, + pub flags: u32, + pub start_code: u16, + pub data_reference_index: u16, + pub reserved0: [u8; 16], + pub width: u16, + pub height: u16, + pub horizresolution: (u16, u16), + pub vertresolution: (u16, u16), + pub reserved1: [u8; 4], + pub frame_count: u16, + pub compressorname: [u8; 32], + pub depth: u16, + pub end_code: u16, + pub vpcc: VpccBox, +} + +impl Vp09Box { + pub const DEFAULT_START_CODE: u16 = 0; + pub const DEFAULT_END_CODE: u16 = 0xFFFF; + pub const DEFAULT_DATA_REFERENCE_INDEX: u16 = 1; + pub const DEFAULT_HORIZRESOLUTION: (u16, u16) = (0x48, 0x00); + pub const DEFAULT_VERTRESOLUTION: (u16, u16) = (0x48, 0x00); + pub const DEFAULT_FRAME_COUNT: u16 = 1; + pub const DEFAULT_COMPRESSORNAME: [u8; 32] = [0; 32]; + pub const DEFAULT_DEPTH: u16 = 24; + + pub fn new(config: &Vp9Config) -> Self { + Vp09Box { + version: 0, + flags: 0, + start_code: Vp09Box::DEFAULT_START_CODE, + data_reference_index: Vp09Box::DEFAULT_DATA_REFERENCE_INDEX, + reserved0: Default::default(), + width: config.width, + height: config.height, + horizresolution: Vp09Box::DEFAULT_HORIZRESOLUTION, + vertresolution: Vp09Box::DEFAULT_VERTRESOLUTION, + reserved1: Default::default(), + frame_count: Vp09Box::DEFAULT_FRAME_COUNT, + compressorname: Vp09Box::DEFAULT_COMPRESSORNAME, + depth: Vp09Box::DEFAULT_DEPTH, + end_code: Vp09Box::DEFAULT_END_CODE, + vpcc: VpccBox { + version: VpccBox::DEFAULT_VERSION, + flags: 0, + profile: 0, + level: 0x1F, + bit_depth: VpccBox::DEFAULT_BIT_DEPTH, + chroma_subsampling: 0, + video_full_range_flag: false, + color_primaries: 0, + transfer_characteristics: 0, + matrix_coefficients: 0, + codec_initialization_data_size: 0, + }, + } + } +} + +impl Mp4Box for Vp09Box { + fn box_type(&self) -> BoxType { + BoxType::Vp09Box + } + + fn box_size(&self) -> u64 { + 0x6A + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + Ok(format!("{self:?}")) + } +} + +impl ReadBox<&mut R> for Vp09Box { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + let (version, flags) = read_box_header_ext(reader)?; + + let start_code: u16 = reader.read_u16::()?; + let data_reference_index: u16 = reader.read_u16::()?; + let reserved0: [u8; 16] = { + let mut buf = [0u8; 16]; + reader.read_exact(&mut buf)?; + buf + }; + let width: u16 = reader.read_u16::()?; + let height: u16 = reader.read_u16::()?; + let horizresolution: (u16, u16) = ( + reader.read_u16::()?, + reader.read_u16::()?, + ); + let vertresolution: (u16, u16) = ( + reader.read_u16::()?, + reader.read_u16::()?, + ); + let reserved1: [u8; 4] = { + let mut buf = [0u8; 4]; + reader.read_exact(&mut buf)?; + buf + }; + let frame_count: u16 = reader.read_u16::()?; + let compressorname: [u8; 32] = { + let mut buf = [0u8; 32]; + reader.read_exact(&mut buf)?; + buf + }; + let depth: u16 = reader.read_u16::()?; + let end_code: u16 = reader.read_u16::()?; + + let vpcc = { + let header = BoxHeader::read(reader)?; + if header.size > size { + return Err(Error::InvalidData( + "vp09 box contains a box with a larger size than it", + )); + } + VpccBox::read_box(reader, header.size)? + }; + + skip_bytes_to(reader, start + size)?; + + Ok(Self { + version, + flags, + start_code, + data_reference_index, + reserved0, + width, + height, + horizresolution, + vertresolution, + reserved1, + frame_count, + compressorname, + depth, + end_code, + vpcc, + }) + } +} + +impl WriteBox<&mut W> for Vp09Box { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u16::(self.start_code)?; + writer.write_u16::(self.data_reference_index)?; + writer.write_all(&self.reserved0)?; + writer.write_u16::(self.width)?; + writer.write_u16::(self.height)?; + writer.write_u16::(self.horizresolution.0)?; + writer.write_u16::(self.horizresolution.1)?; + writer.write_u16::(self.vertresolution.0)?; + writer.write_u16::(self.vertresolution.1)?; + writer.write_all(&self.reserved1)?; + writer.write_u16::(self.frame_count)?; + writer.write_all(&self.compressorname)?; + writer.write_u16::(self.depth)?; + writer.write_u16::(self.end_code)?; + VpccBox::write_box(&self.vpcc, writer)?; + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_vpcc() { + let src_box = Vp09Box::new(&Vp9Config { + width: 1920, + height: 1080, + }); + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::Vp09Box); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = Vp09Box::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/mp4box/vpcc.rs b/repos/moq-rs/third_party/mp4-rust/src/mp4box/vpcc.rs new file mode 100644 index 0000000..c9861c6 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/mp4box/vpcc.rs @@ -0,0 +1,132 @@ +use crate::mp4box::*; +use crate::Mp4Box; +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct VpccBox { + pub version: u8, + pub flags: u32, + pub profile: u8, + pub level: u8, + pub bit_depth: u8, + pub chroma_subsampling: u8, + pub video_full_range_flag: bool, + pub color_primaries: u8, + pub transfer_characteristics: u8, + pub matrix_coefficients: u8, + pub codec_initialization_data_size: u16, +} + +impl VpccBox { + pub const DEFAULT_VERSION: u8 = 1; + pub const DEFAULT_BIT_DEPTH: u8 = 8; +} + +impl Mp4Box for VpccBox { + fn box_type(&self) -> BoxType { + BoxType::VpccBox + } + + fn box_size(&self) -> u64 { + HEADER_SIZE + HEADER_EXT_SIZE + 8 + } + + fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self).unwrap()) + } + + fn summary(&self) -> Result { + Ok(format!("{self:?}")) + } +} + +impl ReadBox<&mut R> for VpccBox { + fn read_box(reader: &mut R, size: u64) -> Result { + let start = box_start(reader)?; + let (version, flags) = read_box_header_ext(reader)?; + + let profile: u8 = reader.read_u8()?; + let level: u8 = reader.read_u8()?; + let (bit_depth, chroma_subsampling, video_full_range_flag) = { + let b = reader.read_u8()?; + (b >> 4, b << 4 >> 5, b & 0x01 == 1) + }; + let transfer_characteristics: u8 = reader.read_u8()?; + let matrix_coefficients: u8 = reader.read_u8()?; + let codec_initialization_data_size: u16 = reader.read_u16::()?; + + skip_bytes_to(reader, start + size)?; + + Ok(Self { + version, + flags, + profile, + level, + bit_depth, + chroma_subsampling, + video_full_range_flag, + color_primaries: 0, + transfer_characteristics, + matrix_coefficients, + codec_initialization_data_size, + }) + } +} + +impl WriteBox<&mut W> for VpccBox { + fn write_box(&self, writer: &mut W) -> Result { + let size = self.box_size(); + BoxHeader::new(self.box_type(), size).write(writer)?; + + write_box_header_ext(writer, self.version, self.flags)?; + + writer.write_u8(self.profile)?; + writer.write_u8(self.level)?; + writer.write_u8( + (self.bit_depth << 4) + | (self.chroma_subsampling << 1) + | (self.video_full_range_flag as u8), + )?; + writer.write_u8(self.color_primaries)?; + writer.write_u8(self.transfer_characteristics)?; + writer.write_u8(self.matrix_coefficients)?; + writer.write_u16::(self.codec_initialization_data_size)?; + + Ok(size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mp4box::BoxHeader; + use std::io::Cursor; + + #[test] + fn test_vpcc() { + let src_box = VpccBox { + version: VpccBox::DEFAULT_VERSION, + flags: 0, + profile: 0, + level: 0x1F, + bit_depth: VpccBox::DEFAULT_BIT_DEPTH, + chroma_subsampling: 0, + video_full_range_flag: false, + color_primaries: 0, + transfer_characteristics: 0, + matrix_coefficients: 0, + codec_initialization_data_size: 0, + }; + let mut buf = Vec::new(); + src_box.write_box(&mut buf).unwrap(); + assert_eq!(buf.len(), src_box.box_size() as usize); + + let mut reader = Cursor::new(&buf); + let header = BoxHeader::read(&mut reader).unwrap(); + assert_eq!(header.name, BoxType::VpccBox); + assert_eq!(src_box.box_size(), header.size); + + let dst_box = VpccBox::read_box(&mut reader, header.size).unwrap(); + assert_eq!(src_box, dst_box); + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/reader.rs b/repos/moq-rs/third_party/mp4-rust/src/reader.rs new file mode 100644 index 0000000..e5ac296 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/reader.rs @@ -0,0 +1,284 @@ +use std::collections::HashMap; +use std::io::{Read, Seek}; +use std::time::Duration; + +use crate::meta::MetaBox; +use crate::*; + +#[derive(Debug)] +pub struct Mp4Reader { + reader: R, + pub ftyp: FtypBox, + pub moov: MoovBox, + pub moofs: Vec, + pub emsgs: Vec, + + tracks: HashMap, + size: u64, +} + +impl Mp4Reader { + pub fn read_header(mut reader: R, size: u64) -> Result { + let start = reader.stream_position()?; + + let mut ftyp = None; + let mut moov = None; + let mut moofs = Vec::new(); + let mut moof_offsets = Vec::new(); + let mut emsgs = Vec::new(); + + let mut current = start; + while current < size { + // Get box header. + let header = BoxHeader::read(&mut reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "file contains a box with a larger size than it", + )); + } + + // Break if size zero BoxHeader, which can result in dead-loop. + if s == 0 { + break; + } + + // Match and parse the atom boxes. + match name { + BoxType::FtypBox => { + ftyp = Some(FtypBox::read_box(&mut reader, s)?); + } + BoxType::FreeBox => { + skip_box(&mut reader, s)?; + } + BoxType::MdatBox => { + skip_box(&mut reader, s)?; + } + BoxType::MoovBox => { + moov = Some(MoovBox::read_box(&mut reader, s)?); + } + BoxType::MoofBox => { + let moof_offset = reader.stream_position()? - 8; + let moof = MoofBox::read_box(&mut reader, s)?; + moofs.push(moof); + moof_offsets.push(moof_offset); + } + BoxType::EmsgBox => { + let emsg = EmsgBox::read_box(&mut reader, s)?; + emsgs.push(emsg); + } + _ => { + // XXX warn!() + skip_box(&mut reader, s)?; + } + } + current = reader.stream_position()?; + } + + if ftyp.is_none() { + return Err(Error::BoxNotFound(BoxType::FtypBox)); + } + if moov.is_none() { + return Err(Error::BoxNotFound(BoxType::MoovBox)); + } + + let size = current - start; + let mut tracks = if let Some(ref moov) = moov { + if moov.traks.iter().any(|trak| trak.tkhd.track_id == 0) { + return Err(Error::InvalidData("illegal track id 0")); + } + moov.traks + .iter() + .map(|trak| (trak.tkhd.track_id, Mp4Track::from(trak))) + .collect() + } else { + HashMap::new() + }; + + // Update tracks if any fragmented (moof) boxes are found. + if !moofs.is_empty() { + let mut default_sample_duration = 0; + if let Some(ref moov) = moov { + if let Some(ref mvex) = &moov.mvex { + default_sample_duration = mvex.trex.default_sample_duration + } + } + + for (moof, moof_offset) in moofs.iter().zip(moof_offsets) { + for traf in moof.trafs.iter() { + let track_id = traf.tfhd.track_id; + if let Some(track) = tracks.get_mut(&track_id) { + track.default_sample_duration = default_sample_duration; + track.moof_offsets.push(moof_offset); + track.trafs.push(traf.clone()) + } else { + return Err(Error::TrakNotFound(track_id)); + } + } + } + } + + Ok(Mp4Reader { + reader, + ftyp: ftyp.unwrap(), + moov: moov.unwrap(), + moofs, + emsgs, + size, + tracks, + }) + } + + pub fn read_fragment_header( + &self, + mut reader: FR, + size: u64, + ) -> Result> { + let start = reader.stream_position()?; + + let mut moofs = Vec::new(); + let mut moof_offsets = Vec::new(); + + let mut current = start; + while current < size { + // Get box header. + let header = BoxHeader::read(&mut reader)?; + let BoxHeader { name, size: s } = header; + if s > size { + return Err(Error::InvalidData( + "file contains a box with a larger size than it", + )); + } + + // Break if size zero BoxHeader, which can result in dead-loop. + if s == 0 { + break; + } + + // Match and parse the atom boxes. + match name { + BoxType::MdatBox => { + skip_box(&mut reader, s)?; + } + BoxType::MoofBox => { + let moof_offset = reader.stream_position()? - 8; + let moof = MoofBox::read_box(&mut reader, s)?; + moofs.push(moof); + moof_offsets.push(moof_offset); + } + _ => { + // XXX warn!() + skip_box(&mut reader, s)?; + } + } + current = reader.stream_position()?; + } + + if moofs.is_empty() { + return Err(Error::BoxNotFound(BoxType::MoofBox)); + } + + let size = current - start; + let mut tracks: HashMap = self + .moov + .traks + .iter() + .map(|trak| (trak.tkhd.track_id, Mp4Track::from(trak))) + .collect(); + + let mut default_sample_duration = 0; + if let Some(ref mvex) = &self.moov.mvex { + default_sample_duration = mvex.trex.default_sample_duration + } + + for (moof, moof_offset) in moofs.iter().zip(moof_offsets) { + for traf in moof.trafs.iter() { + let track_id = traf.tfhd.track_id; + if let Some(track) = tracks.get_mut(&track_id) { + track.default_sample_duration = default_sample_duration; + track.moof_offsets.push(moof_offset); + track.trafs.push(traf.clone()) + } else { + return Err(Error::TrakNotFound(track_id)); + } + } + } + + Ok(Mp4Reader { + reader, + ftyp: self.ftyp.clone(), + moov: self.moov.clone(), + moofs, + emsgs: Vec::new(), + tracks, + size, + }) + } + + pub fn size(&self) -> u64 { + self.size + } + + pub fn major_brand(&self) -> &FourCC { + &self.ftyp.major_brand + } + + pub fn minor_version(&self) -> u32 { + self.ftyp.minor_version + } + + pub fn compatible_brands(&self) -> &[FourCC] { + &self.ftyp.compatible_brands + } + + pub fn duration(&self) -> Duration { + Duration::from_millis(self.moov.mvhd.duration * 1000 / self.moov.mvhd.timescale as u64) + } + + pub fn timescale(&self) -> u32 { + self.moov.mvhd.timescale + } + + pub fn is_fragmented(&self) -> bool { + !self.moofs.is_empty() + } + + pub fn tracks(&self) -> &HashMap { + &self.tracks + } + + pub fn sample_count(&self, track_id: u32) -> Result { + if let Some(track) = self.tracks.get(&track_id) { + Ok(track.sample_count()) + } else { + Err(Error::TrakNotFound(track_id)) + } + } + + pub fn read_sample(&mut self, track_id: u32, sample_id: u32) -> Result> { + if let Some(track) = self.tracks.get(&track_id) { + track.read_sample(&mut self.reader, sample_id) + } else { + Err(Error::TrakNotFound(track_id)) + } + } + + pub fn sample_offset(&mut self, track_id: u32, sample_id: u32) -> Result { + if let Some(track) = self.tracks.get(&track_id) { + track.sample_offset(sample_id) + } else { + Err(Error::TrakNotFound(track_id)) + } + } +} + +impl Mp4Reader { + pub fn metadata(&self) -> impl Metadata<'_> { + self.moov.udta.as_ref().and_then(|udta| { + udta.meta.as_ref().and_then(|meta| match meta { + MetaBox::Mdir { ilst } => ilst.as_ref(), + _ => None, + }) + }) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/track.rs b/repos/moq-rs/third_party/mp4-rust/src/track.rs new file mode 100644 index 0000000..7eada83 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/track.rs @@ -0,0 +1,918 @@ +use bytes::BytesMut; +use std::cmp; +use std::convert::TryFrom; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::time::Duration; + +use crate::mp4box::traf::TrafBox; +use crate::mp4box::trak::TrakBox; +use crate::mp4box::trun::TrunBox; +use crate::mp4box::{ + avc1::Avc1Box, co64::Co64Box, ctts::CttsBox, ctts::CttsEntry, hev1::Hev1Box, mp4a::Mp4aBox, + smhd::SmhdBox, stco::StcoBox, stsc::StscEntry, stss::StssBox, stts::SttsEntry, tx3g::Tx3gBox, + vmhd::VmhdBox, vp09::Vp09Box, +}; +use crate::*; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TrackConfig { + pub track_type: TrackType, + pub timescale: u32, + pub language: String, + pub media_conf: MediaConfig, +} + +impl From for TrackConfig { + fn from(media_conf: MediaConfig) -> Self { + match media_conf { + MediaConfig::AvcConfig(avc_conf) => Self::from(avc_conf), + MediaConfig::HevcConfig(hevc_conf) => Self::from(hevc_conf), + MediaConfig::AacConfig(aac_conf) => Self::from(aac_conf), + MediaConfig::TtxtConfig(ttxt_conf) => Self::from(ttxt_conf), + MediaConfig::Vp9Config(vp9_config) => Self::from(vp9_config), + } + } +} + +impl From for TrackConfig { + fn from(avc_conf: AvcConfig) -> Self { + Self { + track_type: TrackType::Video, + timescale: 1000, // XXX + language: String::from("und"), // XXX + media_conf: MediaConfig::AvcConfig(avc_conf), + } + } +} + +impl From for TrackConfig { + fn from(hevc_conf: HevcConfig) -> Self { + Self { + track_type: TrackType::Video, + timescale: 1000, // XXX + language: String::from("und"), // XXX + media_conf: MediaConfig::HevcConfig(hevc_conf), + } + } +} + +impl From for TrackConfig { + fn from(aac_conf: AacConfig) -> Self { + Self { + track_type: TrackType::Audio, + timescale: 1000, // XXX + language: String::from("und"), // XXX + media_conf: MediaConfig::AacConfig(aac_conf), + } + } +} + +impl From for TrackConfig { + fn from(txtt_conf: TtxtConfig) -> Self { + Self { + track_type: TrackType::Subtitle, + timescale: 1000, // XXX + language: String::from("und"), // XXX + media_conf: MediaConfig::TtxtConfig(txtt_conf), + } + } +} + +impl From for TrackConfig { + fn from(vp9_conf: Vp9Config) -> Self { + Self { + track_type: TrackType::Video, + timescale: 1000, // XXX + language: String::from("und"), // XXX + media_conf: MediaConfig::Vp9Config(vp9_conf), + } + } +} + +#[derive(Debug)] +pub struct Mp4Track { + pub trak: TrakBox, + pub trafs: Vec, + pub moof_offsets: Vec, + + // Fragmented Tracks Defaults. + pub default_sample_duration: u32, +} + +impl Mp4Track { + pub(crate) fn from(trak: &TrakBox) -> Self { + let trak = trak.clone(); + Self { + trak, + trafs: Vec::new(), + moof_offsets: Vec::new(), + default_sample_duration: 0, + } + } + + pub fn track_id(&self) -> u32 { + self.trak.tkhd.track_id + } + + pub fn track_type(&self) -> Result { + TrackType::try_from(&self.trak.mdia.hdlr.handler_type) + } + + pub fn media_type(&self) -> Result { + if self.trak.mdia.minf.stbl.stsd.avc1.is_some() { + Ok(MediaType::H264) + } else if self.trak.mdia.minf.stbl.stsd.hev1.is_some() { + Ok(MediaType::H265) + } else if self.trak.mdia.minf.stbl.stsd.vp09.is_some() { + Ok(MediaType::VP9) + } else if self.trak.mdia.minf.stbl.stsd.mp4a.is_some() { + Ok(MediaType::AAC) + } else if self.trak.mdia.minf.stbl.stsd.tx3g.is_some() { + Ok(MediaType::TTXT) + } else { + Err(Error::InvalidData("unsupported media type")) + } + } + + pub fn box_type(&self) -> Result { + if self.trak.mdia.minf.stbl.stsd.avc1.is_some() { + Ok(FourCC::from(BoxType::Avc1Box)) + } else if self.trak.mdia.minf.stbl.stsd.hev1.is_some() { + Ok(FourCC::from(BoxType::Hev1Box)) + } else if self.trak.mdia.minf.stbl.stsd.vp09.is_some() { + Ok(FourCC::from(BoxType::Vp09Box)) + } else if self.trak.mdia.minf.stbl.stsd.mp4a.is_some() { + Ok(FourCC::from(BoxType::Mp4aBox)) + } else if self.trak.mdia.minf.stbl.stsd.tx3g.is_some() { + Ok(FourCC::from(BoxType::Tx3gBox)) + } else { + Err(Error::InvalidData("unsupported sample entry box")) + } + } + + pub fn width(&self) -> u16 { + if let Some(ref avc1) = self.trak.mdia.minf.stbl.stsd.avc1 { + avc1.width + } else { + self.trak.tkhd.width.value() + } + } + + pub fn height(&self) -> u16 { + if let Some(ref avc1) = self.trak.mdia.minf.stbl.stsd.avc1 { + avc1.height + } else { + self.trak.tkhd.height.value() + } + } + + pub fn frame_rate(&self) -> f64 { + let dur = self.duration(); + if dur.is_zero() { + 0.0 + } else { + self.sample_count() as f64 / dur.as_secs_f64() + } + } + + pub fn sample_freq_index(&self) -> Result { + if let Some(ref mp4a) = self.trak.mdia.minf.stbl.stsd.mp4a { + if let Some(ref esds) = mp4a.esds { + SampleFreqIndex::try_from(esds.es_desc.dec_config.dec_specific.freq_index) + } else { + Err(Error::BoxInStblNotFound(self.track_id(), BoxType::EsdsBox)) + } + } else { + Err(Error::BoxInStblNotFound(self.track_id(), BoxType::Mp4aBox)) + } + } + + pub fn channel_config(&self) -> Result { + if let Some(ref mp4a) = self.trak.mdia.minf.stbl.stsd.mp4a { + if let Some(ref esds) = mp4a.esds { + ChannelConfig::try_from(esds.es_desc.dec_config.dec_specific.chan_conf) + } else { + Err(Error::BoxInStblNotFound(self.track_id(), BoxType::EsdsBox)) + } + } else { + Err(Error::BoxInStblNotFound(self.track_id(), BoxType::Mp4aBox)) + } + } + + pub fn language(&self) -> &str { + &self.trak.mdia.mdhd.language + } + + pub fn timescale(&self) -> u32 { + self.trak.mdia.mdhd.timescale + } + + pub fn duration(&self) -> Duration { + Duration::from_micros( + self.trak.mdia.mdhd.duration * 1_000_000 / self.trak.mdia.mdhd.timescale as u64, + ) + } + + pub fn bitrate(&self) -> u32 { + if let Some(ref mp4a) = self.trak.mdia.minf.stbl.stsd.mp4a { + if let Some(ref esds) = mp4a.esds { + esds.es_desc.dec_config.avg_bitrate + } else { + 0 + } + // mp4a.esds.es_desc.dec_config.avg_bitrate + } else { + let dur = self.duration(); + if dur.is_zero() { + 0 + } else { + let bitrate = self.total_sample_size() as f64 * 8.0 / dur.as_secs_f64(); + bitrate as u32 + } + } + } + + pub fn sample_count(&self) -> u32 { + if !self.trafs.is_empty() { + let mut sample_count = 0u32; + for traf in self.trafs.iter() { + if let Some(ref trun) = traf.trun { + sample_count = sample_count + .checked_add(trun.sample_count) + .expect("attempt to sum trun sample_count with overflow"); + } + } + sample_count + } else { + self.trak.mdia.minf.stbl.stsz.sample_count + } + } + + pub fn video_profile(&self) -> Result { + if let Some(ref avc1) = self.trak.mdia.minf.stbl.stsd.avc1 { + AvcProfile::try_from(( + avc1.avcc.avc_profile_indication, + avc1.avcc.profile_compatibility, + )) + } else { + Err(Error::BoxInStblNotFound(self.track_id(), BoxType::Avc1Box)) + } + } + + pub fn sequence_parameter_set(&self) -> Result<&[u8]> { + if let Some(ref avc1) = self.trak.mdia.minf.stbl.stsd.avc1 { + match avc1.avcc.sequence_parameter_sets.get(0) { + Some(nal) => Ok(nal.bytes.as_ref()), + None => Err(Error::EntryInStblNotFound( + self.track_id(), + BoxType::AvcCBox, + 0, + )), + } + } else { + Err(Error::BoxInStblNotFound(self.track_id(), BoxType::Avc1Box)) + } + } + + pub fn picture_parameter_set(&self) -> Result<&[u8]> { + if let Some(ref avc1) = self.trak.mdia.minf.stbl.stsd.avc1 { + match avc1.avcc.picture_parameter_sets.get(0) { + Some(nal) => Ok(nal.bytes.as_ref()), + None => Err(Error::EntryInStblNotFound( + self.track_id(), + BoxType::AvcCBox, + 0, + )), + } + } else { + Err(Error::BoxInStblNotFound(self.track_id(), BoxType::Avc1Box)) + } + } + + pub fn audio_profile(&self) -> Result { + if let Some(ref mp4a) = self.trak.mdia.minf.stbl.stsd.mp4a { + if let Some(ref esds) = mp4a.esds { + AudioObjectType::try_from(esds.es_desc.dec_config.dec_specific.profile) + } else { + Err(Error::BoxInStblNotFound(self.track_id(), BoxType::EsdsBox)) + } + } else { + Err(Error::BoxInStblNotFound(self.track_id(), BoxType::Mp4aBox)) + } + } + + fn stsc_index(&self, sample_id: u32) -> Result { + if self.trak.mdia.minf.stbl.stsc.entries.is_empty() { + return Err(Error::InvalidData("no stsc entries")); + } + for (i, entry) in self.trak.mdia.minf.stbl.stsc.entries.iter().enumerate() { + if sample_id < entry.first_sample { + return if i == 0 { + Err(Error::InvalidData("sample not found")) + } else { + Ok(i - 1) + }; + } + } + Ok(self.trak.mdia.minf.stbl.stsc.entries.len() - 1) + } + + fn chunk_offset(&self, chunk_id: u32) -> Result { + if self.trak.mdia.minf.stbl.stco.is_none() && self.trak.mdia.minf.stbl.co64.is_none() { + return Err(Error::InvalidData("must have either stco or co64 boxes")); + } + if let Some(ref stco) = self.trak.mdia.minf.stbl.stco { + if let Some(offset) = stco.entries.get(chunk_id as usize - 1) { + return Ok(*offset as u64); + } else { + return Err(Error::EntryInStblNotFound( + self.track_id(), + BoxType::StcoBox, + chunk_id, + )); + } + } else if let Some(ref co64) = self.trak.mdia.minf.stbl.co64 { + if let Some(offset) = co64.entries.get(chunk_id as usize - 1) { + return Ok(*offset); + } else { + return Err(Error::EntryInStblNotFound( + self.track_id(), + BoxType::Co64Box, + chunk_id, + )); + } + } + Err(Error::Box2NotFound(BoxType::StcoBox, BoxType::Co64Box)) + } + + fn ctts_index(&self, sample_id: u32) -> Result<(usize, u32)> { + let ctts = self.trak.mdia.minf.stbl.ctts.as_ref().unwrap(); + let mut sample_count: u32 = 1; + for (i, entry) in ctts.entries.iter().enumerate() { + let next_sample_count = + sample_count + .checked_add(entry.sample_count) + .ok_or(Error::InvalidData( + "attempt to sum ctts entries sample_count with overflow", + ))?; + if sample_id < next_sample_count { + return Ok((i, sample_count)); + } + sample_count = next_sample_count; + } + + Err(Error::EntryInStblNotFound( + self.track_id(), + BoxType::CttsBox, + sample_id, + )) + } + + /// return `(traf_idx, sample_idx_in_trun)` + fn find_traf_idx_and_sample_idx(&self, sample_id: u32) -> Option<(usize, usize)> { + let global_idx = sample_id - 1; + let mut offset = 0; + for traf_idx in 0..self.trafs.len() { + if let Some(trun) = &self.trafs[traf_idx].trun { + let sample_count = trun.sample_count; + if sample_count > (global_idx - offset) { + return Some((traf_idx, (global_idx - offset) as _)); + } + offset = offset + .checked_add(sample_count) + .expect("attempt to sum trun sample_count with overflow"); + } + } + None + } + + fn sample_size(&self, sample_id: u32) -> Result { + if !self.trafs.is_empty() { + if let Some((traf_idx, sample_idx)) = self.find_traf_idx_and_sample_idx(sample_id) { + if let Some(size) = self.trafs[traf_idx] + .trun + .as_ref() + .unwrap() + .sample_sizes + .get(sample_idx) + { + Ok(*size) + } else { + Err(Error::EntryInTrunNotFound( + self.track_id(), + BoxType::TrunBox, + sample_id, + )) + } + } else { + Err(Error::BoxInTrafNotFound(self.track_id(), BoxType::TrafBox)) + } + } else { + let stsz = &self.trak.mdia.minf.stbl.stsz; + if stsz.sample_size > 0 { + return Ok(stsz.sample_size); + } + if let Some(size) = stsz.sample_sizes.get(sample_id as usize - 1) { + Ok(*size) + } else { + Err(Error::EntryInStblNotFound( + self.track_id(), + BoxType::StszBox, + sample_id, + )) + } + } + } + + fn total_sample_size(&self) -> u64 { + let stsz = &self.trak.mdia.minf.stbl.stsz; + if stsz.sample_size > 0 { + stsz.sample_size as u64 * self.sample_count() as u64 + } else { + let mut total_size = 0; + for size in stsz.sample_sizes.iter() { + total_size += *size as u64; + } + total_size + } + } + + pub fn sample_offset(&self, sample_id: u32) -> Result { + if !self.trafs.is_empty() { + if let Some((traf_idx, sample_idx)) = self.find_traf_idx_and_sample_idx(sample_id) { + let mut sample_offset = self.trafs[traf_idx] + .tfhd + .base_data_offset + .unwrap_or(self.moof_offsets[traf_idx]); + + if let Some(data_offset) = self.trafs[traf_idx] + .trun + .as_ref() + .and_then(|trun| trun.data_offset) + { + sample_offset = sample_offset.checked_add_signed(data_offset as i64).ok_or( + Error::InvalidData("attempt to calculate trun sample offset with overflow"), + )?; + } + + let first_sample_in_trun = sample_id - sample_idx as u32; + for i in first_sample_in_trun..sample_id { + sample_offset = sample_offset + .checked_add(self.sample_size(i)? as u64) + .ok_or(Error::InvalidData( + "attempt to calculate trun entry sample offset with overflow", + ))?; + } + + Ok(sample_offset) + } else { + Err(Error::BoxInTrafNotFound(self.track_id(), BoxType::TrafBox)) + } + } else { + let stsc_index = self.stsc_index(sample_id)?; + + let stsc = &self.trak.mdia.minf.stbl.stsc; + let stsc_entry = stsc.entries.get(stsc_index).unwrap(); + + let first_chunk = stsc_entry.first_chunk; + let first_sample = stsc_entry.first_sample; + let samples_per_chunk = stsc_entry.samples_per_chunk; + + let chunk_id = sample_id + .checked_sub(first_sample) + .map(|n| n / samples_per_chunk) + .and_then(|n| n.checked_add(first_chunk)) + .ok_or(Error::InvalidData( + "attempt to calculate stsc chunk_id with overflow", + ))?; + + let chunk_offset = self.chunk_offset(chunk_id)?; + + let first_sample_in_chunk = sample_id - (sample_id - first_sample) % samples_per_chunk; + + let mut sample_offset = 0; + for i in first_sample_in_chunk..sample_id { + sample_offset += self.sample_size(i)?; + } + + Ok(chunk_offset + sample_offset as u64) + } + } + + fn sample_time(&self, sample_id: u32) -> Result<(u64, u32)> { + if !self.trafs.is_empty() { + let mut base_start_time = 0; + let mut default_sample_duration = self.default_sample_duration; + if let Some((traf_idx, sample_idx)) = self.find_traf_idx_and_sample_idx(sample_id) { + let traf = &self.trafs[traf_idx]; + if let Some(tfdt) = &traf.tfdt { + base_start_time = tfdt.base_media_decode_time; + } + if let Some(duration) = traf.tfhd.default_sample_duration { + default_sample_duration = duration; + } + if let Some(trun) = &traf.trun { + if TrunBox::FLAG_SAMPLE_DURATION & trun.flags != 0 { + let mut start_offset = 0u64; + for duration in &trun.sample_durations[..sample_idx] { + start_offset = start_offset.checked_add(*duration as u64).ok_or( + Error::InvalidData("attempt to sum sample durations with overflow"), + )?; + } + let duration = trun.sample_durations[sample_idx]; + return Ok((base_start_time + start_offset, duration)); + } + } + } + let start_offset = ((sample_id - 1) * default_sample_duration) as u64; + Ok((base_start_time + start_offset, default_sample_duration)) + } else { + let stts = &self.trak.mdia.minf.stbl.stts; + + let mut sample_count: u32 = 1; + let mut elapsed = 0; + + for entry in stts.entries.iter() { + let new_sample_count = + sample_count + .checked_add(entry.sample_count) + .ok_or(Error::InvalidData( + "attempt to sum stts entries sample_count with overflow", + ))?; + if sample_id < new_sample_count { + let start_time = + (sample_id - sample_count) as u64 * entry.sample_delta as u64 + elapsed; + return Ok((start_time, entry.sample_delta)); + } + + sample_count = new_sample_count; + elapsed += entry.sample_count as u64 * entry.sample_delta as u64; + } + + Err(Error::EntryInStblNotFound( + self.track_id(), + BoxType::SttsBox, + sample_id, + )) + } + } + + fn sample_rendering_offset(&self, sample_id: u32) -> i32 { + if !self.trafs.is_empty() { + if let Some((traf_idx, sample_idx)) = self.find_traf_idx_and_sample_idx(sample_id) { + if let Some(cts) = self.trafs[traf_idx] + .trun + .as_ref() + .and_then(|trun| trun.sample_cts.get(sample_idx)) + { + return *cts as i32; + } + } + } else if let Some(ref ctts) = self.trak.mdia.minf.stbl.ctts { + if let Ok((ctts_index, _)) = self.ctts_index(sample_id) { + let ctts_entry = ctts.entries.get(ctts_index).unwrap(); + return ctts_entry.sample_offset; + } + } + 0 + } + + fn is_sync_sample(&self, sample_id: u32) -> bool { + if !self.trafs.is_empty() { + let sample_sizes_count = self.sample_count() / self.trafs.len() as u32; + return sample_id == 1 || sample_id % sample_sizes_count == 0; + } + + if let Some(ref stss) = self.trak.mdia.minf.stbl.stss { + stss.entries.binary_search(&sample_id).is_ok() + } else { + true + } + } + + pub(crate) fn read_sample( + &self, + reader: &mut R, + sample_id: u32, + ) -> Result> { + let sample_offset = match self.sample_offset(sample_id) { + Ok(offset) => offset, + Err(Error::EntryInStblNotFound(_, _, _)) => return Ok(None), + Err(err) => return Err(err), + }; + let sample_size = match self.sample_size(sample_id) { + Ok(size) => size, + Err(Error::EntryInStblNotFound(_, _, _)) => return Ok(None), + Err(err) => return Err(err), + }; + + let mut buffer = vec![0x0u8; sample_size as usize]; + reader.seek(SeekFrom::Start(sample_offset))?; + reader.read_exact(&mut buffer)?; + + let (start_time, duration) = self.sample_time(sample_id).unwrap(); // XXX + let rendering_offset = self.sample_rendering_offset(sample_id); + let is_sync = self.is_sync_sample(sample_id); + + Ok(Some(Mp4Sample { + start_time, + duration, + rendering_offset, + is_sync, + bytes: Bytes::from(buffer), + })) + } +} + +// TODO creation_time, modification_time +#[derive(Debug, Default)] +pub(crate) struct Mp4TrackWriter { + trak: TrakBox, + + sample_id: u32, + fixed_sample_size: u32, + is_fixed_sample_size: bool, + chunk_samples: u32, + chunk_duration: u32, + chunk_buffer: BytesMut, + + samples_per_chunk: u32, + duration_per_chunk: u32, +} + +impl Mp4TrackWriter { + pub(crate) fn new(track_id: u32, config: &TrackConfig) -> Result { + let mut trak = TrakBox::default(); + trak.tkhd.track_id = track_id; + trak.mdia.mdhd.timescale = config.timescale; + trak.mdia.mdhd.language = config.language.to_owned(); + trak.mdia.hdlr.handler_type = config.track_type.into(); + trak.mdia.minf.stbl.co64 = Some(Co64Box::default()); + match config.media_conf { + MediaConfig::AvcConfig(ref avc_config) => { + trak.tkhd.set_width(avc_config.width); + trak.tkhd.set_height(avc_config.height); + + let vmhd = VmhdBox::default(); + trak.mdia.minf.vmhd = Some(vmhd); + + let avc1 = Avc1Box::new(avc_config); + trak.mdia.minf.stbl.stsd.avc1 = Some(avc1); + } + MediaConfig::HevcConfig(ref hevc_config) => { + trak.tkhd.set_width(hevc_config.width); + trak.tkhd.set_height(hevc_config.height); + + let vmhd = VmhdBox::default(); + trak.mdia.minf.vmhd = Some(vmhd); + + let hev1 = Hev1Box::new(hevc_config); + trak.mdia.minf.stbl.stsd.hev1 = Some(hev1); + } + MediaConfig::Vp9Config(ref config) => { + trak.tkhd.set_width(config.width); + trak.tkhd.set_height(config.height); + + trak.mdia.minf.stbl.stsd.vp09 = Some(Vp09Box::new(config)); + } + MediaConfig::AacConfig(ref aac_config) => { + let smhd = SmhdBox::default(); + trak.mdia.minf.smhd = Some(smhd); + + let mp4a = Mp4aBox::new(aac_config); + trak.mdia.minf.stbl.stsd.mp4a = Some(mp4a); + } + MediaConfig::TtxtConfig(ref _ttxt_config) => { + let tx3g = Tx3gBox::default(); + trak.mdia.minf.stbl.stsd.tx3g = Some(tx3g); + } + } + Ok(Mp4TrackWriter { + trak, + chunk_buffer: BytesMut::new(), + sample_id: 1, + duration_per_chunk: config.timescale, // 1 second + ..Self::default() + }) + } + + fn update_sample_sizes(&mut self, size: u32) { + if self.trak.mdia.minf.stbl.stsz.sample_count == 0 { + if size == 0 { + self.trak.mdia.minf.stbl.stsz.sample_size = 0; + self.is_fixed_sample_size = false; + self.trak.mdia.minf.stbl.stsz.sample_sizes.push(0); + } else { + self.trak.mdia.minf.stbl.stsz.sample_size = size; + self.fixed_sample_size = size; + self.is_fixed_sample_size = true; + } + } else if self.is_fixed_sample_size { + if self.fixed_sample_size != size { + self.is_fixed_sample_size = false; + if self.trak.mdia.minf.stbl.stsz.sample_size > 0 { + self.trak.mdia.minf.stbl.stsz.sample_size = 0; + for _ in 0..self.trak.mdia.minf.stbl.stsz.sample_count { + self.trak + .mdia + .minf + .stbl + .stsz + .sample_sizes + .push(self.fixed_sample_size); + } + } + self.trak.mdia.minf.stbl.stsz.sample_sizes.push(size); + } + } else { + self.trak.mdia.minf.stbl.stsz.sample_sizes.push(size); + } + self.trak.mdia.minf.stbl.stsz.sample_count += 1; + } + + fn update_sample_times(&mut self, dur: u32) { + if let Some(ref mut entry) = self.trak.mdia.minf.stbl.stts.entries.last_mut() { + if entry.sample_delta == dur { + entry.sample_count += 1; + return; + } + } + + let entry = SttsEntry { + sample_count: 1, + sample_delta: dur, + }; + self.trak.mdia.minf.stbl.stts.entries.push(entry); + } + + fn update_rendering_offsets(&mut self, offset: i32) { + let ctts = if let Some(ref mut ctts) = self.trak.mdia.minf.stbl.ctts { + ctts + } else { + if offset == 0 { + return; + } + let mut ctts = CttsBox::default(); + if self.sample_id > 1 { + let entry = CttsEntry { + sample_count: self.sample_id - 1, + sample_offset: 0, + }; + ctts.entries.push(entry); + } + self.trak.mdia.minf.stbl.ctts = Some(ctts); + self.trak.mdia.minf.stbl.ctts.as_mut().unwrap() + }; + + if let Some(ref mut entry) = ctts.entries.last_mut() { + if entry.sample_offset == offset { + entry.sample_count += 1; + return; + } + } + + let entry = CttsEntry { + sample_count: 1, + sample_offset: offset, + }; + ctts.entries.push(entry); + } + + fn update_sync_samples(&mut self, is_sync: bool) { + if let Some(ref mut stss) = self.trak.mdia.minf.stbl.stss { + if !is_sync { + return; + } + + stss.entries.push(self.sample_id); + } else { + if !is_sync { + return; + } + + // Create the stts box if not found and push the entry. + let mut stss = StssBox::default(); + stss.entries.push(self.sample_id); + self.trak.mdia.minf.stbl.stss = Some(stss); + }; + } + + fn is_chunk_full(&self) -> bool { + if self.samples_per_chunk > 0 { + self.chunk_samples >= self.samples_per_chunk + } else { + self.chunk_duration >= self.duration_per_chunk + } + } + + fn update_durations(&mut self, dur: u32, movie_timescale: u32) { + self.trak.mdia.mdhd.duration += dur as u64; + if self.trak.mdia.mdhd.duration > (u32::MAX as u64) { + self.trak.mdia.mdhd.version = 1 + } + self.trak.tkhd.duration += + dur as u64 * movie_timescale as u64 / self.trak.mdia.mdhd.timescale as u64; + if self.trak.tkhd.duration > (u32::MAX as u64) { + self.trak.tkhd.version = 1 + } + } + + pub(crate) fn write_sample( + &mut self, + writer: &mut W, + sample: &Mp4Sample, + movie_timescale: u32, + ) -> Result { + self.chunk_buffer.extend_from_slice(&sample.bytes); + self.chunk_samples += 1; + self.chunk_duration += sample.duration; + self.update_sample_sizes(sample.bytes.len() as u32); + self.update_sample_times(sample.duration); + self.update_rendering_offsets(sample.rendering_offset); + self.update_sync_samples(sample.is_sync); + if self.is_chunk_full() { + self.write_chunk(writer)?; + } + self.update_durations(sample.duration, movie_timescale); + + self.sample_id += 1; + + Ok(self.trak.tkhd.duration) + } + + fn chunk_count(&self) -> u32 { + let co64 = self.trak.mdia.minf.stbl.co64.as_ref().unwrap(); + co64.entries.len() as u32 + } + + fn update_sample_to_chunk(&mut self, chunk_id: u32) { + if let Some(entry) = self.trak.mdia.minf.stbl.stsc.entries.last() { + if entry.samples_per_chunk == self.chunk_samples { + return; + } + } + + let entry = StscEntry { + first_chunk: chunk_id, + samples_per_chunk: self.chunk_samples, + sample_description_index: 1, + first_sample: self.sample_id - self.chunk_samples + 1, + }; + self.trak.mdia.minf.stbl.stsc.entries.push(entry); + } + + fn update_chunk_offsets(&mut self, offset: u64) { + let co64 = self.trak.mdia.minf.stbl.co64.as_mut().unwrap(); + co64.entries.push(offset); + } + + fn write_chunk(&mut self, writer: &mut W) -> Result<()> { + if self.chunk_buffer.is_empty() { + return Ok(()); + } + let chunk_offset = writer.stream_position()?; + + writer.write_all(&self.chunk_buffer)?; + + self.update_sample_to_chunk(self.chunk_count() + 1); + self.update_chunk_offsets(chunk_offset); + + self.chunk_buffer.clear(); + self.chunk_samples = 0; + self.chunk_duration = 0; + + Ok(()) + } + + fn max_sample_size(&self) -> u32 { + if self.trak.mdia.minf.stbl.stsz.sample_size > 0 { + self.trak.mdia.minf.stbl.stsz.sample_size + } else { + let mut max_size = 0; + for sample_size in self.trak.mdia.minf.stbl.stsz.sample_sizes.iter() { + max_size = cmp::max(max_size, *sample_size); + } + max_size + } + } + + pub(crate) fn write_end(&mut self, writer: &mut W) -> Result { + self.write_chunk(writer)?; + + let max_sample_size = self.max_sample_size(); + if let Some(ref mut mp4a) = self.trak.mdia.minf.stbl.stsd.mp4a { + if let Some(ref mut esds) = mp4a.esds { + esds.es_desc.dec_config.buffer_size_db = max_sample_size; + } + // TODO + // mp4a.esds.es_desc.dec_config.max_bitrate + // mp4a.esds.es_desc.dec_config.avg_bitrate + } + if let Ok(stco) = StcoBox::try_from(self.trak.mdia.minf.stbl.co64.as_ref().unwrap()) { + self.trak.mdia.minf.stbl.stco = Some(stco); + self.trak.mdia.minf.stbl.co64 = None; + } + + Ok(self.trak.clone()) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/types.rs b/repos/moq-rs/third_party/mp4-rust/src/types.rs new file mode 100644 index 0000000..540f7fb --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/types.rs @@ -0,0 +1,741 @@ +use serde::Serialize; +use std::borrow::Cow; +use std::convert::TryFrom; +use std::fmt; + +use crate::mp4box::*; +use crate::*; + +pub use bytes::Bytes; +pub use num_rational::Ratio; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub struct FixedPointU8(Ratio); + +impl FixedPointU8 { + pub fn new(val: u8) -> Self { + Self(Ratio::new_raw(val as u16 * 0x100, 0x100)) + } + + pub fn new_raw(val: u16) -> Self { + Self(Ratio::new_raw(val, 0x100)) + } + + pub fn value(&self) -> u8 { + self.0.to_integer() as u8 + } + + pub fn raw_value(&self) -> u16 { + *self.0.numer() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub struct FixedPointI8(Ratio); + +impl FixedPointI8 { + pub fn new(val: i8) -> Self { + Self(Ratio::new_raw(val as i16 * 0x100, 0x100)) + } + + pub fn new_raw(val: i16) -> Self { + Self(Ratio::new_raw(val, 0x100)) + } + + pub fn value(&self) -> i8 { + self.0.to_integer() as i8 + } + + pub fn raw_value(&self) -> i16 { + *self.0.numer() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub struct FixedPointU16(Ratio); + +impl FixedPointU16 { + pub fn new(val: u16) -> Self { + Self(Ratio::new_raw(val as u32 * 0x10000, 0x10000)) + } + + pub fn new_raw(val: u32) -> Self { + Self(Ratio::new_raw(val, 0x10000)) + } + + pub fn value(&self) -> u16 { + self.0.to_integer() as u16 + } + + pub fn raw_value(&self) -> u32 { + *self.0.numer() + } +} + +impl fmt::Debug for BoxType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let fourcc: FourCC = From::from(*self); + write!(f, "{fourcc}") + } +} + +impl fmt::Display for BoxType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let fourcc: FourCC = From::from(*self); + write!(f, "{fourcc}") + } +} + +#[derive(Default, PartialEq, Eq, Clone, Copy, Serialize)] +pub struct FourCC { + pub value: [u8; 4], +} + +impl std::str::FromStr for FourCC { + type Err = Error; + + fn from_str(s: &str) -> Result { + if let [a, b, c, d] = s.as_bytes() { + Ok(Self { + value: [*a, *b, *c, *d], + }) + } else { + Err(Error::InvalidData("expected exactly four bytes in string")) + } + } +} + +impl From for FourCC { + fn from(number: u32) -> Self { + FourCC { + value: number.to_be_bytes(), + } + } +} + +impl From for u32 { + fn from(fourcc: FourCC) -> u32 { + (&fourcc).into() + } +} + +impl From<&FourCC> for u32 { + fn from(fourcc: &FourCC) -> u32 { + u32::from_be_bytes(fourcc.value) + } +} + +impl From<[u8; 4]> for FourCC { + fn from(value: [u8; 4]) -> FourCC { + FourCC { value } + } +} + +impl From for FourCC { + fn from(t: BoxType) -> FourCC { + let box_num: u32 = Into::into(t); + From::from(box_num) + } +} + +impl fmt::Debug for FourCC { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let code: u32 = self.into(); + let string = String::from_utf8_lossy(&self.value[..]); + write!(f, "{string} / {code:#010X}") + } +} + +impl fmt::Display for FourCC { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", String::from_utf8_lossy(&self.value[..])) + } +} + +const DISPLAY_TYPE_VIDEO: &str = "Video"; +const DISPLAY_TYPE_AUDIO: &str = "Audio"; +const DISPLAY_TYPE_SUBTITLE: &str = "Subtitle"; + +const HANDLER_TYPE_VIDEO: &str = "vide"; +const HANDLER_TYPE_VIDEO_FOURCC: [u8; 4] = [b'v', b'i', b'd', b'e']; + +const HANDLER_TYPE_AUDIO: &str = "soun"; +const HANDLER_TYPE_AUDIO_FOURCC: [u8; 4] = [b's', b'o', b'u', b'n']; + +const HANDLER_TYPE_SUBTITLE: &str = "sbtl"; +const HANDLER_TYPE_SUBTITLE_FOURCC: [u8; 4] = [b's', b'b', b't', b'l']; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TrackType { + Video, + Audio, + Subtitle, +} + +impl fmt::Display for TrackType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let s = match self { + TrackType::Video => DISPLAY_TYPE_VIDEO, + TrackType::Audio => DISPLAY_TYPE_AUDIO, + TrackType::Subtitle => DISPLAY_TYPE_SUBTITLE, + }; + write!(f, "{s}") + } +} + +impl TryFrom<&str> for TrackType { + type Error = Error; + fn try_from(handler: &str) -> Result { + match handler { + HANDLER_TYPE_VIDEO => Ok(TrackType::Video), + HANDLER_TYPE_AUDIO => Ok(TrackType::Audio), + HANDLER_TYPE_SUBTITLE => Ok(TrackType::Subtitle), + _ => Err(Error::InvalidData("unsupported handler type")), + } + } +} + +impl TryFrom<&FourCC> for TrackType { + type Error = Error; + fn try_from(fourcc: &FourCC) -> Result { + match fourcc.value { + HANDLER_TYPE_VIDEO_FOURCC => Ok(TrackType::Video), + HANDLER_TYPE_AUDIO_FOURCC => Ok(TrackType::Audio), + HANDLER_TYPE_SUBTITLE_FOURCC => Ok(TrackType::Subtitle), + _ => Err(Error::InvalidData("unsupported handler type")), + } + } +} + +impl From for FourCC { + fn from(t: TrackType) -> FourCC { + match t { + TrackType::Video => HANDLER_TYPE_VIDEO_FOURCC.into(), + TrackType::Audio => HANDLER_TYPE_AUDIO_FOURCC.into(), + TrackType::Subtitle => HANDLER_TYPE_SUBTITLE_FOURCC.into(), + } + } +} + +const MEDIA_TYPE_H264: &str = "h264"; +const MEDIA_TYPE_H265: &str = "h265"; +const MEDIA_TYPE_VP9: &str = "vp9"; +const MEDIA_TYPE_AAC: &str = "aac"; +const MEDIA_TYPE_TTXT: &str = "ttxt"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MediaType { + H264, + H265, + VP9, + AAC, + TTXT, +} + +impl fmt::Display for MediaType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let s: &str = self.into(); + write!(f, "{s}") + } +} + +impl TryFrom<&str> for MediaType { + type Error = Error; + fn try_from(media: &str) -> Result { + match media { + MEDIA_TYPE_H264 => Ok(MediaType::H264), + MEDIA_TYPE_H265 => Ok(MediaType::H265), + MEDIA_TYPE_VP9 => Ok(MediaType::VP9), + MEDIA_TYPE_AAC => Ok(MediaType::AAC), + MEDIA_TYPE_TTXT => Ok(MediaType::TTXT), + _ => Err(Error::InvalidData("unsupported media type")), + } + } +} + +impl From for &str { + fn from(t: MediaType) -> &'static str { + match t { + MediaType::H264 => MEDIA_TYPE_H264, + MediaType::H265 => MEDIA_TYPE_H265, + MediaType::VP9 => MEDIA_TYPE_VP9, + MediaType::AAC => MEDIA_TYPE_AAC, + MediaType::TTXT => MEDIA_TYPE_TTXT, + } + } +} + +impl From<&MediaType> for &str { + fn from(t: &MediaType) -> &'static str { + match t { + MediaType::H264 => MEDIA_TYPE_H264, + MediaType::H265 => MEDIA_TYPE_H265, + MediaType::VP9 => MEDIA_TYPE_VP9, + MediaType::AAC => MEDIA_TYPE_AAC, + MediaType::TTXT => MEDIA_TYPE_TTXT, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum AvcProfile { + AvcConstrainedBaseline, // 66 with constraint set 1 + AvcBaseline, // 66, + AvcMain, // 77, + AvcExtended, // 88, + AvcHigh, // 100 + // TODO Progressive High Profile, Constrained High Profile, ... +} + +impl TryFrom<(u8, u8)> for AvcProfile { + type Error = Error; + fn try_from(value: (u8, u8)) -> Result { + let profile = value.0; + let constraint_set1_flag = (value.1 & 0x40) >> 7; + match (profile, constraint_set1_flag) { + (66, 1) => Ok(AvcProfile::AvcConstrainedBaseline), + (66, 0) => Ok(AvcProfile::AvcBaseline), + (77, _) => Ok(AvcProfile::AvcMain), + (88, _) => Ok(AvcProfile::AvcExtended), + (100, _) => Ok(AvcProfile::AvcHigh), + _ => Err(Error::InvalidData("unsupported avc profile")), + } + } +} + +impl fmt::Display for AvcProfile { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let profile = match self { + AvcProfile::AvcConstrainedBaseline => "Constrained Baseline", + AvcProfile::AvcBaseline => "Baseline", + AvcProfile::AvcMain => "Main", + AvcProfile::AvcExtended => "Extended", + AvcProfile::AvcHigh => "High", + }; + write!(f, "{profile}") + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum AudioObjectType { + AacMain = 1, // AAC Main Profile + AacLowComplexity = 2, // AAC Low Complexity + AacScalableSampleRate = 3, // AAC Scalable Sample Rate + AacLongTermPrediction = 4, // AAC Long Term Predictor + SpectralBandReplication = 5, // Spectral band Replication + AACScalable = 6, // AAC Scalable + TwinVQ = 7, // Twin VQ + CodeExcitedLinearPrediction = 8, // CELP + HarmonicVectorExcitationCoding = 9, // HVXC + TextToSpeechtInterface = 12, // TTSI + MainSynthetic = 13, // Main Synthetic + WavetableSynthesis = 14, // Wavetable Synthesis + GeneralMIDI = 15, // General MIDI + AlgorithmicSynthesis = 16, // Algorithmic Synthesis + ErrorResilientAacLowComplexity = 17, // ER AAC LC + ErrorResilientAacLongTermPrediction = 19, // ER AAC LTP + ErrorResilientAacScalable = 20, // ER AAC Scalable + ErrorResilientAacTwinVQ = 21, // ER AAC TwinVQ + ErrorResilientAacBitSlicedArithmeticCoding = 22, // ER Bit Sliced Arithmetic Coding + ErrorResilientAacLowDelay = 23, // ER AAC Low Delay + ErrorResilientCodeExcitedLinearPrediction = 24, // ER CELP + ErrorResilientHarmonicVectorExcitationCoding = 25, // ER HVXC + ErrorResilientHarmonicIndividualLinesNoise = 26, // ER HILN + ErrorResilientParametric = 27, // ER Parametric + SinuSoidalCoding = 28, // SSC + ParametricStereo = 29, // PS + MpegSurround = 30, // MPEG Surround + MpegLayer1 = 32, // MPEG Layer 1 + MpegLayer2 = 33, // MPEG Layer 2 + MpegLayer3 = 34, // MPEG Layer 3 + DirectStreamTransfer = 35, // DST Direct Stream Transfer + AudioLosslessCoding = 36, // ALS Audio Lossless Coding + ScalableLosslessCoding = 37, // SLC Scalable Lossless Coding + ScalableLosslessCodingNoneCore = 38, // SLC non-core + ErrorResilientAacEnhancedLowDelay = 39, // ER AAC ELD + SymbolicMusicRepresentationSimple = 40, // SMR Simple + SymbolicMusicRepresentationMain = 41, // SMR Main + UnifiedSpeechAudioCoding = 42, // USAC + SpatialAudioObjectCoding = 43, // SAOC + LowDelayMpegSurround = 44, // LD MPEG Surround + SpatialAudioObjectCodingDialogueEnhancement = 45, // SAOC-DE + AudioSync = 46, // Audio Sync +} + +impl TryFrom for AudioObjectType { + type Error = Error; + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(AudioObjectType::AacMain), + 2 => Ok(AudioObjectType::AacLowComplexity), + 3 => Ok(AudioObjectType::AacScalableSampleRate), + 4 => Ok(AudioObjectType::AacLongTermPrediction), + 5 => Ok(AudioObjectType::SpectralBandReplication), + 6 => Ok(AudioObjectType::AACScalable), + 7 => Ok(AudioObjectType::TwinVQ), + 8 => Ok(AudioObjectType::CodeExcitedLinearPrediction), + 9 => Ok(AudioObjectType::HarmonicVectorExcitationCoding), + 12 => Ok(AudioObjectType::TextToSpeechtInterface), + 13 => Ok(AudioObjectType::MainSynthetic), + 14 => Ok(AudioObjectType::WavetableSynthesis), + 15 => Ok(AudioObjectType::GeneralMIDI), + 16 => Ok(AudioObjectType::AlgorithmicSynthesis), + 17 => Ok(AudioObjectType::ErrorResilientAacLowComplexity), + 19 => Ok(AudioObjectType::ErrorResilientAacLongTermPrediction), + 20 => Ok(AudioObjectType::ErrorResilientAacScalable), + 21 => Ok(AudioObjectType::ErrorResilientAacTwinVQ), + 22 => Ok(AudioObjectType::ErrorResilientAacBitSlicedArithmeticCoding), + 23 => Ok(AudioObjectType::ErrorResilientAacLowDelay), + 24 => Ok(AudioObjectType::ErrorResilientCodeExcitedLinearPrediction), + 25 => Ok(AudioObjectType::ErrorResilientHarmonicVectorExcitationCoding), + 26 => Ok(AudioObjectType::ErrorResilientHarmonicIndividualLinesNoise), + 27 => Ok(AudioObjectType::ErrorResilientParametric), + 28 => Ok(AudioObjectType::SinuSoidalCoding), + 29 => Ok(AudioObjectType::ParametricStereo), + 30 => Ok(AudioObjectType::MpegSurround), + 32 => Ok(AudioObjectType::MpegLayer1), + 33 => Ok(AudioObjectType::MpegLayer2), + 34 => Ok(AudioObjectType::MpegLayer3), + 35 => Ok(AudioObjectType::DirectStreamTransfer), + 36 => Ok(AudioObjectType::AudioLosslessCoding), + 37 => Ok(AudioObjectType::ScalableLosslessCoding), + 38 => Ok(AudioObjectType::ScalableLosslessCodingNoneCore), + 39 => Ok(AudioObjectType::ErrorResilientAacEnhancedLowDelay), + 40 => Ok(AudioObjectType::SymbolicMusicRepresentationSimple), + 41 => Ok(AudioObjectType::SymbolicMusicRepresentationMain), + 42 => Ok(AudioObjectType::UnifiedSpeechAudioCoding), + 43 => Ok(AudioObjectType::SpatialAudioObjectCoding), + 44 => Ok(AudioObjectType::LowDelayMpegSurround), + 45 => Ok(AudioObjectType::SpatialAudioObjectCodingDialogueEnhancement), + 46 => Ok(AudioObjectType::AudioSync), + _ => Err(Error::InvalidData("invalid audio object type")), + } + } +} + +impl fmt::Display for AudioObjectType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let type_str = match self { + AudioObjectType::AacMain => "AAC Main", + AudioObjectType::AacLowComplexity => "LC", + AudioObjectType::AacScalableSampleRate => "SSR", + AudioObjectType::AacLongTermPrediction => "LTP", + AudioObjectType::SpectralBandReplication => "SBR", + AudioObjectType::AACScalable => "Scalable", + AudioObjectType::TwinVQ => "TwinVQ", + AudioObjectType::CodeExcitedLinearPrediction => "CELP", + AudioObjectType::HarmonicVectorExcitationCoding => "HVXC", + AudioObjectType::TextToSpeechtInterface => "TTSI", + AudioObjectType::MainSynthetic => "Main Synthetic", + AudioObjectType::WavetableSynthesis => "Wavetable Synthesis", + AudioObjectType::GeneralMIDI => "General MIDI", + AudioObjectType::AlgorithmicSynthesis => "Algorithmic Synthesis", + AudioObjectType::ErrorResilientAacLowComplexity => "ER AAC LC", + AudioObjectType::ErrorResilientAacLongTermPrediction => "ER AAC LTP", + AudioObjectType::ErrorResilientAacScalable => "ER AAC scalable", + AudioObjectType::ErrorResilientAacTwinVQ => "ER AAC TwinVQ", + AudioObjectType::ErrorResilientAacBitSlicedArithmeticCoding => "ER AAC BSAC", + AudioObjectType::ErrorResilientAacLowDelay => "ER AAC LD", + AudioObjectType::ErrorResilientCodeExcitedLinearPrediction => "ER CELP", + AudioObjectType::ErrorResilientHarmonicVectorExcitationCoding => "ER HVXC", + AudioObjectType::ErrorResilientHarmonicIndividualLinesNoise => "ER HILN", + AudioObjectType::ErrorResilientParametric => "ER Parametric", + AudioObjectType::SinuSoidalCoding => "SSC", + AudioObjectType::ParametricStereo => "Parametric Stereo", + AudioObjectType::MpegSurround => "MPEG surround", + AudioObjectType::MpegLayer1 => "MPEG Layer 1", + AudioObjectType::MpegLayer2 => "MPEG Layer 2", + AudioObjectType::MpegLayer3 => "MPEG Layer 3", + AudioObjectType::DirectStreamTransfer => "DST", + AudioObjectType::AudioLosslessCoding => "ALS", + AudioObjectType::ScalableLosslessCoding => "SLS", + AudioObjectType::ScalableLosslessCodingNoneCore => "SLS Non-core", + AudioObjectType::ErrorResilientAacEnhancedLowDelay => "ER AAC ELD", + AudioObjectType::SymbolicMusicRepresentationSimple => "SMR Simple", + AudioObjectType::SymbolicMusicRepresentationMain => "SMR Main", + AudioObjectType::UnifiedSpeechAudioCoding => "USAC", + AudioObjectType::SpatialAudioObjectCoding => "SAOC", + AudioObjectType::LowDelayMpegSurround => "LD MPEG Surround", + AudioObjectType::SpatialAudioObjectCodingDialogueEnhancement => "SAOC-DE", + AudioObjectType::AudioSync => "Audio Sync", + }; + write!(f, "{type_str}") + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum SampleFreqIndex { + Freq96000 = 0x0, + Freq88200 = 0x1, + Freq64000 = 0x2, + Freq48000 = 0x3, + Freq44100 = 0x4, + Freq32000 = 0x5, + Freq24000 = 0x6, + Freq22050 = 0x7, + Freq16000 = 0x8, + Freq12000 = 0x9, + Freq11025 = 0xa, + Freq8000 = 0xb, + Freq7350 = 0xc, +} + +impl TryFrom for SampleFreqIndex { + type Error = Error; + fn try_from(value: u8) -> Result { + match value { + 0x0 => Ok(SampleFreqIndex::Freq96000), + 0x1 => Ok(SampleFreqIndex::Freq88200), + 0x2 => Ok(SampleFreqIndex::Freq64000), + 0x3 => Ok(SampleFreqIndex::Freq48000), + 0x4 => Ok(SampleFreqIndex::Freq44100), + 0x5 => Ok(SampleFreqIndex::Freq32000), + 0x6 => Ok(SampleFreqIndex::Freq24000), + 0x7 => Ok(SampleFreqIndex::Freq22050), + 0x8 => Ok(SampleFreqIndex::Freq16000), + 0x9 => Ok(SampleFreqIndex::Freq12000), + 0xa => Ok(SampleFreqIndex::Freq11025), + 0xb => Ok(SampleFreqIndex::Freq8000), + 0xc => Ok(SampleFreqIndex::Freq7350), + _ => Err(Error::InvalidData("invalid sampling frequency index")), + } + } +} + +impl SampleFreqIndex { + pub fn freq(&self) -> u32 { + match *self { + SampleFreqIndex::Freq96000 => 96000, + SampleFreqIndex::Freq88200 => 88200, + SampleFreqIndex::Freq64000 => 64000, + SampleFreqIndex::Freq48000 => 48000, + SampleFreqIndex::Freq44100 => 44100, + SampleFreqIndex::Freq32000 => 32000, + SampleFreqIndex::Freq24000 => 24000, + SampleFreqIndex::Freq22050 => 22050, + SampleFreqIndex::Freq16000 => 16000, + SampleFreqIndex::Freq12000 => 12000, + SampleFreqIndex::Freq11025 => 11025, + SampleFreqIndex::Freq8000 => 8000, + SampleFreqIndex::Freq7350 => 7350, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum ChannelConfig { + Mono = 0x1, + Stereo = 0x2, + Three = 0x3, + Four = 0x4, + Five = 0x5, + FiveOne = 0x6, + SevenOne = 0x7, +} + +impl TryFrom for ChannelConfig { + type Error = Error; + fn try_from(value: u8) -> Result { + match value { + 0x1 => Ok(ChannelConfig::Mono), + 0x2 => Ok(ChannelConfig::Stereo), + 0x3 => Ok(ChannelConfig::Three), + 0x4 => Ok(ChannelConfig::Four), + 0x5 => Ok(ChannelConfig::Five), + 0x6 => Ok(ChannelConfig::FiveOne), + 0x7 => Ok(ChannelConfig::SevenOne), + _ => Err(Error::InvalidData("invalid channel configuration")), + } + } +} + +impl fmt::Display for ChannelConfig { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let s = match self { + ChannelConfig::Mono => "mono", + ChannelConfig::Stereo => "stereo", + ChannelConfig::Three => "three", + ChannelConfig::Four => "four", + ChannelConfig::Five => "five", + ChannelConfig::FiveOne => "five.one", + ChannelConfig::SevenOne => "seven.one", + }; + write!(f, "{s}") + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct AvcConfig { + pub width: u16, + pub height: u16, + pub seq_param_set: Vec, + pub pic_param_set: Vec, +} + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct HevcConfig { + pub width: u16, + pub height: u16, +} + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct Vp9Config { + pub width: u16, + pub height: u16, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct AacConfig { + pub bitrate: u32, + pub profile: AudioObjectType, + pub freq_index: SampleFreqIndex, + pub chan_conf: ChannelConfig, +} + +impl Default for AacConfig { + fn default() -> Self { + Self { + bitrate: 0, + profile: AudioObjectType::AacLowComplexity, + freq_index: SampleFreqIndex::Freq48000, + chan_conf: ChannelConfig::Stereo, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct TtxtConfig {} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum MediaConfig { + AvcConfig(AvcConfig), + HevcConfig(HevcConfig), + Vp9Config(Vp9Config), + AacConfig(AacConfig), + TtxtConfig(TtxtConfig), +} + +#[derive(Debug)] +pub struct Mp4Sample { + pub start_time: u64, + pub duration: u32, + pub rendering_offset: i32, + pub is_sync: bool, + pub bytes: Bytes, +} + +impl PartialEq for Mp4Sample { + fn eq(&self, other: &Self) -> bool { + self.start_time == other.start_time + && self.duration == other.duration + && self.rendering_offset == other.rendering_offset + && self.is_sync == other.is_sync + && self.bytes.len() == other.bytes.len() // XXX for easy check + } +} + +impl fmt::Display for Mp4Sample { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "start_time {}, duration {}, rendering_offset {}, is_sync {}, length {}", + self.start_time, + self.duration, + self.rendering_offset, + self.is_sync, + self.bytes.len() + ) + } +} + +pub fn creation_time(creation_time: u64) -> u64 { + // convert from MP4 epoch (1904-01-01) to Unix epoch (1970-01-01) + if creation_time >= 2082844800 { + creation_time - 2082844800 + } else { + creation_time + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum DataType { + Binary = 0x000000, + Text = 0x000001, + Image = 0x00000D, + TempoCpil = 0x000015, +} + +#[allow(clippy::derivable_impls)] +impl std::default::Default for DataType { + fn default() -> Self { + DataType::Binary + } +} + +impl TryFrom for DataType { + type Error = Error; + fn try_from(value: u32) -> Result { + match value { + 0x000000 => Ok(DataType::Binary), + 0x000001 => Ok(DataType::Text), + 0x00000D => Ok(DataType::Image), + 0x000015 => Ok(DataType::TempoCpil), + _ => Err(Error::InvalidData("invalid data type")), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub enum MetadataKey { + Title, + Year, + Poster, + Summary, +} + +pub trait Metadata<'a> { + /// The video's title + fn title(&self) -> Option>; + /// The video's release year + fn year(&self) -> Option; + /// The video's poster (cover art) + fn poster(&self) -> Option<&[u8]>; + /// The video's summary + fn summary(&self) -> Option>; +} + +impl<'a, T: Metadata<'a>> Metadata<'a> for &'a T { + fn title(&self) -> Option> { + (**self).title() + } + + fn year(&self) -> Option { + (**self).year() + } + + fn poster(&self) -> Option<&[u8]> { + (**self).poster() + } + + fn summary(&self) -> Option> { + (**self).summary() + } +} + +impl<'a, T: Metadata<'a>> Metadata<'a> for Option { + fn title(&self) -> Option> { + self.as_ref().and_then(|t| t.title()) + } + + fn year(&self) -> Option { + self.as_ref().and_then(|t| t.year()) + } + + fn poster(&self) -> Option<&[u8]> { + self.as_ref().and_then(|t| t.poster()) + } + + fn summary(&self) -> Option> { + self.as_ref().and_then(|t| t.summary()) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/src/writer.rs b/repos/moq-rs/third_party/mp4-rust/src/writer.rs new file mode 100644 index 0000000..a83a888 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/src/writer.rs @@ -0,0 +1,149 @@ +use byteorder::{BigEndian, WriteBytesExt}; +use std::io::{Seek, SeekFrom, Write}; + +use crate::mp4box::*; +use crate::track::Mp4TrackWriter; +use crate::*; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Mp4Config { + pub major_brand: FourCC, + pub minor_version: u32, + pub compatible_brands: Vec, + pub timescale: u32, +} + +#[derive(Debug)] +pub struct Mp4Writer { + writer: W, + tracks: Vec, + mdat_pos: u64, + timescale: u32, + duration: u64, +} + +impl Mp4Writer { + /// Consume self, returning the inner writer. + /// + /// This can be useful to recover the inner writer after completion in case + /// it's owned by the [Mp4Writer] instance. + /// + /// # Examples + /// + /// ```rust + /// use mp4::{Mp4Writer, Mp4Config}; + /// use std::io::Cursor; + /// + /// # fn main() -> mp4::Result<()> { + /// let config = Mp4Config { + /// major_brand: str::parse("isom").unwrap(), + /// minor_version: 512, + /// compatible_brands: vec![ + /// str::parse("isom").unwrap(), + /// str::parse("iso2").unwrap(), + /// str::parse("avc1").unwrap(), + /// str::parse("mp41").unwrap(), + /// ], + /// timescale: 1000, + /// }; + /// + /// let data = Cursor::new(Vec::::new()); + /// let mut writer = mp4::Mp4Writer::write_start(data, &config)?; + /// writer.write_end()?; + /// + /// let data: Vec = writer.into_writer().into_inner(); + /// # Ok(()) } + /// ``` + pub fn into_writer(self) -> W { + self.writer + } +} + +impl Mp4Writer { + pub fn write_start(mut writer: W, config: &Mp4Config) -> Result { + let ftyp = FtypBox { + major_brand: config.major_brand, + minor_version: config.minor_version, + compatible_brands: config.compatible_brands.clone(), + }; + ftyp.write_box(&mut writer)?; + + // TODO largesize + let mdat_pos = writer.stream_position()?; + BoxHeader::new(BoxType::MdatBox, HEADER_SIZE).write(&mut writer)?; + BoxHeader::new(BoxType::WideBox, HEADER_SIZE).write(&mut writer)?; + + let tracks = Vec::new(); + let timescale = config.timescale; + let duration = 0; + Ok(Self { + writer, + tracks, + mdat_pos, + timescale, + duration, + }) + } + + pub fn add_track(&mut self, config: &TrackConfig) -> Result<()> { + let track_id = self.tracks.len() as u32 + 1; + let track = Mp4TrackWriter::new(track_id, config)?; + self.tracks.push(track); + Ok(()) + } + + fn update_durations(&mut self, track_dur: u64) { + if track_dur > self.duration { + self.duration = track_dur; + } + } + + pub fn write_sample(&mut self, track_id: u32, sample: &Mp4Sample) -> Result<()> { + if track_id == 0 { + return Err(Error::TrakNotFound(track_id)); + } + + let track_dur = if let Some(ref mut track) = self.tracks.get_mut(track_id as usize - 1) { + track.write_sample(&mut self.writer, sample, self.timescale)? + } else { + return Err(Error::TrakNotFound(track_id)); + }; + + self.update_durations(track_dur); + + Ok(()) + } + + fn update_mdat_size(&mut self) -> Result<()> { + let mdat_end = self.writer.stream_position()?; + let mdat_size = mdat_end - self.mdat_pos; + if mdat_size > std::u32::MAX as u64 { + self.writer.seek(SeekFrom::Start(self.mdat_pos))?; + self.writer.write_u32::(1)?; + self.writer.seek(SeekFrom::Start(self.mdat_pos + 8))?; + self.writer.write_u64::(mdat_size)?; + } else { + self.writer.seek(SeekFrom::Start(self.mdat_pos))?; + self.writer.write_u32::(mdat_size as u32)?; + } + self.writer.seek(SeekFrom::Start(mdat_end))?; + Ok(()) + } + + pub fn write_end(&mut self) -> Result<()> { + let mut moov = MoovBox::default(); + + for track in self.tracks.iter_mut() { + moov.traks.push(track.write_end(&mut self.writer)?); + } + self.update_mdat_size()?; + + moov.mvhd.timescale = self.timescale; + moov.mvhd.duration = self.duration; + if moov.mvhd.duration > (u32::MAX as u64) { + moov.mvhd.version = 1 + } + moov.write_box(&mut self.writer)?; + Ok(()) + } +} diff --git a/repos/moq-rs/third_party/mp4-rust/tests/lib.rs b/repos/moq-rs/third_party/mp4-rust/tests/lib.rs new file mode 100644 index 0000000..7c81f95 --- /dev/null +++ b/repos/moq-rs/third_party/mp4-rust/tests/lib.rs @@ -0,0 +1,211 @@ +use mp4::{ + AudioObjectType, AvcProfile, ChannelConfig, MediaType, Metadata, Mp4Reader, SampleFreqIndex, + TrackType, +}; +use std::fs::{self, File}; +use std::io::BufReader; +use std::time::Duration; + +#[test] +fn test_read_mp4() { + let mut mp4 = get_reader("tests/samples/minimal.mp4"); + + assert_eq!(2591, mp4.size()); + + // ftyp. + assert_eq!(4, mp4.compatible_brands().len()); + + // Check compatible_brands. + let brands = vec![ + String::from("isom"), + String::from("iso2"), + String::from("avc1"), + String::from("mp41"), + ]; + + for b in brands { + let t = mp4.compatible_brands().iter().any(|x| x.to_string() == b); + assert!(t); + } + + assert_eq!(mp4.duration(), Duration::from_millis(62)); + assert_eq!(mp4.timescale(), 1000); + assert_eq!(mp4.tracks().len(), 2); + + let sample_count = mp4.sample_count(1).unwrap(); + assert_eq!(sample_count, 1); + let sample_1_1 = mp4.read_sample(1, 1).unwrap().unwrap(); + assert_eq!(sample_1_1.bytes.len(), 751); + assert_eq!( + sample_1_1, + mp4::Mp4Sample { + start_time: 0, + duration: 512, + rendering_offset: 0, + is_sync: true, + bytes: mp4::Bytes::from(vec![0x0u8; 751]), + } + ); + let eos = mp4.read_sample(1, 2).unwrap(); + assert!(eos.is_none()); + + let sample_count = mp4.sample_count(2).unwrap(); + assert_eq!(sample_count, 3); + let sample_2_1 = mp4.read_sample(2, 1).unwrap().unwrap(); + assert_eq!(sample_2_1.bytes.len(), 179); + assert_eq!( + sample_2_1, + mp4::Mp4Sample { + start_time: 0, + duration: 1024, + rendering_offset: 0, + is_sync: true, + bytes: mp4::Bytes::from(vec![0x0u8; 179]), + } + ); + + let sample_2_2 = mp4.read_sample(2, 2).unwrap().unwrap(); + assert_eq!( + sample_2_2, + mp4::Mp4Sample { + start_time: 1024, + duration: 1024, + rendering_offset: 0, + is_sync: true, + bytes: mp4::Bytes::from(vec![0x0u8; 180]), + } + ); + + let sample_2_3 = mp4.read_sample(2, 3).unwrap().unwrap(); + assert_eq!( + sample_2_3, + mp4::Mp4Sample { + start_time: 2048, + duration: 896, + rendering_offset: 0, + is_sync: true, + bytes: mp4::Bytes::from(vec![0x0u8; 160]), + } + ); + + let eos = mp4.read_sample(2, 4).unwrap(); + assert!(eos.is_none()); + + // track #1 + let track1 = mp4.tracks().get(&1).unwrap(); + assert_eq!(track1.track_id(), 1); + assert_eq!(track1.track_type().unwrap(), TrackType::Video); + assert_eq!(track1.media_type().unwrap(), MediaType::H264); + assert_eq!(track1.video_profile().unwrap(), AvcProfile::AvcHigh); + assert_eq!(track1.width(), 320); + assert_eq!(track1.height(), 240); + assert_eq!(track1.bitrate(), 150200); + assert_eq!(track1.frame_rate(), 25.00); + + // track #2 + let track2 = mp4.tracks().get(&2).unwrap(); + assert_eq!(track2.track_type().unwrap(), TrackType::Audio); + assert_eq!(track2.media_type().unwrap(), MediaType::AAC); + assert_eq!( + track2.audio_profile().unwrap(), + AudioObjectType::AacLowComplexity + ); + assert_eq!( + track2.sample_freq_index().unwrap(), + SampleFreqIndex::Freq48000 + ); + assert_eq!(track2.channel_config().unwrap(), ChannelConfig::Mono); + assert_eq!(track2.bitrate(), 67695); +} + +#[test] +fn test_read_extended_audio_object_type() { + // Extended audio object type and sample rate index of 15 + let mp4 = get_reader("tests/samples/extended_audio_object_type.mp4"); + + let track = mp4.tracks().get(&1).unwrap(); + assert_eq!(track.track_type().unwrap(), TrackType::Audio); + assert_eq!(track.media_type().unwrap(), MediaType::AAC); + assert_eq!( + track.audio_profile().unwrap(), + AudioObjectType::AudioLosslessCoding + ); + assert_eq!( + track + .trak + .mdia + .minf + .stbl + .stsd + .mp4a + .as_ref() + .unwrap() + .esds + .as_ref() + .unwrap() + .es_desc + .dec_config + .dec_specific + .freq_index, + 15 + ); + assert_eq!(track.channel_config().unwrap(), ChannelConfig::Stereo); + assert_eq!(track.bitrate(), 839250); +} + +fn get_reader(path: &str) -> Mp4Reader> { + let f = File::open(path).unwrap(); + let f_size = f.metadata().unwrap().len(); + let reader = BufReader::new(f); + + mp4::Mp4Reader::read_header(reader, f_size).unwrap() +} + +#[test] +fn test_read_metadata() { + let want_poster = fs::read("tests/samples/big_buck_bunny.jpg").unwrap(); + let want_summary = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue."; + let mp4 = get_reader("tests/samples/big_buck_bunny_metadata.m4v"); + let metadata = mp4.metadata(); + assert_eq!(metadata.title(), Some("Big Buck Bunny".into())); + assert_eq!(metadata.year(), Some(2008)); + assert_eq!(metadata.summary(), Some(want_summary.into())); + + assert!(metadata.poster().is_some()); + let poster = metadata.poster().unwrap(); + assert_eq!(poster.len(), want_poster.len()); + assert_eq!(poster, want_poster.as_slice()); +} + +#[test] +fn test_read_fragments() { + let mp4 = get_reader("tests/samples/minimal_init.mp4"); + + assert_eq!(692, mp4.size()); + assert_eq!(5, mp4.compatible_brands().len()); + + let sample_count = mp4.sample_count(1).unwrap(); + assert_eq!(sample_count, 0); + + let f = File::open("tests/samples/minimal_fragment.m4s").unwrap(); + let f_size = f.metadata().unwrap().len(); + let frag_reader = BufReader::new(f); + + let mut mp4_fragment = mp4.read_fragment_header(frag_reader, f_size).unwrap(); + let sample_count = mp4_fragment.sample_count(1).unwrap(); + assert_eq!(sample_count, 1); + let sample_1_1 = mp4_fragment.read_sample(1, 1).unwrap().unwrap(); + assert_eq!(sample_1_1.bytes.len(), 751); + assert_eq!( + sample_1_1, + mp4::Mp4Sample { + start_time: 0, + duration: 512, + rendering_offset: 0, + is_sync: true, + bytes: mp4::Bytes::from(vec![0x0u8; 751]), + } + ); + let eos = mp4_fragment.read_sample(1, 2); + assert!(eos.is_err()); +} diff --git a/repos/moq-rs/third_party/mp4-rust/tests/samples/big_buck_bunny.jpg b/repos/moq-rs/third_party/mp4-rust/tests/samples/big_buck_bunny.jpg new file mode 100644 index 0000000..8c30bc6 Binary files /dev/null and b/repos/moq-rs/third_party/mp4-rust/tests/samples/big_buck_bunny.jpg differ diff --git a/repos/moq-rs/third_party/mp4-rust/tests/samples/big_buck_bunny_metadata.m4v b/repos/moq-rs/third_party/mp4-rust/tests/samples/big_buck_bunny_metadata.m4v new file mode 100644 index 0000000..6002e06 Binary files /dev/null and b/repos/moq-rs/third_party/mp4-rust/tests/samples/big_buck_bunny_metadata.m4v differ diff --git a/repos/moq-rs/third_party/mp4-rust/tests/samples/extended_audio_object_type.mp4 b/repos/moq-rs/third_party/mp4-rust/tests/samples/extended_audio_object_type.mp4 new file mode 100644 index 0000000..3d1a711 Binary files /dev/null and b/repos/moq-rs/third_party/mp4-rust/tests/samples/extended_audio_object_type.mp4 differ diff --git a/repos/moq-rs/third_party/mp4-rust/tests/samples/minimal.mp4 b/repos/moq-rs/third_party/mp4-rust/tests/samples/minimal.mp4 new file mode 100644 index 0000000..9fe1e67 Binary files /dev/null and b/repos/moq-rs/third_party/mp4-rust/tests/samples/minimal.mp4 differ diff --git a/repos/moq-rs/third_party/mp4-rust/tests/samples/minimal_fragment.m4s b/repos/moq-rs/third_party/mp4-rust/tests/samples/minimal_fragment.m4s new file mode 100644 index 0000000..25532bc Binary files /dev/null and b/repos/moq-rs/third_party/mp4-rust/tests/samples/minimal_fragment.m4s differ diff --git a/repos/moq-rs/third_party/mp4-rust/tests/samples/minimal_init.mp4 b/repos/moq-rs/third_party/mp4-rust/tests/samples/minimal_init.mp4 new file mode 100644 index 0000000..fcfe892 Binary files /dev/null and b/repos/moq-rs/third_party/mp4-rust/tests/samples/minimal_init.mp4 differ diff --git a/scripts/cert b/scripts/cert new file mode 100755 index 0000000..399d785 --- /dev/null +++ b/scripts/cert @@ -0,0 +1,18 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" + +# Generate a new RSA key/cert for local development +HOST="localhost" +CRT="$HOST.crt" +KEY="$HOST.key" + +# Install the system certificate if it's not already +# NOTE: The ecdsa flag does nothing but I wish it did +go run filippo.io/mkcert -ecdsa -install + +# Generate a new certificate for localhost +# This fork of mkcert supports the -days flag. +# TODO remove the -days flag when Chrome accepts self-signed certs. +go run filippo.io/mkcert -ecdsa -days 10 -cert-file "$CRT" -key-file "$KEY" localhost 127.0.0.1 ::1 relay demo publish diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100755 index 0000000..3019320 --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,11 @@ +#!/bin/bash +while [ $# -gt 0 ]; do + if [[ $1 == *"--"* ]] && [[ ! -z $2 ]] && [[ $2 != *"--"* ]]; then + param="${1/--/}" + declare $param="$2" + # echo $1 $2 # Optional to see the parameter:value result + shift 2 + else + shift + fi +done diff --git a/scripts/go.mod b/scripts/go.mod new file mode 100644 index 0000000..ac3c3d0 --- /dev/null +++ b/scripts/go.mod @@ -0,0 +1,14 @@ +module github.com/kixelated/warp/cert + +go 1.18 + +require ( + filippo.io/mkcert v1.4.4 // indirect + golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 // indirect + golang.org/x/text v0.3.7 // indirect + howett.net/plist v1.0.0 // indirect + software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect +) + +replace filippo.io/mkcert => github.com/kixelated/mkcert v1.4.4-days diff --git a/scripts/go.sum b/scripts/go.sum new file mode 100644 index 0000000..94fb636 --- /dev/null +++ b/scripts/go.sum @@ -0,0 +1,22 @@ +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/kixelated/mkcert v1.4.4-days h1:T2P9W4ruEfgLHOl5UljPwh0d79FbFWkSe2IONcUBxG8= +github.com/kixelated/mkcert v1.4.4-days/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 h1:yssD99+7tqHWO5Gwh81phT+67hg+KttniBr6UnEXOY8= +golang.org/x/net v0.0.0-20220421235706-1d1ef9303861/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= +software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= diff --git a/scripts/pub-dash.sh b/scripts/pub-dash.sh new file mode 100755 index 0000000..c63b1d6 --- /dev/null +++ b/scripts/pub-dash.sh @@ -0,0 +1,110 @@ +#!/bin/bash +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +source $DIR/common.sh + +# Change directory to the root of the project +cd "$(dirname "$0")/.." + +SERVER="${server:-localhost}" +PORT="${port:-8079}" +FF="${ffmpeg:-ffmpeg}" +TESTSRC="${testsrc:-1}" +USE_HTTPS="${use_https:-0}" +PUT_TIMESTAMP="${put_timestamp:-0}" +FONT_PATH="${font_path:-/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf}" +# N-> millisecond. It may not work. +TS_FORMAT="${ts_format:-%T.%N}" + +if [ "${TESTSRC}" == "1" ]; then + INPUT="-f lavfi -re -i testsrc=s=1280x720:r=30" +else + INPUT="-f avfoundation -framerate 30 -video_size 1280x720 -i 0" +fi + +ID=live +ACODEC=aac +VCODEC=libx264 +COLOR=bt709 +TARGET_LATENCY="1.5" + +# construct video filter +VIDEO_FILTER="null" +if [[ $PUT_TIMESTAMP == "1" ]]; then + if [[ ! -f $FONT_PATH ]]; then + echo "WARNING: Font does not exist at $FONT_PATH. Timestamp embedding will not work!" + else + VIDEO_FILTER="drawtext=fontfile=$FONT_PATH:fontsize=32:box=1:boxborderw=4:boxcolor=black@0.6:fontcolor=white:x=32:y=H-32:text='T0\: %{localtime\:$TS_FORMAT}'[time];" + fi +fi + +# Add bitrates and resolutions from MoQ script +video_size="1280x720" + +res_0="640x360" +res_1="768x432" +res_2="960x540" +res_3="1280x720" + +bitrate_0="360000" +bitrate_1="1100000" +bitrate_2="2000000" +bitrate_3="3000000" + +bufsize_0="720K" +bufsize_1="2.2M" +bufsize_2="4M" +bufsize_3="6M" + +if [ "${USE_HTTPS}" == "1" ]; then + PROTO=https + TLS_KEY=${tls_key:-repos/dash-origin/certs/tls.key} + TLS_CRT=${tls_crt:-repos/dash-origin/certs/cert.crt} + + HTTP_OPTS="-http_opts key_file=${TLS_KEY}:cert_file=${TLS_CRT}:tls_verify=0" +else + PROTO=http + HTTP_OPTS="" +fi + +echo "Ingesting to: ${PROTO}://${SERVER}:${PORT}/${ID}/${ID}.mpd" + +# TODO: What is this for? +# if [ "${TS_OUT}" != "" ]; then +# TS_OUT_FILE="${TS_OUT}/${ID}.ts" +# TS_OUT_CMD="-map 0:v:0 -y ${TS_OUT_FILE}" +# echo "Storing input TS to: ${TS_OUT_FILE}" +# fi + +$FF $INPUT \ + -c:v $VCODEC \ + -b:v:0 $bitrate_0 -s:v:0 $res_0 -minrate:v:0 $bitrate_0 -maxrate:v:0 $bitrate_0 -bufsize:v:0 $bufsize_0 \ + -b:v:1 $bitrate_1 -s:v:1 $res_1 -minrate:v:1 $bitrate_1 -maxrate:v:1 $bitrate_1 -bufsize:v:1 $bufsize_1 \ + -b:v:2 $bitrate_2 -s:v:2 $res_2 -minrate:v:2 $bitrate_2 -maxrate:v:2 $bitrate_2 -bufsize:v:2 $bufsize_2 \ + -b:v:3 $bitrate_3 -s:v:3 $res_3 -minrate:v:3 $bitrate_3 -maxrate:v:3 $bitrate_3 -bufsize:v:3 $bufsize_3 \ + -map 0:v:0 \ + -map 0:v:0 \ + -map 0:v:0 \ + -map 0:v:0 \ + -preset veryfast \ + -use_timeline 0 \ + -utc_timing_url "https://time.akamai.com" \ + -format_options "movflags=cmaf" \ + -frag_type every_frame \ + -adaptation_sets "id=0,streams=0,1,2,3" \ + -streaming 1 \ + -ldash 1 \ + -write_prft 1 \ + -export_side_data prft \ + -g:v 30 -keyint_min:v 30 \ + -sc_threshold:v 0 \ + -tune zerolatency \ + -target_latency ${TARGET_LATENCY} \ + -remove_at_exit 1 \ + -color_primaries ${COLOR} -color_trc ${COLOR} -colorspace ${COLOR} \ + -vf "$VIDEO_FILTER" \ + -f dash \ + ${HTTP_OPTS} \ + ${PROTO}://${SERVER}:${PORT}/${ID}/${ID}.mpd + diff --git a/scripts/pub-moq.sh b/scripts/pub-moq.sh new file mode 100755 index 0000000..04f018a --- /dev/null +++ b/scripts/pub-moq.sh @@ -0,0 +1,132 @@ +#!/bin/bash +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +source $DIR/common.sh + +# Change directory to the root of the project +cd "$(dirname "$0")/.." + +# Use debug logging by default +export RUST_LOG="${RUST_LOG:-debug}" + +ACODEC=aac +VCODEC=libx264 +COLOR=bt709 + +video_size="1280x720" + +FF="${ffmpeg:-ffmpeg}" + +# Decide whether to use testsrc or avfoundation +TESTSRC="${testsrc:-1}" +if [ "${TESTSRC}" == "1" ]; then + INPUT="-f lavfi -re -i testsrc=s=$video_size:r=30" +else + INPUT="-f avfoundation -framerate 30 -video_size $video_size -i '0:none'" +fi + +# Decide whether to use docker or not +USE_DOCKER="${docker:-0}" +if [ "${USE_DOCKER}" == "1" ]; then + ENVS="-e RUST_LOG=${RUST_LOG}" + VOLUMES="-v $(realpath ./repos/moq-rs):/project" + ARGS="--build --name publish-moq --rm -T publish-moq" + + MOQ_EXEC="docker compose run ${ENVS} ${VOLUMES} ${ARGS} moq-pub" + echo "Using docker to run moq-pub with: ${MOQ_EXEC}" + HOST=localhost +else + MOQ_EXEC="cargo run --bin moq-pub --" + cd repos/moq-rs +fi + +# Connect to localhost by default. +HOST="${HOST:-localhost}" +PORT="${PORT:-4443}" +ADDR="${ADDR:-$HOST:$PORT}" +PUT_TIMESTAMP="${put_timestamp:-0}" +FONT_PATH="${font_path:-/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf}" +# timestamp format: T -> time N->millisecond +# use millisecond with caution, it may not work all the time! +TS_FORMAT="${ts_format:-%T.%N}" +# construct video filter +VIDEO_FILTER="null" +if [[ $PUT_TIMESTAMP == "1" ]]; then + if [[ ! -f $FONT_PATH ]]; then + echo "WARNING: Font does not exist at $FONT_PATH. Timestamp embedding will not work!" + else + VIDEO_FILTER="drawtext=fontfile='$FONT_PATH':fontsize=32:box=1:boxborderw=4:boxcolor=black@0.6:fontcolor=white:x=32:y=H-32:text='T0\: %{localtime\:$ts_format}'[time];" + echo "VIDEO_FILTER: $VIDEO_FILTER" + fi +fi + +# Generate a random 16 character name by default. +#NAME="${NAME:-$(head /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' | head -c 16)}" + +# JK use the name "dev" instead +# TODO use that random name if the host is not localhost +NAME="${name:-dev}" + +# Combine the host and name into a URL. +URL="${URL:-"https://$ADDR/$NAME"}" + +res_0="640x360" +res_1="768x432" +res_2="960x540" +res_3="1280x720" + +bitrate_0="360000" +bitrate_1="1100000" +bitrate_2="2000000" +bitrate_3="3000000" + +bufsize_0="720K" +bufsize_1="2.2M" +bufsize_2="4M" +bufsize_3="6M" + +bitrates="${bitrate_0},${bitrate_1},${bitrate_2},${bitrate_3}" + +# for higher quality, use greater GOP size. But this time switching happens more slowly. +gop_size=30 + +execute() { + $FF -hide_banner -probesize 10M $INPUT -an -fflags nobuffer \ + -f mp4 -c $VCODEC -movflags cmaf+separate_moof+delay_moov+skip_trailer -x264-params "nal-hrd=cbr" \ + -map 0 -map 0 -map 0 -map 0 \ + -s:v:0 ${res_0} -b:v:0 ${bitrate_0} -minrate ${bitrate_0} -maxrate ${bitrate_0} -bufsize ${bufsize_0} \ + -s:v:1 ${res_1} -b:v:1 ${bitrate_1} -minrate ${bitrate_1} -maxrate ${bitrate_1} -bufsize ${bufsize_1} \ + -s:v:2 ${res_2} -b:v:2 ${bitrate_2} -minrate ${bitrate_2} -maxrate ${bitrate_2} -bufsize ${bufsize_2} \ + -s:v:3 ${res_3} -b:v:3 ${bitrate_3} -minrate ${bitrate_3} -maxrate ${bitrate_3} -bufsize ${bufsize_3} \ + -write_prft wallclock \ + -vf "$VIDEO_FILTER" \ + -g:v $gop_size -keyint_min:v $gop_size -sc_threshold:v 0 -streaming 1 -tune zerolatency \ + -color_primaries ${COLOR} -color_trc ${COLOR} -colorspace ${COLOR} \ + -frag_type duration -frag_duration 1 - | $MOQ_EXEC "$URL" --bitrates $bitrates --tls-disable-verify +} + +# Signal handler to stop the stream +terminate() { + echo "Stopping stream..." + kill $PID + + # Kill the docker container if it was used + if [ "${USE_DOCKER}" == "1" ]; then + docker kill publish-moq + fi + + # Exit the script + exit 0 +} + +trap 'terminate' INT TERM + +#while true; do + # Start the stream + execute + PID=$! + + # Wait for the stream to finish +# wait $PID || true +#done