diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index c9aa7e2bfe..0000000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,73 +0,0 @@ -# Contributing to mediasoup - -Thanks for taking the time to contribute to mediasoup! 🎉👍 - - -## License - -By contributing to mediasoup, you agree that your contributions will be licensed under its ISC License. - - -## Reporting Bugs - -We primarily use GitHub as an issue tracker. Just open an issue in GitHub if you have encountered a bug in mediasoup. - -If you have questions or doubts about mediasoup or need support, please use the mediasoup Discourse Group instead: - -* https://mediasoup.discourse.group - -If you got a crash in mediasoup, please try to provide a core dump into the issue report: - -* https://mediasoup.org/support/#crashes-in-mediasoup-get-a-core-dump - - -## Pull Request Process - -When creating a Pull Request for mediasoup, ensure that you run the following commands to verify that the code in your PR conforms to the code syntax of the project and does not break existing funtionality: - -* `npm run lint`: Check JavaScript and C++ linting rules. -* `npm run typescript:build`: Compile TypeScript code (under `src/` folder) into JavaScript code (under `lib/` folder). -* `npm run test`: Run JavaScript and C++ test units. - -The full list of `npm` scripts (and `make` tasks) is available in the [doc/Building.md](/doc/Building.md) file. - -Once all these commands succeed, wait for the Travis CI checks to complete and verify they run successfully (otherwise the PR won't be accepted). - - -## Coding Style - -In adition to automatic checks performed by commands above, we also enforce other minor things related to coding style: - -### Comments in JavaScript and C++ - -We use `//` for inline comments in both JavaScript and C++ source files. - -* Comments must start with upercase letter. -* Comments must not exceed 80 columns (split into different lines if necessary). -* Comments must end with a dot. - -Example (good): - -```js -// Calculate foo based on bar value. -const foo = bar / 2; -``` - -Example (bad): - -```js -// calculate foo based on bar value -const foo = bar / 2; -``` - -When adding inline documentation for methods or functions, we use `/** */` syntax. Example: - -```js -/** - * Calculates current score for foo and bar. - */ -function calculateScore(): number -{ - // [...] -} -``` diff --git a/.github/ISSUE_TEMPLATE/Bug_Report.md b/.github/ISSUE_TEMPLATE/Bug_Report.md index 590d6de3eb..1abdb77562 100644 --- a/.github/ISSUE_TEMPLATE/Bug_Report.md +++ b/.github/ISSUE_TEMPLATE/Bug_Report.md @@ -16,7 +16,6 @@ If you got a crash in mediasoup, please try to provide a core dump into the issu https://mediasoup.org/support/#crashes-in-mediasoup-get-a-core-dump - ### Your environment - Operating system: @@ -26,5 +25,4 @@ https://mediasoup.org/support/#crashes-in-mediasoup-get-a-core-dump - mediasoup version: - mediasoup-client version: - ### Issue description diff --git a/.github/ISSUE_TEMPLATE/Support_Question.md b/.github/ISSUE_TEMPLATE/Support_Question.md deleted file mode 100644 index fe989f59fd..0000000000 --- a/.github/ISSUE_TEMPLATE/Support_Question.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: 🙈 Support Question -about: If you have any questions use the mediasoup Discourse Group ---- - -**IMPORTANT:** We primarily use GitHub as an issue tracker. Please, use the mediasoup Discourse Group if you have questions or doubts or if you need support about mediasoup and its ecosystem: - -https://mediasoup.discourse.group - -Before asking any questions, please check the mediasoup official documentation: - -https://mediasoup.org/documentation/ diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..415576c32d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +blank_issues_enabled: true +contact_links: + - name: 🙈 Support Question + url: https://mediasoup.discourse.group + about: | + We primarily use GitHub as an issue tracker. Please, use the mediasoup Discourse Group if you have questions or doubts or if you need support about mediasoup and its ecosystem. + Before asking any questions, please check the mediasoup official documentation at https://mediasoup.org/documentation diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml new file mode 100644 index 0000000000..331661b59c --- /dev/null +++ b/.github/workflows/codeql.yaml @@ -0,0 +1,89 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: CodeQL + +on: [pull_request, workflow_dispatch] + +concurrency: + # Cancel a currently running workflow from the same PR, branch or tag when a + # new workflow is triggered. + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + timeout-minutes: 360 + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ['c-cpp', 'javascript-typescript', 'python'] + # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] + # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both. + # Use only 'javascript-typescript' to analyze code written in JavaScript, + # TypeScript or both. + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support. + + env: + MEDIASOUP_SKIP_WORKER_PREBUILT_DOWNLOAD: 'true' + MEDIASOUP_LOCAL_DEV: 'true' + MEDIASOUP_BUILDTYPE: 'Release' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a + # config file. By default, queries listed here will override any + # specified in a config file. Prefix the list here with "+" to use + # these queries and those in the config file. + # + # Details on CodeQL's query packs refer to: + # https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + # - name: Autobuild + # uses: github/codeql-action/autobuild@v3 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following + # three lines. Modify them (or add more) to build your code if your + # project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + # Use npm ci to build mediasoup Node and worker instead of relying on + # Autobuild. + - name: npm ci + run: npm ci --foreground-scripts + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: '/language:${{matrix.language}}' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 9ec056a646..0000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,74 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ "v3" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "v3" ] - schedule: - - cron: '37 9 * * 5' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'cpp', 'javascript', 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/mediasoup-node.yaml b/.github/workflows/mediasoup-node.yaml index 6757c20211..d9e0e634f6 100644 --- a/.github/workflows/mediasoup-node.yaml +++ b/.github/workflows/mediasoup-node.yaml @@ -1,41 +1,106 @@ name: mediasoup-node -on: [push, pull_request] +on: [pull_request, workflow_dispatch] + +concurrency: + # Cancel a currently running workflow from the same PR, branch or tag when a + # new workflow is triggered. + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: ci: strategy: matrix: - # Different Node versions on Ubuntu, the latest Node on other platforms - ci: + build: + - os: ubuntu-20.04 + node: 18 + cc: gcc + cxx: g++ - os: ubuntu-22.04 - node: 16 + node: 20 + cc: gcc + cxx: g++ - os: ubuntu-22.04 + node: 22 + cc: gcc + cxx: g++ + - os: ubuntu-24.04 + node: 22 + cc: gcc + cxx: g++ + meson_args: '-Db_sanitize=address' + - os: ubuntu-24.04 + node: 22 + cc: clang + cxx: clang++ + meson_args: '-Db_sanitize=undefined' + - os: ubuntu-24.04 + node: 22 + cc: gcc + cxx: g++ + meson_args: '-Db_sanitize=thread' + - os: macos-13 node: 18 - - os: macos-12 - node: 18 + cc: clang + cxx: clang++ + - os: macos-14 + node: 20 + cc: clang + cxx: clang++ + - os: macos-15 + node: 22 + cc: clang + cxx: clang++ - os: windows-2022 - node: 18 - runs-on: ${{ matrix.ci.os }} + node: 20 + cc: cl + cxx: cl + - os: windows-2022 + node: 22 + cc: cl + cxx: cl + build-type: + - Release + - Debug + + runs-on: ${{ matrix.build.os }} + + env: + CC: ${{ matrix.build.cc }} + CXX: ${{ matrix.build.cxx }} + MEDIASOUP_SKIP_WORKER_PREBUILT_DOWNLOAD: 'true' + MEDIASOUP_LOCAL_DEV: 'true' + MEDIASOUP_BUILDTYPE: ${{ matrix.build-type }} + MESON_ARGS: ${{ matrix.build.meson_args }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: - node-version: ${{ matrix.ci.node }} + node-version: ${{ matrix.build.node }} - name: Configure cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ~/.npm - key: ${{ matrix.ci.os }}-node-${{ hashFiles('**/package.json') }} + key: ${{ matrix.build.os }}-node-${{ hashFiles('**/package.json') }} restore-keys: | - ${{ matrix.ci.os }}-node- + ${{ matrix.build.os }}-node- + + - name: npm ci + run: npm ci --foreground-scripts + env: + # Disable leak detection because it's detected by the tool flatc uses + # to build. + ASAN_OPTIONS: 'detect_leaks=0' + + - name: npm run lint:node + run: npm run lint:node - - run: npm ci - - run: npm run lint:node - - run: npm run test:node + - name: npm run test:node + run: npm run test:node diff --git a/.github/workflows/mediasoup-rust.yaml b/.github/workflows/mediasoup-rust.yaml index 1cf1584694..cf0db637d5 100644 --- a/.github/workflows/mediasoup-rust.yaml +++ b/.github/workflows/mediasoup-rust.yaml @@ -1,6 +1,12 @@ name: mediasoup-rust -on: [push, pull_request] +on: [pull_request, workflow_dispatch] + +concurrency: + # Cancel a currently running workflow from the same PR, branch or tag when a + # new workflow is triggered. + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true env: CARGO_TERM_COLOR: always @@ -9,42 +15,47 @@ jobs: ci: strategy: matrix: - os: - - ubuntu-20.04 - - ubuntu-22.04 - - macos-12 - - windows-2022 + build: + - os: ubuntu-20.04 + - os: ubuntu-22.04 + - os: ubuntu-24.04 + - os: macos-13 + - os: macos-14 + - os: macos-15 + - os: windows-2022 + + runs-on: ${{ matrix.build.os }} - runs-on: ${{ matrix.os }} + env: + KEEP_BUILD_ARTIFACTS: '1' steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Configure cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ~/.cargo/registry ~/.cargo/git - key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} + key: ${{ matrix.build.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} - name: cargo fmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + run: cargo fmt --all -- --check - name: cargo clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all-targets -- -D warnings + run: cargo clippy --all-targets -- -D warnings + # NOTE: In Windows this will build and test libmediasoupworker in release + # mode twice since build.rs doesn't allow debug mode on Windows. - name: cargo test - uses: actions-rs/cargo@v1 + run: | + cargo test --verbose + cargo test --release --verbose + + - name: cargo doc + run: cargo doc --locked --all --no-deps --lib env: - KEEP_BUILD_ARTIFACTS: '1' - with: - command: test - args: --verbose + DOCS_RS: '1' + RUSTDOCFLAGS: '-D rustdoc::broken-intra-doc-links -D rustdoc::private_intra_doc_links' diff --git a/.github/workflows/mediasoup-worker-fuzzer.yaml b/.github/workflows/mediasoup-worker-fuzzer.yaml new file mode 100644 index 0000000000..c748ce6e67 --- /dev/null +++ b/.github/workflows/mediasoup-worker-fuzzer.yaml @@ -0,0 +1,53 @@ +name: mediasoup-worker-fuzzer + +on: [pull_request, workflow_dispatch] + +concurrency: + # Cancel a currently running workflow from the same PR, branch or tag when a + # new workflow is triggered. + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + ci: + strategy: + matrix: + build: + - os: ubuntu-24.04 + cc: clang + cxx: clang++ + pip-break-system-packages: true + build-type: + - Release + - Debug + + runs-on: ${{ matrix.build.os }} + + env: + CC: ${{ matrix.build.cc }} + CXX: ${{ matrix.build.cxx }} + MEDIASOUP_SKIP_WORKER_PREBUILT_DOWNLOAD: 'true' + MEDIASOUP_LOCAL_DEV: 'true' + MEDIASOUP_BUILDTYPE: ${{ matrix.build-type }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # We need to install pip invoke manually. + - if: ${{ !matrix.build.pip-break-system-packages }} + name: pip3 install invoke + run: pip3 install invoke + + # In modern OSs we need to run pip with this option. + - if: ${{ matrix.build.pip-break-system-packages }} + name: pip3 install --break-system-packages invoke + run: pip3 install --break-system-packages invoke + + # Build the mediasoup-worker-fuzzer binary (which uses libFuzzer). + - name: invoke -r worker fuzzer + run: invoke -r worker fuzzer + + # Run mediasoup-worker-fuzzer for 5 minutes. + - name: run-fuzzer.sh 300 + run: cd worker && ./scripts/run-fuzzer.sh 300 diff --git a/.github/workflows/mediasoup-worker-prebuild.yaml b/.github/workflows/mediasoup-worker-prebuild.yaml new file mode 100644 index 0000000000..49d260a811 --- /dev/null +++ b/.github/workflows/mediasoup-worker-prebuild.yaml @@ -0,0 +1,75 @@ +name: mediasoup-worker-prebuild + +# Only trigger on GitHub releases. +on: + release: + types: [published] + +jobs: + ci: + strategy: + matrix: + build: + # Worker prebuild for Linux with kernel version 5 Ubuntu (20.04). + # Let's use an old version of Ubuntu (20.04) that builds the + # mediasoup-worker binary using an old version of GLib, so it will work + # on Linux hosts running more modern GLib versions. + # See https://github.com/versatica/mediasoup/issues/1089. + - os: ubuntu-20.04 + cc: gcc + cxx: g++ + # Worker prebuild for Linux with kernel version 6 Ubuntu (22.04). + # Let's not use Ubutu 24.04 to avoid same potential problem as described + # above. + - os: ubuntu-22.04 + cc: gcc + cxx: g++ + - os: macos-13 + cc: clang + cxx: clang++ + - os: macos-14 + cc: clang + cxx: clang++ + - os: macos-15 + cc: clang + cxx: clang++ + - os: windows-2022 + cc: cl + cxx: cl + node: + - 22 + + runs-on: ${{ matrix.build.os }} + + env: + CC: ${{ matrix.build.cc }} + CXX: ${{ matrix.build.cxx }} + MEDIASOUP_SKIP_WORKER_PREBUILT_DOWNLOAD: 'true' + MEDIASOUP_LOCAL_DEV: 'true' + MEDIASOUP_BUILDTYPE: 'Release' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + # We need to install some NPM production deps for npm-scripts.mjs to + # work. + - name: npm ci --ignore-scripts + run: npm ci --ignore-scripts --omit=dev --foreground-scripts + + - name: npm run worker:build + run: npm run worker:build + + # Publish prebuild binaries on tag. + - name: npm run worker:prebuild + run: npm run worker:prebuild + + - name: Upload mediasoup-worker prebuilt binary + uses: softprops/action-gh-release@v1 + with: + files: worker/prebuild/mediasoup-worker-*.tgz diff --git a/.github/workflows/mediasoup-worker.yaml b/.github/workflows/mediasoup-worker.yaml index c879093f62..78c7ddde6d 100644 --- a/.github/workflows/mediasoup-worker.yaml +++ b/.github/workflows/mediasoup-worker.yaml @@ -1,68 +1,121 @@ name: mediasoup-worker -on: [push, pull_request] +on: [pull_request, workflow_dispatch] + +concurrency: + # Cancel a currently running workflow from the same PR, branch or tag when a + # new workflow is triggered. + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: ci: strategy: + # Here we want to see all errors, not just the first one. fail-fast: false matrix: build: - - os: ubuntu-18.04 - cc: gcc - cxx: g++ - - os: ubuntu-18.04 - cc: clang - cxx: clang++ - os: ubuntu-20.04 cc: gcc cxx: g++ + # Workaround for this issue in Ubunt 20.04: + # https://github.com/versatica/mediasoup/actions/runs/9992113733/job/27616379442?pr=1427 + workaround-ubuntu-20-04: true + run-test-asan-address: true + # Skip test-asan-undefined in Linux with GCC due to a bug in GCC + # that affects abseil-cpp: + # https://github.com/abseil/abseil-cpp/issues/1634 + run-test-asan-undefined: false + run-test-asan-thread: true - os: ubuntu-20.04 cc: clang cxx: clang++ + workaround-ubuntu-20-04: true + run-test-asan-address: true + run-test-asan-undefined: true + run-test-asan-thread: true - os: ubuntu-22.04 cc: gcc cxx: g++ + run-test-asan-address: true + run-test-asan-undefined: false + run-test-asan-thread: true - os: ubuntu-22.04 cc: clang cxx: clang++ - - os: macos-12 + run-test-asan-address: true + run-test-asan-undefined: true + run-test-asan-thread: true + - os: ubuntu-24.04 cc: gcc cxx: g++ - - os: macos-12 + pip-break-system-packages: true + run-test-asan-address: true + run-test-asan-undefined: false + run-test-asan-thread: true + - os: ubuntu-24.04 + cc: clang + cxx: clang++ + pip-break-system-packages: true + run-test-asan-address: true + run-test-asan-undefined: true + run-test-asan-thread: true + - os: macos-13 + cc: gcc + cxx: g++ + pip-break-system-packages: true + # Address Sanitizer does not work on MacOS. + run-test-asan-address: false + run-test-asan-undefined: false + run-test-asan-thread: false + - os: macos-14 + cc: clang + cxx: clang++ + pip-break-system-packages: true + run-test-asan-address: false + run-test-asan-undefined: false + run-test-asan-thread: false + - os: macos-15 cc: clang cxx: clang++ + pip-break-system-packages: true + run-test-asan-address: false + run-test-asan-undefined: false + run-test-asan-thread: false - os: windows-2022 cc: cl cxx: cl + # Address Sanitizer does not work on Windows. + run-test-asan-address: false + run-test-asan-undefined: false + run-test-asan-thread: false # A single Node.js version should be fine for C++. node: - - 16 + - 22 + build-type: + - Release + - Debug runs-on: ${{ matrix.build.os }} env: CC: ${{ matrix.build.cc }} CXX: ${{ matrix.build.cxx }} + MEDIASOUP_SKIP_WORKER_PREBUILT_DOWNLOAD: 'true' + MEDIASOUP_LOCAL_DEV: 'false' + MEDIASOUP_BUILDTYPE: ${{ matrix.build-type }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - run: npm run install-clang-tools - # TODO: Maybe fix this one day. - if: runner.os != 'Windows' - - run: npm run lint:worker - # TODO: Maybe fix this one day. - if: runner.os != 'Windows' - - name: Configure cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ~/.npm @@ -70,6 +123,51 @@ jobs: restore-keys: | ${{ matrix.build.os }}-node-${{matrix.build.cc}}- - - run: npm run worker:build - - run: npm run test:worker - if: runner.os != 'Windows' + # We need to install pip invoke manually. + - if: ${{ !matrix.build.pip-break-system-packages }} + name: pip3 install invoke + run: pip3 install invoke + + # In modern OSs we need to run pip with this option. + - if: ${{ matrix.build.pip-break-system-packages }} + name: pip3 install --break-system-packages invoke + run: pip3 install --break-system-packages invoke + + # We need to install npm deps of worker/scripts/package.json. + - if: runner.os != 'Windows' + name: npm ci --prefix worker/scripts + run: npm ci --prefix worker/scripts --foreground-scripts + + # Workaround for this issue in Ubuntu 20.04: + # https://github.com/versatica/mediasoup/actions/runs/9992113733/job/27616379442?pr=1427 + - if: ${{ matrix.build.workaround-ubuntu-20-04 }} + name: workaround for Ubuntu 20.04 + run: sudo apt install -y libgcc-10-dev && sudo ln -s /usr/lib/gcc/x86_64-linux-gnu/10/libtsan_preinit.o /usr/lib/libtsan_preinit.o + + # NOTE: Maybe make it work on Windows someday. + - if: runner.os != 'Windows' + name: invoke -r worker lint + run: invoke -r worker lint + + - name: invoke -r worker mediasoup-worker + run: invoke -r worker mediasoup-worker + + # NOTE: Maybe make it work on Windows someday. + - if: runner.os != 'Windows' + name: invoke -r worker test + run: invoke -r worker test + + # Let's clean everything before rebuilding worker tests with ASAN. + - if: ${{ matrix.build.run-test-asan-address }} + name: invoke -r worker test-asan-address + run: invoke -r worker clean-all && invoke -r worker test-asan-address + + # Let's clean everything before rebuilding worker tests with ASAN. + - if: ${{ matrix.build.run-test-asan-undefined }} + name: invoke -r worker test-asan-undefined + run: invoke -r worker clean-all && invoke -r worker test-asan-undefined + + # Let's clean everything before rebuilding worker tests with ASAN. + - if: ${{ matrix.build.run-test-asan-thread }} + name: invoke -r worker test-asan-thread + run: invoke -r worker clean-all && invoke -r worker test-asan-thread diff --git a/.github/workflows/pull-request-stats.yml b/.github/workflows/pull-request-stats.yml deleted file mode 100644 index bbc6a41f3a..0000000000 --- a/.github/workflows/pull-request-stats.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Pull Request Stats - -on: - pull_request: - types: [opened] - schedule: - - cron: "0 13 * * 1" # Run it every Monday at 1PM UTC. - -jobs: - stats: - runs-on: ubuntu-latest - steps: - - name: Run pull request stats - uses: flowwer-dev/pull-request-stats@master - with: - period: 365 - charts: true - disable-links: false - sort-by: 'REVIEWS' - telemetry: false diff --git a/.gitignore b/.gitignore index abfd211219..e2f65fca29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,30 @@ ## Meson. +/worker/out /worker/subprojects/* !/worker/subprojects/*.wrap +!/worker/subprojects/.clang-tidy ## Node. /node_modules /node/lib +# flatc generated files. +/node/src/fbs ## Rust. -/Cargo.lock /rust/examples-frontend/*/node_modules /rust/examples-frontend/*/package-lock.json /target ## Worker. -/worker/out /worker/scripts/node_modules -# Vistual Studio generated Stuff. +# Flatc generated files. +/worker/include/FBS +/worker/prebuild +# Python invoke. +/worker/pip_invoke +# Build artifacts. /worker/**/Debug /worker/**/Release -/worker/.vs # clang-fuzzer stuff is too big. /worker/deps/clang-fuzzer # Ignore all fuzzer generated test inputs. @@ -28,11 +34,5 @@ ## Others. /coverage /.cache -/NO_GIT -*.swp -*.swo -.DS_Store -# Vistual Studio Code stuff. -/.vscode -# JetBrains IDE stuff. -/.idea +# Packaged module. +*.tgz diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000000..a824e54211 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "useTabs": true, + "tabWidth": 2, + "arrowParens": "avoid", + "bracketSpacing": true, + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "endOfLine": "auto" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 80818464cc..e549f4b45e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,1517 +1,1603 @@ # Changelog +### NEXT -### 3.11.8 +### 3.15.2 -* `SimulcastConsumer::GetDesiredBitrate()`: Choose the highest bitrate among all Producer streams ([PR #992](https://github.com/versatica/mediasoup/pull/992)). -* `SimulcastConsumer`: Fix frozen video when syncing keyframe is discarded due to too high RTP timestamp extra offset needed ([PR #999](https://github.com/versatica/mediasoup/pull/999), thanks to @satoren for properly reporting the issue and helping with the solution). -* Update NPM deps. +- `Worker`: Fix crash when using colliding `portRange` values in different transports ([PR #1469](https://github.com/versatica/mediasoup/pull/1469)). +### 3.15.1 -### 3.11.7 +- Expose `extras` namespace which exports `EnhancedEventEmitter` and `enhancedOnce()` for now ([PR #1464](https://github.com/versatica/mediasoup/pull/1464)). -* libwebrtc: Fix crash due to invalid `arrival_time` value ([PR #985](https://github.com/versatica/mediasoup/pull/985) by @ggarber). -* libwebrtc: Replace `MS_ASSERT()` with `MS_ERROR()` ([PR #988](https://github.com/versatica/mediasoup/pull/988)). -* Update NPM deps. +### 3.15.0 +- Node: Add TypeScript interfaces for all exported classes ([PR #1463](https://github.com/versatica/mediasoup/pull/1463)). +- Node: Add new `transport.type` getter than returns `'webrtc' | 'plain' | 'pipe' | 'direct'` ([PR #1463](https://github.com/versatica/mediasoup/pull/1463)). +- Node: Add new `rtpObserver.type` getter than returns `'activespeaker' | 'audiolevel'` ([PR #1463](https://github.com/versatica/mediasoup/pull/1463)). -### 3.11.6 +### 3.14.16 -* Fix wrong `PictureID` rolling over in VP9 and VP8 ([PR #984](https://github.com/versatica/mediasoup/pull/984) by @jcague). -* Update NPM deps. +- `SimulcastConsumer`: Fix cannot switch layers if initial `tsReferenceSpatialLayer disappears` disappears ([PR #1459](https://github.com/versatica/mediasoup/pull/1459) by @Lynnworld). +### 3.14.15 -### 3.11.5 +- Update worker abseil-cpp dependency to 20240722.0 LTS (fixes compilation for FreeBSD systems) ([PR #1457](https://github.com/versatica/mediasoup/pull/1457), credits to @garrettboone). -* Require Node.js >= 16 ([PR #973](https://github.com/versatica/mediasoup/pull/973)). -* Fix wrong `Consumer` bandwidth estimation under `Producer` packet loss ([PR #962](https://github.com/versatica/mediasoup/pull/962) by @ggarber). -* Update NPM deps. +### 3.14.14 +- Sign self generated DTLS certificate with SHA256 ([PR #1450](https://github.com/versatica/mediasoup/pull/1450)). +- Node: Fix `mediasoup.types` exported types are empty ([PR #1453](https://github.com/versatica/mediasoup/pull/1453)). -### 3.11.4 +### 3.14.13 -* Node: Migrate tests to TypeScript ([PR #958](https://github.com/versatica/mediasoup/pull/958)). -* Node: Remove compiled JavaScript from repository and compile TypeScript code on NPM `prepare` script on demand when installed via git ([PR #954](https://github.com/versatica/mediasoup/pull/954)). -* `Worker`: Add `RTC::Shared` singleton for RTC entities ([PR #953](https://github.com/versatica/mediasoup/pull/953)). -* Update OpenSSL to 3.0.7. -* Update NPM deps. +- Node: Fix regression in exported `mediasoup.types` (classes are now exported as classes instead of types). +### 3.14.12 -### 3.11.3 +- `Worker`: Fix `io_uring` support detection ([PR #1445](https://github.com/versatica/mediasoup/pull/1445)). +- Mitigate libsrtp wraparound with loss decryption failure ([PR #1438](https://github.com/versatica/mediasoup/pull/1438)). +- Node: New `setLogEventListeners()` utility to get log events ([PR #1448](https://github.com/versatica/mediasoup/pull/1448)). -* `ChannelMessageHandlers`: Make `RegisterHandler()` not remove the existing handler if another one with same `id` is given ([PR #952](https://github.com/versatica/mediasoup/pull/952)). +### 3.14.11 +- `Worker`: Fix `disableLiburing` option in `WorkerSettings` ([PR #1444](https://github.com/versatica/mediasoup/pull/1444)). -### 3.11.2 +### 3.14.10 -* Fix installation issue in Linux due to a bug in ninja latest version 1.11.1 ([PR #948](https://github.com/versatica/mediasoup/pull/948)). +- CI: Support Node 22 ([PR #1434](https://github.com/versatica/mediasoup/pull/1434)). +- Update ESLint to version 9 ([PR #1435](https://github.com/versatica/mediasoup/pull/1435)). +- `Worker`: Add `disableLiburing` boolean option (`false` by default) to disable `io_uring` even if it's supported by the prebuilt `mediasoup-worker` and by current host ([PR #1442](https://github.com/versatica/mediasoup/pull/1442)). +### 3.14.9 -### 3.11.1 +- Worker: Test, fix buffer overflow ([PR #1419](https://github.com/versatica/mediasoup/pull/1419)). +- Bump up Meson from 1.3.0 to 1.5.0 ([PR #1424](https://github.com/versatica/mediasoup/pull/1424)). +- Node: Export new `WorkerObserver`, `ProducerObserver`, etc. TypeScript types ([PR #1430](https://github.com/versatica/mediasoup/pull/1430)). +- Fix frozen video in simulcast due to wrong dropping of padding only packets ([PR #1431](https://github.com/versatica/mediasoup/pull/1431), thanks to @quanli168). -* `ActiveSpeakerObserver`: Revert 'dominantspeaker' event changes in [PR #941](https://github.com/versatica/mediasoup/pull/941) to avoid breaking changes ([PR #947](https://github.com/versatica/mediasoup/pull/947)). +### 3.14.8 +- Add support for 'playout-delay' RTP extension ([PR #1412](https://github.com/versatica/mediasoup/pull/1412) by @DavidNegro). -### 3.11.0 +### 3.14.7 -* `Transport`: Remove duplicate call to method ([PR #931](https://github.com/versatica/mediasoup/pull/931)). -* RTCP: Adjust maximum compound packet size ([PR #934](https://github.com/versatica/mediasoup/pull/934)). -* `DataConsumer`: Fix `bufferedAmount` type to be a number again ([PR #936](https://github.com/versatica/mediasoup/pull/936)). -* `ActiveSpeakerObserver`: Fix 'dominantspeaker' event by having a single `Producer` as argument rather than an array with a single `Producer` into it ([PR #941](https://github.com/versatica/mediasoup/pull/941)). -* `ActiveSpeakerObserver`: Fix memory leak ([PR #942](https://github.com/versatica/mediasoup/pull/942)). -* Fix some libwebrtc issues ([PR #944](https://github.com/versatica/mediasoup/pull/944)). -* Tests: Normalize hexadecimal data representation ([PR #945](https://github.com/versatica/mediasoup/pull/945)). -* Update NPM deps. -* `SctpAssociation`: Fix memory violation ([PR #943](https://github.com/versatica/mediasoup/pull/943)). +- `SimulcastConsumer`: Fix increase layer when current layer has not receive SR ([PR #1098](https://github.com/versatica/mediasoup/pull/1098) by @penguinol). +- Ignore RTP packets with empty payload ([PR #1403](https://github.com/versatica/mediasoup/pull/1403), credits to @ggarber). +### 3.14.6 -### 3.10.12 +- Worker: Fix potential double free when ICE consent check fails ([PR #1393](https://github.com/versatica/mediasoup/pull/1393)). -* Fix worker crash due to `std::out_of_range` exception ([PR #933](https://github.com/versatica/mediasoup/pull/933)). -* Update NPM deps. +### 3.14.5 +- Worker: Fix memory leak when using `WebRtcServer` with TCP enabled ([PR #1389](https://github.com/versatica/mediasoup/pull/1389)). +- Worker: Fix crash when closing `WebRtcServer` with active `WebRtcTransports` ([PR #1390](https://github.com/versatica/mediasoup/pull/1390)). -### 3.10.11 +### 3.14.4 -* RTCP: Fix trailing space needed by `srtp_protect_rtcp()` ([PR #929](https://github.com/versatica/mediasoup/pull/929)). +- Worker: Fix crash. `RtcpFeedback` parameter is optional ([PR #1387](https://github.com/versatica/mediasoup/pull/1387), credits to @Lynnworld). +### 3.14.3 -### 3.10.10 +- Worker: Fix possible value overflow in `FeedbackRtpTransport.cpp` ([PR #1386](https://github.com/versatica/mediasoup/pull/1386), credits to @Lynnworld). -* Fix the JSON serialization for the payload channel `rtp` event ([PR #926](https://github.com/versatica/mediasoup/pull/926) by @mhammo). -* Update NPM deps. +### 3.14.2 +- Update worker subprojects ([PR #1376](https://github.com/versatica/mediasoup/pull/1376)). +- OPUS: Fix DTX detection ([PR #1357](https://github.com/versatica/mediasoup/pull/1357)). +- Worker: Fix sending callback leaks ([PR #1383](https://github.com/versatica/mediasoup/pull/1383), credits to @Lynnworld). -### 3.10.9 +### 3.14.1 -* RTCP enhancements ([PR #914](https://github.com/versatica/mediasoup/pull/914)). +- Node: Bring transport `rtpPacketLossReceived` and `rtpPacketLossSent` stats back ([PR #1371](https://github.com/versatica/mediasoup/pull/1371)). +### 3.14.0 -### 3.10.8 +- `TransportListenInfo`: Add `portRange` (deprecate worker port range) ([PR #1365](https://github.com/versatica/mediasoup/pull/1365)). +- Require Node.js >= 18 ([PR #1365](https://github.com/versatica/mediasoup/pull/1365)). -* `Consumer`: use a bitset instead of a set for supported payload types ([PR #919](https://github.com/versatica/mediasoup/pull/919)). -* RtpPacket: optimize UpdateMid() ([PR #920](https://github.com/versatica/mediasoup/pull/920)). -* Little optimizations and modernization ([PR #916](https://github.com/versatica/mediasoup/pull/916)). -* Fix SIGSEGV at `RTC::WebRtcTransport::OnIceServerTupleRemoved()` ([PR #915](https://github.com/versatica/mediasoup/pull/915), credits to @ybybwdwd). -* `WebRtcServer`: Make `port` optional (if not given, a random available port from the `Worker` port range is used) ([PR #908](https://github.com/versatica/mediasoup/pull/908) by @satoren). +### 3.13.24 +- Node: Fix missing `bitrateByLayer` field in stats of `RtpRecvStream` in Node ([PR #1349](https://github.com/versatica/mediasoup/pull/1349)). +- Update worker dependency libuv to 1.48.0. +- Update worker FlatBuffers to 24.3.6-1 (fix cannot set temporal layer 0) ([PR #1348](https://github.com/versatica/mediasoup/pull/1348)). -### 3.10.7 +### 3.13.23 -* Forward `abs-capture-time` RTP extension also for audio packets ([PR #911](https://github.com/versatica/mediasoup/pull/911)). -* Update NPM deps. +- Fix DTLS packets do not honor configured DTLS MTU (attempt 3) ([PR #1345](https://github.com/versatica/mediasoup/pull/1345)). +### 3.13.22 -### 3.10.6 +- Fix wrong publication of mediasoup NPM 3.13.21. + +### 3.13.21 + +- Revert ([PR #1156](https://github.com/versatica/mediasoup/pull/1156)) "Make DTLS fragment stay within MTU size range" because it causes a memory leak ([PR #1342](https://github.com/versatica/mediasoup/pull/1342)). + +### 3.13.20 + +- Add server side ICE consent checks to detect silent WebRTC disconnections ([PR #1332](https://github.com/versatica/mediasoup/pull/1332)). +- Fix regression (crash) in transport-cc feedback generation ([PR #1339](https://github.com/versatica/mediasoup/pull/1339)). + +### 3.13.19 + +- Node: Fix `router.createWebRtcTransport()` with `listenIps` ([PR #1330](https://github.com/versatica/mediasoup/pull/1330)). + +### 3.13.18 + +- Make transport-cc feedback work similarly to libwebrtc ([PR #1088](https://github.com/versatica/mediasoup/pull/1088) by @penguinol). +- `TransportListenInfo`: "announced ip" can also be a hostname ([PR #1322](https://github.com/versatica/mediasoup/pull/1322)). +- `TransportListenInfo`: Rename "announced ip" to "announced address" ([PR #1324](https://github.com/versatica/mediasoup/pull/1324)). +- CI: Add `macos-14`. + +### 3.13.17 + +- Fix prebuilt worker download ([PR #1319](https://github.com/versatica/mediasoup/pull/1319) by @brynrichards). +- libsrtp: Update to v3.0-alpha version in our fork. + +### 3.13.16 + +- Node: Add new `worker.on('subprocessclose')` event ([PR #1307](https://github.com/versatica/mediasoup/pull/1307)). + +### 3.13.15 + +- Add worker prebuild binary for Linux kernel 6 ([PR #1300](https://github.com/versatica/mediasoup/pull/1300)). + +### 3.13.14 + +- Avoid modification of user input data ([PR #1285](https://github.com/versatica/mediasoup/pull/1285)). +- `TransportListenInfo`: Add transport socket flags ([PR #1291](https://github.com/versatica/mediasoup/pull/1291)). + - Note that `flags.ipv6Only` is `false` by default. +- `TransportListenInfo`: Ignore given socket flags if not suitable for given IP family or transport ([PR #1294](https://github.com/versatica/mediasoup/pull/1294)). +- Meson: Remove `-Db_pie=true -Db_staticpic=true` args ([PR #1293](https://github.com/versatica/mediasoup/pull/1293)). +- Add RTCP Sender Report trace event ([PR #1267](https://github.com/versatica/mediasoup/pull/1267) by @GithubUser8080). + +### 3.13.13 + +- Worker: Do not use references for async callbacks ([PR #1274](https://github.com/versatica/mediasoup/pull/1274)). +- liburing: Enable zero copy ([PR #1273](https://github.com/versatica/mediasoup/pull/1273)). +- Fix build on musl based systems (such as Alpine Linux) ([PR #1279](https://github.com/versatica/mediasoup/pull/1279)). + +### 3.13.12 + +- Worker: Disable `RtcLogger` usage if not enabled ([PR #1264](https://github.com/versatica/mediasoup/pull/1264)). +- npm installation: Don't require Python if valid worker prebuilt binary is fetched ([PR #1265](https://github.com/versatica/mediasoup/pull/1265)). +- Update h264-profile-level-id NPM dependency to 1.1.0. + +### 3.13.11 + +- liburing: Avoid extra memcpy on RTP ([PR #1258](https://github.com/versatica/mediasoup/pull/1258)). +- libsrtp: Use our own fork with performance gain ([PR #1260](https://github.com/versatica/mediasoup/pull/1260)). +- `DataConsumer`: Add `addSubchannel()` and `removeSubchannel()` methods ([PR #1263](https://github.com/versatica/mediasoup/pull/1263)). +- Fix Rust `DataConsumer` ([PR #1262](https://github.com/versatica/mediasoup/pull/1262)). + +### 3.13.10 + +- `tasks.py`: Always include `--no-user` in `pip install` commands to avoid the "can not combine --user and --target" error in Windows ([PR #1257](https://github.com/versatica/mediasoup/pull/1257)). + +### 3.13.9 + +- Update worker liburing dependency to 2.4-2 ([PR #1254](https://github.com/versatica/mediasoup/pull/1254)). +- liburing: Enable by default ([PR 1255](https://github.com/versatica/mediasoup/pull/1255)). + +### 3.13.8 + +- liburing: Enable liburing usage for SCTP data delivery ([PR 1249](https://github.com/versatica/mediasoup/pull/1249)). +- liburing: Disable by default ([PR 1253](https://github.com/versatica/mediasoup/pull/1253)). + +### 3.13.7 + +- Update worker dependencies ([PR #1201](https://github.com/versatica/mediasoup/pull/1201)): + - abseil-cpp 20230802.0-2 + - libuv 1.47.0-1 + - OpenSSL 3.0.8-2 + - usrsctp snapshot ebb18adac6501bad4501b1f6dccb67a1c85cc299 +- Enable `liburing` usage for Linux (kernel versions >= 6) ([PR #1218](https://github.com/versatica/mediasoup/pull/1218)). + +### 3.13.6 + +- Replace make + Makefile with Python Invoke library + tasks.py (also fix installation under path with whitespaces) ([PR #1239](https://github.com/versatica/mediasoup/pull/1239)). + +### 3.13.5 + +- Fix RTCP SDES packet size calculation ([PR #1236](https://github.com/versatica/mediasoup/pull/1236) based on PR [PR #1234](https://github.com/versatica/mediasoup/pull/1234) by @ybybwdwd). +- RTCP Compound Packet: Use a single DLRR report to hold all ssrc info sub-blocks ([PR #1237](https://github.com/versatica/mediasoup/pull/1237)). + +### 3.13.4 + +- Fix RTCP DLRR (Delay Since Last Receiver Report) block parsing ([PR #1234](https://github.com/versatica/mediasoup/pull/1234)). + +### 3.13.3 + +- Node: Fix issue when 'pause'/'resume' events are not emitted ([PR #1231](https://github.com/versatica/mediasoup/pull/1231) by @douglaseel). + +### 3.13.2 + +- FBS: `LayersChangeNotification` body must be optional (fixes a crash) ([PR #1227](https://github.com/versatica/mediasoup/pull/1227)). + +### 3.13.1 + +- Node: Extract version from `package.json` using `require()` ([PR #1217](https://github.com/versatica/mediasoup/pull/1217) by @arcinston). + +### 3.13.0 + +- Switch from JSON based messages to FlatBuffers ([PR #1064](https://github.com/versatica/mediasoup/pull/1064)). +- Add `TransportListenInfo` in all transports and send/recv buffer size options ([PR #1084](https://github.com/versatica/mediasoup/pull/1084)). +- Add optional `rtcpListenInfo` in `PlainTransportOptions` ([PR #1099](https://github.com/versatica/mediasoup/pull/1099)). +- Add pause/resume API in `DataProducer` and `DataConsumer` ([PR #1104](https://github.com/versatica/mediasoup/pull/1104)). +- DataChannel subchannels feature ([PR #1152](https://github.com/versatica/mediasoup/pull/1152)). +- Worker: Make DTLS fragment stay within MTU size range ([PR #1156](https://github.com/versatica/mediasoup/pull/1156), based on [PR #1143](https://github.com/versatica/mediasoup/pull/1143) by @vpnts-se). + +### 3.12.16 + +- Fix `IceServer` crash when client uses ICE renomination ([PR #1182](https://github.com/versatica/mediasoup/pull/1182)). + +### 3.12.15 + +- Fix NPM "postinstall" task in Windows ([PR #1187](https://github.com/versatica/mediasoup/pull/1187)). + +### 3.12.14 + +- CI: Use Node.js version 20 ([PR #1177](https://github.com/versatica/mediasoup/pull/1177)). +- Use given `PYTHON` environment variable (if given) when running `worker/scripts/getmake.py` ([PR #1186](https://github.com/versatica/mediasoup/pull/1186)). + +### 3.12.13 + +- Bump up Meson from 1.1.0 to 1.2.1 (fixes Xcode 15 build issues) ([PR #1163](https://github.com/versatica/mediasoup/pull/1163) by @arcinston). + +### 3.12.12 + +- Support C++20 ([PR #1150](https://github.com/versatica/mediasoup/pull/1150) by @o-u-p). + +### 3.12.11 + +- Google Transport Feedback: Read Reference Time field as 24bits signed as per spec ([PR #1145](https://github.com/versatica/mediasoup/pull/1145)). + +### 3.12.10 + +- Node: Rename `WebRtcTransport.webRtcServerClosed()` to `listenServerClosed()` ([PR #1141](https://github.com/versatica/mediasoup/pull/1141) by @piranna). + +### 3.12.9 + +- Fix RTCP SDES ([PR #1139](https://github.com/versatica/mediasoup/pull/1139)). + +### 3.12.8 + +- Export `workerBin` absolute path ([PR #1123](https://github.com/versatica/mediasoup/pull/1123)). + +### 3.12.7 + +- `SimulcastConsumer`: Fix lack of "layerschange" event when all streams in the producer die ([PR #1122](https://github.com/versatica/mediasoup/pull/1122)). + +### 3.12.6 + +- Worker: Add `Transport::Destroying()` protected method ([PR #1114](https://github.com/versatica/mediasoup/pull/1114)). +- `RtpStreamRecv`: Fix jitter calculation ([PR #1117](https://github.com/versatica/mediasoup/pull/1117), thanks to @penguinol). +- Revert "Node: make types.ts only export types rather than the entire class/code" ([PR #1109](https://github.com/versatica/mediasoup/pull/1109)) because it requires `typescript` >= 5 in the apps that import mediasoup and we don't want to be that strict yet. + +### 3.12.5 + +- `DataConsumer`: Fix removed 'bufferedamountlow' notification ([PR #1113](https://github.com/versatica/mediasoup/pull/1113)). + +### 3.12.4 + +- Fix downloaded prebuilt binary check on Windows ([PR #1105](https://github.com/versatica/mediasoup/pull/1105) by @woodfe). + +### 3.12.3 + +Migrate `npm-scripts.js` to `npm-scripts.mjs` (ES Module) ([PR #1093](https://github.com/versatica/mediasoup/pull/1093)). + +### 3.12.2 + +- CI: Use `ubuntu-20.04` to build `mediasoup-worker` prebuilt on Linux ([PR #1092](https://github.com/versatica/mediasoup/pull/1092)). + +### 3.12.1 + +- `mediasoup-worker` prebuild: Fallback to local building if fetched binary doesn't run on current host ([PR #1090](https://github.com/versatica/mediasoup/pull/1090)). + +### 3.12.0 + +- Automate and publish prebuilt `mediasoup-worker` binaries ([PR #1087](https://github.com/versatica/mediasoup/pull/1087), thanks to @barlock for his work in ([PR #1083](https://github.com/versatica/mediasoup/pull/1083)). + +### 3.11.26 + +- Worker: Fix NACK timer and avoid negative RTT ([PR #1082](https://github.com/versatica/mediasoup/pull/1082), thanks to @o-u-p for his work in ([PR #1076](https://github.com/versatica/mediasoup/pull/1076)). + +### 3.11.25 + +- Worker: Require C++17, Meson >= 1.1.0 and update subprojects ([PR #1081](https://github.com/versatica/mediasoup/pull/1081)). + +### 3.11.24 + +- `SeqManager`: Fix performance regression ([PR #1068](https://github.com/versatica/mediasoup/pull/1068), thanks to @vpalmisano for properly reporting). -* Node: Define TypeScript types for `internal` and `data` objects ([PR #891](https://github.com/versatica/mediasoup/pull/891)). -* `Channel` and `PayloadChannel`: Refactor `internal` with a single `handlerId` ([PR #889](https://github.com/versatica/mediasoup/pull/889)). -* `Channel` and `PayloadChannel`: Optimize message format and JSON generation ([PR #893](https://github.com/versatica/mediasoup/pull/893)). -* New C++ `ChannelMessageHandlers` class ([PR #894](https://github.com/versatica/mediasoup/pull/894)). -* Fix Rust support after recent changes ([PR #898](https://github.com/versatica/mediasoup/pull/898)). -* Modify `FeedbackRtpTransport` and tests to be compliant with latest libwebrtc code, make reference time to be unsigned ([PR #899](https://github.com/versatica/mediasoup/pull/899) by @penguinol and @sarumjanuch). -* Update NPM deps. +### 3.11.23 +- Node: Fix `appData` for `Transport` and `RtpObserver` parent classes ([PR #1066](https://github.com/versatica/mediasoup/pull/1066)). + +### 3.11.22 + +- `RtpStreamRecv`: Only perform RTP inactivity check on simulcast streams ([PR #1061](https://github.com/versatica/mediasoup/pull/1061)). +- `SeqManager`: Properly remove old dropped entries ([PR #1054](https://github.com/versatica/mediasoup/pull/1054)). +- libwebrtc: Upgrade trendline estimator to improve low bandwidth conditions ([PR #1055](https://github.com/versatica/mediasoup/pull/1055) by @ggarber). +- libwebrtc: Fix bandwidth probation dead state ([PR #1031](https://github.com/versatica/mediasoup/pull/1031) by @vpalmisano). + +### 3.11.21 + +- Fix check division by zero in transport congestion control ([PR #1049](https://github.com/versatica/mediasoup/pull/1049) by @ggarber). +- Fix lost pending statuses in transport CC feedback ([PR #1050](https://github.com/versatica/mediasoup/pull/1050) by @ggarber). + +### 3.11.20 + +- `RtpStreamSend`: Reset RTP retransmission buffer upon RTP sequence number reset ([PR #1041](https://github.com/versatica/mediasoup/pull/1041)). +- `RtpRetransmissionBuffer`: Handle corner case in which received packet has lower seq than newest packet in the buffer but higher timestamp ([PR #1044](https://github.com/versatica/mediasoup/pull/1044)). +- `SeqManager`: Fix crash and add fuzzer ([PR #1045](https://github.com/versatica/mediasoup/pull/1045)). +- Node: Make `appData` TS typed and writable ([PR #1046](https://github.com/versatica/mediasoup/pull/1046), credits to @mango-martin). + +### 3.11.19 + +- `SvcConsumer`: Properly handle VP9 K-SVC bandwidth allocation ([PR #1036](https://github.com/versatica/mediasoup/pull/1036) by @vpalmisano). + +### 3.11.18 + +- `RtpRetransmissionBuffer`: Consider the case of packet with newest timestamp but "old" seq number ([PR #1039](https://github.com/versatica/mediasoup/pull/1039)). + +### 3.11.17 + +- Add `transport.setMinOutgoingBitrate()` method ([PR #1038](https://github.com/versatica/mediasoup/pull/1038), credits to @ jcague). +- `RTC::RetransmissionBuffer`: Increase `RetransmissionBufferMaxItems` from 2500 to 5000. + +### 3.11.16 + +- Fix `SeqManager`: Properly consider previous cycle dropped inputs ([PR #1032](https://github.com/versatica/mediasoup/pull/1032)). +- `RtpRetransmissionBuffer`: Get rid of not necessary `startSeq` private member ([PR #1029](https://github.com/versatica/mediasoup/pull/1029)). +- Node: Upgrade TypeScript to 5.0.2. + +### 3.11.15 + +- `RtpRetransmissionBuffer`: Fix crash and add fuzzer ([PR #1028](https://github.com/versatica/mediasoup/pull/1028)). + +### 3.11.14 + +- Refactor RTP retransmission buffer in a separate and testable `RTC::RetransmissionBuffer` class ([PR #1023](https://github.com/versatica/mediasoup/pull/1023)). + +### 3.11.13 + +- `AudioLevelObserver`: Use multimap rather than map to avoid conflict if various Producers generate same audio level ([PR #1021](https://github.com/versatica/mediasoup/pull/1021), issue reported by @buptlsp). + +### 3.11.12 + +- Fix jitter calculation ([PR #1019](https://github.com/versatica/mediasoup/pull/1019), credits to @alexciarlillo and @snnz). + +### 3.11.11 + +- Add support for RTCP NACK in OPUS ([PR #1015](https://github.com/versatica/mediasoup/pull/1015)). + +### 3.11.10 + +- Download and use MSYS/make locally for Windows postinstall ([PR #792](https://github.com/versatica/mediasoup/pull/792) by @snnz). + +### 3.11.9 + +- Allow simulcast with a single encoding (and N temporal layers) ([PR #1013](https://github.com/versatica/mediasoup/pull/1013)). +- Update libsrtp to 2.5.0. + +### 3.11.8 + +- `SimulcastConsumer::GetDesiredBitrate()`: Choose the highest bitrate among all Producer streams ([PR #992](https://github.com/versatica/mediasoup/pull/992)). +- `SimulcastConsumer`: Fix frozen video when syncing keyframe is discarded due to too high RTP timestamp extra offset needed ([PR #999](https://github.com/versatica/mediasoup/pull/999), thanks to @satoren for properly reporting the issue and helping with the solution). + +### 3.11.7 + +- libwebrtc: Fix crash due to invalid `arrival_time` value ([PR #985](https://github.com/versatica/mediasoup/pull/985) by @ggarber). +- libwebrtc: Replace `MS_ASSERT()` with `MS_ERROR()` ([PR #988](https://github.com/versatica/mediasoup/pull/988)). + +### 3.11.6 + +- Fix wrong `PictureID` rolling over in VP9 and VP8 ([PR #984](https://github.com/versatica/mediasoup/pull/984) by @jcague). + +### 3.11.5 + +- Require Node.js >= 16 ([PR #973](https://github.com/versatica/mediasoup/pull/973)). +- Fix wrong `Consumer` bandwidth estimation under `Producer` packet loss ([PR #962](https://github.com/versatica/mediasoup/pull/962) by @ggarber). + +### 3.11.4 + +- Node: Migrate tests to TypeScript ([PR #958](https://github.com/versatica/mediasoup/pull/958)). +- Node: Remove compiled JavaScript from repository and compile TypeScript code on NPM `prepare` script on demand when installed via git ([PR #954](https://github.com/versatica/mediasoup/pull/954)). +- Worker: Add `RTC::Shared` singleton for RTC entities ([PR #953](https://github.com/versatica/mediasoup/pull/953)). +- Update OpenSSL to 3.0.7. + +### 3.11.3 + +- `ChannelMessageHandlers`: Make `RegisterHandler()` not remove the existing handler if another one with same `id` is given ([PR #952](https://github.com/versatica/mediasoup/pull/952)). + +### 3.11.2 + +- Fix installation issue in Linux due to a bug in ninja latest version 1.11.1 ([PR #948](https://github.com/versatica/mediasoup/pull/948)). + +### 3.11.1 + +- `ActiveSpeakerObserver`: Revert 'dominantspeaker' event changes in [PR #941](https://github.com/versatica/mediasoup/pull/941) to avoid breaking changes ([PR #947](https://github.com/versatica/mediasoup/pull/947)). + +### 3.11.0 + +- `Transport`: Remove duplicate call to method ([PR #931](https://github.com/versatica/mediasoup/pull/931)). +- RTCP: Adjust maximum compound packet size ([PR #934](https://github.com/versatica/mediasoup/pull/934)). +- `DataConsumer`: Fix `bufferedAmount` type to be a number again ([PR #936](https://github.com/versatica/mediasoup/pull/936)). +- `ActiveSpeakerObserver`: Fix 'dominantspeaker' event by having a single `Producer` as argument rather than an array with a single `Producer` into it ([PR #941](https://github.com/versatica/mediasoup/pull/941)). +- `ActiveSpeakerObserver`: Fix memory leak ([PR #942](https://github.com/versatica/mediasoup/pull/942)). +- Fix some libwebrtc issues ([PR #944](https://github.com/versatica/mediasoup/pull/944)). +- Tests: Normalize hexadecimal data representation ([PR #945](https://github.com/versatica/mediasoup/pull/945)). +- `SctpAssociation`: Fix memory violation ([PR #943](https://github.com/versatica/mediasoup/pull/943)). + +### 3.10.12 + +- Fix worker crash due to `std::out_of_range` exception ([PR #933](https://github.com/versatica/mediasoup/pull/933)). + +### 3.10.11 + +- RTCP: Fix trailing space needed by `srtp_protect_rtcp()` ([PR #929](https://github.com/versatica/mediasoup/pull/929)). + +### 3.10.10 + +- Fix the JSON serialization for the payload channel `rtp` event ([PR #926](https://github.com/versatica/mediasoup/pull/926) by @mhammo). + +### 3.10.9 + +- RTCP enhancements ([PR #914](https://github.com/versatica/mediasoup/pull/914)). + +### 3.10.8 + +- `Consumer`: use a bitset instead of a set for supported payload types ([PR #919](https://github.com/versatica/mediasoup/pull/919)). +- RtpPacket: optimize UpdateMid() ([PR #920](https://github.com/versatica/mediasoup/pull/920)). +- Little optimizations and modernization ([PR #916](https://github.com/versatica/mediasoup/pull/916)). +- Fix SIGSEGV at `RTC::WebRtcTransport::OnIceServerTupleRemoved()` ([PR #915](https://github.com/versatica/mediasoup/pull/915), credits to @ybybwdwd). +- `WebRtcServer`: Make `port` optional (if not given, a random available port from the `Worker` port range is used) ([PR #908](https://github.com/versatica/mediasoup/pull/908) by @satoren). + +### 3.10.7 + +- Forward `abs-capture-time` RTP extension also for audio packets ([PR #911](https://github.com/versatica/mediasoup/pull/911)). + +### 3.10.6 + +- Node: Define TypeScript types for `internal` and `data` objects ([PR #891](https://github.com/versatica/mediasoup/pull/891)). +- `Channel` and `PayloadChannel`: Refactor `internal` with a single `handlerId` ([PR #889](https://github.com/versatica/mediasoup/pull/889)). +- `Channel` and `PayloadChannel`: Optimize message format and JSON generation ([PR #893](https://github.com/versatica/mediasoup/pull/893)). +- New C++ `ChannelMessageHandlers` class ([PR #894](https://github.com/versatica/mediasoup/pull/894)). +- Fix Rust support after recent changes ([PR #898](https://github.com/versatica/mediasoup/pull/898)). +- Modify `FeedbackRtpTransport` and tests to be compliant with latest libwebrtc code, make reference time to be unsigned ([PR #899](https://github.com/versatica/mediasoup/pull/899) by @penguinol and @sarumjanuch). ### 3.10.5 -* `RtpStreamSend`: Do not store too old RTP packets ([PR #885](https://github.com/versatica/mediasoup/pull/885)). -* Log error details in channel socket. ([PR #875](https://github.com/versatica/mediasoup/pull/875) by @mstyura). +- `RtpStreamSend`: Do not store too old RTP packets ([PR #885](https://github.com/versatica/mediasoup/pull/885)). +- Log error details in channel socket. ([PR #875](https://github.com/versatica/mediasoup/pull/875) by @mstyura). ### 3.10.4 -* Do not clone RTP packets if not needed ([PR #850](https://github.com/versatica/mediasoup/pull/850)). -* Fix DTLS related crash ([PR #867](https://github.com/versatica/mediasoup/pull/867)). -* Update NPM deps. - +- Do not clone RTP packets if not needed ([PR #850](https://github.com/versatica/mediasoup/pull/850)). +- Fix DTLS related crash ([PR #867](https://github.com/versatica/mediasoup/pull/867)). ### 3.10.3 -* `SimpleConsumer`: Fix. Only process Opus codec ([PR #865](https://github.com/versatica/mediasoup/pull/865)). -* TypeScript: Improve `WebRtcTransportOptions` type to not allow `webRtcServer` and `listenIps`options at the same time ([PR #852](https://github.com/versatica/mediasoup/pull/852)). - +- `SimpleConsumer`: Fix. Only process Opus codec ([PR #865](https://github.com/versatica/mediasoup/pull/865)). +- TypeScript: Improve `WebRtcTransportOptions` type to not allow `webRtcServer` and `listenIps`options at the same time ([PR #852](https://github.com/versatica/mediasoup/pull/852)). ### 3.10.2 -* Fix release contents by including meson_options.txt ([PR #863](https://github.com/versatica/mediasoup/pull/863)). - +- Fix release contents by including `meson_options.txt` ([PR #863](https://github.com/versatica/mediasoup/pull/863)). ### 3.10.1 -* `RtpStreamSend`: Memory optimizations ([PR #840](https://github.com/versatica/mediasoup/pull/840)). Extracted from #675, by @nazar-pc. -* `SimpleConsumer`: Opus DTX ignore capabilities ([PR #846](https://github.com/versatica/mediasoup/pull/846)). -* Update `libuv` to 1.44.1: Fixes `libuv` build ([PR #857](https://github.com/versatica/mediasoup/pull/857)). -* Update NPM deps. - +- `RtpStreamSend`: Memory optimizations ([PR #840](https://github.com/versatica/mediasoup/pull/840)). Extracted from #675, by @nazar-pc. +- `SimpleConsumer`: Opus DTX ignore capabilities ([PR #846](https://github.com/versatica/mediasoup/pull/846)). +- Update `libuv` to 1.44.1: Fixes `libuv` build ([PR #857](https://github.com/versatica/mediasoup/pull/857)). ### 3.10.0 -* `WebRtcServer`: A new class that brings to `WebRtcTransports` the ability to listen on a single UDP/TCP port ([PR #834](https://github.com/versatica/mediasoup/pull/834)). -* More SRTP crypto suites ([PR #837](https://github.com/versatica/mediasoup/pull/837)). -* Improve `EnhancedEventEmitter` ([PR #836](https://github.com/versatica/mediasoup/pull/836)). -* `TransportCongestionControlClient`: Allow setting max outgoing bitrate before `tccClient` is created ([PR #833](https://github.com/versatica/mediasoup/pull/833)). -* Update NPM deps and TypeScript version. - +- `WebRtcServer`: A new class that brings to `WebRtcTransports` the ability to listen on a single UDP/TCP port ([PR #834](https://github.com/versatica/mediasoup/pull/834)). +- More SRTP crypto suites ([PR #837](https://github.com/versatica/mediasoup/pull/837)). +- Improve `EnhancedEventEmitter` ([PR #836](https://github.com/versatica/mediasoup/pull/836)). +- `TransportCongestionControlClient`: Allow setting max outgoing bitrate before `tccClient` is created ([PR #833](https://github.com/versatica/mediasoup/pull/833)). +- Update TypeScript version. ### 3.9.17 -* `RateCalculator`: Fix old buffer items cleanup ([PR #830](https://github.com/versatica/mediasoup/pull/830) by @dsdolzhenko). -* Update NPM deps and TypeScript version. - +- `RateCalculator`: Fix old buffer items cleanup ([PR #830](https://github.com/versatica/mediasoup/pull/830) by @dsdolzhenko). +- Update TypeScript version. ### 3.9.16 -* `SimulcastConsumer`: Fix spatial layer switch with unordered packets ([PR #823](https://github.com/versatica/mediasoup/pull/823) by @jcague). -* Update NPM deps and TypeScript version. - +- `SimulcastConsumer`: Fix spatial layer switch with unordered packets ([PR #823](https://github.com/versatica/mediasoup/pull/823) by @jcague). +- Update TypeScript version. ### 3.9.15 -* `RateCalculator`: Revert Fix old buffer items cleanup ([PR #819](https://github.com/versatica/mediasoup/pull/819) by @dsdolzhenko). - +- `RateCalculator`: Revert Fix old buffer items cleanup ([PR #819](https://github.com/versatica/mediasoup/pull/819) by @dsdolzhenko). ### 3.9.14 -* `NackGenerator`: Add a configurable delay before sending NACK ([PR #827](https://github.com/versatica/mediasoup/pull/827), credits to @penguinol). -* `SimulcastConsumer`: Fix a race condition in SimulcastConsumer ([PR #825](https://github.com/versatica/mediasoup/pull/825) by @dsdolzhenko). -* Add support for H264 SVC (#798 by @prtmD). -* `RtpStreamSend`: Support receive RTCP-XR RRT and send RTCP-XR DLRR ([PR #781](https://github.com/versatica/mediasoup/pull/781) by @aggresss). -* `RateCalculator`: Fix old buffer items cleanup ([PR #819](https://github.com/versatica/mediasoup/pull/819) by @dsdolzhenko). -* `DirectTransport`: Create a buffer to process RTP packets ([PR #730](https://github.com/versatica/mediasoup/pull/730) by @rtctt). -* Node: Improve `appData` TypeScript syntax and initialization. -* Allow setting max outgoing bitrate below the initial value ([PR #826](https://github.com/versatica/mediasoup/pull/826) by @ggarber). -* Update NPM deps and TypeScript version. - +- `NackGenerator`: Add a configurable delay before sending NACK ([PR #827](https://github.com/versatica/mediasoup/pull/827), credits to @penguinol). +- `SimulcastConsumer`: Fix a race condition in SimulcastConsumer ([PR #825](https://github.com/versatica/mediasoup/pull/825) by @dsdolzhenko). +- Add support for H264 SVC (#798 by @prtmD). +- `RtpStreamSend`: Support receive RTCP-XR RRT and send RTCP-XR DLRR ([PR #781](https://github.com/versatica/mediasoup/pull/781) by @aggresss). +- `RateCalculator`: Fix old buffer items cleanup ([PR #819](https://github.com/versatica/mediasoup/pull/819) by @dsdolzhenko). +- `DirectTransport`: Create a buffer to process RTP packets ([PR #730](https://github.com/versatica/mediasoup/pull/730) by @rtctt). +- Node: Improve `appData` TypeScript syntax and initialization. +- Allow setting max outgoing bitrate below the initial value ([PR #826](https://github.com/versatica/mediasoup/pull/826) by @ggarber). +- Update TypeScript version. ### 3.9.13 -* `VP8`: Do not discard `TL0PICIDX` from Temporal Layers higher than 0 (PR @817 by @jcague). -* Update NPM deps and TypeScript version. - +- `VP8`: Do not discard `TL0PICIDX` from Temporal Layers higher than 0 (PR @817 by @jcague). +- Update TypeScript version. ### 3.9.12 -* `DtlsTransport`: Make DTLS negotiation run immediately ([PR #815](https://github.com/versatica/mediasoup/pull/815)). -* Update NPM deps and TypeScript version. - +- `DtlsTransport`: Make DTLS negotiation run immediately ([PR #815](https://github.com/versatica/mediasoup/pull/815)). +- Update TypeScript version. ### 3.9.11 -* Modify `SimulcastConsumer` to keep using layers without good scores ([PR #804](https://github.com/versatica/mediasoup/pull/804) by @ggarber). -* Update NPM deps. - +- Modify `SimulcastConsumer` to keep using layers without good scores ([PR #804](https://github.com/versatica/mediasoup/pull/804) by @ggarber). ### 3.9.10 -* Update worker dependencies: - * OpenSSL 3.0.2. - * abseil-cpp 20211102.0. - * nlohmann_json 3.10.5. - * usrsctp snapshot 4e06feb01cadcd127d119486b98a4bd3d64aa1e7. - * wingetopt 1.00. -* Update NPM deps and TypeScript version. -* Fix RTP marker bit not being reseted after mangling in each `Consumer` ([PR #811](https://github.com/versatica/mediasoup/pull/811) by @ggarber). - +- Update worker dependencies: + - OpenSSL 3.0.2. + - abseil-cpp 20211102.0. + - nlohmann_json 3.10.5. + - usrsctp snapshot 4e06feb01cadcd127d119486b98a4bd3d64aa1e7. + - wingetopt 1.00. +- Update TypeScript version. +- Fix RTP marker bit not being reseted after mangling in each `Consumer` ([PR #811](https://github.com/versatica/mediasoup/pull/811) by @ggarber). ### 3.9.9 -* Optimize RTP header extension handling ([PR #786](https://github.com/versatica/mediasoup/pull/786)). -* `RateCalculator`: Reset optimization ([PR #785](https://github.com/versatica/mediasoup/pull/785)). -* Fix frozen video due to double call to `Consumer::UserOnTransportDisconnected()` ([PR #788](https://github.com/versatica/mediasoup/pull/788), thanks to @ggarber for exposing this issue in [PR #787](https://github.com/versatica/mediasoup/pull/787)). - +- Optimize RTP header extension handling ([PR #786](https://github.com/versatica/mediasoup/pull/786)). +- `RateCalculator`: Reset optimization ([PR #785](https://github.com/versatica/mediasoup/pull/785)). +- Fix frozen video due to double call to `Consumer::UserOnTransportDisconnected()` ([PR #788](https://github.com/versatica/mediasoup/pull/788), thanks to @ggarber for exposing this issue in [PR #787](https://github.com/versatica/mediasoup/pull/787)). ### 3.9.8 -* Fix VP9 kSVC forwarding logic to not forward lower unneded layers ([PR #778](https://github.com/versatica/mediasoup/pull/778) by @ggarber). -* Fix update bandwidth estimation configuration and available bitrate when updating max outgoing bitrate ([PR #779](https://github.com/versatica/mediasoup/pull/779) by @ggarber). -* Replace outdated `random-numbers` package by native `crypto.randomInt()` ([PR #776](https://github.com/versatica/mediasoup/pull/776) by @piranna). -* Update NPM deps and TypeScript version. - +- Fix VP9 kSVC forwarding logic to not forward lower unneded layers ([PR #778](https://github.com/versatica/mediasoup/pull/778) by @ggarber). +- Fix update bandwidth estimation configuration and available bitrate when updating max outgoing bitrate ([PR #779](https://github.com/versatica/mediasoup/pull/779) by @ggarber). +- Replace outdated `random-numbers` package by native `crypto.randomInt()` ([PR #776](https://github.com/versatica/mediasoup/pull/776) by @piranna). +- Update TypeScript version. ### 3.9.7 -* Typing event emitters in mediasoup Node ([PR #764](https://github.com/versatica/mediasoup/pull/764) by @unao). -* Update NPM deps. - +- Typing event emitters in mediasoup Node ([PR #764](https://github.com/versatica/mediasoup/pull/764) by @unao). ### 3.9.6 -* TCC client optimizations for faster and more stable BWE ([PR #712](https://github.com/versatica/mediasoup/pull/712) by @ggarber). -* Added support for RTP abs-capture-time header ([PR #761](https://github.com/versatica/mediasoup/pull/761) by @oto313). -* Update NPM deps. - +- TCC client optimizations for faster and more stable BWE ([PR #712](https://github.com/versatica/mediasoup/pull/712) by @ggarber). +- Added support for RTP abs-capture-time header ([PR #761](https://github.com/versatica/mediasoup/pull/761) by @oto313). ### 3.9.5 -* ICE renomination support ([PR #756](https://github.com/versatica/mediasoup/pull/756)). -* Update `libuv` to 1.43.0. -* Update NPM deps. - +- ICE renomination support ([PR #756](https://github.com/versatica/mediasoup/pull/756)). +- Update `libuv` to 1.43.0. ### 3.9.4 -* `Worker`: Fix bad printing of error messages from Worker ([PR #750](https://github.com/versatica/mediasoup/pull/750) by @j1elo). -* Update NPM deps. - +- Worker: Fix bad printing of error messages from Worker ([PR #750](https://github.com/versatica/mediasoup/pull/750) by @j1elo). ### 3.9.3 -* Single H264/H265 codec configuration in `supportedRtpCapabilities` ([PR #718](https://github.com/versatica/mediasoup/pull/718)). -* Improve Windows support by not requiring MSVC configuration ([PR #741](https://github.com/versatica/mediasoup/pull/741)). -* Update NPM deps. - +- Single H264/H265 codec configuration in `supportedRtpCapabilities` ([PR #718](https://github.com/versatica/mediasoup/pull/718)). +- Improve Windows support by not requiring MSVC configuration ([PR #741](https://github.com/versatica/mediasoup/pull/741)). ### 3.9.2 -* `pipeToRouter()`: Reuse same `PipeTransport` when possible ([PR #697](https://github.com/versatica/mediasoup/pull/697)). -* Add `worker.died` boolean getter. -* Update TypeScript version to 4.X.X and use `target: "esnext"` so transpilation of ECMAScript private fields (`#xxxxx`) don't use `WeakMaps` tricks but use standard syntax instead. -* Use more than one core for compilation on Windows ([PR #709](https://github.com/versatica/mediasoup/pull/709)). -* `Consumer`: Modification of bitrate allocation algorithm ([PR #708](https://github.com/versatica/mediasoup/pull/708)). -* Update NPM deps. - +- `pipeToRouter()`: Reuse same `PipeTransport` when possible ([PR #697](https://github.com/versatica/mediasoup/pull/697)). +- Add `worker.died` boolean getter. +- Update TypeScript version to 4.X.X and use `target: "esnext"` so transpilation of ECMAScript private fields (`#xxxxx`) don't use `WeakMaps` tricks but use standard syntax instead. +- Use more than one core for compilation on Windows ([PR #709](https://github.com/versatica/mediasoup/pull/709)). +- `Consumer`: Modification of bitrate allocation algorithm ([PR #708](https://github.com/versatica/mediasoup/pull/708)). ### 3.9.1 -* NixOS friendly build process ([PR #683](https://github.com/versatica/mediasoup/pull/683)). -* `Worker`: Emit "died" event before observer "close" ([PR #684](https://github.com/versatica/mediasoup/pull/684)). -* Transport: Hide debug message for RTX RTCP-RR packets ([PR #688](https://github.com/versatica/mediasoup/pull/688)). -* Update `libuv` to 1.42.0. -* Improve Windows support ([PR #692](https://github.com/versatica/mediasoup/pull/692)). -* Avoid build commands when MEDIASOUP_WORKER_BIN is set ([PR #695](https://github.com/versatica/mediasoup/pull/695)). -* Update NPM deps. - +- NixOS friendly build process ([PR #683](https://github.com/versatica/mediasoup/pull/683)). +- Worker: Emit "died" event before observer "close" ([PR #684](https://github.com/versatica/mediasoup/pull/684)). +- Transport: Hide debug message for RTX RTCP-RR packets ([PR #688](https://github.com/versatica/mediasoup/pull/688)). +- Update `libuv` to 1.42.0. +- Improve Windows support ([PR #692](https://github.com/versatica/mediasoup/pull/692)). +- Avoid build commands when MEDIASOUP_WORKER_BIN is set ([PR #695](https://github.com/versatica/mediasoup/pull/695)). ### 3.9.0 -* Replaces GYP build system with fully-functional Meson build system ([PR #622](https://github.com/versatica/mediasoup/pull/622)). -* Worker communication optimization (aka removing netstring dependency) ([PR #644](https://github.com/versatica/mediasoup/pull/644)). -* Move TypeScript and compiled JavaScript code to a new `node` folder. -* Use ES6 private fields. -* Require Node.js version >= 12. - +- Replaces GYP build system with fully-functional Meson build system ([PR #622](https://github.com/versatica/mediasoup/pull/622)). +- Worker communication optimization (aka removing netstring dependency) ([PR #644](https://github.com/versatica/mediasoup/pull/644)). +- Move TypeScript and compiled JavaScript code to a new `node` folder. +- Use ES6 private fields. +- Require Node.js version >= 12. ### 3.8.4 -* OPUS multi-channel (Surround sound) support ([PR #647](https://github.com/versatica/mediasoup/pull/647)). -* Add `packetLoss` stats to transport ([PR #648](https://github.com/versatica/mediasoup/pull/648) by @ggarber). -* Fixes for active speaker observer ([PR #655](https://github.com/versatica/mediasoup/pull/655) by @ggarber). -* Fix big endian issues ([PR #639](https://github.com/versatica/mediasoup/pull/639)). -* Update NPM deps. - +- OPUS multi-channel (Surround sound) support ([PR #647](https://github.com/versatica/mediasoup/pull/647)). +- Add `packetLoss` stats to transport ([PR #648](https://github.com/versatica/mediasoup/pull/648) by @ggarber). +- Fixes for active speaker observer ([PR #655](https://github.com/versatica/mediasoup/pull/655) by @ggarber). +- Fix big endian issues ([PR #639](https://github.com/versatica/mediasoup/pull/639)). ### 3.8.3 -* Fix wrong `size_t*` to `int*` conversion in 64bit Big-Endian hosts ([PR #637](https://github.com/versatica/mediasoup/pull/637)). - +- Fix wrong `size_t*` to `int*` conversion in 64bit Big-Endian hosts ([PR #637](https://github.com/versatica/mediasoup/pull/637)). ### 3.8.2 -* `ActiveSpeakerObserver`: Fix crash due to a `nullptr` ([PR #634](https://github.com/versatica/mediasoup/pull/634)). -* Update NPM deps. - +- `ActiveSpeakerObserver`: Fix crash due to a `nullptr` ([PR #634](https://github.com/versatica/mediasoup/pull/634)). ### 3.8.1 -* `SimulcastConsumer`: Fix RTP timestamp when switching layers ([PR #626](https://github.com/versatica/mediasoup/pull/626) by @penguinol). -* Update NPM deps. - +- `SimulcastConsumer`: Fix RTP timestamp when switching layers ([PR #626](https://github.com/versatica/mediasoup/pull/626) by @penguinol). ### 3.8.0 -* Update `libuv` to 1.42.0. -* Use non-ASM OpenSSL on Windows ([PR #614](https://github.com/versatica/mediasoup/pull/614)). -* Fix minor memory leak caused by non-virtual destructor ([PR #625](https://github.com/versatica/mediasoup/pull/625)). -* Dominant Speaker Event ([PR #603](https://github.com/versatica/mediasoup/pull/603) by @SteveMcFarlin). -* Update NPM deps. - +- Update `libuv` to 1.42.0. +- Use non-ASM OpenSSL on Windows ([PR #614](https://github.com/versatica/mediasoup/pull/614)). +- Fix minor memory leak caused by non-virtual destructor ([PR #625](https://github.com/versatica/mediasoup/pull/625)). +- Dominant Speaker Event ([PR #603](https://github.com/versatica/mediasoup/pull/603) by @SteveMcFarlin). ### 3.7.19 -* Update `libuv` to 1.41.0. -* Update NPM deps. -* C++: +- Update `libuv` to 1.41.0. +- C++: - Move header includes ([PR #608](https://github.com/versatica/mediasoup/pull/608)). - Enhance debugging on channel request/notification error ([PR #607](https://github.com/versatica/mediasoup/pull/607)). - ### 3.7.18 -* Support for optional fixed port on transports ([PR #593](https://github.com/versatica/mediasoup/pull/593) by @nazar-pc). -* Upgrade and optimize OpenSSL dependency ([PR #598](https://github.com/versatica/mediasoup/pull/598) by @vpalmisano): +- Support for optional fixed port on transports ([PR #593](https://github.com/versatica/mediasoup/pull/593) by @nazar-pc). +- Upgrade and optimize OpenSSL dependency ([PR #598](https://github.com/versatica/mediasoup/pull/598) by @vpalmisano): - OpenSSL upgraded to version 1.1.1k. - Enable the compilation of assembly extensions for OpenSSL. - Optimize the worker build (`-O3`) and disable the debug flag (`-g`). -* Update NPM deps. - ### 3.7.17 -* Introduce `PipeConsumerOptions` to avoid incorrect type information on `PipeTransport.consume()` arguments. -* Make `ConsumerOptions.rtpCapabilities` field required as it should have always been. - +- Introduce `PipeConsumerOptions` to avoid incorrect type information on `PipeTransport.consume()` arguments. +- Make `ConsumerOptions.rtpCapabilities` field required as it should have always been. ### 3.7.16 -* Add `mid` option in `ConsumerOptions` to provide way to override MID ([PR #586](https://github.com/versatica/mediasoup/pull/586) by @mstyura). -* Update NPM deps. - +- Add `mid` option in `ConsumerOptions` to provide way to override MID ([PR #586](https://github.com/versatica/mediasoup/pull/586) by @mstyura). ### 3.7.15 -* `kind` field of `RtpHeaderExtension` is no longer optional. It must be 'audio' or 'video'. -* Refactor API inconsistency in internal RTP Observer communication with worker. - +- `kind` field of `RtpHeaderExtension` is no longer optional. It must be 'audio' or 'video'. +- Refactor API inconsistency in internal RTP Observer communication with worker. ### 3.7.14 -* Update `usrsctp` to include a "possible use after free bug" fix (commit [here](https://github.com/sctplab/usrsctp/commit/0f8d58300b1fdcd943b4a9dd3fbd830825390d4d)). -* Update NPM deps. - +- Update `usrsctp` to include a "possible use after free bug" fix (commit [here](https://github.com/sctplab/usrsctp/commit/0f8d58300b1fdcd943b4a9dd3fbd830825390d4d)). ### 3.7.13 -* Fix build on FreeBSD ([PR #585](https://github.com/versatica/mediasoup/pull/585) by @smortex). - +- Fix build on FreeBSD ([PR #585](https://github.com/versatica/mediasoup/pull/585) by @smortex). ### 3.7.12 -* `mediasoup-worker`: Fix memory leaks on error exit ([PR #581](https://github.com/versatica/mediasoup/pull/581)). -* Update NPM deps. - +- `mediasoup-worker`: Fix memory leaks on error exit ([PR #581](https://github.com/versatica/mediasoup/pull/581)). ### 3.7.11 -* Fix `DepUsrSCTP::Checker::timer` not being freed on `Worker` close ([PR #576](https://github.com/versatica/mediasoup/pull/576)). Thanks @nazar-pc for discovering this. -* Update NPM deps. - +- Fix `DepUsrSCTP::Checker::timer` not being freed on `Worker` close ([PR #576](https://github.com/versatica/mediasoup/pull/576)). Thanks @nazar-pc for discovering this. ### 3.7.10 -* Remove clang tools binaries from regular installation. - +- Remove clang tools binaries from regular installation. ### 3.7.9 -* Code clean up. - +- Code clean up. ### 3.7.8 -* `PayloadChannel`: Copy received messages into a separate buffer to avoid memory corruption if the message is later modified ([PR #570](https://github.com/versatica/mediasoup/pull/570) by @aggresss). - +- `PayloadChannel`: Copy received messages into a separate buffer to avoid memory corruption if the message is later modified ([PR #570](https://github.com/versatica/mediasoup/pull/570) by @aggresss). ### 3.7.7 -* Thread and memory safety fixes needed for mediasoup-rust ([PR #562](https://github.com/versatica/mediasoup/pull/562) by @nazar-pc). -* mediasoup-rust support on macOS ([PR #567](https://github.com/versatica/mediasoup/pull/567) by @nazar-pc). -* mediasoup-rust release 0.7.2. -* Update NPM deps. - +- Thread and memory safety fixes needed for mediasoup-rust ([PR #562](https://github.com/versatica/mediasoup/pull/562) by @nazar-pc). +- mediasoup-rust support on macOS ([PR #567](https://github.com/versatica/mediasoup/pull/567) by @nazar-pc). +- mediasoup-rust release 0.7.2. ### 3.7.6 -* `Transport`: Implement new `setMaxOutgoingBitrate()` method ([PR #555](https://github.com/versatica/mediasoup/pull/555) by @t-mullen). -* `SctpAssociation`: Don't warn if SCTP send buffer is full. -* Rust: Update modules structure and other minor improvements for Rust version ([PR #558](https://github.com/versatica/mediasoup/pull/558)). -* `mediasoup-worker`: Avoid duplicated basenames so that libmediasoup-worker is compilable on macOS ([PR #557](https://github.com/versatica/mediasoup/pull/557)). -* Update NPM deps. - +- `Transport`: Implement new `setMaxOutgoingBitrate()` method ([PR #555](https://github.com/versatica/mediasoup/pull/555) by @t-mullen). +- `SctpAssociation`: Don't warn if SCTP send buffer is full. +- Rust: Update modules structure and other minor improvements for Rust version ([PR #558](https://github.com/versatica/mediasoup/pull/558)). +- `mediasoup-worker`: Avoid duplicated basenames so that `libmediasoup-worker` is compilable on macOS ([PR #557](https://github.com/versatica/mediasoup/pull/557)). ### 3.7.5 -* SctpAssociation: provide 'sctpsendbufferfull' reason on send error (#552). - +- SctpAssociation: provide 'sctpsendbufferfull' reason on send error (#552). ### 3.7.4 -* Improve `RateCalculator` ([PR #547](https://github.com/versatica/mediasoup/pull/547) by @vpalmisano). -* Update NPM deps. - +- Improve `RateCalculator` ([PR #547](https://github.com/versatica/mediasoup/pull/547) by @vpalmisano). ### 3.7.3 -* Make worker M1 compilable. - +- Make worker M1 compilable. ### 3.7.2 -* `RateCalculator` optimization ([PR #538](https://github.com/versatica/mediasoup/pull/538) by @vpalmisano). -* Update `Catch` to 2.13.5. -* Update NPM deps. - +- `RateCalculator` optimization ([PR #538](https://github.com/versatica/mediasoup/pull/538) by @vpalmisano). ### 3.7.1 -* `SimulcastConsumer`: Fix miscalculation when increasing layer ([PR #541](https://github.com/versatica/mediasoup/pull/541) by @penguinol). -* Rust version with thread-based worker ([PR #540](https://github.com/versatica/mediasoup/pull/540)). -* Update NPM deps. - +- `SimulcastConsumer`: Fix miscalculation when increasing layer ([PR #541](https://github.com/versatica/mediasoup/pull/541) by @penguinol). +- Rust version with thread-based worker ([PR #540](https://github.com/versatica/mediasoup/pull/540)). ### 3.7.0 -* Welcome to `mediasoup-rust`! Authored by @nazar-pc (PRs #518 and #533). -* Update NPM deps. -* Update `usrsctp`. - +- Welcome to `mediasoup-rust`! Authored by @nazar-pc (PRs #518 and #533). +- Update `usrsctp`. ### 3.6.37 -* Fix crash if empty `fingerprints` array is given in `webrtcTransport.connect()` (issue #537). +- Fix crash if empty `fingerprints` array is given in `webrtcTransport.connect()` (issue #537). ### 3.6.36 -* `Producer`: Add new stats field 'rtxPacketsDiscarded' ([PR #536](https://github.com/versatica/mediasoup/pull/536)). - +- `Producer`: Add new stats field 'rtxPacketsDiscarded' ([PR #536](https://github.com/versatica/mediasoup/pull/536)). ### 3.6.35 -* `XxxxConsumer.hpp`: make `IsActive()` return `true` (even if `Producer`'s score is 0) when DTX is enabled ([PR #534](https://github.com/versatica/mediasoup/pull/534) due to issue #532). -* Update NPM deps. - +- `XxxxConsumer.hpp`: make `IsActive()` return `true` (even if `Producer`'s score is 0) when DTX is enabled ([PR #534](https://github.com/versatica/mediasoup/pull/534) due to issue #532). ### 3.6.34 -* Fix crash (regression, issue #529). - +- Fix crash (regression, issue #529). ### 3.6.33 -* Add missing `delete cb` that otherwise would leak ([PR #527](https://github.com/versatica/mediasoup/pull/527) based on [PR #526](https://github.com/versatica/mediasoup/pull/526) by @vpalmisano). -* `router.pipeToRouter()`: Fix possible inconsistency in `pipeProducer.paused` status (as discussed in this [thread](https://mediasoup.discourse.group/t/concurrency-architecture/2515/) in the mediasoup forum). -* Update `nlohmann/json` to 3.9.1. -* Update `usrsctp`. -* Update NPM deps. -* Enhance Jitter calculation. - +- Add missing `delete cb` that otherwise would leak ([PR #527](https://github.com/versatica/mediasoup/pull/527) based on [PR #526](https://github.com/versatica/mediasoup/pull/526) by @vpalmisano). +- `router.pipeToRouter()`: Fix possible inconsistency in `pipeProducer.paused` status (as discussed in this [thread](https://mediasoup.discourse.group/t/concurrency-architecture/2515/) in the mediasoup forum). +- Update `nlohmann/json` to 3.9.1. +- Update `usrsctp`. +- Enhance Jitter calculation. ### 3.6.32 -* Fix notifications from `mediasoup-worker` being processed before responses received before them (issue #501). - +- Fix notifications from `mediasoup-worker` being processed before responses received before them (issue #501). ### 3.6.31 -* Move `bufferedAmount` from `dataConsumer.dump()` to `dataConsumer.getStats()`. -* Update NPM deps. - +- Move `bufferedAmount` from `dataConsumer.dump()` to `dataConsumer.getStats()`. ### 3.6.30 -* Add `pipe` option to `transport.consume()`([PR #494](https://github.com/versatica/mediasoup/pull/494)). +- Add `pipe` option to `transport.consume()`([PR #494](https://github.com/versatica/mediasoup/pull/494)). - So the receiver will get all streams from the `Producer`. - It works for any kind of transport (but `PipeTransport` which is always like this). -* Update NPM deps. -* Add `LICENSE` and `PATENTS` files in `libwebrtc` dependency (issue #495). -* Added `worker/src/Utils/README_BASE64_UTILS` (issue #497). -* Update `Catch` to 2.13.4. -* Update `usrsctp`. - +- Add `LICENSE` and `PATENTS` files in `libwebrtc` dependency (issue #495). +- Added `worker/src/Utils/README_BASE64_UTILS` (issue #497). +- Update `usrsctp`. ### 3.6.29 -* Fix wrong message about `rtcMinPort` and `rtcMaxPort`. -* Update deps. -* Improve `EnhancedEventEmitter.safeAsPromise()` (although not used). - +- Fix wrong message about `rtcMinPort` and `rtcMaxPort`. +- Update deps. +- Improve `EnhancedEventEmitter.safeAsPromise()` (although not used). ### 3.6.28 -* Fix replacement of `__MEDIASOUP_VERSION__` in `lib/index.d.ts` (issue #483). -* Update NPM deps. -* `worker/scripts/configure.py`: Handle 'mips64' ([PR #485](https://github.com/versatica/mediasoup/pull/485)). - +- Fix replacement of `__MEDIASOUP_VERSION__` in `lib/index.d.ts` (issue #483). +- `worker/scripts/configure.py`: Handle 'mips64' ([PR #485](https://github.com/versatica/mediasoup/pull/485)). ### 3.6.27 -* Update NPM deps. -* Allow the `mediasoup-worker` process to inherit all environment variables (issue #480). - +- Allow the `mediasoup-worker` process to inherit all environment variables (issue #480). ### 3.6.26 -* BWE tweaks and debug logs. -* Update NPM deps. - +- BWE tweaks and debug logs. ### 3.6.25 -* Update `Catch` to 2.13.2. -* Update NPM deps. -* sctp fixes #479. - +- SCTP fixes ([PR #479](https://github.com/versatica/mediasoup/pull/479)). ### 3.6.24 -* Update `awaitqueue` dependency. - +- Update `awaitqueue` dependency. ### 3.6.23 -* Fix yet another memory leak in Node.js layer due to `PayloadChannel` event listener not being removed. -* Update NPM deps. - +- Fix yet another memory leak in Node.js layer due to `PayloadChannel` event listener not being removed. ### 3.6.22 -* `Transport.cpp`: Provide transport congestion client with RTCP Receiver Reports (#464). -* Update `libuv` to 1.40.0. -* Update Node deps. -* `SctpAssociation.cpp`: increase `sctpBufferedAmount` before sending any data (#472). - +- `Transport.cpp`: Provide transport congestion client with RTCP Receiver Reports (#464). +- Update `libuv` to 1.40.0. +- Update Node deps. +- `SctpAssociation.cpp`: increase `sctpBufferedAmount` before sending any data (#472). ### 3.6.21 -* Fix memory leak in Node.js layer due to `PayloadChannel` event listener not being removed (related to #463). - +- Fix memory leak in Node.js layer due to `PayloadChannel` event listener not being removed (related to #463). ### 3.6.20 -* Remove `-fwrapv` when building mediasoup-worker in `Debug` mode (issue #460). -* Add `MEDIASOUP_MAX_CORES` to limit `NUM_CORES` during mediasoup-worker build ([PR #462](https://github.com/versatica/mediasoup/pull/462)). - +- Remove `-fwrapv` when building `mediasoup-worker` in `Debug` mode (issue #460). +- Add `MEDIASOUP_MAX_CORES` to limit `NUM_CORES` during `mediasoup-worker` build ([PR #462](https://github.com/versatica/mediasoup/pull/462)). ### 3.6.19 -* Update `usrsctp` dependency. -* Update `typescript-eslint` deps. -* Update Node deps. - +- Update `usrsctp` dependency. +- Update `typescript-eslint` deps. +- Update Node deps. ### 3.6.18 -* Fix `ortc.getConsumerRtpParameters()` RTX codec comparison issue ([PR #453](https://github.com/versatica/mediasoup/pull/453)). -* RtpObserver: expose `RtpObserverAddRemoveProducerOptions` for `addProducer()` and `removeProducer()` methods. - +- Fix `ortc.getConsumerRtpParameters()` RTX codec comparison issue ([PR #453](https://github.com/versatica/mediasoup/pull/453)). +- RtpObserver: expose `RtpObserverAddRemoveProducerOptions` for `addProducer()` and `removeProducer()` methods. ### 3.6.17 -* Update `libuv` to 1.39.0. -* Update Node deps. -* SimulcastConsumer: Prefer the highest spatial layer initially ([PR #450](https://github.com/versatica/mediasoup/pull/450)). -* RtpStreamRecv: Set RtpDataCounter window size to 6 secs if DTX (#451) - +- Update `libuv` to 1.39.0. +- Update Node deps. +- SimulcastConsumer: Prefer the highest spatial layer initially ([PR #450](https://github.com/versatica/mediasoup/pull/450)). +- RtpStreamRecv: Set RtpDataCounter window size to 6 secs if DTX (#451) ### 3.6.16 -* `SctpAssociation.cpp`: Fix `OnSctpAssociationBufferedAmount()` call. -* Update deps. -* New API to send data from Node throught SCTP DataConsumer. - +- `SctpAssociation.cpp`: Fix `OnSctpAssociationBufferedAmount()` call. +- Update deps. +- New API to send data from Node throught SCTP DataConsumer. ### 3.6.15 -* Avoid SRTP leak by deleting invalid SSRCs after STRP decryption (issue #437, thanks to @penguinol for reporting). -* Update `usrsctp` dep. -* DataConsumer 'bufferedAmount' implementation ([PR #442](https://github.com/versatica/mediasoup/pull/442)). +- Avoid SRTP leak by deleting invalid SSRCs after STRP decryption (issue #437, thanks to @penguinol for reporting). +- Update `usrsctp` dep. +- DataConsumer 'bufferedAmount' implementation ([PR #442](https://github.com/versatica/mediasoup/pull/442)). ### 3.6.14 -* Fix `usrsctp` vulnerability ([PR #439](https://github.com/versatica/mediasoup/pull/439)). -* Fix issue #435 (thanks to @penguinol for reporting). -* `TransportCongestionControlClient.cpp`: Enable periodic ALR probing to recover faster from network issues. -* Update NPM deps. -* Update `nlohmann::json` C++ dep to 3.9.0. -* Update `Catch` to 2.13.0. +- Fix `usrsctp` vulnerability ([PR #439](https://github.com/versatica/mediasoup/pull/439)). +- Fix issue #435 (thanks to @penguinol for reporting). +- `TransportCongestionControlClient.cpp`: Enable periodic ALR probing to recover faster from network issues. +- Update `nlohmann::json` C++ dep to 3.9.0. ### 3.6.13 -* RTP on `DirectTransport` (issue #433, [PR #434](https://github.com/versatica/mediasoup/pull/434)): +- RTP on `DirectTransport` (issue #433, [PR #434](https://github.com/versatica/mediasoup/pull/434)): - New API `producer.send(rtpPacket: Buffer)`. - New API `consumer.on('rtp', (rtpPacket: Buffer)`. - New API `directTransport.sendRtcp(rtcpPacket: Buffer)`. - New API `directTransport.on('rtcp', (rtpPacket: Buffer)`. - ### 3.6.12 -* Release script. - +- Release script. ### 3.6.11 -* `Transport`: rename `maxSctpSendBufferSize` to `sctpSendBufferSize`. - +- `Transport`: rename `maxSctpSendBufferSize` to `sctpSendBufferSize`. ### 3.6.10 -* `Transport`: Implement `maxSctpSendBufferSize`. -* Update `libuv` to 1.38.1. -* Update `Catch` to 2.12.4. -* Update NPM deps. - +- `Transport`: Implement `maxSctpSendBufferSize`. +- Update `libuv` to 1.38.1. ### 3.6.9 -* `Transport::ReceiveRtpPacket()`: Call `RecvStreamClosed(packet->GetSsrc())` if received RTP packet does not match any `Producer`. -* `Transport::HandleRtcpPacket()`: Ensure `Consumer` is found for received NACK Feedback packets. -* Update NPM deps. -* Update C++ `Catch` dep. -* Fix issue #408. - +- `Transport::ReceiveRtpPacket()`: Call `RecvStreamClosed(packet->GetSsrc())` if received RTP packet does not match any `Producer`. +- `Transport::HandleRtcpPacket()`: Ensure `Consumer` is found for received NACK Feedback packets. +- Fix issue #408. ### 3.6.8 -* Fix SRTP leak due to streams not being removed when a `Producer` or `Consumer` is closed. - - [PR #428](https://github.com/versatica/mediasoup/pull/428) (fixes issues #426). +- Fix SRTP leak due to streams not being removed when a `Producer` or `Consumer` is closed. + - [PR #428](https://github.com/versatica/mediasoup/pull/428) (fixes issues #426). - Credits to credits to @penguinol for reporting and initial work at [PR #427](https://github.com/versatica/mediasoup/pull/427). -* Update `nlohmann::json` C++ dep to 3.8.0. -* C++: Enhance `const` correctness. -* Update NPM deps. - +- Update `nlohmann::json` C++ dep to 3.8.0. +- C++: Enhance `const` correctness. ### 3.6.7 -* `ConsumerScore`: Add `producerScores`, scores of all RTP streams in the producer ordered by encoding (just useful when the producer uses simulcast). +- `ConsumerScore`: Add `producerScores`, scores of all RTP streams in the producer ordered by encoding (just useful when the producer uses simulcast). - [PR #421](https://github.com/versatica/mediasoup/pull/421) (fixes issues #420). -* Hide worker executable console in Windows. +- Hide worker executable console in Windows. - [PR #419](https://github.com/versatica/mediasoup/pull/419) (credits to @BlueMagnificent). -* `RtpStream.cpp`: Fix wrong `std::round()` usage. +- `RtpStream.cpp`: Fix wrong `std::round()` usage. - Issue #423. - ### 3.6.6 -* Update `usrsctp` library. -* Update ESlint and TypeScript related dependencies. - +- Update `usrsctp` library. +- Update ESLint and TypeScript related dependencies. ### 3.6.5 -* Set `score:0` when `dtx:true` is set in an `encoding` and there is no RTP for some seconds for that RTP stream. +- Set `score:0` when `dtx:true` is set in an `encoding` and there is no RTP for some seconds for that RTP stream. - Fixes #415. - ### 3.6.4 -* `gyp`: Fix CLT version detection in OSX Catalina when XCode app is not installed. +- `gyp`: Fix CLT version detection in OSX Catalina when XCode app is not installed. - [PR #413](https://github.com/versatica/mediasoup/pull/413) (credits to @enimo). - ### 3.6.3 -* Modernize TypeScript. - +- Modernize TypeScript. ### 3.6.2 -* Fix crash in `Transport.ts` when closing a `DataConsumer` created on a `DirectTransport`. - +- Fix crash in `Transport.ts` when closing a `DataConsumer` created on a `DirectTransport`. ### 3.6.1 -* Export new `DirectTransport` in `types`. -* Make `DataProducerOptions` optional (not needed when in a `DirectTransport`). - +- Export new `DirectTransport` in `types`. +- Make `DataProducerOptions` optional (not needed when in a `DirectTransport`). ### 3.6.0 -* SCTP/DataChannel termination: +- SCTP/DataChannel termination: - [PR #409](https://github.com/versatica/mediasoup/pull/409) - - Allow the Node application to directly send text/binary messages to mediasoup-worker C++ process so others can consume them using `DataConsumers`. + - Allow the Node application to directly send text/binary messages to `mediasoup-worker` C++ process so others can consume them using `DataConsumers`. - And vice-versa: allow the Node application to directly consume in Node messages send by `DataProducers`. -* Add `WorkerLogTag` TypeScript enum and also add a new 'message' tag into it. - +- Add `WorkerLogTag` TypeScript enum and also add a new 'message' tag into it. ### 3.5.15 -* Simulcast and SVC: Better computation of desired bitrate based on `maxBitrate` field in the `producer.rtpParameters.encodings`. - +- Simulcast and SVC: Better computation of desired bitrate based on `maxBitrate` field in the `producer.rtpParameters.encodings`. ### 3.5.14 -* Update deps, specially `uuid` and `@types/uuid` that had a TypeScript related bug. -* `TransportCongestionClient.cpp`: Improve sender side bandwidth estimation by do not reporting `this->initialAvailableBitrate` as available bitrate due to strange behavior in the algorithm. - +- Update deps, specially `uuid` and `@types/uuid` that had a TypeScript related bug. +- `TransportCongestionClient.cpp`: Improve sender side bandwidth estimation by do not reporting `this->initialAvailableBitrate` as available bitrate due to strange behavior in the algorithm. ### 3.5.13 -* Simplify `GetDesiredBitrate()` in `SimulcastConsumer` and `SvcConsumer`. -* Update `libuv` to 1.38.0. - +- Simplify `GetDesiredBitrate()` in `SimulcastConsumer` and `SvcConsumer`. +- Update `libuv` to 1.38.0. ### 3.5.12 -* `SeqManager.cpp`: Improve performance. +- `SeqManager.cpp`: Improve performance. - [PR #398](https://github.com/versatica/mediasoup/pull/398) (credits to @penguinol). - ### 3.5.11 -* `SeqManager.cpp`: Fix a bug and improve performance. +- `SeqManager.cpp`: Fix a bug and improve performance. - Fixes issue #395 via [PR #396](https://github.com/versatica/mediasoup/pull/396) (credits to @penguinol). -* Drop Node.js 8 support. Minimum supported Node.js version is now 10. -* Upgrade `eslint` and `jest` major versions. - +- Drop Node.js 8 support. Minimum supported Node.js version is now 10. +- Upgrade `eslint` and `jest` major versions. ### 3.5.10 -* `SimulcastConsumer.cpp`: Fix `IncreaseLayer()` method (fixes #394). -* Udpate Node deps. - +- `SimulcastConsumer.cpp`: Fix `IncreaseLayer()` method (fixes #394). +- Udpate Node deps. ### 3.5.9 -* `libwebrtc`: Apply patch by @sspanak and @Ivaka to avoid crash. Related issue: #357. -* `PortManager.cpp`: Do not use `UV_UDP_RECVMMSG` in Windows due to a bug in `libuv` 1.37.0. -* Update Node deps. - +- `libwebrtc`: Apply patch by @sspanak and @Ivaka to avoid crash. Related issue: #357. +- `PortManager.cpp`: Do not use `UV_UDP_RECVMMSG` in Windows due to a bug in `libuv` 1.37.0. +- Update Node deps. ### 3.5.8 -* Enable `UV_UDP_RECVMMSG`: +- Enable `UV_UDP_RECVMMSG`: - Upgrade `libuv` to 1.37.0. - Use `uv_udp_init_ex()` with `UV_UDP_RECVMMSG` flag. - Add our own `uv.gyp` now that `libuv` has removed support for GYP (fixes #384). - ### 3.5.7 -* Fix crash in mediasoup-worker due to conversion from `uint64_t` to `int64_t` (used within `libwebrtc` code. Fixes #357. -* Update `usrsctp` library. -* Update Node deps. - +- Fix crash in `mediasoup-worker` due to conversion from `uint64_t` to `int64_t` (used within `libwebrtc` code. Fixes #357. +- Update `usrsctp` library. +- Update Node deps. ### 3.5.6 -* `SeqManager.cpp`: Fix video lag after a long time. +- `SeqManager.cpp`: Fix video lag after a long time. - Fixes #372 (thanks @penguinol for reporting it and giving the solution). - ### 3.5.5 -* `UdpSocket.cpp`: Revert `uv__udp_recvmmsg()` usage since it notifies about received UDP packets in reverse order. Feature on hold until fixed. - +- `UdpSocket.cpp`: Revert `uv__udp_recvmmsg()` usage since it notifies about received UDP packets in reverse order. Feature on hold until fixed. ### 3.5.4 -* `Transport.cpp`: Enable transport congestion client for the first video Consumer, no matter it's uses simulcast, SVC or a single stream. -* Update `libuv` to 1.35.0. -* `UdpSocket.cpp`: Ensure the new libuv's `uv__udp_recvmmsg()` is used, which is more efficient. - +- `Transport.cpp`: Enable transport congestion client for the first video Consumer, no matter it's uses simulcast, SVC or a single stream. +- Update `libuv` to 1.35.0. +- `UdpSocket.cpp`: Ensure the new libuv's `uv__udp_recvmmsg()` is used, which is more efficient. ### 3.5.3 -* `PlainTransport`: Remove `multiSource` option. It was a hack nobody should use. - +- `PlainTransport`: Remove `multiSource` option. It was a hack nobody should use. ### 3.5.2 -* Enable MID RTP extension in mediasoup to receivers direction (for consumers). +- Enable MID RTP extension in mediasoup to receivers direction (for consumers). - This **requires** mediasoup-client 3.5.2 to work. - ### 3.5.1 -* `PlainTransport`: Fix event name: 'rtcpTuple' => 'rtcptuple'. - +- `PlainTransport`: Fix event name: 'rtcpTuple' => 'rtcptuple'. ### 3.5.0 -* `PipeTransport`: Add support for SRTP and RTP retransmission (RTX + NACK). Useful when connecting two mediasoup servers running in different hosts via pipe transports. -* `PlainTransport`: Add support for SRTP. -* Rename `PlainRtpTransport` to `PlainTransport` everywhere (classes, methods, TypeScript types, etc). Keep previous names and mark them as DEPRECATED. -* Fix vulnarability in IPv6 parser. - +- `PipeTransport`: Add support for SRTP and RTP retransmission (RTX + NACK). Useful when connecting two mediasoup servers running in different hosts via pipe transports. +- `PlainTransport`: Add support for SRTP. +- Rename `PlainRtpTransport` to `PlainTransport` everywhere (classes, methods, TypeScript types, etc). Keep previous names and mark them as DEPRECATED. +- Fix vulnarability in IPv6 parser. ### 3.4.13 -* Update `uuid` dep to 7.0.X (new API). -* Fix crash due wrong array index in `PipeConsumer::FillJson()`. +- Update `uuid` dep to 7.0.X (new API). +- Fix crash due wrong array index in `PipeConsumer::FillJson()`. - Fixes #364 - ### 3.4.12 -* TypeScript: generate `es2020` instead of `es6`. -* Update `usrsctp` library. +- TypeScript: generate `es2020` instead of `es6`. +- Update `usrsctp` library. - Fixes #362 (thanks @chvarlam for reporting it). - ### 3.4.11 -* `IceServer.cpp`: Reject received STUN Binding request with 487 if remote peer indicates ICE-CONTROLLED into it. - +- `IceServer.cpp`: Reject received STUN Binding request with 487 if remote peer indicates ICE-CONTROLLED into it. ### 3.4.10 -* `ProducerOptions`: Rename `keyFrameWaitTime` option to `keyFrameRequestDelay` and make it work as expected. - +- `ProducerOptions`: Rename `keyFrameWaitTime` option to `keyFrameRequestDelay` and make it work as expected. ### 3.4.9 -* Add `Utils::Json::IsPositiveInteger()` to not rely on `is_number_unsigned()` of json lib, which is unreliable due to its design. -* Avoid ES6 `export default` and always use named `export`. -* `router.pipeToRouter()`: Ensure a single `PipeTransport` pair is created between `router1` and `router2`. - - Since the operation is async, it may happen that two simultaneous calls to `router1.pipeToRouter({ producerId: xxx, router: router2 })` would end up generating two pairs of `PipeTranports`. To prevent that, let's use an async queue. -* Add `keyFrameWaitTime` option to `ProducerOptions`. -* Update Node and C++ deps. - +- Add `Utils::Json::IsPositiveInteger()` to not rely on `is_number_unsigned()` of json lib, which is unreliable due to its design. +- Avoid ES6 `export default` and always use named `export`. +- `router.pipeToRouter()`: Ensure a single `PipeTransport` pair is created between `router1` and `router2`. + - Since the operation is async, it may happen that two simultaneous calls to `router1.pipeToRouter({ producerId: xxx, router: router2 })` would end up generating two pairs of `PipeTranports`. To prevent that, let's use an async queue. +- Add `keyFrameWaitTime` option to `ProducerOptions`. +- Update Node and C++ deps. ### 3.4.8 -* `libsrtp.gyp`: Fix regression in mediasoup for Windows. +- `libsrtp.gyp`: Fix regression in mediasoup for Windows. - `libsrtp.gyp`: Modernize it based on the new `BUILD.gn` in Chromium. - `libsrtp.gyp`: Don't include "test" and other targets. - Assume `HAVE_INTTYPES_H`, `HAVE_INT8_T`, etc. in Windows. - Issue details: https://github.com/sctplab/usrsctp/issues/353 -* `gyp` dependency: Add support for Microsoft Visual Studio 2019. +- `gyp` dependency: Add support for Microsoft Visual Studio 2019. - Modify our own `gyp` sources to fix the issue. - CL uploaded to GYP project with the fix. - Issue details: https://github.com/sctplab/usrsctp/issues/347 - ### 3.4.7 -* `PortManager.cpp`: Do not limit the number of failed `bind()` attempts to 20 since it does not work well in scenarios that launch tons of `Workers` with same port range. Instead iterate all ports in the range given to the Worker. -* Do not copy `catch.hpp` into `test/include/` but make the GYP `mediasoup-worker-test` target include the corresponding folder in `deps/catch`. - +- `PortManager.cpp`: Do not limit the number of failed `bind()` attempts to 20 since it does not work well in scenarios that launch tons of `Workers` with same port range. Instead iterate all ports in the range given to the Worker. +- Do not copy `catch.hpp` into `test/include/` but make the GYP `mediasoup-worker-test` target include the corresponding folder in `deps/catch`. ### 3.4.6 -* Update libsrtp to 2.3.0. -* Update ESLint and TypeScript deps. - +- Update libsrtp to 2.3.0. +- Update ESLint and TypeScript deps. ### 3.4.5 -* Update deps. -* Fix text in `./github/Bug_Report.md` so it no longer references the deprecated mailing list. - +- Update deps. +- Fix text in `./github/Bug_Report.md` so it no longer references the deprecated mailing list. ### 3.4.4 -* `Transport.cpp`: Ignore RTCP SDES packets (we don't do anything with them anyway). -* `Producer` and `Consumer` stats: Always show `roundTripTime` (even if calculated value is 0) after a `roundTripTime` > 0 has been seen. - +- `Transport.cpp`: Ignore RTCP SDES packets (we don't do anything with them anyway). +- `Producer` and `Consumer` stats: Always show `roundTripTime` (even if calculated value is 0) after a `roundTripTime` > 0 has been seen. ### 3.4.3 -* `Transport.cpp`: Fix RTCP FIR processing: +- `Transport.cpp`: Fix RTCP FIR processing: - Instead of looking at the media ssrc in the common header, iterate FIR items and look for associated `Consumers` based on ssrcs in each FIR item. - Fixes #350 (thanks @j1elo for reporting and documenting the issue). - ### 3.4.2 -* `SctpAssociation.cpp`: Improve/fix logs. -* Improve Node `EventEmitter` events inline documentation. -* `test-node-sctp.js`: Wait for SCTP association to be open before sending data. - +- `SctpAssociation.cpp`: Improve/fix logs. +- Improve Node `EventEmitter` events inline documentation. +- `test-node-sctp.js`: Wait for SCTP association to be open before sending data. ### 3.4.1 -* Improve mediasoup-worker build system by using `sh` instead of `bash` and default to 4 cores (thanks @smoke, [PR #349](https://github.com/versatica/mediasoup/pull/349)). - +- Improve `mediasoup-worker` build system by using `sh` instead of `bash` and default to 4 cores (thanks @smoke, [PR #349](https://github.com/versatica/mediasoup/pull/349)). ### 3.4.0 -* Add `worker.getResourceUsage()` API. -* Update OpenSSL to 1.1.1d. -* Update `libuv` to 1.34.0. -* Update TypeScript and ESLint NPM dependencies. - +- Add `worker.getResourceUsage()` API. +- Update OpenSSL to 1.1.1d. +- Update `libuv` to 1.34.0. +- Update TypeScript version. ### 3.3.8 -* Update usrsctp dependency (it fixes a potential wrong memory access). +- Update usrsctp dependency (it fixes a potential wrong memory access). - More details in the reported issue: https://github.com/sctplab/usrsctp/issues/408 - ### 3.3.7 -* Fix `version` getter. - +- Fix `version` getter. ### 3.3.6 -* `SctpAssociation.cpp`: Initialize the `usrsctp` socket in the class constructor. Fixes #348. - +- `SctpAssociation.cpp`: Initialize the `usrsctp` socket in the class constructor. Fixes #348. ### 3.3.5 -* Fix usage of a deallocated `RTC::TcpConnection` instance under heavy CPU usage due to mediasoup deleting the instance in the middle of a receiving iteration. Fixes #333. +- Fix usage of a deallocated `RTC::TcpConnection` instance under heavy CPU usage due to mediasoup deleting the instance in the middle of a receiving iteration. Fixes #333. - More details in the commit: https://github.com/versatica/mediasoup/commit/49824baf102ab6d2b01e5bca565c29b8ac0fec22 - ### 3.3.4 -* IPv6 fix: Use `INET6_ADDRSTRLEN` instead of `INET_ADDRSTRLEN`. - +- IPv6 fix: Use `INET6_ADDRSTRLEN` instead of `INET_ADDRSTRLEN`. ### 3.3.3 -* Add `consumer.setPriority()` and `consumer.priority` API to prioritize how the estimated outgoing bitrate in a transport is distributed among all video consumers (in case there is not enough bitrate to satisfy them). -* Make video `SimpleConsumers` play the BWE game by helping in probation generation and bitrate distribution. -* Add `consumer.preferredLayers` getter. -* Rename `enablePacketEvent()` and "packet" event to `enableTraceEvent()` and "trace" event (sorry SEMVER). -* Transport: Add a new "trace" event of type "bwe" with detailed information about bitrates. - +- Add `consumer.setPriority()` and `consumer.priority` API to prioritize how the estimated outgoing bitrate in a transport is distributed among all video consumers (in case there is not enough bitrate to satisfy them). +- Make video `SimpleConsumers` play the BWE game by helping in probation generation and bitrate distribution. +- Add `consumer.preferredLayers` getter. +- Rename `enablePacketEvent()` and "packet" event to `enableTraceEvent()` and "trace" event (sorry SEMVER). +- Transport: Add a new "trace" event of type "bwe" with detailed information about bitrates. ### 3.3.2 -* Improve "packet" event by not firing both "keyframe" and "rtp" types for the same RTP packet. - +- Improve "packet" event by not firing both "keyframe" and "rtp" types for the same RTP packet. ### 3.3.1 - -* Add type "keyframe" as a valid type for "packet" event in `Producers` and `Consumers`. - +- Add type "keyframe" as a valid type for "packet" event in `Producers` and `Consumers`. ### 3.3.0 -* Add transport-cc bandwidth estimation and congestion control in sender and receiver side. -* Run in Windows. -* Rewrite to TypeScript. -* Tons of improvements. - +- Add transport-cc bandwidth estimation and congestion control in sender and receiver side. +- Run in Windows. +- Rewrite to TypeScript. +- Tons of improvements. ### 3.2.5 -* Fix TCP leak (#325). - +- Fix TCP leak (#325). ### 3.2.4 -* `PlainRtpTransport`: Fix comedia mode. - +- `PlainRtpTransport`: Fix comedia mode. ### 3.2.3 -* `RateCalculator`: improve efficiency in `GetRate()` method (#324). - +- `RateCalculator`: improve efficiency in `GetRate()` method (#324). ### 3.2.2 -* `RtpDataCounter`: use window size of 2500 ms instead of 1000 ms. +- `RtpDataCounter`: use window size of 2500 ms instead of 1000 ms. - Fixes false "lack of RTP" detection in some screen sharing usages with simulcast. - Fixes #312. - ### 3.2.1 -* Add RTCP Extended Reports for RTT calculation on receiver RTP stream (thanks @yangjinechofor for initial pull request #314). -* Make mediasoup-worker compile in Armbian Debian Buster (thanks @krishisola, fixes #321). - +- Add RTCP Extended Reports for RTT calculation on receiver RTP stream (thanks @yangjinechofor for initial pull request #314). +- Make `mediasoup-worker` compile in Armbian Debian Buster (thanks @krishisola, fixes #321). ### 3.2.0 -* Add DataChannel support via DataProducers and DataConsumers (#10). -* SRTP: Add support for AEAD GCM (#320). - +- Add DataChannel support via DataProducers and DataConsumers (#10). +- SRTP: Add support for AEAD GCM (#320). ### 3.1.7 -* `PipeConsumer.cpp`: Fix RTCP generation (thanks @vpalmisano). - +- `PipeConsumer.cpp`: Fix RTCP generation (thanks @vpalmisano). ### 3.1.6 -* VP8 and H264: Fix regression in 3.1.5 that produces lot of changes in current temporal layer detection. - +- VP8 and H264: Fix regression in 3.1.5 that produces lot of changes in current temporal layer detection. ### 3.1.5 -* VP8 and H264: Allow packets without temporal layer information even if N temporal layers were announced. - +- VP8 and H264: Allow packets without temporal layer information even if N temporal layers were announced. ### 3.1.4 -* Add `-fPIC` in `cflags` to compile in x86-64. Fixes #315. - +- Add `-fPIC` in `cflags` to compile in x86-64. Fixes #315. ### 3.1.3 -* Set the sender SSRC on PLI and FIR requests [related thread](https://mediasoup.discourse.group/t/broadcasting-a-vp8-rtp-stream-from-gstreamer/93). - +- Set the sender SSRC on PLI and FIR requests [related thread](https://mediasoup.discourse.group/t/broadcasting-a-vp8-rtp-stream-from-gstreamer/93). ### 3.1.2 -* Workaround to detect H264 key frames when Chrome uses external encoder (related [issue](https://bugs.chromium.org/p/webrtc/issues/detail?id=10746)). Fixes #313. - +- Workaround to detect H264 key frames when Chrome uses external encoder (related [issue](https://bugs.chromium.org/p/webrtc/issues/detail?id=10746)). Fixes #313. ### 3.1.1 -* Improve `GetBitratePriority()` method in `SimulcastConsumer` and `SvcConsumer` by checking the total bitrate of all temporal layers in a given producer stream or spatial layer. - +- Improve `GetBitratePriority()` method in `SimulcastConsumer` and `SvcConsumer` by checking the total bitrate of all temporal layers in a given producer stream or spatial layer. ### 3.1.0 -* Add SVC support. It includes VP9 full SVC and VP9 K-SVC as implemented by libwebrtc. -* Prefer Python 2 (if available) over Python 3. This is because there are yet pending issues with gyp + Python 3. - +- Add SVC support. It includes VP9 full SVC and VP9 K-SVC as implemented by libwebrtc. +- Prefer Python 2 (if available) over Python 3. This is because there are yet pending issues with gyp + Python 3. ### 3.0.12 -* Do not require Python 2 to compile mediasoup worker (#207). Both Python 2 and 3 can now be used. - +- Do not require Python 2 to compile mediasoup worker (#207). Both Python 2 and 3 can now be used. ### 3.0.11 -* Codecs: Improve temporal layer switching in VP8 and H264. -* Skip worker compilation if `MEDIASOUP_WORKER_BIN` environment variable is given (#309). This makes it possible to install mediasoup in platforms in which, somehow, gcc > 4.8 is not available during `npm install mediasoup` but it's available later. -* Fix `RtpStreamRecv::TransmissionCounter::GetBitrate()`. - +- Codecs: Improve temporal layer switching in VP8 and H264. +- Skip worker compilation if `MEDIASOUP_WORKER_BIN` environment variable is given (#309). This makes it possible to install mediasoup in platforms in which, somehow, gcc > 4.8 is not available during `npm install mediasoup` but it's available later. +- Fix `RtpStreamRecv::TransmissionCounter::GetBitrate()`. ### 3.0.10 -* `parseScalabilityMode()`: allow "S" as spatial layer (and not just "L"). "L" means "dependent spatial layer" while "S" means "independent spatial layer", which is used in K-SVC (VP9, AV1, etc). - +- `parseScalabilityMode()`: allow "S" as spatial layer (and not just "L"). "L" means "dependent spatial layer" while "S" means "independent spatial layer", which is used in K-SVC (VP9, AV1, etc). ### 3.0.9 -* `RtpStreamSend::ReceiveRtcpReceiverReport()`: improve `rtt` calculation if no Sender Report info is reported in received Received Report. -* Update `libuv` to version 1.29.1. - +- `RtpStreamSend::ReceiveRtcpReceiverReport()`: improve `rtt` calculation if no Sender Report info is reported in received Received Report. +- Update `libuv` to version 1.29.1. ### 3.0.8 -* VP8 & H264: Improve temporal layer switching. - +- VP8 & H264: Improve temporal layer switching. ### 3.0.7 -* RTP frame-marking: Add some missing checks. - +- RTP frame-marking: Add some missing checks. ### 3.0.6 -* Fix regression in proxied RTP header extensions. - +- Fix regression in proxied RTP header extensions. ### 3.0.5 -* Add support for frame-marking RTP extensions and use it to enable temporal layers switching in H264 codec (#305). - +- Add support for frame-marking RTP extensions and use it to enable temporal layers switching in H264 codec (#305). ### 3.0.4 -* Improve RTP probation for simulcast/svc consumers by using proper RTP retransmission with increasing sequence number. - +- Improve RTP probation for simulcast/svc consumers by using proper RTP retransmission with increasing sequence number. ### 3.0.3 -* Simulcast: Improve timestamps extra offset handling by having a map of extra offsets indexed by received timestamps. This helps in case of packet retransmission. - +- Simulcast: Improve timestamps extra offset handling by having a map of extra offsets indexed by received timestamps. This helps in case of packet retransmission. ### 3.0.2 -* Simulcast: proper RTP stream switching by rewriting packet timestamp with a new timestamp calculated from the SenderReports' NTP relationship. - +- Simulcast: proper RTP stream switching by rewriting packet timestamp with a new timestamp calculated from the SenderReports' NTP relationship. ### 3.0.1 -* Fix crash in `SimulcastConsumer::IncreaseLayer()` with Safari and H264 (#300). - +- Fix crash in `SimulcastConsumer::IncreaseLayer()` with Safari and H264 (#300). ### 3.0.0 -* v3 is here! - +- v3 is here! ### 2.6.19 -* `RtpStreamSend.cpp`: Fix a crash in `StorePacket()` when it receives an old packet and there is no space left in the storage buffer (thanks to zkfun for reporting it and providing us with the solution). -* Update deps. - +- `RtpStreamSend.cpp`: Fix a crash in `StorePacket()` when it receives an old packet and there is no space left in the storage buffer (thanks to zkfun for reporting it and providing us with the solution). +- Update deps. ### 2.6.18 -* Fix usage of a deallocated `RTC::TcpConnection` instance under heavy CPU usage due to mediasoup deleting the instance in the middle of a receiving iteration. - +- Fix usage of a deallocated `RTC::TcpConnection` instance under heavy CPU usage due to mediasoup deleting the instance in the middle of a receiving iteration. ### 2.6.17 -* Improve build system by using all available CPU cores in parallel. - +- Improve build system by using all available CPU cores in parallel. ### 2.6.16 -* Don't mandate server port range to be >= 99. - +- Don't mandate server port range to be >= 99. ### 2.6.15 -* Fix NACK retransmissions. - +- Fix NACK retransmissions. ### 2.6.14 -* Fix TCP leak (#325). - +- Fix TCP leak (#325). ### 2.6.13 -* Make mediasoup-worker compile in Armbian Debian Buster (thanks @krishisola, fixes #321). -* Update deps. - +- Make `mediasoup-worker` compile in Armbian Debian Buster (thanks @krishisola, fixes #321). +- Update deps. ### 2.6.12 -* Fix RTCP Receiver Report handling. - +- Fix RTCP Receiver Report handling. ### 2.6.11 -* Update deps. -* Simulcast: Increase profiles one by one unless explicitly forced (fixes #188). - +- Update deps. +- Simulcast: Increase profiles one by one unless explicitly forced (fixes #188). ### 2.6.10 -* `PlainRtpTransport.js`: Add missing methods and events. - +- `PlainRtpTransport.js`: Add missing methods and events. ### 2.6.9 -* Remove a potential crash if a single `encoding` is given in the Producer `rtpParameters` and it has a `profile` value. - +- Remove a potential crash if a single `encoding` is given in the Producer `rtpParameters` and it has a `profile` value. ### 2.6.8 -* C++: Verify in libuv static callbacks that the associated C++ instance has not been deallocated (thanks @artushin and @mariat-atg for reporting and providing valuable help in #258). - +- C++: Verify in libuv static callbacks that the associated C++ instance has not been deallocated (thanks @artushin and @mariat-atg for reporting and providing valuable help in #258). ### 2.6.7 -* Fix wrong destruction of Transports in Router.cpp that generates 100% CPU usage in mediasoup-worker processes. - +- Fix wrong destruction of Transports in Router.cpp that generates 100% CPU usage in `mediasoup-worker` processes. ### 2.6.6 -* Fix a port leak when a WebRtcTransport is remotely closed due to a DTLS close alert (thanks @artushin for reporting it in #259). - +- Fix a port leak when a WebRtcTransport is remotely closed due to a DTLS close alert (thanks @artushin for reporting it in #259). ### 2.6.5 -* RtpPacket: Fix Two-Byte header extensions parsing. - +- RtpPacket: Fix Two-Byte header extensions parsing. ### 2.6.4 -* Upgrade again to OpenSSL 1.1.0j (20 Nov 2018) after adding a workaround for issue [#257](https://github.com/versatica/mediasoup/issues/257). - +- Upgrade again to OpenSSL 1.1.0j (20 Nov 2018) after adding a workaround for issue [#257](https://github.com/versatica/mediasoup/issues/257). ### 2.6.3 -* Downgrade OpenSSL to version 1.1.0h (27 Mar 2018) until issue [#257](https://github.com/versatica/mediasoup/issues/257) is fixed. - +- Downgrade OpenSSL to version 1.1.0h (27 Mar 2018) until issue [#257](https://github.com/versatica/mediasoup/issues/257) is fixed. ### 2.6.2 -* C++: Remove all `Destroy()` class methods and no longer do `delete this`. -* Update libuv to 1.24.1. -* Update OpenSSL to 1.1.0g. - +- C++: Remove all `Destroy()` class methods and no longer do `delete this`. +- Update libuv to 1.24.1. +- Update OpenSSL to 1.1.0g. ### 2.6.1 -* worker: Internal refactor and code cleanup. -* Remove announced support for certain RTCP feedback types that mediasoup does nothing with (and avoid forwarding them to the remote RTP sender). -* fuzzer: fix some wrong memory access in `RtpPacket::Dump()` and `StunMessage::Dump()` (just used during development). +- worker: Internal refactor and code cleanup. +- Remove announced support for certain RTCP feedback types that mediasoup does nothing with (and avoid forwarding them to the remote RTP sender). +- fuzzer: fix some wrong memory access in `RtpPacket::Dump()` and `StunMessage::Dump()` (just used during development). ### 2.6.0 -* Integrate [libFuzzer](http://llvm.org/docs/LibFuzzer.html) into mediasoup (documentation in the `doc` folder). Extensive testing done. Several heap-buffer-overflow and memory leaks fixed. - +- Integrate [libFuzzer](http://llvm.org/docs/LibFuzzer.html) into mediasoup (documentation in the `doc` folder). Extensive testing done. Several heap-buffer-overflow and memory leaks fixed. ### 2.5.6 -* `Producer.cpp`: Remove `UpdateRtpParameters()`. It was broken since Consumers +- `Producer.cpp`: Remove `UpdateRtpParameters()`. It was broken since Consumers were not notified about profile removed and so on, so they may crash. -* `Producer.cpp: Remove some maps and simplify streams handling by having a - single `mapSsrcRtpStreamInfo`. Just keep `mapActiveProfiles` because - `GetActiveProfiles()` method needs it. -* `Producer::MayNeedNewStream()`: Ignore new media streams with new SSRC if +- `Producer.cpp: Remove some maps and simplify streams handling by having a +single `mapSsrcRtpStreamInfo`. Just keep `mapActiveProfiles`because`GetActiveProfiles()` method needs it. +- `Producer::MayNeedNewStream()`: Ignore new media streams with new SSRC if its RID is already in use by other media stream (fixes #235). -* Fix a bad memory access when using two byte RTP header extensions. - +- Fix a bad memory access when using two byte RTP header extensions. ### 2.5.5 -* `Server.js`: If a worker crashes make sure `_latestWorkerIdx` becomes 0. - +- `Server.js`: If a worker crashes make sure `_latestWorkerIdx` becomes 0. ### 2.5.4 -* `server.Room()`: Assign workers incrementally or explicitly via new `workerIdx` argument. -* Add `server.numWorkers` getter. - +- `server.Room()`: Assign workers incrementally or explicitly via new `workerIdx` argument. +- Add `server.numWorkers` getter. ### 2.5.3 -* Don't announce `muxId` nor RTP MID extension support in `Consumer` RTP parameters. - +- Don't announce `muxId` nor RTP MID extension support in `Consumer` RTP parameters. ### 2.5.2 -* Enable RTP MID extension again. - +- Enable RTP MID extension again. ### 2.5.1 -* Disable RTP MID extension until [#230](https://github.com/versatica/mediasoup/issues/230) is fixed. - +- Disable RTP MID extension until [#230](https://github.com/versatica/mediasoup/issues/230) is fixed. ### 2.5.0 -* Add RTP MID extension support. +- Add RTP MID extension support. ### 2.4.6 -* Do not close `Transport` on ICE disconnected (as it would prevent ICE restart on "recv" TCP transports). - +- Do not close `Transport` on ICE disconnected (as it would prevent ICE restart on "recv" TCP transports). ### 2.4.5 -* Improve codec matching. - +- Improve codec matching. ### 2.4.4 -* Fix audio codec matching when `channels` parameter is not given. - +- Fix audio codec matching when `channels` parameter is not given. ### 2.4.3 -* Make `PlainRtpTransport` not leak if port allocation fails (related issue [#224](https://github.com/versatica/mediasoup/issues/224)). - +- Make `PlainRtpTransport` not leak if port allocation fails (related issue [#224](https://github.com/versatica/mediasoup/issues/224)). ### 2.4.2 -* Fix a crash in when no more RTP ports were available (see related issue [#222](https://github.com/versatica/mediasoup/issues/222)). - +- Fix a crash in when no more RTP ports were available (see related issue [#222](https://github.com/versatica/mediasoup/issues/222)). ### 2.4.1 -* Update dependencies. - +- Update dependencies. ### 2.4.0 -* Allow non WebRTC peers to create plain RTP transports (no ICE/DTLS/SRTP but just plain RTP and RTCP) for sending and receiving media. - +- Allow non WebRTC peers to create plain RTP transports (no ICE/DTLS/SRTP but just plain RTP and RTCP) for sending and receiving media. ### 2.3.3 -* Fix C++ syntax to avoid an error when building the worker with clang 8.0.0 (OSX 10.11.6). - +- Fix C++ syntax to avoid an error when building the worker with clang 8.0.0 (OSX 10.11.6). ### 2.3.2 -* `Channel.js`: Upgrade `REQUEST_TIMEOUT` to 20 seconds to avoid timeout errors when the Node or worker thread usage is too high (related to this [issue](https://github.com/versatica/mediasoup-client/issues/48)). - +- `Channel.js`: Upgrade `REQUEST_TIMEOUT` to 20 seconds to avoid timeout errors when the Node or worker thread usage is too high (related to this [issue](https://github.com/versatica/mediasoup-client/issues/48)). ### 2.3.1 -* H264: Check if there is room for the indicated NAL unit size (thanks @ggarber). -* H264: Code cleanup. - +- H264: Check if there is room for the indicated NAL unit size (thanks @ggarber). +- H264: Code cleanup. ### 2.3.0 -* Add new "spy" feature. A "spy" peer cannot produce media and is invisible for other peers in the room. - +- Add new "spy" feature. A "spy" peer cannot produce media and is invisible for other peers in the room. ### 2.2.7 -* Fix H264 simulcast by properly detecting when the profile switching should be done. -* Fix a crash in `Consumer::GetStats()` (see related issue [#196](https://github.com/versatica/mediasoup/issues/196)). - +- Fix H264 simulcast by properly detecting when the profile switching should be done. +- Fix a crash in `Consumer::GetStats()` (see related issue [#196](https://github.com/versatica/mediasoup/issues/196)). ### 2.2.6 -* Add H264 simulcast capability. - +- Add H264 simulcast capability. ### 2.2.5 -* Avoid calling deprecated (NOOP) `SSL_CTX_set_ecdh_auto()` function in OpenSSL >= 1.1.0. - +- Avoid calling deprecated (NOOP) `SSL_CTX_set_ecdh_auto()` function in OpenSSL >= 1.1.0. ### 2.2.4 -* [Fix #4](https://github.com/versatica/mediasoup/issues/4): Avoid DTLS handshake fragmentation. - +- [Fix #4](https://github.com/versatica/mediasoup/issues/4): Avoid DTLS handshake fragmentation. ### 2.2.3 -* [Fix #196](https://github.com/versatica/mediasoup/issues/196): Crash in `Consumer::getStats()` due to wrong `targetProfile`. - +- [Fix #196](https://github.com/versatica/mediasoup/issues/196): Crash in `Consumer::getStats()` due to wrong `targetProfile`. ### 2.2.2 -* Improve [issue #209](https://github.com/versatica/mediasoup/issues/209). - +- Improve [issue #209](https://github.com/versatica/mediasoup/issues/209). ### 2.2.1 -* [Fix #209](https://github.com/versatica/mediasoup/issues/209): `DtlsTransport`: don't crash when signaled fingerprint and DTLS fingerprint do not match (thanks @yangjinecho for reporting it). - +- [Fix #209](https://github.com/versatica/mediasoup/issues/209): `DtlsTransport`: don't crash when signaled fingerprint and DTLS fingerprint do not match (thanks @yangjinecho for reporting it). ### 2.2.0 -* Update Node and C/C++ dependencies. - +- Update Node and C/C++ dependencies. ### 2.1.0 -* Add `localIP` option for `room.createRtpStreamer()` and `transport.startMirroring()` [[PR #199](https://github.com/versatica/mediasoup/pull/199)](https://github.com/versatica/mediasoup/pull/199). - +- Add `localIP` option for `room.createRtpStreamer()` and `transport.startMirroring()` [[PR #199](https://github.com/versatica/mediasoup/pull/199)](https://github.com/versatica/mediasoup/pull/199). ### 2.0.16 -* Improve C++ usage (remove "warning: missing initializer for member" [-Wmissing-field-initializers]). -* Update Travis-CI settings. - +- Improve C++ usage (remove "warning: missing initializer for member" [-Wmissing-field-initializers]). +- Update Travis-CI settings. ### 2.0.15 -* Make `PlainRtpTransport` also send RTCP SR/RR reports (thanks @artushin for reporting). - +- Make `PlainRtpTransport` also send RTCP SR/RR reports (thanks @artushin for reporting). ### 2.0.14 -* [Fix #193](https://github.com/versatica/mediasoup/issues/193): `preferTcp` not honored (thanks @artushin). - +- [Fix #193](https://github.com/versatica/mediasoup/issues/193): `preferTcp` not honored (thanks @artushin). ### 2.0.13 -* Avoid crash when no remote IP/port is given. - +- Avoid crash when no remote IP/port is given. ### 2.0.12 -* Add `handled` and `unhandled` events to `Consumer`. - +- Add `handled` and `unhandled` events to `Consumer`. ### 2.0.11 -* [Fix #185](https://github.com/versatica/mediasoup/issues/185): Consumer: initialize effective profile to 'NONE' (thanks @artushin). -* [Fix #186](https://github.com/versatica/mediasoup/issues/186): NackGenerator code being executed after instance deletion (thanks @baiyufei). - +- [Fix #185](https://github.com/versatica/mediasoup/issues/185): Consumer: initialize effective profile to 'NONE' (thanks @artushin). +- [Fix #186](https://github.com/versatica/mediasoup/issues/186): NackGenerator code being executed after instance deletion (thanks @baiyufei). ### 2.0.10 -* [Fix #183](https://github.com/versatica/mediasoup/issues/183): Always reset the effective `Consumer` profile when removed (thanks @thehappycoder). - +- [Fix #183](https://github.com/versatica/mediasoup/issues/183): Always reset the effective `Consumer` profile when removed (thanks @thehappycoder). ### 2.0.9 -* Make ICE+DTLS more flexible by allowing sending DTLS handshake when ICE is just connected. - +- Make ICE+DTLS more flexible by allowing sending DTLS handshake when ICE is just connected. ### 2.0.8 -* Disable stats periodic retrieval also on remote closure of `Producer` and `WebRtcTransport`. - +- Disable stats periodic retrieval also on remote closure of `Producer` and `WebRtcTransport`. ### 2.0.7 -* [Fix #180](https://github.com/versatica/mediasoup/issues/180): Added missing include `cmath` so that `std::round` can be used (thanks @jacobEAdamson). - +- [Fix #180](https://github.com/versatica/mediasoup/issues/180): Added missing include `cmath` so that `std::round` can be used (thanks @jacobEAdamson). ### 2.0.6 -* [Fix #173](https://github.com/versatica/mediasoup/issues/173): Avoid buffer overflow in `()` (thanks @lightmare). -* Improve stream layers management in `Consumer` by using the new `RtpMonitor` class. - +- [Fix #173](https://github.com/versatica/mediasoup/issues/173): Avoid buffer overflow in `()` (thanks @lightmare). +- Improve stream layers management in `Consumer` by using the new `RtpMonitor` class. ### 2.0.5 -* [Fix #164](https://github.com/versatica/mediasoup/issues/164): Sometimes video freezes forever (no RTP received in browser at all). -* [Fix #160](https://github.com/versatica/mediasoup/issues/160): Assert error in `RTC::Consumer::GetStats()`. - +- [Fix #164](https://github.com/versatica/mediasoup/issues/164): Sometimes video freezes forever (no RTP received in browser at all). +- [Fix #160](https://github.com/versatica/mediasoup/issues/160): Assert error in `RTC::Consumer::GetStats()`. ### 2.0.4 -* [Fix #159](https://github.com/versatica/mediasoup/issues/159): Don’t rely on VP8 payload descriptor flags to assure the existence of data. -* [Fix #160](https://github.com/versatica/mediasoup/issues/160): Reset `targetProfile` when the corresponding profile is removed. - +- [Fix #159](https://github.com/versatica/mediasoup/issues/159): Don’t rely on VP8 payload descriptor flags to assure the existence of data. +- [Fix #160](https://github.com/versatica/mediasoup/issues/160): Reset `targetProfile` when the corresponding profile is removed. ### 2.0.3 -* worker: Fix crash when VP8 payload has no `PictureId`. - +- worker: Fix crash when VP8 payload has no `PictureId`. ### 2.0.2 -* worker: Remove wrong `assert` on `Producer::DeactivateStreamProfiles()`. - +- worker: Remove wrong `assert` on `Producer::DeactivateStreamProfiles()`. ### 2.0.1 -* Update README file. - +- Update README file. ### 2.0.0 -* New design based on `Producers` and `Consumer` plus a mediasoup protocol and the **mediasoup-client** client side SDK. - +- New design based on `Producers` and `Consumer` plus a mediasoup protocol and the **mediasoup-client** client side SDK. ### 1.2.8 -* Fix a crash due to RTX packet processing while the associated `NackGenerator` is not yet created. - +- Fix a crash due to RTX packet processing while the associated `NackGenerator` is not yet created. ### 1.2.7 -* Habemus RTX ([RFC 4588](https://tools.ietf.org/html/rfc4588)) for proper RTP retransmission. - +- Habemus RTX ([RFC 4588](https://tools.ietf.org/html/rfc4588)) for proper RTP retransmission. ### 1.2.6 -* Fix an issue in `buffer.toString()` that makes mediasoup fail in Node 8. -* Update libuv to version 1.12.0. - +- Fix an issue in `buffer.toString()` that makes mediasoup fail in Node 8. +- Update libuv to version 1.12.0. ### 1.2.5 -* Add support for [ICE renomination](https://tools.ietf.org/html/draft-thatcher-ice-renomination). - +- Add support for [ICE renomination](https://tools.ietf.org/html/draft-thatcher-ice-renomination). ### 1.2.4 -* Fix a SDP negotiation issue when the remote peer does not have compatible codecs. - +- Fix a SDP negotiation issue when the remote peer does not have compatible codecs. ### 1.2.3 -* Add video codecs supported by Microsoft Edge. - +- Add video codecs supported by Microsoft Edge. ### 1.2.2 -* `RtpReceiver`: generate RTCP PLI when "rtpraw" or "rtpobject" event listener is set. - +- `RtpReceiver`: generate RTCP PLI when "rtpraw" or "rtpobject" event listener is set. ### 1.2.1 -* `RtpReceiver`: fix an error producing packets when "rtpobject" event is set. - +- `RtpReceiver`: fix an error producing packets when "rtpobject" event is set. ### 1.2.0 -* `RtpSender`: allow `disable()`/`enable()` without forcing SDP renegotiation (#114). - +- `RtpSender`: allow `disable()`/`enable()` without forcing SDP renegotiation (#114). ### 1.1.0 -* Add `Room.on('audiolevels')` event. - +- Add `Room.on('audiolevels')` event. ### 1.0.2 -* Set a maximum value of 1500 bytes for packet storage in `RtpStreamSend`. - +- Set a maximum value of 1500 bytes for packet storage in `RtpStreamSend`. ### 1.0.1 -* Avoid possible segfault if `RemoteBitrateEstimator` generates a bandwidth estimation with zero SSRCs. - +- Avoid possible segfault if `RemoteBitrateEstimator` generates a bandwidth estimation with zero SSRCs. ### 1.0.0 -* First stable release. +- First stable release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..d6daf18d34 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# Contributing to mediasoup + +Thanks for taking the time to contribute to mediasoup! 🎉👍 + +## License + +By contributing to mediasoup, you agree that your contributions will be licensed under its ISC License. + +## Reporting Bugs + +We primarily use GitHub as an issue tracker. Just open an issue in GitHub if you have encountered a bug in mediasoup. + +If you have questions or doubts about mediasoup or need support, please use the mediasoup Discourse Group instead: + +- https://mediasoup.discourse.group + +If you got a crash in mediasoup, please try to provide a core dump into the issue report: + +- https://mediasoup.org/support/#crashes-in-mediasoup-get-a-core-dump + +## Pull Request Process + +When creating a Pull Request for mediasoup: + +- Ensure that changes/additions done in TypeScript files (in `node/src` folder) are also applied to the Rust layer (in `rust` folder), and vice-versa. +- Test units must be added for both Node.js and Rust. +- Changes/additions in C++ code may need tests in `worker/test` folder. + +Once all changes are done, run the following commands to verify that the code in your PR conforms to the code syntax of the project, it does not break existing funtionality and tests pass: + +- `npm run lint`: Check TypeScript and C++ linting rules. Formating errors can be automatically fixed by running `npm run format`. +- `npm run typescript:build`: Compile TypeScript code (under `src` folder) into JavaScript code (under `lib` folder). +- `npm run test`: Run JavaScript and C++ test units. +- Instead, you can run `npm run release:check` which will run all those steps. +- `cargo fmt`, `cargo clippy` and `cargo test` to ensure that everything is good in Rust side. + +The full list of `npm` scripts, `invoke` tasks and `cargo` commands is available in the [doc/Building.md](/doc/Building.md) file. + +## Coding Style + +In adition to automatic checks performed by commands above, we also enforce other minor things related to coding style: + +### Comments in TypeScript and C++ + +We use `//` for inline comments in both JavaScript and C++ source files. + +- Comments must start with upercase letter. +- Comments must not exceed 80 columns (split into different lines if necessary). +- Comments must end with a dot. + +Example (good): + +```ts +// Calculate foo based on bar value. +const foo = bar / 2; +``` + +Example (bad): + +```ts +// calculate foo based on bar value +const foo = bar / 2; +``` + +When adding inline documentation for methods or functions, we use `/** */` syntax. Example: + +```ts +/** + * Calculates current score for foo and bar. + */ +function calculateScore(): number { + // [...] +} +``` diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..5cf4281fc2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2656 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba56612922b907719d4a01cf11c8d5b458e7d3dba946d0435f20f58d6795ed2" +dependencies = [ + "actix-macros", + "actix-rt", + "actix_derive", + "bitflags 2.4.0", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot 0.12.1", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" +dependencies = [ + "bitflags 1.3.2", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92ef85799cba03f76e4f7c10f533e66d87c9a7e7055f3391f09000ad8351bc9" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash 0.8.3", + "base64", + "bitflags 2.4.0", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "futures-core", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.8.5", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.32", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb13e7eef0423ea6eab0e59f6c72e7cb46d33691ad56a726b3cd07ddec2c2d4" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.3", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste 1.0.14", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4a5b5e29603ca8c94a77c65cf874718ceb60292c5a5c3e5f4ace041af462b9" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash 0.8.3", + "bytes", + "bytestring", + "cfg-if", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.5.3", + "time", + "url", +] + +[[package]] +name = "actix-web-actors" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf6e9ccc371cfddbed7aa842256a4abc7a6dcac9f3fce392fe1d0f68cfd136b2" +dependencies = [ + "actix", + "actix-codec", + "actix-http", + "actix-web", + "bytes", + "bytestring", + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1f50ebbb30eca122b188319a4398b3f7bb4a8cdf50ecfb73bfc6a3c3ce54f5" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "actix_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7db3d5a9718568e4cf4a537cfd7070e6e6ff7481510d0237fb529ac850f6d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +dependencies = [ + "getrandom 0.2.10", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom 0.2.10", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "array-init-cursor" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7d0a018de4f6aa429b9d33d69edf69072b1c5b1cb8d3e4a5f7ef898fc3eb76" + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "askama" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb98f10f371286b177db5eeb9a6e5396609555686a35e1d4f7b9a9c6d8af0139" +dependencies = [ + "askama_derive", + "askama_escape", + "askama_shared", +] + +[[package]] +name = "askama_derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87bf87e6e8b47264efa9bde63d6225c6276a52e05e91bf37eaa8afd0032d6b71" +dependencies = [ + "askama_shared", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_shared" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf722b94118a07fcbc6640190f247334027685d4e218b794dbfe17c32bf38ed0" +dependencies = [ + "askama_escape", + "humansize", + "mime", + "mime_guess", + "nom", + "num-traits", + "percent-encoding", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", + "toml", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix 0.37.27", + "slab", + "socket2 0.4.9", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-oneshot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec7c75bcbcb0139e9177f30692fd617405ca4e0c27802e128d53171f7042e2c" +dependencies = [ + "futures-micro", +] + +[[package]] +name = "async-task" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" + +[[package]] +name = "async-trait" +version = "0.1.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "atomic-take" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8ab6b55fe97976e46f91ddbed8d147d966475dc29b2032757ba47e02376fbc3" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "bitpattern" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fc436c543b5fd522e5428d65c88e7bfb538ba267dad95cfb9d8fb8a465b0935" +dependencies = [ + "paste 0.1.18", + "proc-macro2", + "quote", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "bytestring" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +dependencies = [ + "bytes", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "bitflags 1.3.2", + "clap_lex", + "indexmap 1.9.3", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "codespan" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3362992a0d9f1dd7c3d0e89e0ab2bb540b7a95fea8cd798090e758fda2899b5e" +dependencies = [ + "codespan-reporting", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "ena" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533630cf40e9caa44bd91aadc88a75d75a4c3a12b4cfde353cbed41daa1e1f1" +dependencies = [ + "log", +] + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener-primitives" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5384093b4255393cc8c42f0fc3dd4f19977ad3b1025f968257854895a993028c" +dependencies = [ + "nohash-hasher", + "parking_lot 0.11.2", + "smallvec", +] + +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-micro" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b460264b3593d68b16a7bc35f7bc226ddfebdf9a1c8db1ed95d5cc6b7168c826" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "h264-profile-level-id" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45d0dbf6a9847b64c9b06938660891f983434dcc009b5ae6a58f5e34af74a649" +dependencies = [ + "bitpattern", + "log", + "thiserror", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hash_hasher" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74721d007512d0cb3338cd20f0654ac913920061a4c4d0d8708edb3f2a698c0c" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.7", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.3", +] + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humansize" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown 0.14.2", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.2", + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.2", + "rustix 0.38.13", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lalrpop" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da4081d44f4611b66c6dd725e6de3169f9f63905421e8626fcb86b6a898998b8" +dependencies = [ + "ascii-canvas", + "bit-set", + "diff", + "ena", + "is-terminal", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax 0.7.5", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f35c735096c0293d313e8f2a641627472b83d01b937177fe76e5e2708d31e0d" +dependencies = [ + "regex", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" + +[[package]] +name = "local-channel" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", + "serde", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "logos" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf8b031682c67a8e3d5446840f9573eb7fe26efe7ec8d195c9ac4c0647c502f1" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d849148dbaf9661a6151d1ca82b13bb4c4c128146a88d05253b38d4e2f496c" +dependencies = [ + "beef", + "fnv", + "proc-macro2", + "quote", + "regex-syntax 0.6.29", + "syn 1.0.109", +] + +[[package]] +name = "lru" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6e8aaa3f231bb4bd57b84b2d5dc3ae7f350265df8aa96492e0bc394a1571909" + +[[package]] +name = "mediasoup" +version = "0.17.1" +dependencies = [ + "actix", + "actix-web", + "actix-web-actors", + "async-channel", + "async-executor", + "async-io", + "async-lock", + "async-oneshot", + "async-trait", + "atomic-take", + "criterion", + "env_logger", + "event-listener-primitives", + "fastrand", + "futures-lite", + "h264-profile-level-id", + "hash_hasher", + "log", + "lru", + "mediasoup-sys", + "nohash-hasher", + "once_cell", + "parking_lot 0.12.1", + "planus", + "portpicker", + "regex", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "uuid", +] + +[[package]] +name = "mediasoup-sys" +version = "0.9.1" +dependencies = [ + "planus", + "planus-codegen", + "planus-translation", + "serde", +] + +[[package]] +name = "memchr" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.2", + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "os_str_bytes" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" + +[[package]] +name = "parking" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.8", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" +dependencies = [ + "paste-impl", + "proc-macro-hack", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "paste-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" +dependencies = [ + "proc-macro-hack", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.0.2", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "planus" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cbec589fda965e8d0cf7dddaa87d37ae5ccaed0c43986e8faf5998f24ef82f5" +dependencies = [ + "array-init-cursor", + "hashbrown 0.13.2", +] + +[[package]] +name = "planus-codegen" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1bb11066c61d46d6d8f21d342d300f06d08bcce15f88399fdd40e1341c421" +dependencies = [ + "askama", + "eyre", + "heck", + "planus-types", + "random_color", + "thiserror", + "vec_map", +] + +[[package]] +name = "planus-lexer" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecb00082b1324541f02f323585333e88ed3bf90cdbad3741e52eabf166fbcd6" +dependencies = [ + "codespan", + "derive_more", + "logos", +] + +[[package]] +name = "planus-translation" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7bd8ffea859844a23ea28d58354e9e466f3454981b368ba7d8226edac50398b" +dependencies = [ + "atty", + "bitflags 2.4.0", + "codespan", + "codespan-reporting", + "indexmap 1.9.3", + "lalrpop", + "lalrpop-util", + "num-traits", + "planus-lexer", + "planus-types", + "string-interner", +] + +[[package]] +name = "planus-types" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10306bda3579cc7a43537cb3f81212d93ce4fea32a219d9c0a7788d8c5d8e114" +dependencies = [ + "codespan", + "indexmap 1.9.3", + "lalrpop-util", + "planus-lexer", + "string-interner", +] + +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys", +] + +[[package]] +name = "portpicker" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.10", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "random_color" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f34bd6526786b2ce5141fd37a4084b5da1ebae74595b5b0d05482a7cef7181" +dependencies = [ + "rand 0.7.3", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom 0.2.10", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax 0.7.5", +] + +[[package]] +name = "regex-automata" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys", +] + +[[package]] +name = "rustix" +version = "0.38.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys 0.4.10", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + +[[package]] +name = "serde" +version = "1.0.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "serde_json" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "string-interner" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e2531d8525b29b514d25e275a43581320d587b86db302b9a7e464bac579648" +dependencies = [ + "cfg-if", + "hashbrown 0.11.2", + "serde", +] + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot 0.12.1", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "time" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +dependencies = [ + "deranged", + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +dependencies = [ + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.3", + "windows-sys", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "getrandom 0.2.10", + "serde", +] + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.32", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml index efb0f433ee..fac71da63a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "rust", "worker" diff --git a/README.md b/README.md index aa709787ab..28b1204556 100644 --- a/README.md +++ b/README.md @@ -10,58 +10,51 @@ [![][codeql-shield-mediasoup]][codeql-mediasoup] [![][opencollective-shield-mediasoup]][opencollective-mediasoup] - ## Website and Documentation -* [mediasoup.org][mediasoup-website] - +- [mediasoup.org][mediasoup-website] ## Support Forum -* [mediasoup.discourse.group][mediasoup-discourse] - +- [mediasoup.discourse.group][mediasoup-discourse] ## Design Goals mediasoup and its client side libraries are designed to accomplish with the following goals: -* Be a [SFU](https://webrtcglossary.com/sfu/) (Selective Forwarding Unit). -* Support both WebRTC and plain RTP input and output. -* Be a Node.js module/Rust crate in server side. -* Be a tiny JavaScript and C++ libraries in client side. -* Be minimalist: just handle the media layer. -* Be signaling agnostic: do not mandate any signaling protocol. -* Be super low level API. -* Support all existing WebRTC endpoints. -* Enable integration with well known multimedia libraries/tools. - +- Be a [SFU](https://webrtcglossary.com/sfu) (Selective Forwarding Unit). +- Support both WebRTC and plain RTP input and output. +- Be a Node.js module or Rust crate in server side. +- Be a tiny TypeScript and C++ libraries in client side. +- Be minimalist: just handle the media layer. +- Be signaling agnostic: do not mandate any signaling protocol. +- Be super low level API. +- Support all existing WebRTC endpoints. +- Enable integration with well known multimedia libraries/tools. ## Architecture ![][mediasoup-architecture] - ## Use Cases mediasoup and its client side libraries provide a super low level API. They are intended to enable different use cases and scenarios, without any constraint or assumption. Some of these use cases are: -* Group video chat applications. -* One-to-many (or few-to-many) broadcasting applications in real-time. -* RTP streaming. - +- Group video chat applications. +- One-to-many (or few-to-many) broadcasting applications in real-time. +- RTP streaming. ## Features -* ECMAScript 6/Idiomatic Rust low level API. -* Multi-stream: multiple audio/video streams over a single ICE + DTLS transport. -* IPv6 ready. -* ICE / DTLS / RTP / RTCP over UDP and TCP. -* Simulcast and SVC support. -* Congestion control. -* Sender and receiver bandwidth estimation with spatial/temporal layers distribution algorithm. -* Data message exchange (via WebRTC DataChannels, SCTP over plain UDP, and direct termination in Node.js/Rust). -* Extremely powerful (media worker thread/subprocess coded in C++ on top of [libuv](https://libuv.org)). - +- ECMAScript 6/Idiomatic Rust low level API. +- Multi-stream: multiple audio/video streams over a single ICE + DTLS transport. +- IPv6 ready. +- ICE / DTLS / RTP / RTCP over UDP and TCP. +- Simulcast and SVC support. +- Congestion control. +- Sender and receiver bandwidth estimation with spatial/temporal layers distribution algorithm. +- Data message exchange (via WebRTC DataChannels, SCTP over plain UDP, and direct termination in Node.js/Rust). +- Extremely powerful (media worker thread/subprocess coded in C++ on top of [libuv](https://libuv.org)). ## Demo Online @@ -69,31 +62,24 @@ mediasoup and its client side libraries provide a super low level API. They are Try it at [v3demo.mediasoup.org](https://v3demo.mediasoup.org) ([source code](https://github.com/versatica/mediasoup-demo)). - ## Authors -* IÃąaki Baz Castillo [[website](https://inakibaz.me)|[github](https://github.com/ibc/)] -* JosÊ Luis MillÃĄn [[github](https://github.com/jmillan/)] -* Nazar Mokynskyi [[github](https://github.com/nazar-pc/)] - +- IÃąaki Baz Castillo [[website](https://inakibaz.me)|[github](https://github.com/ibc/)] +- JosÊ Luis MillÃĄn [[github](https://github.com/jmillan/)] +- Nazar Mokynskyi [[github](https://github.com/nazar-pc/)] ## Social -* Twitter: [@mediasoup_sfu](https://twitter.com/mediasoup_sfu) - +- Twitter: [@mediasoup_sfu](https://twitter.com/mediasoup_sfu) ## Sponsor You can support mediasoup by [sponsoring][sponsor] it. Thanks! - ## License [ISC](./LICENSE) - - - [mediasoup-banner]: /art/mediasoup-banner.png [mediasoup-website]: https://mediasoup.org [mediasoup-discourse]: https://mediasoup.discourse.group diff --git a/art/mediasoup-v3-architecture-01.svg b/art/mediasoup-v3-architecture-01.svg index 9613ddb17f..89966d0f70 100644 --- a/art/mediasoup-v3-architecture-01.svg +++ b/art/mediasoup-v3-architecture-01.svg @@ -1,2 +1,2 @@ -
Host
Host
Worker 1
Worker 1
WebRtcTransport
WebRtcTransport
Audio Producer
Audio Producer
Video Producer
Video Producer
PlainRtpTransport
PlainRtpTransport
Video Producer
Video Producer
PipeTransport
PipeTransport
Video Consumer
Video Consumer
SRTPSRTP
Participant
(mic/webcam on)
Participant<br>(mic/webcam on)<br>
Participant
(viewer)
[Not supported by viewer]
RTP
FFmpeg
(recording)
[Not supported by viewer]
GStreamer
(mp4 broadcaster)
GStreamer<br>(mp4 broadcaster)<br>
RTP
Router 2
Router 2
Router 1
Router 1
Worker 2
Worker 2
PipeTransport
PipeTransport
Video Consumer
Video Consumer
Router 3
Router 3
PipeTransport
PipeTransport
Video Producer
Video Producer
Worker 3
Worker 3
PipeTransport
PipeTransport
Video Producer
Video Producer
Router 4
Router 4
RTPRTP
Participant
(viewer)
[Not supported by viewer]
Participant
(viewer)
[Not supported by viewer]
SRTPSRTPSRTP
Participant
(viewer)
[Not supported by viewer]
WebRtcTransport
WebRtcTransport
Video Consumer
Video Consumer
WebRtcTransport
WebRtcTransport
Video Consumer
Video Consumer
WebRtcTransport
WebRtcTransport
Video Consumer
Video Consumer
WebRtcTransport
WebRtcTransport
Video Consumer
Video Consumer
Participant
(viewer)
[Not supported by viewer]
SRTP
WebRtcTransport
WebRtcTransport
Audio Consumer
Audio Consumer
Video Consumer
Video Consumer
PlainRtpTransport
PlainRtpTransport
Audio Consumer
Audio Consumer
\ No newline at end of file +
Host
Host
Worker 1
Worker 1
WebRtcTransport
WebRtcTransport
Audio Producer
Audio Producer
Video Producer
Video Producer
PlainTransport
PlainTransport
Video Producer
Video Producer
PipeTransport
PipeTransport
Video Consumer
Video Consumer
SRTPSRTP
Participant
(mic/webcam on)
Participant<br>(mic/webcam on)<br>
Participant
(viewer)
[Not supported by viewer]
RTP
FFmpeg
(recording)
[Not supported by viewer]
GStreamer
(mp4 broadcaster)
GStreamer<br>(mp4 broadcaster)<br>
RTP
Router 2
Router 2
Router 1
Router 1
Worker 2
Worker 2
PipeTransport
PipeTransport
Video Consumer
Video Consumer
Router 3
Router 3
PipeTransport
PipeTransport
Video Producer
Video Producer
Worker 3
Worker 3
PipeTransport
PipeTransport
Video Producer
Video Producer
Router 4
Router 4
RTPRTP
Participant
(viewer)
[Not supported by viewer]
Participant
(viewer)
[Not supported by viewer]
SRTPSRTPSRTP
Participant
(viewer)
[Not supported by viewer]
WebRtcTransport
WebRtcTransport
Video Consumer
Video Consumer
WebRtcTransport
WebRtcTransport
Video Consumer
Video Consumer
WebRtcTransport
WebRtcTransport
Video Consumer
Video Consumer
WebRtcTransport
WebRtcTransport
Video Consumer
Video Consumer
Participant
(viewer)
[Not supported by viewer]
SRTP
WebRtcTransport
WebRtcTransport
Audio Consumer
Audio Consumer
Video Consumer
Video Consumer
PlainTransport
PlainTransport
Audio Consumer
Audio Consumer
diff --git a/doc/Building.md b/doc/Building.md index 456c312724..aaf183591a 100644 --- a/doc/Building.md +++ b/doc/Building.md @@ -2,227 +2,278 @@ This document is intended for mediasoup developers. - ## NPM scripts The `package.json` file in the main folder includes the following scripts: - ### `npm run typescript:build` -Compiles mediasoup TypeScript code (`lib` folder) JavaScript and places it into the `lib` directory. - +Compiles mediasoup TypeScript code (`node/src` folder) JavaScript and places it into the `node/lib` directory. ### `npm run typescript:watch` -Compiles mediasoup TypeScript code (`lib` folder) JavaScript, places it into the `lib` directory an watches for changes in the TypeScript files. - +Compiles mediasoup TypeScript code (`node/src` folder) JavaScript, places it into the `node/lib` directory an watches for changes in the TypeScript files. ### `npm run worker:build` -Builds the `mediasoup-worker` binary. It invokes `make`below. +Builds the `mediasoup-worker` binary. It invokes `invoke`below. +### `npm run worker:prebuild` + +Creates a prebuilt of `mediasoup-worker` binary in the `worker/prebuild` folder. ### `npm run lint` Runs both `npm run lint:node` and `npm run lint:worker`. - ### `npm run lint:node` Validates mediasoup JavaScript files using [ESLint](https://eslint.org). - ### `npm run lint:worker` -Validates mediasoup-worker C++ files using [clang-format](https://clang.llvm.org/docs/ClangFormat.html). It invokes `make lint` below. +Validates mediasoup worker C++ files using [clang-format](https://clang.llvm.org/docs/ClangFormat.html). It invokes `invoke lint` below. +### `npm run format` -### `npm run format:worker` +Runs both `npm run format:node` and `npm run format:worker`. -Rewrites mediasoup-worker C++ files using [clang-format](https://clang.llvm.org/docs/ClangFormat.html). It invokes `make format` below. +### `npm run format:node` +Format TypeScript and JavaScript code using [Prettier](https://prettier.io). -### `npm run test` +### `npm run format:worker` -Runs both `npm run test:node` and `npm run test:worker`. +Rewrites mediasoup worker C++ files using [clang-format](https://clang.llvm.org/docs/ClangFormat.html). It invokes `invoke format` below. +### `npm run flatc` -### `npm run test:node` +Runs both `npm run flatc:node` and `npm run flatc:worker`. -Runs [Jest](https://jestjs.io) test units located at `test/` folder. +### `npm run flatc:node` +Compiles [FlatBuffers](https://github.com/google/flatbuffers) `.fbs` files in `worker/fbs` to TypeScript code. -### `npm run test:worker` +### `npm run flatc:worker` -Runs [Catch2](https://github.com/catchorg/Catch2) test units located at `worker/test/` folder. It invokes `make test` below. +Compiles [FlatBuffers](https://github.com/google/flatbuffers) `.fbs` files in `worker/fbs` to C++ code. +### `npm run test` -### `npm run coverage:node` +Runs both `npm run test:node` and `npm run test:worker`. -Same as `npm run test:node` but it also opens a browser window with JavaScript coverage results. +### `npm run test:node` +Runs [Jest](https://jestjs.io) test units located at `node/test` folder. -### `npm run install-deps:node` +Jest command arguments can be given using `--` as follows: -Installs NPM dependencies and updates `package-lock.json`. +```bash +npm run test:node -- --testPathPattern "test-Worker.ts" --testNamePattern "createWorker" +``` +### `npm run test:worker` -### `npm run install-clang-tools` +Runs [Catch2](https://github.com/catchorg/Catch2) test units located at `worker/test` folder. It invokes `invoke test` below. -Installs clang tools needed for local development. +### `npm run coverage:node` + +Same as `npm run test:node` but it also opens a browser window with JavaScript coverage results. +### `npm run release:check` + +Runs linters and tests in Node and C++ code. ## Rust -The only special feature in Rust case is special environment variable `KEEP_BUILD_ARTIFACTS`, that when set to `1` will allow incremental recompilation of changed C++ sources during hacking on mediasoup. +The only special feature in Rust case is special environment variable "KEEP_BUILD_ARTIFACTS", that when set to "1" will allow incremental recompilation of changed C++ sources during hacking on mediasoup. + It is not necessary for normal usage of mediasoup as a dependency. +## Python Invoke and `tasks.py` file -## Makefile +mediasoup uses Python [Invoke](https://www.pyinvoke.org) library for managing and organizing tasks in the `worker` folder (mediasoup worker C++ subproject). `Invoke` is basically a replacemente of `make` + `Makefile` written in Python. mediasoup automatically installs `Invoke` in a local custom path during the installation process (in both Node and Rust) so the user doesn't need to worry about it. -The `worker` folder contains a `Makefile` for the mediasoup-worker C++ subproject. It includes the following tasks: +Tasks are defined in `worker/tasks.py`. For development purposes, developers or contributors can install `Invoke` using `pip3 install invoke` and run tasks below within the `worker` folder. +See all the tasks by running `invoke --list` within the `worker` folder. -### `make` or `make mediasoup-worker` +_NOTE:_ For some of these tasks to work, npm dependencies of `worker/scripts/package.json` must be installed: -Alias of ``make mediasoup-worker` below. +```bash +npm ci --prefix worker/scripts +``` +### `invoke` (default task) -### `make meson-ninja` +Alias of `invoke mediasoup-worker` task below. -Installs `meson` and `ninja`. +### `invoke meson-ninja` +Installs `meson` and `ninja` into a local custom path. -### `make clean` +### `invoke clean` Cleans built objects and binaries. - -### `make clean-build` +### `invoke clean-build` Cleans built objects and other artifacts, but keeps `mediasoup-worker` binary in place. - -### `make clean-pip` +### `invoke clean-pip` Cleans `meson` and `ninja` installed in local prefix with pip. - -### `make clean-subprojects` +### `invoke clean-subprojects` Cleans subprojects downloaded with Meson. - -### `make clean-all` +### `invoke clean-all` Cleans built objects and binaries, `meson` and `ninja` installed in local prefix with pip and all subprojects downloaded with Meson. +### `invoke update-wrap-file [subproject]` -### `make update-wrap-file` - -Update the wrap file of a subproject with Meson. Usage example: +Updates the wrap file of a subproject (those in `worker/subprojects` folder) with Meson. After updating it, `invoke setup` must be called by passing `MESON_ARGS="--reconfigure"` environment variable. Usage example: ```bash -$ cd worker -$ make update-wrap-file SUBPROJECT=openssl +cd worker +invoke update-wrap-file openssl +MESON_ARGS="--reconfigure" invoke setup ``` +### `invoke mediasoup-worker` -### `make mediasoup-worker` - -Builds the `mediasoup-worker` binary at `worker/out/Release/`. +Builds the `mediasoup-worker` binary at `worker/out/Release`. If the "MEDIASOUP_MAX_CORES" environment variable is set, the build process will use that number of CPU cores. Otherwise it will auto-detect the number of cores in the machine. -"MEDIASOUP_BUILDTYPE" environment variable controls build types, `Release` and `Debug` are presets optimized for those use cases. -Other build types are possible too, but they are not presets and will require "MESON_ARGS" use to customize build configuration. +"MEDIASOUP_BUILDTYPE" environment variable controls build types, "Release" and "Debug" are presets optimized for those use cases. Other build types are possible too, but they are not presets and will require "MESON_ARGS" use to customize build configuration. + Check the meaning of useful macros in the `worker/include/Logger.hpp` header file if you want to enable tracing or other debug information. -Binary is built at `worker/out/MEDIASOUP_BUILDTYPE/build`. +Binary is built at `worker/out/MEDIASOUP_BUILDTYPE/build`. -In order to instruct the mediasoup Node.js module to use the `Debug` mediasoup-worker binary, an environment variable must be set before running the Node.js application: +In order to instruct the mediasoup Node.js module to use the "Debug" mediasoup-worker` binary, an environment variable must be set before running the Node.js application: ```bash -$ MEDIASOUP_BUILDTYPE=Debug node myapp.js +MEDIASOUP_BUILDTYPE=Debug node myapp.js ``` -If the "MEDIASOUP_WORKER_BIN" environment variable is set, mediasoup will use the it as mediasoup-worker binary and **won't** compile the binary: +If the "MEDIASOUP_WORKER_BIN" environment variable is set (it must be an absolute file path), mediasoup will use the it as `mediasoup-worker` binary and **won't** compile the binary: ```bash -$ MEDIASOUP_WORKER_BIN="/home/xxx/src/foo/mediasoup-worker" node myapp.js +MEDIASOUP_WORKER_BIN="/home/xxx/src/foo/mediasoup-worker" node myapp.js ``` +### `invoke libmediasoup-worker` +Builds the `libmediasoup-worker` static library at `worker/out/Release`. -### `make libmediasoup-worker` +"MEDIASOUP_MAX_CORES"` and "MEDIASOUP_BUILDTYPE" environment variables from above still apply for static library build. -Builds the `libmediasoup-worker` static library at `worker/out/Release/`. +### `invoke xcode` -`MEDIASOUP_MAX_CORES` and `MEDIASOUP_BUILDTYPE` environment variables from above still apply for static library build. +Builds a Xcode project for the mediasoup worker subproject. +### `invoke lint` -### `make xcode` +Validates mediasoup worker C++ files using [clang-format](https://clang.llvm.org/docs/ClangFormat.html) and rules in `worker/.clang-format`. -Builds a Xcode project for the mediasoup-worker subproject. +### `invoke format` +Rewrites mediasoup worker C++ files using [clang-format](https://clang.llvm.org/docs/ClangFormat.html). -### `make lint` +### `invoke test` -Validates mediasoup-worker C++ files using [clang-format](https://clang.llvm.org/docs/ClangFormat.html) and rules in `worker/.clang-format`. +Builds and runs the `mediasoup-worker-test` binary at `worker/out/Release` (or at `worker/out/Debug` if the "MEDIASOUP_BUILDTYPE" environment variable is set to "Debug"), which uses [Catch2](https://github.com/catchorg/Catch2) to run test units located at `worker/test` folder. +### `invoke test-asan-address` -### `make format` +Run test with Address Sanitizer with `-fsanitize=address`. -Rewrites mediasoup-worker C++ files using [clang-format](https://clang.llvm.org/docs/ClangFormat.html). +### `invoke test-asan-undefined` +Run test with Address Sanitizer with `-fsanitize=undefined`. -### `make test` +### `invoke test-asan-thread` -Builds and runs the `mediasoup-worker-test` binary at `worker/out/Release/` (or at `worker/out/Debug/` if the "MEDIASOUP_BUILDTYPE" environment variable is set to "Debug"), which uses [Catch2](https://github.com/catchorg/Catch2) to run test units located at `worker/test/` folder. +Run test with Address Sanitizer with `-fsanitize=thread`. +### `invoke tidy` -### `make tidy` - -Runs [clang-tidy](http://clang.llvm.org/extra/clang-tidy/) and performs C++ code checks following `worker/.clang-tidy` rules. +Runs [clang-tidy](http://clang.llvm.org/extra/clang-tidy) and performs C++ code checks following `worker/.clang-tidy` rules. **Requirements:** -* `make clean` and `make` must have been called first. -* [PyYAML](https://pyyaml.org/) is required. - - In OSX install it with `brew install libyaml` and `sudo easy_install-X.Y pyyaml`. +- `invoke clean` and `invoke mediasoup-worker` must have been called first. +- [clang-tools-extra](https://clang.llvm.org/extra) is required. + - In OSX install it with `brew install llvm`. + - In linux the package name is `clang-tools-extra`. + +**Environment variables:** +- "MEDIASOUP_TIDY_CHECKS": Comma separated list of checks. Overrides the checks defined in `worker/.clang-tidy` file. +- "MEDIASOUP_TIDY_FILES": Space separated source files to process, including their path. All `.cpp` files will be processes by default. +- "MEDIASOUP_CLANG_TIDY_DIR": Path to directory containing clang tools (`run-clang-tidy`, `clang-tidy`, `clang-apply-replacements`). -### `make fuzzer` +**Usage example in macOS:** + +```bash +MEDIASOUP_CLANG_TIDY_DIR=/usr/local/opt/llvm/bin invoke tidy +``` -Builds the `mediasoup-worker-fuzzer` binary (which uses [libFuzzer](http://llvm.org/docs/LibFuzzer.html)) at `worker/out/Release/` (or at `worker/out/Debug/` if the "MEDIASOUP_BUILDTYPE" environment variable is set to "Debug"). +### `invoke fuzzer` + +Builds the `mediasoup-worker-fuzzer` binary (which uses [libFuzzer](http://llvm.org/docs/LibFuzzer.html)) at `worker/out/Release` (or at `worker/out/Debug/` if the "MEDIASOUP_BUILDTYPE" environment variable is set to "Debug"). **Requirements:** -* Linux with fuzzer capable clang++. -* `CC` environment variable must point to "clang". -* `CXX` environment variable must point to "clang++". +- Linux with fuzzer capable clang++. +- "CC" environment variable must point to `clang`. +- "CXX" environment variable must point to `clang++`. Read the [Fuzzer](Fuzzer.md) documentation for detailed information. - -### `make fuzzer-run-all` +### `invoke fuzzer-run-all` Runs all fuzzer cases. +### `invoke docker` -### `make docker` - -Builds a Linux image with fuzzer capable clang++. +Builds a Linux Ubuntu Docker image with fuzzer capable clang++ and all dependencies to run mediasoup. **NOTE:** Before running this command, a specific version of Linux clang must be downloaded. To get it, run: ```bash -$ cd worker -$ ./scripts/get-dep.sh clang-fuzzer +cd worker +scripts/get-dep.sh clang-fuzzer ``` +### `invoke docker-run` + +Runs a container of the Ubuntu Docker image created with `invoke docker`. It automatically executes a `bash` session in the `/mediasoup` directory, which is a Docker volume that points to the mediasoup root folder. + +**NOTE:** To install and run mediasoup in the container, previous installation (if any) must be properly cleaned by entering the `worker` directory and running `invoke clean-all`. + +### `invoke docker-alpine` + +Builds a Linux Alpine Docker image with all dependencies to run mediasoup. + +### `invoke docker-alpine-run` + +Runs a container of the Alpine Docker image created with `invoke docker-alpine`. It automatically executes an `ash` session in the `/mediasoup` directory, which is a Docker volume that points to the mediasoup root folder. + +**NOTE:** To install and run mediasoup in the container, previous installation (if any) must be properly cleaned by entering the `worker` directory and running `invoke clean-all`. + +## Makefile + +The `worker` folder contains a `Makefile` file for the mediasoup worker C++ subproject. It acts as a proxy to the `Invoke` tasks defined in `tasks.py`. The `Makefile` file exists to help developers or contributors that prefer keep using `make` commands. -### `make docker-run` +All tasks defined in `tasks.py` (see above) are available in `Makefile`. There is only one exception: -Runs a container of the Docker image created with `make docker`. It automatically executes a `bash` session in the `/mediasoup` directory, which is a Docker volume that points to the real `mediasoup` directory. +- The `update-wrap-file` needs a "SUBPROJECT" environment variable indicating the subproject to update. Usage example: + ```bash + cd worker + make update-wrap-file SUBPROJECT=openssl + ``` diff --git a/doc/Charts.md b/doc/Charts.md index 6be4f00efc..266700b464 100644 --- a/doc/Charts.md +++ b/doc/Charts.md @@ -1,6 +1,5 @@ # Charts - ## Broadcasting mediasoup **v2** (a room uses a single media worker subprocess by design, so a single CPU). @@ -9,9 +8,8 @@ Charts provided by [CoSMo](https://www.cosmosoftware.io) team. Scenario: -* 1 peer producing audio and video tracks. -* N spy peers receiving them. - +- 1 peer producing audio and video tracks. +- N spy peers receiving them. #### Bandwidth out (Mbps) / number of viewers diff --git a/doc/Consumer.md b/doc/Consumer.md index 6c246e8d2d..824ac03a53 100644 --- a/doc/Consumer.md +++ b/doc/Consumer.md @@ -1,92 +1,109 @@ # Consumer - ## RTP sequence number handling ------------------------------------------------- +--- INIT: -* rtpLastSeq: 1000 -* rtpPreviousBaseSeq: 0 -* rtpBaseSeq: 0 +- rtpLastSeq: 1000 +- rtpPreviousBaseSeq: 0 +- rtpBaseSeq: 0 ------------------------------------------------- +--- packet (SYNC): -* seq: 3000 -* rtpPreviousBaseSeq: 1000 -* rtpBaseSeq: 3000 + +- seq: 3000 +- rtpPreviousBaseSeq: 1000 +- rtpBaseSeq: 3000 out: -* rtpLastSeq: 3000 - 3000 + 1000 + 1 = 1001 ------------------------------------------------- +- rtpLastSeq: 3000 - 3000 + 1000 + 1 = 1001 + +--- packet: -* seq: 3001 + +- seq: 3001 out: -* rtpLastSeq: 3001 - 3000 + 1000 + 1 = 1002 ------------------------------------------------- +- rtpLastSeq: 3001 - 3000 + 1000 + 1 = 1002 + +--- packet: -* seq: 3003 + +- seq: 3003 out: -* rtpLastSeq: 3003 - 3000 + 1000 + 1 = 1004 ------------------------------------------------- +- rtpLastSeq: 3003 - 3000 + 1000 + 1 = 1004 + +--- packet (SYNC): -* seq: 4050 -- rtpPreviousBaseSeq: 1004 -- rtpBaseSeq: 4050 + +- seq: 4050 + +* rtpPreviousBaseSeq: 1004 +* rtpBaseSeq: 4050 out: -* rtpLastSeq: 4050 - 4050 + 1004 + 1 = 1005 ------------------------------------------------- +- rtpLastSeq: 4050 - 4050 + 1004 + 1 = 1005 + +--- packet: -* seq: 4051 + +- seq: 4051 out: -* rtpLastSeq: 4051 - 4050 + 1004 + 1 = 1006 ------------------------------------------------- +- rtpLastSeq: 4051 - 4050 + 1004 + 1 = 1006 + +--- packet (DROP): -* seq: 4052 + +- seq: 4052 if (seq > rtpLastSeq) -* rtpBaseSeq++: 4051 ------------------------------------------------- +- rtpBaseSeq++: 4051 + +--- packet: -* seq: 4053 + +- seq: 4053 out: -* rtpLastSeq: 4053 - 4051 + 1004 + 1 = 1007 ------------------------------------------------- +- rtpLastSeq: 4053 - 4051 + 1004 + 1 = 1007 + +--- probation packet: -* rtpLastSeq++ = 1008 + +- rtpLastSeq++ = 1008 out: -* rtpLastSeq: 1008 -* rtpBaseSeq--: 4050 + +- rtpLastSeq: 1008 +- rtpBaseSeq--: 4050 NOTE: probation packets should just be sent (assuming same RTP timestamp) **after** a video packet with `marker` bit set to 1. ------------------------------------------------- +--- packet: -* seq: 4054 + +- seq: 4054 out: -* rtpLastSeq: 4054 - 4050 + 1004 + 1 = 1009 +- rtpLastSeq: 4054 - 4050 + 1004 + 1 = 1009 diff --git a/doc/Fuzzer.md b/doc/Fuzzer.md index 8e39c13406..9514743f42 100644 --- a/doc/Fuzzer.md +++ b/doc/Fuzzer.md @@ -4,14 +4,12 @@ Once we have built the `mediasoup-worker-fuzzer` target in a Linux environment w **NOTE:** From now on, we assume we are in the mediasoup `worker` directory. - ## Related documentation -* [libFuzzer documentation](http://llvm.org/docs/LibFuzzer.html) -* [libFuzzer Tutorial](https://github.com/google/fuzzer-test-suite/blob/master/tutorial/libFuzzerTutorial.md) -* [webrtcH4cKS ~ Lets get better at fuzzing in 2019](https://webrtchacks.com/lets-get-better-at-fuzzing-in-2019-heres-how/) -* [OSS-Fuzz](https://github.com/google/oss-fuzz) - Continuous fuzzing of open source software ("fuzz for me") - +- [libFuzzer documentation](http://llvm.org/docs/LibFuzzer.html) +- [libFuzzer Tutorial](https://github.com/google/fuzzer-test-suite/blob/master/tutorial/libFuzzerTutorial.md) +- [webrtcH4cKS ~ Lets get better at fuzzing in 2019](https://webrtchacks.com/lets-get-better-at-fuzzing-in-2019-heres-how/) +- [OSS-Fuzz](https://github.com/google/oss-fuzz) - Continuous fuzzing of open source software ("fuzz for me") ## Corpus files @@ -19,7 +17,6 @@ The `deps/webrtc-fuzzer-corpora/corpora` directory has corpus directories taken However, given how `libFuzzer` [works](http://llvm.org/docs/LibFuzzer.html#options), the first directory given as command line parameter is not just used for reading corpus files, but also to store newly generated ones. So, it's recommended to pass `fuzzer/new-corpus` as first directory. Such a directory is gitignored. - ## Crash reports When the fuzzer detects an issue it generates a crash report file which contains the bytes given as input. Those files can be individually used later (instead of passing corpus directories) to the fuzzer to verify that the issue has been fixed. @@ -34,57 +31,70 @@ The `fuzzer/reports` directory should be used to store those new crash reports. It's recommended to (also) pass the following options to the fuzzer: -* `-max_len=1400`: We don't need much more input size. +- `-max_len=1400`: We don't need much more input size. For memory leak detection enable the following environment variable: -* `LSAN_OPTIONS=verbosity=1:log_threads=1` +- `LSAN_OPTIONS=verbosity=1:log_threads=1` The mediasoup-worker fuzzer reads some custom environment variables to decide which kind of fuzzing perform: -* `MS_FUZZ_STUN=1`: Do STUN fuzzing. -* `MS_FUZZ_RTP=1`: Do RTP fuzzing. -* `MS_FUZZ_RTCP=1`: Do RTCP fuzzing. -* `MS_FUZZ_UTILS=1`: Do C++ utils fuzzing. -* If none of them is given, then **all** fuzzers are enabled. +- `MS_FUZZ_STUN=1`: Enable STUN fuzzer. +- `MS_FUZZ_DTLS=1`: Enable DTLS fuzzer. +- `MS_FUZZ_RTP=1`: Enable RTP fuzzer. +- `MS_FUZZ_RTCP=1`: Enable RTCP fuzzer. +- `MS_FUZZ_CODECS=1`: Enable audio/video codecs fuzzer. +- `MS_FUZZ_UTILS=1`: Enable C++ utils fuzzer. +- If none of them is given, then **all** fuzzers are enabled. The log level can also be set by setting the `MS_FUZZ_LOG_LEVEL` environment variable to "debug", "warn" or "error" (it is "none" if unset). - ## Usage examples -* Detect memory leaks and just fuzz STUN: +- Detect memory leaks and just fuzz STUN: + +```bash +MS_FUZZ_STUN=1 LSAN_OPTIONS=verbosity=1:log_threads=1 ./out/Release/mediasoup-worker-fuzzer -artifact_prefix=fuzzer/reports/ -max_len=1400 fuzzer/new-corpus deps/webrtc-fuzzer-corpora/corpora/stun-corpus +``` + +- Detect memory leaks and just fuzz DTLS: + +```bash +MS_FUZZ_DTLS=1 LSAN_OPTIONS=verbosity=1:log_threads=1 ./out/Release/mediasoup-worker-fuzzer -artifact_prefix=fuzzer/reports/ -max_len=1400 fuzzer/new-corpus +``` + +- Detect memory leaks and just fuzz RTP: ```bash -$ MS_FUZZ_STUN=1 LSAN_OPTIONS=verbosity=1:log_threads=1 ./out/Release/mediasoup-worker-fuzzer -artifact_prefix=fuzzer/reports/ -max_len=1400 fuzzer/new-corpus deps/webrtc-fuzzer-corpora/corpora/stun-corpus +MS_FUZZ_RTP=1 LSAN_OPTIONS=verbosity=1:log_threads=1 ./out/Release/mediasoup-worker-fuzzer -artifact_prefix=fuzzer/reports/ -max_len=1400 fuzzer/new-corpus deps/webrtc-fuzzer-corpora/corpora/rtp-corpus ``` -* Detect memory leaks and just fuzz RTP: +- Detect memory leaks and just fuzz RTCP: ```bash -$ MS_FUZZ_RTP=1 LSAN_OPTIONS=verbosity=1:log_threads=1 ./out/Release/mediasoup-worker-fuzzer -artifact_prefix=fuzzer/reports/ -max_len=1400 fuzzer/new-corpus deps/webrtc-fuzzer-corpora/corpora/rtp-corpus +MS_FUZZ_RTCP=1 LSAN_OPTIONS=verbosity=1:log_threads=1 ./out/Release/mediasoup-worker-fuzzer -artifact_prefix=fuzzer/reports/ -max_len=1400 fuzzer/new-corpus deps/webrtc-fuzzer-corpora/corpora/rtcp-corpus ``` -* Detect memory leaks and just fuzz RTCP: +- Detect memory leaks and just fuzz audio/video codecs: ```bash -$ MS_FUZZ_RTCP=1 LSAN_OPTIONS=verbosity=1:log_threads=1 ./out/Release/mediasoup-worker-fuzzer -artifact_prefix=fuzzer/reports/ -max_len=1400 fuzzer/new-corpus deps/webrtc-fuzzer-corpora/corpora/rtcp-corpus +MS_FUZZ_CODECS=1 LSAN_OPTIONS=verbosity=1:log_threads=1 ./out/Release/mediasoup-worker-fuzzer -artifact_prefix=fuzzer/reports/ -max_len=1400 fuzzer/new-corpus ``` -* Detect memory leaks and just fuzz mediasoup-worker C++ utils: +- Detect memory leaks and just fuzz mediasoup-worker C++ utils: ```bash -$ MS_FUZZ_UTILS=1 LSAN_OPTIONS=verbosity=1:log_threads=1 ./out/Release/mediasoup-worker-fuzzer -artifact_prefix=fuzzer/reports/ -max_len=2000 fuzzer/new-corpus +MS_FUZZ_UTILS=1 LSAN_OPTIONS=verbosity=1:log_threads=1 ./out/Release/mediasoup-worker-fuzzer -artifact_prefix=fuzzer/reports/ -max_len=2000 fuzzer/new-corpus ``` -* Detect memory leaks and fuzz everything with log level "warn": +- Detect memory leaks and fuzz everything with log level "warn": ```bash -$ MS_FUZZ_LOG_LEVEL=warn LSAN_OPTIONS=verbosity=1:log_threads=1 ./out/Release/mediasoup-worker-fuzzer -artifact_prefix=fuzzer/reports/ -max_len=1400 fuzzer/new-corpus deps/webrtc-fuzzer-corpora/corpora/stun-corpus deps/webrtc-fuzzer-corpora/corpora/rtp-corpus deps/webrtc-fuzzer-corpora/corpora/rtcp-corpus +MS_FUZZ_LOG_LEVEL=warn LSAN_OPTIONS=verbosity=1:log_threads=1 ./out/Release/mediasoup-worker-fuzzer -artifact_prefix=fuzzer/reports/ -max_len=1400 fuzzer/new-corpus deps/webrtc-fuzzer-corpora/corpora/stun-corpus deps/webrtc-fuzzer-corpora/corpora/rtp-corpus deps/webrtc-fuzzer-corpora/corpora/rtcp-corpus ``` -* Verify that a specific crash is fixed: +- Verify that a specific crash is fixed: ```bash -$ LSAN_OPTIONS=verbosity=1:log_threads=1 ./out/Release/mediasoup-worker-fuzzer fuzzer/reports/crash-f39771f7a03c0e7e539d4e52f48f7adad8976404 +LSAN_OPTIONS=verbosity=1:log_threads=1 ./out/Release/mediasoup-worker-fuzzer fuzzer/reports/crash-f39771f7a03c0e7e539d4e52f48f7adad8976404 ``` diff --git a/doc/README.md b/doc/README.md index f17d5a4944..3efef33aa6 100644 --- a/doc/README.md +++ b/doc/README.md @@ -2,9 +2,8 @@ **NOTE:** Internal documentation for developing purposes. Get the mediasoup public documentation at [mediasoup.org](https://mediasoup.org). -* [Building](Building.md) -* [Fuzzer](Fuzzer.md) -* [RTCP](RTCP.md) -* [Consumer](Consumer.md) -* [Charts](Charts.md) - +- [Building](Building.md) +- [Fuzzer](Fuzzer.md) +- [RTCP](RTCP.md) +- [Consumer](Consumer.md) +- [Charts](Charts.md) diff --git a/doc/RTCP.md b/doc/RTCP.md index dac8e9287a..13fde78342 100644 --- a/doc/RTCP.md +++ b/doc/RTCP.md @@ -8,7 +8,6 @@ This document also describes which RTCP packets are locally generated by mediaso mediasoup does not forward feedback information from a remote RTP receiver, which could incur in the remote sender modifying the transmition rate in a way that it would affect the reception quality for the overall participants in a router (i.e. limiting the remote RTP sender transmition rate). - ### Sender Reports mediasoup locally generates the Sender Reports of the streams it sends and processes the Sender Reports is receives from producer endpoints. @@ -150,31 +149,29 @@ This information is locally consumed to perform sender side bandwidth estimation REMB RTCP is generated locally based on the remote bitrate estimation. - ## Mediasoup internal behaviour for each type of RTCP ### Generic RTCP -| | SR | RR | SDES | BYE | APP | -| --------- | -- | -- | ---- | --- | --- | -| Consumer | G | C | | | | -| Producer | C | G | C | I | I | +| | SR | RR | SDES | BYE | APP | +| -------- | --- | --- | ---- | --- | --- | +| Consumer | G | C | | | | +| Producer | C | G | C | I | I | ### RTP Feedback RTCP -| | NACK | TMMBR | TMMBN | TLLEI | ECN-FB | PAUSE-RESUME | TCC | -| --------- | ---- | ----- | ----- | ----- | ------ | ------------ | --- | -| Consumer | C | I | I | I | I | I | | -| Producer | G | | I | I | | | | +| | NACK | TMMBR | TMMBN | TLLEI | ECN-FB | PAUSE-RESUME | TCC | +| -------- | ---- | ----- | ----- | ----- | ------ | ------------ | --- | +| Consumer | C | I | I | I | I | I | | +| Producer | G | | I | I | | | | ## PS Feedback RTCP | | PLI | SLI | RPSI | FIR | TSTR | TSTN | VBCM | PSLI | AFB | REMB | | --------- | --- | --- | ---- | --- | ---- | ---- | ---- | ---- | --- | ---- | -| Consumer | C | I | I | C | I | | I | | I | C | -| Producer | G | | | | | I | | I | | | -| Transport | | | | | | | | | | G | - +| Consumer | C | I | I | C | I | | I | | I | C | +| Producer | G | | | | | I | | I | | | +| Transport | | | | | | | | | | G | ( ): Does not apply. diff --git a/doc/closures.md b/doc/closures.md index 11086ae6c9..bb3dca3150 100644 --- a/doc/closures.md +++ b/doc/closures.md @@ -2,147 +2,147 @@ Some considerations: -* Any JS `xxxxx.yyyyyClosed()` method is equivalent to the corresponding C++ `~Xxxxx()` destructor. They both silently destroy things without generating any internal notifications/events. - - *NOTE:* Yes, the JS `xxxxx.yyyyyClosed()` produces **public** JS event `xxxxx.on('yyyyyclose')`, but that's not internal stuff. - +- Any JS `xxxxx.yyyyyClosed()` method is equivalent to the corresponding C++ `~Xxxxx()` destructor. They both silently destroy things without generating any internal notifications/events. + - _NOTE:_ Yes, the JS `xxxxx.yyyyyClosed()` produces **public** JS event `xxxxx.on('yyyyyclose')`, but that's not internal stuff. ## JS worker.close() -* Public API. -* Kills the mediasoup-worker process (if not already died) via signal. -* Iterates all JS Routers and calls `router.workerClosed()`. +- Public API. +- Kills the mediasoup-worker process (if not already died) via signal. +- Iterates all JS Routers and calls `router.workerClosed()`. ## mediasoup-worker process dies unexpectely -* The JS Worker emits public JS `worker.on('died')`. -* Iterates all JS Routers and calls `router.workerClosed()`. +- The JS Worker emits public JS `worker.on('died')`. +- Iterates all JS Routers and calls `router.workerClosed()`. ## C++ Worker::Close() -* Called when the mediasoup-worker process is killed. -* Iterates all C++ Routers and calls `delete router`. +- Called when the mediasoup-worker process is killed. +- Iterates all C++ Routers and calls `delete router`. ## JS router.workerClosed() -* Private API. -* Emits public JS `router.on('workerclose')`. +- Private API. +- Emits public JS `router.on('workerclose')`. ## JS router.close() -* Public API. -* Sends channel request `WORKER_CLOSE_ROUTER`: +- Public API. +- Sends channel request `WORKER_CLOSE_ROUTER`: - Processed by the C++ Worker. - It removes the C++ Router from its map. - It calls C++ `delete router`. -* Iterates all JS Transports and calls `transport.routerClosed()`. -* Emits private JS `router.on('@close')` (so the JS Worker cleans its map). +- Iterates all JS Transports and calls `transport.routerClosed()`. +- Emits private JS `router.on('@close')` (so the JS Worker cleans its map). ## C++ ~Router() destructor -* Iterates all C++ Transports and calls `delete transport`. +- Iterates all C++ Transports and calls `delete transport`. ## JS transport.routerClosed() -* Private API. -* Iterates all JS Producers and calls `producer.transportClosed()`. -* Iterates all JS Consumers and calls `consumer.transportClosed()`. -* Emits public JS `transport.on('routerclose')`. +- Private API. +- Iterates all JS Producers and calls `producer.transportClosed()`. +- Iterates all JS Consumers and calls `consumer.transportClosed()`. +- Emits public JS `transport.on('routerclose')`. ## JS transport.close() -* Public API. -* Sends channel request `ROUTER_CLOSE_TRANSPORT`. +- Public API. +- Sends channel request `ROUTER_CLOSE_TRANSPORT`. - Processed by the C++ Router. - It calls C++ `transport->Close()` (so the C++ Transport will notify the C++ Router about closed Producers and Consumers in that Transport). - It removes the C++ Transport from its map. - It calls C++ `delete transport`. -* Iterates all JS Producers and calls `producer.transportClosed()`. -* For each JS Producer, the JS Transport emits private JS `transport.on('@producerclose')` (so the JS Router cleans its maps). -* Iterates all JS Consumers and calls `consumer.transportClosed()`. -* Emits private JS `transport.on('@close')` (so the JS Router cleans its map). +- Iterates all JS Producers and calls `producer.transportClosed()`. +- For each JS Producer, the JS Transport emits private JS `transport.on('@producerclose')` (so the JS Router cleans its maps). +- Iterates all JS Consumers and calls `consumer.transportClosed()`. +- Emits private JS `transport.on('@close')` (so the JS Router cleans its map). ## C++ ~Transport() destructor -* Iterates all C++ Producers and calls `delete producer`. -* Iterates all C++ Consumer and calls `delete consumer`. +- Iterates all C++ Producers and calls `delete producer`. +- Iterates all C++ Consumer and calls `delete consumer`. ## C++ Transport::Close() -* Iterates all C++ Producers. For each Producer: +- Iterates all C++ Producers. For each Producer: - Removes it from its map of Producers. - Calls its `listener->OnTransportProducerClosed(this, producer)` (so the C++ Router cleans its maps and calls `consumer->ProducerClosed()` on its associated Consumers). - Calls `delete producer`. -* It clears its map of C++ Producers. -* Iterates all C++ Consumer. For each Consumer: +- It clears its map of C++ Producers. +- Iterates all C++ Consumer. For each Consumer: - Removes it from its map of Consumers. - Call its `listener->OnTransportConsumerClosed(this, consumer)` (so the C++ Router cleans its maps). - Calls `delete consumer`. -* It clears its map of C++ Consumers. +- It clears its map of C++ Consumers. + +_NOTE:_ If a Transport holds a Producer and a Consumer associated to that Producer, ugly things may happen when calling `Transport::Close()`: -*NOTE:* If a Transport holds a Producer and a Consumer associated to that Producer, ugly things may happen when calling `Transport::Close()`: - - While iterating the C++ Producers as above, the C++ Consumer would be deleted (via `Consumer::ProducerClosed()`). - + As far as it's properly removed from the `Transport::mapConsumers` everything would be ok when later iterating the map of Consumers. - - Must ensure that, in this scenario, the JS event `consumer.on('producerclose')` is not called since `consumer.on('transportclose')` is supposed to happen before. - + This won't happen since the JS `Consumer` has removed its channel notifications within its `transportClosed()` method. +- While iterating the C++ Producers as above, the C++ Consumer would be deleted (via `Consumer::ProducerClosed()`). + - As far as it's properly removed from the `Transport::mapConsumers` everything would be ok when later iterating the map of Consumers. +- Must ensure that, in this scenario, the JS event `consumer.on('producerclose')` is not called since `consumer.on('transportclose')` is supposed to happen before. + - This won't happen since the JS `Consumer` has removed its channel notifications within its `transportClosed()` method. ## C++ Router::OnTransportProducerClosed(transport, producer) -* Gets the set of C++ Consumers associated to the closed Producer in its `mapProducerConsumers`. For each Consumer: +- Gets the set of C++ Consumers associated to the closed Producer in its `mapProducerConsumers`. For each Consumer: - Calls `consumer->ProducerClosed()`. -* Deletes the entry in `mapProducerConsumers` with key `producer`. -* Deletes the entry in `mapProducers` with key `producer->id`. +- Deletes the entry in `mapProducerConsumers` with key `producer`. +- Deletes the entry in `mapProducers` with key `producer->id`. ## C++ Router::OnTransportConsumerClosed(transport, consumer) -* Get the associated C++ Producer from `mapConsumerProducer`. -* Remove the closed C++ Consumer from the set of Consumers in the corresponding `mapProducerConsumers` entry for the given Producer. -* Deletes the entry in `mapConsumerProducer` with key `consumer`. +- Get the associated C++ Producer from `mapConsumerProducer`. +- Remove the closed C++ Consumer from the set of Consumers in the corresponding `mapProducerConsumers` entry for the given Producer. +- Deletes the entry in `mapConsumerProducer` with key `consumer`. ## JS producer.transportClosed() -* Private API. -* Emits public JS `producer.on('transportclose')`. +- Private API. +- Emits public JS `producer.on('transportclose')`. ## JS producer.close() -* Public API. -* Sends channel request `TRANSPORT_CLOSE_PRODUCER`. +- Public API. +- Sends channel request `TRANSPORT_CLOSE_PRODUCER`. - Processed by the C++ Transport. - Removes it from its map of Producers. - Calls its `listener->OnTransportProducerClosed(this, producer)` (so the C++ Router cleans its maps and calls `consumer->ProducerClose()` on its associated Consumers). - Calls `delete producer`. -* Emits private JS `producer.on('@close')` (so the JS Transport cleans its map and will also emit private JS `transport.on('@producerclose')` so the JS Router cleans its map). +- Emits private JS `producer.on('@close')` (so the JS Transport cleans its map and will also emit private JS `transport.on('@producerclose')` so the JS Router cleans its map). ## C++ ~Producer() destructor -* Destroys its stuff. +- Destroys its stuff. ## JS consumer.transportClosed() -* Private API. -* Emits public JS `consumer.on('transportclose')`. +- Private API. +- Emits public JS `consumer.on('transportclose')`. ## JS consumer.close() -* Public API. -* Sends channel request `TRANSPORT_CLOSE_CONSUMER`. +- Public API. +- Sends channel request `TRANSPORT_CLOSE_CONSUMER`. - Processed by the C++ Transport. - Removes it from its map of Consumers. - Calls its `listener->OnTransportConsumerClosed(this, consumer)` (so the C++ Router cleans its maps). - Calls `delete consumer`. -* Emits private JS `consumer.on('@close')` (so the JS Transport cleans its map). +- Emits private JS `consumer.on('@close')` (so the JS Transport cleans its map). ## C++ ~Consumer() destructor -* Destroys its stuff. +- Destroys its stuff. ## C++ Consumer::ProducerClosed() -* Called from the C++ Router within the `Router::OnTransportProducerClosed()` listener. -* Send a channel notification `producerclose` to the JS Consumer. +- Called from the C++ Router within the `Router::OnTransportProducerClosed()` listener. +- Send a channel notification `producerclose` to the JS Consumer. - The JS Consumer emits private JS `consumer.on('@produceclose')` (so the JS Transport cleans its map). - The JS Consumer emits public JS `consumer.on('produceclose')`. -* Notifies its C++ Transport via `listener->onConsumerProducerClosed()` which: +- Notifies its C++ Transport via `listener->onConsumerProducerClosed()` which: - cleans its map of Consumers, - notifies the Router via `listener->OnTransportConsumerClosed()`), and - deletes the Consumer. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000000..1e596554d8 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,179 @@ +import eslint from '@eslint/js'; +import tsEslint from 'typescript-eslint'; +import jestEslint from 'eslint-plugin-jest'; +import prettierRecommendedEslint from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; + +const config = tsEslint.config( + { + languageOptions: { + sourceType: 'module', + globals: { ...globals.nodeBuiltin }, + }, + linterOptions: { + noInlineConfig: false, + reportUnusedDisableDirectives: 'error', + }, + }, + eslint.configs.recommended, + { + rules: { + 'constructor-super': 2, + curly: [2, 'all'], + // Unfortunatelly `curly` does not apply to blocks in `switch` cases so + // this is needed. + 'no-restricted-syntax': [ + 2, + { + selector: 'SwitchCase > *.consequent[type!="BlockStatement"]', + message: 'Switch cases without blocks are disallowed', + }, + ], + 'guard-for-in': 2, + 'newline-after-var': 2, + 'newline-before-return': 2, + 'no-alert': 2, + 'no-caller': 2, + 'no-case-declarations': 2, + 'no-catch-shadow': 2, + 'no-class-assign': 2, + 'no-console': 2, + 'no-const-assign': 2, + 'no-debugger': 2, + 'no-dupe-args': 2, + 'no-dupe-keys': 2, + 'no-duplicate-case': 2, + 'no-div-regex': 2, + 'no-empty': [2, { allowEmptyCatch: true }], + 'no-empty-pattern': 2, + 'no-eval': 2, + 'no-extend-native': 2, + 'no-ex-assign': 2, + 'no-extra-bind': 2, + 'no-extra-boolean-cast': 2, + 'no-extra-label': 2, + 'no-fallthrough': 2, + 'no-func-assign': 2, + 'no-global-assign': 2, + 'no-implicit-coercion': 2, + 'no-implicit-globals': 2, + 'no-inner-declarations': 2, + 'no-invalid-regexp': 2, + 'no-invalid-this': 2, + 'no-irregular-whitespace': 2, + 'no-lonely-if': 2, + 'no-multi-str': 2, + 'no-native-reassign': 2, + 'no-negated-in-lhs': 2, + 'no-new': 2, + 'no-new-func': 2, + 'no-new-wrappers': 2, + 'no-obj-calls': 2, + 'no-proto': 2, + 'no-prototype-builtins': 0, + 'no-redeclare': 2, + 'no-regex-spaces': 2, + 'no-restricted-imports': 2, + 'no-return-assign': 2, + 'no-self-assign': 2, + 'no-self-compare': 2, + 'no-sequences': 2, + 'no-shadow': 2, + 'no-shadow-restricted-names': 2, + 'no-sparse-arrays': 2, + 'no-this-before-super': 2, + 'no-throw-literal': 2, + 'no-undef': 2, + 'no-unmodified-loop-condition': 2, + 'no-unreachable': 2, + 'no-unused-vars': [ + 2, + { vars: 'all', args: 'after-used', caughtErrors: 'none' }, + ], + 'no-unused-private-class-members': 2, + 'no-use-before-define': 0, + 'no-useless-call': 2, + 'no-useless-computed-key': 2, + 'no-useless-concat': 2, + 'no-useless-rename': 2, + 'no-var': 2, + 'object-curly-newline': 0, + 'prefer-const': 2, + 'prefer-rest-params': 2, + 'prefer-spread': 2, + 'prefer-template': 2, + 'spaced-comment': [2, 'always'], + strict: 2, + 'valid-typeof': 2, + yoda: 2, + }, + }, + // NOTE: We need to apply this only to .ts files (and not to .mjs files). + ...tsEslint.configs.recommendedTypeChecked.map(item => ({ + ...item, + files: ['node/src/**/*.ts'], + })), + // NOTE: We need to apply this only to .ts files (and not to .mjs files). + ...tsEslint.configs.stylisticTypeChecked.map(item => ({ + ...item, + files: ['node/src/**/*.ts'], + })), + { + name: 'mediasoup .ts files', + files: ['node/src/**/*.ts'], + languageOptions: { + parserOptions: { + projectService: true, + project: 'tsconfig.json', + }, + }, + rules: { + '@typescript-eslint/class-literal-property-style': [2, 'getters'], + '@typescript-eslint/consistent-generic-constructors': [ + 2, + 'type-annotation', + ], + '@typescript-eslint/dot-notation': 0, + '@typescript-eslint/no-unused-vars': [ + 2, + { + vars: 'all', + args: 'after-used', + caughtErrors: 'none', + ignoreRestSiblings: false, + }, + ], + // We want to use `type` instead of `interface`. + '@typescript-eslint/consistent-type-definitions': 0, + // Sorry, we need many `any` usage. + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/explicit-function-return-type': 2, + '@typescript-eslint/no-unsafe-member-access': 0, + '@typescript-eslint/no-unsafe-assignment': 0, + '@typescript-eslint/no-unsafe-call': 0, + '@typescript-eslint/no-unsafe-return': 0, + '@typescript-eslint/no-unsafe-argument': 0, + '@typescript-eslint/consistent-indexed-object-style': 0, + '@typescript-eslint/no-empty-function': 0, + '@typescript-eslint/restrict-template-expressions': 0, + '@typescript-eslint/no-duplicate-type-constituents': [ + 2, + { ignoreUnions: true }, + ], + }, + }, + { + name: 'mediasoup .ts test files', + ...jestEslint.configs['flat/recommended'], + files: ['node/src/test/**/*.ts'], + rules: { + ...jestEslint.configs['flat/recommended'].rules, + 'jest/no-disabled-tests': 2, + 'jest/prefer-expect-assertions': 0, + '@typescript-eslint/no-unnecessary-type-assertion': 0, + }, + }, + prettierRecommendedEslint +); + +export default config; diff --git a/node/.eslintrc.js b/node/.eslintrc.js deleted file mode 100644 index 1fd2b5775c..0000000000 --- a/node/.eslintrc.js +++ /dev/null @@ -1,242 +0,0 @@ -const os = require('os'); - -const isWindows = os.platform() === 'win32'; - -const eslintConfig = -{ - env : - { - es6 : true, - node : true - }, - plugins : [], - settings : {}, - parserOptions : - { - ecmaVersion : 2018, - sourceType : 'module', - ecmaFeatures : - { - impliedStrict : true - }, - lib : [ 'es2018' ], - project : 'node/tsconfig.json' - }, - globals : - { - NodeJS : 'readonly' - }, - rules : - { - 'array-bracket-spacing' : [ 2, 'always', - { - objectsInArrays : true, - arraysInArrays : true - } - ], - 'arrow-parens' : [ 2, 'always' ], - 'arrow-spacing' : 2, - 'block-spacing' : [ 2, 'always' ], - 'brace-style' : [ 2, 'allman', { allowSingleLine: true } ], - 'camelcase' : 2, - 'comma-dangle' : 2, - 'comma-spacing' : [ 2, { before: false, after: true } ], - 'comma-style' : 2, - 'computed-property-spacing' : 2, - 'constructor-super' : 2, - 'func-call-spacing' : 2, - 'generator-star-spacing' : 2, - 'guard-for-in' : 2, - 'indent' : [ 2, 'tab', { 'SwitchCase': 1 } ], - 'key-spacing' : [ 2, - { - singleLine : - { - beforeColon : false, - afterColon : true - }, - multiLine : - { - beforeColon : true, - afterColon : true, - mode : 'minimum', - align : 'colon' - } - } - ], - 'keyword-spacing' : 2, - 'linebreak-style' : [ 2, isWindows ? 'windows' : 'unix' ], - 'lines-around-comment' : [ 2, - { - allowBlockStart : true, - allowObjectStart : true, - beforeBlockComment : false, - beforeLineComment : false - } - ], - 'max-len' : [ 2, 90, - { - tabWidth : 2, - comments : 90, - ignoreUrls : true, - ignoreStrings : true, - ignoreTemplateLiterals : true, - ignoreRegExpLiterals : true - } - ], - 'newline-after-var' : 2, - 'newline-before-return' : 2, - 'newline-per-chained-call' : 2, - 'no-alert' : 2, - 'no-caller' : 2, - 'no-case-declarations' : 2, - 'no-catch-shadow' : 2, - 'no-class-assign' : 2, - 'no-confusing-arrow' : 2, - 'no-console' : 2, - 'no-const-assign' : 2, - 'no-debugger' : 2, - 'no-dupe-args' : 2, - 'no-dupe-keys' : 2, - 'no-duplicate-case' : 2, - 'no-div-regex' : 2, - 'no-empty' : [ 2, { allowEmptyCatch: true } ], - 'no-empty-pattern' : 2, - 'no-else-return' : 0, - 'no-eval' : 2, - 'no-extend-native' : 2, - 'no-ex-assign' : 2, - 'no-extra-bind' : 2, - 'no-extra-boolean-cast' : 2, - 'no-extra-label' : 2, - 'no-extra-semi' : 2, - 'no-fallthrough' : 2, - 'no-func-assign' : 2, - 'no-global-assign' : 2, - 'no-implicit-coercion' : 2, - 'no-implicit-globals' : 2, - 'no-inner-declarations' : 2, - 'no-invalid-regexp' : 2, - 'no-invalid-this' : 2, - 'no-irregular-whitespace' : 2, - 'no-lonely-if' : 2, - 'no-mixed-operators' : 2, - 'no-mixed-spaces-and-tabs' : 2, - 'no-multi-spaces' : 2, - 'no-multi-str' : 2, - 'no-multiple-empty-lines' : [ 1, { max: 1, maxEOF: 0, maxBOF: 0 } ], - 'no-native-reassign' : 2, - 'no-negated-in-lhs' : 2, - 'no-new' : 2, - 'no-new-func' : 2, - 'no-new-wrappers' : 2, - 'no-obj-calls' : 2, - 'no-proto' : 2, - 'no-prototype-builtins' : 0, - 'no-redeclare' : 2, - 'no-regex-spaces' : 2, - 'no-restricted-imports' : 2, - 'no-return-assign' : 2, - 'no-self-assign' : 2, - 'no-self-compare' : 2, - 'no-sequences' : 2, - 'no-shadow' : 2, - 'no-shadow-restricted-names' : 2, - 'no-spaced-func' : 2, - 'no-sparse-arrays' : 2, - 'no-this-before-super' : 2, - 'no-throw-literal' : 2, - 'no-undef' : 2, - 'no-unexpected-multiline' : 2, - 'no-unmodified-loop-condition' : 2, - 'no-unreachable' : 2, - 'no-unused-vars' : [ 1, { vars: 'all', args: 'after-used' } ], - 'no-use-before-define' : 0, - 'no-useless-call' : 2, - 'no-useless-computed-key' : 2, - 'no-useless-concat' : 2, - 'no-useless-rename' : 2, - 'no-var' : 2, - 'no-whitespace-before-property' : 2, - 'object-curly-newline' : 0, - 'object-curly-spacing' : [ 2, 'always' ], - 'object-property-newline' : [ 2, { allowMultiplePropertiesPerLine: true } ], - 'prefer-const' : 2, - 'prefer-rest-params' : 2, - 'prefer-spread' : 2, - 'prefer-template' : 2, - 'quotes' : [ 2, 'single', { avoidEscape: true } ], - 'semi' : [ 2, 'always' ], - 'semi-spacing' : 2, - 'space-before-blocks' : 2, - 'space-before-function-paren' : [ 2, - { - anonymous : 'never', - named : 'never', - asyncArrow : 'always' - } - ], - 'space-in-parens' : [ 2, 'never' ], - 'spaced-comment' : [ 2, 'always' ], - 'strict' : 2, - 'valid-typeof' : 2, - 'yoda' : 2 - }, - overrides : [] -}; - -eslintConfig.overrides.push( - { - files : [ '*.ts' ], - parser : '@typescript-eslint/parser', - plugins : [ - ...eslintConfig.plugins, - '@typescript-eslint' - ], - extends : [ - 'eslint:recommended', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended' - ], - rules : { - ...eslintConfig.rules, - 'no-unused-vars' : 0, - '@typescript-eslint/ban-types' : 0, - '@typescript-eslint/ban-ts-comment' : 0, - '@typescript-eslint/ban-ts-ignore' : 0, - '@typescript-eslint/explicit-module-boundary-types' : 0, - '@typescript-eslint/semi' : 2, - '@typescript-eslint/member-delimiter-style' : [ 2, - { - multiline : { delimiter: 'semi', requireLast: true }, - singleline : { delimiter: 'semi', requireLast: false } - } - ], - '@typescript-eslint/no-explicit-any' : 0, - '@typescript-eslint/no-unused-vars' : [ 2, - { - vars : 'all', - args : 'after-used', - ignoreRestSiblings : false - } - ], - '@typescript-eslint/no-use-before-define' : [ 2, { functions: false } ], - '@typescript-eslint/no-empty-function' : 0, - '@typescript-eslint/no-non-null-assertion' : 0 - } - }); - -eslintConfig.overrides.push( - { - files : [ '*.ts' ], - env : { - ...eslintConfig.env, - 'jest/globals' : true - }, - plugins : [ - ...eslintConfig.plugins, - 'jest' - ] - }); - -module.exports = eslintConfig; diff --git a/node/src/ActiveSpeakerObserver.ts b/node/src/ActiveSpeakerObserver.ts index 063853c882..a46ac16ab4 100644 --- a/node/src/ActiveSpeakerObserver.ts +++ b/node/src/ActiveSpeakerObserver.ts @@ -1,94 +1,94 @@ import { Logger } from './Logger'; -import { EnhancedEventEmitter } from './EnhancedEventEmitter'; -import { - RtpObserver, - RtpObserverEvents, - RtpObserverObserverEvents, - RtpObserverConstructorOptions -} from './RtpObserver'; -import { Producer } from './Producer'; - -export type ActiveSpeakerObserverOptions = -{ - interval?: number; - - /** - * Custom application data. - */ - appData?: Record; -}; - -export type ActiveSpeakerObserverDominantSpeaker = -{ - /** - * The audio Producer instance. - */ - producer: Producer; -}; - -export type ActiveSpeakerObserverEvents = RtpObserverEvents & -{ - dominantspeaker: [ActiveSpeakerObserverDominantSpeaker]; -}; - -export type ActiveSpeakerObserverObserverEvents = RtpObserverObserverEvents & -{ - dominantspeaker: [ActiveSpeakerObserverDominantSpeaker]; -}; - -type RtpObserverObserverConstructorOptions = RtpObserverConstructorOptions; +import { EnhancedEventEmitter } from './enhancedEvents'; +import type { + ActiveSpeakerObserver, + ActiveSpeakerObserverDominantSpeaker, + ActiveSpeakerObserverEvents, + ActiveSpeakerObserverObserver, + ActiveSpeakerObserverObserverEvents, +} from './ActiveSpeakerObserverTypes'; +import type { RtpObserver } from './RtpObserverTypes'; +import { RtpObserverImpl, RtpObserverConstructorOptions } from './RtpObserver'; +import type { AppData } from './types'; +import { Event, Notification } from './fbs/notification'; +import * as FbsActiveSpeakerObserver from './fbs/active-speaker-observer'; + +type RtpObserverObserverConstructorOptions = + RtpObserverConstructorOptions; const logger = new Logger('ActiveSpeakerObserver'); -export class ActiveSpeakerObserver extends RtpObserver +export class ActiveSpeakerObserverImpl< + ActiveSpeakerObserverAppData extends AppData = AppData, + > + extends RtpObserverImpl< + ActiveSpeakerObserverAppData, + ActiveSpeakerObserverEvents, + ActiveSpeakerObserverObserver + > + implements RtpObserver, ActiveSpeakerObserver { - /** - * @private - */ - constructor(options: RtpObserverObserverConstructorOptions) - { - super(options); + constructor( + options: RtpObserverObserverConstructorOptions + ) { + const observer: ActiveSpeakerObserverObserver = + new EnhancedEventEmitter(); + + super(options, observer); this.handleWorkerNotifications(); + this.handleListenerError(); } - /** - * Observer. - */ - get observer(): EnhancedEventEmitter - { + get type(): 'activespeaker' { + return 'activespeaker'; + } + + get observer(): ActiveSpeakerObserverObserver { return super.observer; } - private handleWorkerNotifications(): void - { - this.channel.on(this.internal.rtpObserverId, (event: string, data?: any) => - { - switch (event) - { - case 'dominantspeaker': - { - const producer = this.getProducerById(data.producerId); - - if (!producer) - break; + private handleWorkerNotifications(): void { + this.channel.on( + this.internal.rtpObserverId, + (event: Event, data?: Notification) => { + switch (event) { + case Event.ACTIVESPEAKEROBSERVER_DOMINANT_SPEAKER: { + const notification = + new FbsActiveSpeakerObserver.DominantSpeakerNotification(); - const dominantSpeaker: ActiveSpeakerObserverDominantSpeaker = - { - producer - }; + data!.body(notification); - this.safeEmit('dominantspeaker', dominantSpeaker); - this.observer.safeEmit('dominantspeaker', dominantSpeaker); + const producer = this.getProducerById(notification.producerId()!); - break; - } + if (!producer) { + break; + } + + const dominantSpeaker: ActiveSpeakerObserverDominantSpeaker = { + producer, + }; - default: - { - logger.error('ignoring unknown event "%s"', event); + this.safeEmit('dominantspeaker', dominantSpeaker); + this.observer.safeEmit('dominantspeaker', dominantSpeaker); + + break; + } + + default: { + logger.error(`ignoring unknown event "${event}"`); + } } } + ); + } + + private handleListenerError(): void { + this.on('listenererror', (eventName, error) => { + logger.error( + `event listener threw an error [eventName:${eventName}]:`, + error + ); }); } } diff --git a/node/src/ActiveSpeakerObserverTypes.ts b/node/src/ActiveSpeakerObserverTypes.ts new file mode 100644 index 0000000000..9d00005697 --- /dev/null +++ b/node/src/ActiveSpeakerObserverTypes.ts @@ -0,0 +1,59 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { + RtpObserver, + RtpObserverEvents, + RtpObserverObserverEvents, +} from './RtpObserverTypes'; +import type { Producer } from './ProducerTypes'; +import type { AppData } from './types'; + +export type ActiveSpeakerObserverOptions< + ActiveSpeakerObserverAppData extends AppData = AppData, +> = { + interval?: number; + + /** + * Custom application data. + */ + appData?: ActiveSpeakerObserverAppData; +}; + +export type ActiveSpeakerObserverDominantSpeaker = { + /** + * The audio Producer instance. + */ + producer: Producer; +}; + +export type ActiveSpeakerObserverEvents = RtpObserverEvents & { + dominantspeaker: [ActiveSpeakerObserverDominantSpeaker]; +}; + +export type ActiveSpeakerObserverObserver = + EnhancedEventEmitter; + +export type ActiveSpeakerObserverObserverEvents = RtpObserverObserverEvents & { + dominantspeaker: [ActiveSpeakerObserverDominantSpeaker]; +}; + +export interface ActiveSpeakerObserver< + ActiveSpeakerObserverAppData extends AppData = AppData, +> extends RtpObserver< + ActiveSpeakerObserverAppData, + ActiveSpeakerObserverEvents, + ActiveSpeakerObserverObserver + > { + /** + * RtpObserver type. + * + * @override + */ + get type(): 'activespeaker'; + + /** + * Observer. + * + * @override + */ + get observer(): ActiveSpeakerObserverObserver; +} diff --git a/node/src/AudioLevelObserver.ts b/node/src/AudioLevelObserver.ts index 0b0f8cf6c3..5890956fdb 100644 --- a/node/src/AudioLevelObserver.ts +++ b/node/src/AudioLevelObserver.ts @@ -1,132 +1,127 @@ import { Logger } from './Logger'; -import { EnhancedEventEmitter } from './EnhancedEventEmitter'; -import { - RtpObserver, - RtpObserverEvents, - RtpObserverObserverEvents, - RtpObserverConstructorOptions -} from './RtpObserver'; -import { Producer } from './Producer'; - -export type AudioLevelObserverOptions = -{ - /** - * Maximum number of entries in the 'volumes”' event. Default 1. - */ - maxEntries?: number; - - /** - * Minimum average volume (in dBvo from -127 to 0) for entries in the - * 'volumes' event. Default -80. - */ - threshold?: number; - - /** - * Interval in ms for checking audio volumes. Default 1000. - */ - interval?: number; - - /** - * Custom application data. - */ - appData?: Record; -}; - -export type AudioLevelObserverVolume = -{ - /** - * The audio Producer instance. - */ - producer: Producer; - - /** - * The average volume (in dBvo from -127 to 0) of the audio Producer in the - * last interval. - */ - volume: number; -}; - -export type AudioLevelObserverEvents = RtpObserverEvents & -{ - volumes: [AudioLevelObserverVolume[]]; - silence: []; -}; - -export type AudioLevelObserverObserverEvents = RtpObserverObserverEvents & -{ - volumes: [AudioLevelObserverVolume[]]; - silence: []; -}; - -type AudioLevelObserverConstructorOptions = RtpObserverConstructorOptions; +import { EnhancedEventEmitter } from './enhancedEvents'; +import type { + AudioLevelObserver, + AudioLevelObserverVolume, + AudioLevelObserverEvents, + AudioLevelObserverObserver, + AudioLevelObserverObserverEvents, +} from './AudioLevelObserverTypes'; +import type { RtpObserver } from './RtpObserverTypes'; +import { RtpObserverImpl, RtpObserverConstructorOptions } from './RtpObserver'; +import type { Producer } from './ProducerTypes'; +import type { AppData } from './types'; +import * as fbsUtils from './fbsUtils'; +import { Event, Notification } from './fbs/notification'; +import * as FbsAudioLevelObserver from './fbs/audio-level-observer'; + +type AudioLevelObserverConstructorOptions = + RtpObserverConstructorOptions; const logger = new Logger('AudioLevelObserver'); -export class AudioLevelObserver extends RtpObserver +export class AudioLevelObserverImpl< + AudioLevelObserverAppData extends AppData = AppData, + > + extends RtpObserverImpl< + AudioLevelObserverAppData, + AudioLevelObserverEvents, + AudioLevelObserverObserver + > + implements RtpObserver, AudioLevelObserver { - /** - * @private - */ - constructor(options: AudioLevelObserverConstructorOptions) - { - super(options); + constructor( + options: AudioLevelObserverConstructorOptions + ) { + const observer: AudioLevelObserverObserver = + new EnhancedEventEmitter(); + + super(options, observer); this.handleWorkerNotifications(); + this.handleListenerError(); } - /** - * Observer. - */ - get observer(): EnhancedEventEmitter - { - return super.observer; + get type(): 'audiolevel' { + return 'audiolevel'; } - private handleWorkerNotifications(): void - { - this.channel.on(this.internal.rtpObserverId, (event: string, data?: any) => - { - switch (event) - { - case 'volumes': - { - // Get the corresponding Producer instance and remove entries with - // no Producer (it may have been closed in the meanwhile). - const volumes: AudioLevelObserverVolume[] = data - .map(({ producerId, volume }: { producerId: string; volume: number }) => ( - { - producer : this.getProducerById(producerId), - volume - } - )) - .filter(({ producer }: { producer: Producer }) => producer); - - if (volumes.length > 0) - { - this.safeEmit('volumes', volumes); + get observer(): AudioLevelObserverObserver { + return super.observer; + } - // Emit observer event. - this.observer.safeEmit('volumes', volumes); + private handleWorkerNotifications(): void { + this.channel.on( + this.internal.rtpObserverId, + (event: Event, data?: Notification) => { + switch (event) { + case Event.AUDIOLEVELOBSERVER_VOLUMES: { + const notification = + new FbsAudioLevelObserver.VolumesNotification(); + + data!.body(notification); + + // Get the corresponding Producer instance and remove entries with + // no Producer (it may have been closed in the meanwhile). + const volumes: AudioLevelObserverVolume[] = fbsUtils + .parseVector(notification, 'volumes', parseVolume) + .map( + ({ + producerId, + volume, + }: { + producerId: string; + volume: number; + }) => ({ + producer: this.getProducerById(producerId)!, + volume, + }) + ) + .filter(({ producer }: { producer: Producer }) => producer); + + if (volumes.length > 0) { + this.safeEmit('volumes', volumes); + + // Emit observer event. + this.observer.safeEmit('volumes', volumes); + } + + break; } - break; - } - - case 'silence': - { - this.safeEmit('silence'); + case Event.AUDIOLEVELOBSERVER_SILENCE: { + this.safeEmit('silence'); - // Emit observer event. - this.observer.safeEmit('silence'); + // Emit observer event. + this.observer.safeEmit('silence'); - break; - } + break; + } - default: - { - logger.error('ignoring unknown event "%s"', event); + default: { + logger.error(`ignoring unknown event "${event}"`); + } } } + ); + } + + private handleListenerError(): void { + this.on('listenererror', (eventName, error) => { + logger.error( + `event listener threw an error [eventName:${eventName}]:`, + error + ); }); } } + +function parseVolume(binary: FbsAudioLevelObserver.Volume): { + producerId: string; + volume: number; +} { + return { + producerId: binary.producerId()!, + volume: binary.volume(), + }; +} diff --git a/node/src/AudioLevelObserverTypes.ts b/node/src/AudioLevelObserverTypes.ts new file mode 100644 index 0000000000..dca5b0e568 --- /dev/null +++ b/node/src/AudioLevelObserverTypes.ts @@ -0,0 +1,81 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { + RtpObserver, + RtpObserverEvents, + RtpObserverObserverEvents, +} from './RtpObserverTypes'; +import type { Producer } from './ProducerTypes'; +import type { AppData } from './types'; + +export type AudioLevelObserverOptions< + AudioLevelObserverAppData extends AppData = AppData, +> = { + /** + * Maximum number of entries in the 'volumes”' event. Default 1. + */ + maxEntries?: number; + + /** + * Minimum average volume (in dBvo from -127 to 0) for entries in the + * 'volumes' event. Default -80. + */ + threshold?: number; + + /** + * Interval in ms for checking audio volumes. Default 1000. + */ + interval?: number; + + /** + * Custom application data. + */ + appData?: AudioLevelObserverAppData; +}; + +export type AudioLevelObserverVolume = { + /** + * The audio Producer instance. + */ + producer: Producer; + + /** + * The average volume (in dBvo from -127 to 0) of the audio Producer in the + * last interval. + */ + volume: number; +}; + +export type AudioLevelObserverEvents = RtpObserverEvents & { + volumes: [AudioLevelObserverVolume[]]; + silence: []; +}; + +export type AudioLevelObserverObserver = + EnhancedEventEmitter; + +export type AudioLevelObserverObserverEvents = RtpObserverObserverEvents & { + volumes: [AudioLevelObserverVolume[]]; + silence: []; +}; + +export interface AudioLevelObserver< + AudioLevelObserverAppData extends AppData = AppData, +> extends RtpObserver< + AudioLevelObserverAppData, + AudioLevelObserverEvents, + AudioLevelObserverObserver + > { + /** + * RtpObserver type. + * + * @override + */ + get type(): 'audiolevel'; + + /** + * Observer. + * + * @override + */ + get observer(): AudioLevelObserverObserver; +} diff --git a/node/src/Channel.ts b/node/src/Channel.ts index bfa5e677e1..6621d1e188 100644 --- a/node/src/Channel.ts +++ b/node/src/Channel.ts @@ -1,14 +1,25 @@ -import * as os from 'os'; -import { Duplex } from 'stream'; +import * as os from 'node:os'; +import { Duplex } from 'node:stream'; +import { info, warn } from 'node:console'; +import * as flatbuffers from 'flatbuffers'; import { Logger } from './Logger'; -import { EnhancedEventEmitter } from './EnhancedEventEmitter'; +import { EnhancedEventEmitter } from './enhancedEvents'; import { InvalidStateError } from './errors'; +import { Body as RequestBody, Method, Request } from './fbs/request'; +import { Response } from './fbs/response'; +import { Message, Body as MessageBody } from './fbs/message'; +import { + Notification, + Body as NotificationBody, + Event, +} from './fbs/notification'; +import { Log } from './fbs/log'; + +const IS_LITTLE_ENDIAN = os.endianness() === 'LE'; -const littleEndian = os.endianness() == 'LE'; const logger = new Logger('Channel'); -type Sent = -{ +type Sent = { id: number; method: string; resolve: (data?: any) => void; @@ -20,8 +31,7 @@ type Sent = const MESSAGE_MAX_LEN = 4194308; const PAYLOAD_MAX_LEN = 4194304; -export class Channel extends EnhancedEventEmitter -{ +export class Channel extends EnhancedEventEmitter { // Closed flag. #closed = false; @@ -40,21 +50,18 @@ export class Channel extends EnhancedEventEmitter // Buffer for reading messages from the worker. #recvBuffer = Buffer.alloc(0); - /** - * @private - */ - constructor( - { - producerSocket, - consumerSocket, - pid - }: - { - producerSocket: any; - consumerSocket: any; - pid: number; - }) - { + // flatbuffers builder. + #bufferBuilder: flatbuffers.Builder = new flatbuffers.Builder(1024); + + constructor({ + producerSocket, + consumerSocket, + pid, + }: { + producerSocket: any; + consumerSocket: any; + pid: number; + }) { super(); logger.debug('constructor()'); @@ -63,21 +70,17 @@ export class Channel extends EnhancedEventEmitter this.#consumerSocket = consumerSocket as Duplex; // Read Channel responses/notifications from the worker. - this.#consumerSocket.on('data', (buffer: Buffer) => - { - if (!this.#recvBuffer.length) - { + this.#consumerSocket.on('data', (buffer: Buffer) => { + if (!this.#recvBuffer.length) { this.#recvBuffer = buffer; - } - else - { + } else { this.#recvBuffer = Buffer.concat( - [ this.#recvBuffer, buffer ], - this.#recvBuffer.length + buffer.length); + [this.#recvBuffer, buffer], + this.#recvBuffer.length + buffer.length + ); } - if (this.#recvBuffer.length > PAYLOAD_MAX_LEN) - { + if (this.#recvBuffer.length > PAYLOAD_MAX_LEN) { logger.error('receiving buffer is full, discarding all data in it'); // Reset the buffer and exit. @@ -88,115 +91,123 @@ export class Channel extends EnhancedEventEmitter let msgStart = 0; - while (true) // eslint-disable-line no-constant-condition - { + while (true) { const readLen = this.#recvBuffer.length - msgStart; - if (readLen < 4) - { + if (readLen < 4) { // Incomplete data. break; } const dataView = new DataView( this.#recvBuffer.buffer, - this.#recvBuffer.byteOffset + msgStart); - const msgLen = dataView.getUint32(0, littleEndian); + this.#recvBuffer.byteOffset + msgStart + ); + const msgLen = dataView.getUint32(0, IS_LITTLE_ENDIAN); - if (readLen < 4 + msgLen) - { + if (readLen < 4 + msgLen) { // Incomplete data. break; } - const payload = this.#recvBuffer.subarray(msgStart + 4, msgStart + 4 + msgLen); + const payload = this.#recvBuffer.subarray( + msgStart + 4, + msgStart + 4 + msgLen + ); msgStart += 4 + msgLen; - try - { - // We can receive JSON messages (Channel messages) or log strings. - switch (payload[0]) - { - // 123 = '{' (a Channel JSON message). - case 123: - this.processMessage(JSON.parse(payload.toString('utf8'))); - break; + const buf = new flatbuffers.ByteBuffer(new Uint8Array(payload)); + const message = Message.getRootAsMessage(buf); - // 68 = 'D' (a debug log). - case 68: - logger.debug(`[pid:${pid}] ${payload.toString('utf8', 1)}`); - break; + try { + switch (message.dataType()) { + case MessageBody.Response: { + const response = new Response(); - // 87 = 'W' (a warn log). - case 87: - logger.warn(`[pid:${pid}] ${payload.toString('utf8', 1)}`); - break; + message.data(response); + + this.processResponse(response); - // 69 = 'E' (an error log). - case 69: - logger.error(`[pid:${pid} ${payload.toString('utf8', 1)}`); break; + } + + case MessageBody.Notification: { + const notification = new Notification(); + + message.data(notification); + + this.processNotification(notification); - // 88 = 'X' (a dump log). - case 88: - // eslint-disable-next-line no-console - console.log(payload.toString('utf8', 1)); break; + } - default: - // eslint-disable-next-line no-console - console.warn( - `worker[pid:${pid}] unexpected data: %s`, - payload.toString('utf8', 1)); + case MessageBody.Log: { + const log = new Log(); + + message.data(log); + + this.processLog(pid, log); + + break; + } + + default: { + warn( + `worker[pid:${pid}] unexpected data: ${payload.toString( + 'utf8', + 1 + )}` + ); + } } - } - catch (error) - { + } catch (error) { logger.error( - 'received invalid message from the worker process: %s', - String(error)); + `received invalid message from the worker process: ${error}` + ); } } - if (msgStart != 0) - { + if (msgStart != 0) { this.#recvBuffer = this.#recvBuffer.slice(msgStart); } }); - this.#consumerSocket.on('end', () => ( + this.#consumerSocket.on('end', () => logger.debug('Consumer Channel ended by the worker process') - )); + ); - this.#consumerSocket.on('error', (error) => ( - logger.error('Consumer Channel error: %s', String(error)) - )); + this.#consumerSocket.on('error', error => + logger.error(`Consumer Channel error: ${error}`) + ); - this.#producerSocket.on('end', () => ( + this.#producerSocket.on('end', () => logger.debug('Producer Channel ended by the worker process') - )); + ); - this.#producerSocket.on('error', (error) => ( - logger.error('Producer Channel error: %s', String(error)) - )); + this.#producerSocket.on('error', error => + logger.error(`Producer Channel error: ${error}`) + ); } /** - * @private + * flatbuffer builder. */ - close(): void - { - if (this.#closed) + get bufferBuilder(): flatbuffers.Builder { + return this.#bufferBuilder; + } + + close(): void { + if (this.#closed) { return; + } logger.debug('close()'); this.#closed = true; // Close every pending sent. - for (const sent of this.#sents.values()) - { + for (const sent of this.#sents.values()) { sent.close(); } @@ -210,64 +221,175 @@ export class Channel extends EnhancedEventEmitter this.#producerSocket.removeAllListeners('error'); this.#producerSocket.on('error', () => {}); - // Destroy the socket after a while to allow pending incoming messages. - setTimeout(() => - { - try { this.#producerSocket.destroy(); } - catch (error) {} - try { this.#consumerSocket.destroy(); } - catch (error) {} - }, 200); + // Destroy the sockets. + try { + this.#producerSocket.destroy(); + } catch (error) {} + try { + this.#consumerSocket.destroy(); + } catch (error) {} } - /** - * @private - */ - async request(method: string, handlerId?: string, data?: any): Promise - { - this.#nextId < 4294967295 ? ++this.#nextId : (this.#nextId = 1); + notify( + event: Event, + bodyType?: NotificationBody, + bodyOffset?: number, + handlerId?: string + ): void { + logger.debug(`notify() [event:${Event[event]}]`); + + if (this.#closed) { + throw new InvalidStateError( + `Channel closed, cannot send notification [event:${Event[event]}]` + ); + } + + const handlerIdOffset = this.#bufferBuilder.createString(handlerId); + + let notificationOffset: number; + + if (bodyType && bodyOffset) { + notificationOffset = Notification.createNotification( + this.#bufferBuilder, + handlerIdOffset, + event, + bodyType, + bodyOffset + ); + } else { + notificationOffset = Notification.createNotification( + this.#bufferBuilder, + handlerIdOffset, + event, + NotificationBody.NONE, + 0 + ); + } + + const messageOffset = Message.createMessage( + this.#bufferBuilder, + MessageBody.Notification, + notificationOffset + ); + + // Finalizes the buffer and adds a 4 byte prefix with the size of the buffer. + this.#bufferBuilder.finishSizePrefixed(messageOffset); + + // Create a new buffer with this data so multiple contiguous flatbuffers + // do not point to the builder buffer overriding others info. + const buffer = new Uint8Array(this.#bufferBuilder.asUint8Array()); + + // Clear the buffer builder so it's reused for the next request. + this.#bufferBuilder.clear(); + + if (buffer.byteLength > MESSAGE_MAX_LEN) { + throw new Error(`notification too big [event:${Event[event]}]`); + } + + try { + // This may throw if closed or remote side ended. + this.#producerSocket.write(buffer, 'binary'); + } catch (error) { + logger.warn(`notify() | sending notification failed: ${error}`); + + return; + } + } + + async request( + method: Method, + bodyType?: RequestBody, + bodyOffset?: number, + handlerId?: string + ): Promise { + logger.debug(`request() [method:${Method[method]}]`); + + if (this.#closed) { + throw new InvalidStateError( + `Channel closed, cannot send request [method:${Method[method]}]` + ); + } + + if (this.#nextId < 4294967295) { + ++this.#nextId; + } else { + this.#nextId = 1; + } const id = this.#nextId; - logger.debug('request() [method:%s, id:%s]', method, id); + const handlerIdOffset = this.#bufferBuilder.createString(handlerId ?? ''); + + let requestOffset: number; + + if (bodyType && bodyOffset) { + requestOffset = Request.createRequest( + this.#bufferBuilder, + id, + method, + handlerIdOffset, + bodyType, + bodyOffset + ); + } else { + requestOffset = Request.createRequest( + this.#bufferBuilder, + id, + method, + handlerIdOffset, + RequestBody.NONE, + 0 + ); + } + + const messageOffset = Message.createMessage( + this.#bufferBuilder, + MessageBody.Request, + requestOffset + ); - if (this.#closed) - throw new InvalidStateError('Channel closed'); + // Finalizes the buffer and adds a 4 byte prefix with the size of the buffer. + this.#bufferBuilder.finishSizePrefixed(messageOffset); - const request = `${id}:${method}:${handlerId}:${JSON.stringify(data)}`; + // Create a new buffer with this data so multiple contiguous flatbuffers + // do not point to the builder buffer overriding others info. + const buffer = new Uint8Array(this.#bufferBuilder.asUint8Array()); - if (Buffer.byteLength(request) > MESSAGE_MAX_LEN) - throw new Error('Channel request too big'); + // Clear the buffer builder so it's reused for the next request. + this.#bufferBuilder.clear(); + + if (buffer.byteLength > MESSAGE_MAX_LEN) { + throw new Error(`request too big [method:${Method[method]}]`); + } // This may throw if closed or remote side ended. - this.#producerSocket.write( - Buffer.from(Uint32Array.of(Buffer.byteLength(request)).buffer)); - this.#producerSocket.write(request); - - return new Promise((pResolve, pReject) => - { - const sent: Sent = - { - id : id, - method : method, - resolve : (data2) => - { - if (!this.#sents.delete(id)) + this.#producerSocket.write(buffer, 'binary'); + + return new Promise((pResolve, pReject) => { + const sent: Sent = { + id: id, + method: Method[method], + resolve: data2 => { + if (!this.#sents.delete(id)) { return; + } pResolve(data2); }, - reject : (error) => - { - if (!this.#sents.delete(id)) + reject: error => { + if (!this.#sents.delete(id)) { return; + } pReject(error); }, - close : () => - { - pReject(new InvalidStateError('Channel closed')); - } + close: () => { + pReject( + new InvalidStateError( + `Channel closed, pending request aborted [method:${Method[method]}, id:${id}]` + ) + ); + }, }; // Add sent stuff to the map. @@ -275,67 +397,89 @@ export class Channel extends EnhancedEventEmitter }); } - private processMessage(msg: any): void - { - // If a response, retrieve its associated request. - if (msg.id) - { - const sent = this.#sents.get(msg.id); + private processResponse(response: Response): void { + const sent = this.#sents.get(response.id()); - if (!sent) - { - logger.error( - 'received response does not match any sent request [id:%s]', msg.id); + if (!sent) { + logger.error( + `received response does not match any sent request [id:${response.id()}]` + ); - return; + return; + } + + if (response.accepted()) { + logger.debug(`request succeeded [method:${sent.method}, id:${sent.id}]`); + + sent.resolve(response); + } else if (response.error()) { + logger.warn( + `request failed [method:${sent.method}, id:${ + sent.id + }]: ${response.reason()}` + ); + + switch (response.error()!) { + case 'TypeError': { + sent.reject(new TypeError(response.reason()!)); + + break; + } + + default: { + sent.reject(new Error(response.reason()!)); + } } + } else { + logger.error( + `received response is not accepted nor rejected [method:${sent.method}, id:${sent.id}]` + ); + } + } - if (msg.accepted) - { - logger.debug( - 'request succeeded [method:%s, id:%s]', sent.method, sent.id); + private processNotification(notification: Notification): void { + // Due to how Promises work, it may happen that we receive a response + // from the worker followed by a notification from the worker. If we + // emit the notification immediately it may reach its target **before** + // the response, destroying the ordered delivery. So we must wait a bit + // here. + // See https://github.com/versatica/mediasoup/issues/510 + setImmediate(() => + this.emit(notification.handlerId()!, notification.event(), notification) + ); + } - sent.resolve(msg.data); + private processLog(pid: number, log: Log): void { + const logData = log.data()!; + + switch (logData[0]) { + // 'D' (a debug log). + case 'D': { + logger.debug(`[pid:${pid}] ${logData.slice(1)}`); + + break; } - else if (msg.error) - { - logger.warn( - 'request failed [method:%s, id:%s]: %s', - sent.method, sent.id, msg.reason); - - switch (msg.error) - { - case 'TypeError': - sent.reject(new TypeError(msg.reason)); - break; - - default: - sent.reject(new Error(msg.reason)); - } + + // 'W' (a warn log). + case 'W': { + logger.warn(`[pid:${pid}] ${logData.slice(1)}`); + + break; } - else - { - logger.error( - 'received response is not accepted nor rejected [method:%s, id:%s]', - sent.method, sent.id); + + // 'E' (a error log). + case 'E': { + logger.error(`[pid:${pid}] ${logData.slice(1)}`); + + break; + } + + // 'X' (a dump log). + case 'X': { + info(logData.slice(1)); + + break; } - } - // If a notification emit it to the corresponding entity. - else if (msg.targetId && msg.event) - { - // Due to how Promises work, it may happen that we receive a response - // from the worker followed by a notification from the worker. If we - // emit the notification immediately it may reach its target **before** - // the response, destroying the ordered delivery. So we must wait a bit - // here. - // See https://github.com/versatica/mediasoup/issues/510 - setImmediate(() => this.emit(String(msg.targetId), msg.event, msg.data)); - } - // Otherwise unexpected message. - else - { - logger.error( - 'received message is not a response nor a notification'); } } } diff --git a/node/src/Consumer.ts b/node/src/Consumer.ts index 23c5d47a07..ac092a5d2f 100644 --- a/node/src/Consumer.ts +++ b/node/src/Consumer.ts @@ -1,198 +1,54 @@ import { Logger } from './Logger'; -import { EnhancedEventEmitter } from './EnhancedEventEmitter'; +import { EnhancedEventEmitter } from './enhancedEvents'; +import type { + Consumer, + ConsumerType, + ConsumerScore, + ConsumerLayers, + ConsumerDump, + SimpleConsumerDump, + SimulcastConsumerDump, + SvcConsumerDump, + PipeConsumerDump, + BaseConsumerDump, + RtpStreamDump, + RtpStreamParametersDump, + RtxStreamDump, + RtxStreamParameters, + ConsumerStat, + ConsumerTraceEventType, + ConsumerTraceEventData, + ConsumerEvents, + ConsumerObserver, + ConsumerObserverEvents, +} from './ConsumerTypes'; import { Channel } from './Channel'; -import { PayloadChannel } from './PayloadChannel'; -import { TransportInternal } from './Transport'; -import { ProducerStat } from './Producer'; +import type { TransportInternal } from './Transport'; +import type { ProducerStat } from './ProducerTypes'; +import type { MediaKind, RtpParameters } from './rtpParametersTypes'; import { - MediaKind, - RtpCapabilities, - RtpParameters -} from './RtpParameters'; - -export type ConsumerOptions = -{ - /** - * The id of the Producer to consume. - */ - producerId: string; - - /** - * RTP capabilities of the consuming endpoint. - */ - rtpCapabilities: RtpCapabilities; - - /** - * Whether the Consumer must start in paused mode. Default false. - * - * When creating a video Consumer, it's recommended to set paused to true, - * then transmit the Consumer parameters to the consuming endpoint and, once - * the consuming endpoint has created its local side Consumer, unpause the - * server side Consumer using the resume() method. This is an optimization - * to make it possible for the consuming endpoint to render the video as far - * as possible. If the server side Consumer was created with paused: false, - * mediasoup will immediately request a key frame to the remote Producer and - * suych a key frame may reach the consuming endpoint even before it's ready - * to consume it, generating “black” video until the device requests a keyframe - * by itself. - */ - paused?: boolean; - - /** - * The MID for the Consumer. If not specified, a sequentially growing - * number will be assigned. - */ - mid?: string; - - /** - * Preferred spatial and temporal layer for simulcast or SVC media sources. - * If unset, the highest ones are selected. - */ - preferredLayers?: ConsumerLayers; - - /** - * Whether this Consumer should ignore DTX packets (only valid for Opus codec). - * If set, DTX packets are not forwarded to the remote Consumer. - */ - ignoreDtx?: Boolean; - - /** - * Whether this Consumer should consume all RTP streams generated by the - * Producer. - */ - pipe?: boolean; - - /** - * Custom application data. - */ - appData?: Record; -}; - -/** - * Valid types for 'trace' event. - */ -export type ConsumerTraceEventType = 'rtp' | 'keyframe' | 'nack' | 'pli' | 'fir'; - -/** - * 'trace' event data. - */ -export type ConsumerTraceEventData = -{ - /** - * Trace type. - */ - type: ConsumerTraceEventType; - - /** - * Event timestamp. - */ - timestamp: number; - - /** - * Event direction. - */ - direction: 'in' | 'out'; - - /** - * Per type information. - */ - info: any; -}; - -export type ConsumerScore = -{ - /** - * The score of the RTP stream of the consumer. - */ - score: number; - - /** - * The score of the currently selected RTP stream of the producer. - */ - producerScore: number; - - /** - * The scores of all RTP streams in the producer ordered by encoding (just - * useful when the producer uses simulcast). - */ - producerScores: number[]; -}; - -export type ConsumerLayers = -{ - /** - * The spatial layer index (from 0 to N). - */ - spatialLayer: number; - - /** - * The temporal layer index (from 0 to N). - */ - temporalLayer?: number; -}; - -export type ConsumerStat = -{ - // Common to all RtpStreams. - type: string; - timestamp: number; - ssrc: number; - rtxSsrc?: number; - kind: string; - mimeType: string; - packetsLost: number; - fractionLost: number; - packetsDiscarded: number; - packetsRetransmitted: number; - packetsRepaired: number; - nackCount: number; - nackPacketCount: number; - pliCount: number; - firCount: number; - score: number; - packetCount: number; - byteCount: number; - bitrate: number; - roundTripTime?: number; -}; - -/** - * Consumer type. - */ -export type ConsumerType = 'simple' | 'simulcast' | 'svc' | 'pipe'; - -export type ConsumerEvents = -{ - transportclose: []; - producerclose: []; - producerpause: []; - producerresume: []; - score: [ConsumerScore]; - layerschange: [ConsumerLayers?]; - trace: [ConsumerTraceEventData]; - rtp: [Buffer]; - // Private events. - '@close': []; - '@producerclose': []; -}; - -export type ConsumerObserverEvents = -{ - close: []; - pause: []; - resume: []; - score: [ConsumerScore]; - layerschange: [ConsumerLayers?]; - trace: [ConsumerTraceEventData]; -}; - -type ConsumerInternal = TransportInternal & -{ + parseRtpEncodingParameters, + parseRtpParameters, +} from './rtpParametersFbsUtils'; +import { parseRtpStreamStats } from './rtpStreamStatsFbsUtils'; +import type { AppData } from './types'; +import * as fbsUtils from './fbsUtils'; +import { Event, Notification } from './fbs/notification'; +import { TraceDirection as FbsTraceDirection } from './fbs/common'; +import * as FbsRequest from './fbs/request'; +import * as FbsTransport from './fbs/transport'; +import * as FbsConsumer from './fbs/consumer'; +import * as FbsConsumerTraceInfo from './fbs/consumer/trace-info'; +import * as FbsRtpStream from './fbs/rtp-stream'; +import * as FbsRtxStream from './fbs/rtx-stream'; +import { Type as FbsRtpParametersType } from './fbs/rtp-parameters'; +import * as FbsRtpParameters from './fbs/rtp-parameters'; + +type ConsumerInternal = TransportInternal & { consumerId: string; }; -type ConsumerData = -{ +type ConsumerData = { producerId: string; kind: MediaKind; rtpParameters: RtpParameters; @@ -201,7 +57,9 @@ type ConsumerData = const logger = new Logger('Consumer'); -export class Consumer extends EnhancedEventEmitter +export class ConsumerImpl + extends EnhancedEventEmitter + implements Consumer { // Internal data. readonly #internal: ConsumerInternal; @@ -212,14 +70,11 @@ export class Consumer extends EnhancedEventEmitter // Channel instance. readonly #channel: Channel; - // PayloadChannel instance. - #payloadChannel: PayloadChannel; - // Closed flag. #closed = false; // Custom app data. - readonly #appData: Record; + #appData: ConsumerAppData; // Paused flag. #paused = false; @@ -240,35 +95,28 @@ export class Consumer extends EnhancedEventEmitter #currentLayers?: ConsumerLayers; // Observer instance. - readonly #observer = new EnhancedEventEmitter(); - - /** - * @private - */ - constructor( - { - internal, - data, - channel, - payloadChannel, - appData, - paused, - producerPaused, - score = { score: 10, producerScore: 10, producerScores: [] }, - preferredLayers - }: - { - internal: ConsumerInternal; - data: ConsumerData; - channel: Channel; - payloadChannel: PayloadChannel; - appData?: Record; - paused: boolean; - producerPaused: boolean; - score?: ConsumerScore; - preferredLayers?: ConsumerLayers; - }) - { + readonly #observer: ConsumerObserver = + new EnhancedEventEmitter(); + + constructor({ + internal, + data, + channel, + appData, + paused, + producerPaused, + score = { score: 10, producerScore: 10, producerScores: [] }, + preferredLayers, + }: { + internal: ConsumerInternal; + data: ConsumerData; + channel: Channel; + appData?: ConsumerAppData; + paused: boolean; + producerPaused: boolean; + score?: ConsumerScore; + preferredLayers?: ConsumerLayers; + }) { super(); logger.debug('constructor()'); @@ -276,152 +124,89 @@ export class Consumer extends EnhancedEventEmitter this.#internal = internal; this.#data = data; this.#channel = channel; - this.#payloadChannel = payloadChannel; - this.#appData = appData || {}; this.#paused = paused; this.#producerPaused = producerPaused; this.#score = score; this.#preferredLayers = preferredLayers; + this.#appData = appData ?? ({} as ConsumerAppData); this.handleWorkerNotifications(); + this.handleListenerError(); } - /** - * Consumer id. - */ - get id(): string - { + get id(): string { return this.#internal.consumerId; } - /** - * Associated Producer id. - */ - get producerId(): string - { + get producerId(): string { return this.#data.producerId; } - /** - * Whether the Consumer is closed. - */ - get closed(): boolean - { + get closed(): boolean { return this.#closed; } - /** - * Media kind. - */ - get kind(): MediaKind - { + get kind(): MediaKind { return this.#data.kind; } - /** - * RTP parameters. - */ - get rtpParameters(): RtpParameters - { + get rtpParameters(): RtpParameters { return this.#data.rtpParameters; } - /** - * Consumer type. - */ - get type(): ConsumerType - { + get type(): ConsumerType { return this.#data.type; } - /** - * Whether the Consumer is paused. - */ - get paused(): boolean - { + get paused(): boolean { return this.#paused; } - /** - * Whether the associate Producer is paused. - */ - get producerPaused(): boolean - { + get producerPaused(): boolean { return this.#producerPaused; } - /** - * Current priority. - */ - get priority(): number - { + get priority(): number { return this.#priority; } - /** - * Consumer score. - */ - get score(): ConsumerScore - { + get score(): ConsumerScore { return this.#score; } - /** - * Preferred video layers. - */ - get preferredLayers(): ConsumerLayers | undefined - { + get preferredLayers(): ConsumerLayers | undefined { return this.#preferredLayers; } - /** - * Current video layers. - */ - get currentLayers(): ConsumerLayers | undefined - { + get currentLayers(): ConsumerLayers | undefined { return this.#currentLayers; } - /** - * App custom data. - */ - get appData(): Record - { + get appData(): ConsumerAppData { return this.#appData; } - /** - * Invalid setter. - */ - set appData(appData: Record) // eslint-disable-line no-unused-vars - { - throw new Error('cannot override appData object'); + set appData(appData: ConsumerAppData) { + this.#appData = appData; } - /** - * Observer. - */ - get observer(): EnhancedEventEmitter - { + get observer(): ConsumerObserver { return this.#observer; } /** - * @private * Just for testing purposes. + * + * @private */ - get channelForTesting(): Channel - { + get channelForTesting(): Channel { return this.#channel; } - /** - * Close the Consumer. - */ - close(): void - { - if (this.#closed) + close(): void { + if (this.#closed) { return; + } logger.debug('close()'); @@ -429,11 +214,19 @@ export class Consumer extends EnhancedEventEmitter // Remove notification subscriptions. this.#channel.removeAllListeners(this.#internal.consumerId); - this.#payloadChannel.removeAllListeners(this.#internal.consumerId); - const reqData = { consumerId: this.#internal.consumerId }; - - this.#channel.request('transport.closeConsumer', this.#internal.transportId, reqData) + /* Build Request. */ + const requestOffset = new FbsTransport.CloseConsumerRequestT( + this.#internal.consumerId + ).pack(this.#channel.bufferBuilder); + + this.#channel + .request( + FbsRequest.Method.TRANSPORT_CLOSE_CONSUMER, + FbsRequest.Body.Transport_CloseConsumerRequest, + requestOffset, + this.#internal.transportId + ) .catch(() => {}); this.emit('@close'); @@ -442,15 +235,10 @@ export class Consumer extends EnhancedEventEmitter this.#observer.safeEmit('close'); } - /** - * Transport was closed. - * - * @private - */ - transportClosed(): void - { - if (this.#closed) + transportClosed(): void { + if (this.#closed) { return; + } logger.debug('transportClosed()'); @@ -458,7 +246,6 @@ export class Consumer extends EnhancedEventEmitter // Remove notification subscriptions. this.#channel.removeAllListeners(this.#internal.consumerId); - this.#payloadChannel.removeAllListeners(this.#internal.consumerId); this.safeEmit('transportclose'); @@ -466,267 +253,647 @@ export class Consumer extends EnhancedEventEmitter this.#observer.safeEmit('close'); } - /** - * Dump Consumer. - */ - async dump(): Promise - { + async dump(): Promise { logger.debug('dump()'); - return this.#channel.request('consumer.dump', this.#internal.consumerId); + const response = await this.#channel.request( + FbsRequest.Method.CONSUMER_DUMP, + undefined, + undefined, + this.#internal.consumerId + ); + + /* Decode Response. */ + const data = new FbsConsumer.DumpResponse(); + + response.body(data); + + return parseConsumerDumpResponse(data); } - /** - * Get Consumer stats. - */ - async getStats(): Promise> - { + async getStats(): Promise<(ConsumerStat | ProducerStat)[]> { logger.debug('getStats()'); - return this.#channel.request('consumer.getStats', this.#internal.consumerId); + const response = await this.#channel.request( + FbsRequest.Method.CONSUMER_GET_STATS, + undefined, + undefined, + this.#internal.consumerId + ); + + /* Decode Response. */ + const data = new FbsConsumer.GetStatsResponse(); + + response.body(data); + + return parseConsumerStats(data); } - /** - * Pause the Consumer. - */ - async pause(): Promise - { + async pause(): Promise { logger.debug('pause()'); - const wasPaused = this.#paused || this.#producerPaused; + await this.#channel.request( + FbsRequest.Method.CONSUMER_PAUSE, + undefined, + undefined, + this.#internal.consumerId + ); - await this.#channel.request('consumer.pause', this.#internal.consumerId); + const wasPaused = this.#paused; this.#paused = true; // Emit observer event. - if (!wasPaused) + if (!wasPaused && !this.#producerPaused) { this.#observer.safeEmit('pause'); + } } - /** - * Resume the Consumer. - */ - async resume(): Promise - { + async resume(): Promise { logger.debug('resume()'); - const wasPaused = this.#paused || this.#producerPaused; + await this.#channel.request( + FbsRequest.Method.CONSUMER_RESUME, + undefined, + undefined, + this.#internal.consumerId + ); - await this.#channel.request('consumer.resume', this.#internal.consumerId); + const wasPaused = this.#paused; this.#paused = false; // Emit observer event. - if (wasPaused && !this.#producerPaused) + if (wasPaused && !this.#producerPaused) { this.#observer.safeEmit('resume'); + } } - /** - * Set preferred video layers. - */ - async setPreferredLayers( - { - spatialLayer, - temporalLayer - }: ConsumerLayers - ): Promise - { + async setPreferredLayers({ + spatialLayer, + temporalLayer, + }: ConsumerLayers): Promise { logger.debug('setPreferredLayers()'); - const reqData = { spatialLayer, temporalLayer }; - - const data = await this.#channel.request( - 'consumer.setPreferredLayers', this.#internal.consumerId, reqData); + if (typeof spatialLayer !== 'number') { + throw new TypeError('spatialLayer must be a number'); + } + if (temporalLayer && typeof temporalLayer !== 'number') { + throw new TypeError('if given, temporalLayer must be a number'); + } + + const builder = this.#channel.bufferBuilder; + + const preferredLayersOffset = + FbsConsumer.ConsumerLayers.createConsumerLayers( + builder, + spatialLayer, + temporalLayer ?? null + ); + const requestOffset = + FbsConsumer.SetPreferredLayersRequest.createSetPreferredLayersRequest( + builder, + preferredLayersOffset + ); + + const response = await this.#channel.request( + FbsRequest.Method.CONSUMER_SET_PREFERRED_LAYERS, + FbsRequest.Body.Consumer_SetPreferredLayersRequest, + requestOffset, + this.#internal.consumerId + ); + + /* Decode Response. */ + const data = new FbsConsumer.SetPreferredLayersResponse(); + + let preferredLayers: ConsumerLayers | undefined; + + // Response is empty for non Simulcast Consumers. + if (response.body(data)) { + const status = data.unpack(); + + if (status.preferredLayers) { + preferredLayers = { + spatialLayer: status.preferredLayers.spatialLayer, + temporalLayer: status.preferredLayers.temporalLayer ?? undefined, + }; + } + } - this.#preferredLayers = data || undefined; + this.#preferredLayers = preferredLayers; } - /** - * Set priority. - */ - async setPriority(priority: number): Promise - { + async setPriority(priority: number): Promise { logger.debug('setPriority()'); - const reqData = { priority }; + if (typeof priority !== 'number' || priority < 0) { + throw new TypeError('priority must be a positive number'); + } - const data = await this.#channel.request( - 'consumer.setPriority', this.#internal.consumerId, reqData); + const requestOffset = + FbsConsumer.SetPriorityRequest.createSetPriorityRequest( + this.#channel.bufferBuilder, + priority + ); - this.#priority = data.priority; - } + const response = await this.#channel.request( + FbsRequest.Method.CONSUMER_SET_PRIORITY, + FbsRequest.Body.Consumer_SetPriorityRequest, + requestOffset, + this.#internal.consumerId + ); - /** - * Unset priority. - */ - async unsetPriority(): Promise - { - logger.debug('unsetPriority()'); + const data = new FbsConsumer.SetPriorityResponse(); - const reqData = { priority: 1 }; + response.body(data); - const data = await this.#channel.request( - 'consumer.setPriority', this.#internal.consumerId, reqData); + const status = data.unpack(); - this.#priority = data.priority; + this.#priority = status.priority; } - /** - * Request a key frame to the Producer. - */ - async requestKeyFrame(): Promise - { + async unsetPriority(): Promise { + logger.debug('unsetPriority()'); + + await this.setPriority(1); + } + + async requestKeyFrame(): Promise { logger.debug('requestKeyFrame()'); - await this.#channel.request('consumer.requestKeyFrame', this.#internal.consumerId); + await this.#channel.request( + FbsRequest.Method.CONSUMER_REQUEST_KEY_FRAME, + undefined, + undefined, + this.#internal.consumerId + ); } - /** - * Enable 'trace' event. - */ - async enableTraceEvent(types: ConsumerTraceEventType[] = []): Promise - { + async enableTraceEvent(types: ConsumerTraceEventType[] = []): Promise { logger.debug('enableTraceEvent()'); - const reqData = { types }; + if (!Array.isArray(types)) { + throw new TypeError('types must be an array'); + } + if (types.find(type => typeof type !== 'string')) { + throw new TypeError('every type must be a string'); + } + + // Convert event types. + const fbsEventTypes: FbsConsumer.TraceEventType[] = []; + + for (const eventType of types) { + try { + fbsEventTypes.push(consumerTraceEventTypeToFbs(eventType)); + } catch (error) { + logger.warn('enableTraceEvent() | [error:${error}]'); + } + } + + /* Build Request. */ + const requestOffset = new FbsConsumer.EnableTraceEventRequestT( + fbsEventTypes + ).pack(this.#channel.bufferBuilder); await this.#channel.request( - 'consumer.enableTraceEvent', this.#internal.consumerId, reqData); - } - - private handleWorkerNotifications(): void - { - this.#channel.on(this.#internal.consumerId, (event: string, data?: any) => - { - switch (event) - { - case 'producerclose': - { - if (this.#closed) - break; + FbsRequest.Method.CONSUMER_ENABLE_TRACE_EVENT, + FbsRequest.Body.Consumer_EnableTraceEventRequest, + requestOffset, + this.#internal.consumerId + ); + } - this.#closed = true; + private handleWorkerNotifications(): void { + this.#channel.on( + this.#internal.consumerId, + (event: Event, data?: Notification) => { + switch (event) { + case Event.CONSUMER_PRODUCER_CLOSE: { + if (this.#closed) { + break; + } - // Remove notification subscriptions. - this.#channel.removeAllListeners(this.#internal.consumerId); - this.#payloadChannel.removeAllListeners(this.#internal.consumerId); + this.#closed = true; - this.emit('@producerclose'); - this.safeEmit('producerclose'); + // Remove notification subscriptions. + this.#channel.removeAllListeners(this.#internal.consumerId); - // Emit observer event. - this.#observer.safeEmit('close'); + this.emit('@producerclose'); + this.safeEmit('producerclose'); - break; - } + // Emit observer event. + this.#observer.safeEmit('close'); - case 'producerpause': - { - if (this.#producerPaused) break; + } + + case Event.CONSUMER_PRODUCER_PAUSE: { + if (this.#producerPaused) { + break; + } - const wasPaused = this.#paused || this.#producerPaused; + this.#producerPaused = true; - this.#producerPaused = true; + this.safeEmit('producerpause'); - this.safeEmit('producerpause'); + // Emit observer event. + if (!this.#paused) { + this.#observer.safeEmit('pause'); + } - // Emit observer event. - if (!wasPaused) - this.#observer.safeEmit('pause'); + break; + } - break; - } + case Event.CONSUMER_PRODUCER_RESUME: { + if (!this.#producerPaused) { + break; + } + + this.#producerPaused = false; + + this.safeEmit('producerresume'); + + // Emit observer event. + if (!this.#paused) { + this.#observer.safeEmit('resume'); + } - case 'producerresume': - { - if (!this.#producerPaused) break; + } - const wasPaused = this.#paused || this.#producerPaused; + case Event.CONSUMER_SCORE: { + const notification = new FbsConsumer.ScoreNotification(); - this.#producerPaused = false; + data!.body(notification); - this.safeEmit('producerresume'); + const score: ConsumerScore = notification.score()!.unpack(); - // Emit observer event. - if (wasPaused && !this.#paused) - this.#observer.safeEmit('resume'); + this.#score = score; - break; - } + this.safeEmit('score', score); + + // Emit observer event. + this.#observer.safeEmit('score', score); + + break; + } - case 'score': - { - const score = data as ConsumerScore; + case Event.CONSUMER_LAYERS_CHANGE: { + const notification = new FbsConsumer.LayersChangeNotification(); - this.#score = score; + data!.body(notification); - this.safeEmit('score', score); + const layers: ConsumerLayers | undefined = notification.layers() + ? parseConsumerLayers(notification.layers()!) + : undefined; - // Emit observer event. - this.#observer.safeEmit('score', score); + this.#currentLayers = layers; - break; - } + this.safeEmit('layerschange', layers); - case 'layerschange': - { - const layers = data as ConsumerLayers | undefined; + // Emit observer event. + this.#observer.safeEmit('layerschange', layers); - this.#currentLayers = layers; + break; + } - this.safeEmit('layerschange', layers); + case Event.CONSUMER_TRACE: { + const notification = new FbsConsumer.TraceNotification(); - // Emit observer event. - this.#observer.safeEmit('layerschange', layers); + data!.body(notification); - break; - } + const trace: ConsumerTraceEventData = + parseTraceEventData(notification); - case 'trace': - { - const trace = data as ConsumerTraceEventData; + this.safeEmit('trace', trace); - this.safeEmit('trace', trace); + // Emit observer event. + this.observer.safeEmit('trace', trace); - // Emit observer event. - this.#observer.safeEmit('trace', trace); + this.safeEmit('trace', trace); - break; - } + // Emit observer event. + this.#observer.safeEmit('trace', trace); - default: - { - logger.error('ignoring unknown event "%s"', event); - } - } - }); + break; + } - this.#payloadChannel.on( - this.#internal.consumerId, - (event: string, data: any | undefined, payload: Buffer) => - { - switch (event) - { - case 'rtp': - { - if (this.#closed) + case Event.CONSUMER_RTP: { + if (this.#closed) { break; + } + + const notification = new FbsConsumer.RtpNotification(); - const packet = payload; + data!.body(notification); - this.safeEmit('rtp', packet); + this.safeEmit('rtp', Buffer.from(notification.dataArray()!)); break; } - default: - { - logger.error('ignoring unknown event "%s"', event); + default: { + logger.error(`ignoring unknown event "${event}"`); } } - }); + } + ); } + + private handleListenerError(): void { + this.on('listenererror', (eventName, error) => { + logger.error( + `event listener threw an error [eventName:${eventName}]:`, + error + ); + }); + } +} + +export function parseTraceEventData( + trace: FbsConsumer.TraceNotification +): ConsumerTraceEventData { + let info: any; + + if (trace.infoType() !== FbsConsumer.TraceInfo.NONE) { + const accessor = trace.info.bind(trace); + + info = FbsConsumerTraceInfo.unionToTraceInfo(trace.infoType(), accessor); + + trace.info(info); + } + + return { + type: consumerTraceEventTypeFromFbs(trace.type()), + timestamp: Number(trace.timestamp()), + direction: + trace.direction() === FbsTraceDirection.DIRECTION_IN ? 'in' : 'out', + info: info ? info.unpack() : undefined, + }; +} + +function consumerTraceEventTypeToFbs( + eventType: ConsumerTraceEventType +): FbsConsumer.TraceEventType { + switch (eventType) { + case 'keyframe': { + return FbsConsumer.TraceEventType.KEYFRAME; + } + + case 'fir': { + return FbsConsumer.TraceEventType.FIR; + } + + case 'nack': { + return FbsConsumer.TraceEventType.NACK; + } + + case 'pli': { + return FbsConsumer.TraceEventType.PLI; + } + + case 'rtp': { + return FbsConsumer.TraceEventType.RTP; + } + + default: { + throw new TypeError(`invalid ConsumerTraceEventType: ${eventType}`); + } + } +} + +function consumerTraceEventTypeFromFbs( + traceType: FbsConsumer.TraceEventType +): ConsumerTraceEventType { + switch (traceType) { + case FbsConsumer.TraceEventType.KEYFRAME: { + return 'keyframe'; + } + + case FbsConsumer.TraceEventType.FIR: { + return 'fir'; + } + + case FbsConsumer.TraceEventType.NACK: { + return 'nack'; + } + + case FbsConsumer.TraceEventType.PLI: { + return 'pli'; + } + + case FbsConsumer.TraceEventType.RTP: { + return 'rtp'; + } + + default: { + throw new TypeError(`invalid FbsConsumer.TraceEventType: ${traceType}`); + } + } +} + +function parseConsumerLayers(data: FbsConsumer.ConsumerLayers): ConsumerLayers { + const spatialLayer = data.spatialLayer(); + const temporalLayer = + data.temporalLayer() !== null ? data.temporalLayer()! : undefined; + + return { + spatialLayer, + temporalLayer, + }; +} + +function parseRtpStream(data: FbsRtpStream.Dump): RtpStreamDump { + const params = parseRtpStreamParameters(data.params()!); + + let rtxStream: RtxStreamDump | undefined; + + if (data.rtxStream()) { + rtxStream = parseRtxStream(data.rtxStream()!); + } + + return { + params, + score: data.score(), + rtxStream, + }; +} + +function parseRtpStreamParameters( + data: FbsRtpStream.Params +): RtpStreamParametersDump { + return { + encodingIdx: data.encodingIdx(), + ssrc: data.ssrc(), + payloadType: data.payloadType(), + mimeType: data.mimeType()!, + clockRate: data.clockRate(), + rid: data.rid()!.length > 0 ? data.rid()! : undefined, + cname: data.cname()!, + rtxSsrc: data.rtxSsrc() !== null ? data.rtxSsrc()! : undefined, + rtxPayloadType: + data.rtxPayloadType() !== null ? data.rtxPayloadType()! : undefined, + useNack: data.useNack(), + usePli: data.usePli(), + useFir: data.useFir(), + useInBandFec: data.useInBandFec(), + useDtx: data.useDtx(), + spatialLayers: data.spatialLayers(), + temporalLayers: data.temporalLayers(), + }; +} + +function parseRtxStream(data: FbsRtxStream.RtxDump): RtxStreamDump { + const params = parseRtxStreamParameters(data.params()!); + + return { + params, + }; +} + +function parseRtxStreamParameters( + data: FbsRtxStream.Params +): RtxStreamParameters { + return { + ssrc: data.ssrc(), + payloadType: data.payloadType(), + mimeType: data.mimeType()!, + clockRate: data.clockRate(), + rrid: data.rrid()!.length > 0 ? data.rrid()! : undefined, + cname: data.cname()!, + }; +} + +function parseBaseConsumerDump( + data: FbsConsumer.BaseConsumerDump +): BaseConsumerDump { + return { + id: data.id()!, + producerId: data.producerId()!, + kind: data.kind() === FbsRtpParameters.MediaKind.AUDIO ? 'audio' : 'video', + rtpParameters: parseRtpParameters(data.rtpParameters()!), + consumableRtpEncodings: + data.consumableRtpEncodingsLength() > 0 + ? fbsUtils.parseVector( + data, + 'consumableRtpEncodings', + parseRtpEncodingParameters + ) + : undefined, + traceEventTypes: fbsUtils.parseVector( + data, + 'traceEventTypes', + consumerTraceEventTypeFromFbs + ), + supportedCodecPayloadTypes: fbsUtils.parseVector( + data, + 'supportedCodecPayloadTypes' + ), + paused: data.paused(), + producerPaused: data.producerPaused(), + priority: data.priority(), + }; +} + +function parseSimpleConsumerDump( + data: FbsConsumer.ConsumerDump +): SimpleConsumerDump { + const base = parseBaseConsumerDump(data.base()!); + const rtpStream = parseRtpStream(data.rtpStreams(0)!); + + return { + ...base, + type: 'simple', + rtpStream, + }; +} + +function parseSimulcastConsumerDump( + data: FbsConsumer.ConsumerDump +): SimulcastConsumerDump { + const base = parseBaseConsumerDump(data.base()!); + const rtpStream = parseRtpStream(data.rtpStreams(0)!); + + return { + ...base, + type: 'simulcast', + rtpStream, + preferredSpatialLayer: data.preferredSpatialLayer()!, + targetSpatialLayer: data.targetSpatialLayer()!, + currentSpatialLayer: data.currentSpatialLayer()!, + preferredTemporalLayer: data.preferredTemporalLayer()!, + targetTemporalLayer: data.targetTemporalLayer()!, + currentTemporalLayer: data.currentTemporalLayer()!, + }; +} + +function parseSvcConsumerDump(data: FbsConsumer.ConsumerDump): SvcConsumerDump { + const dump = parseSimulcastConsumerDump(data); + + dump.type = 'svc'; + + return dump; +} + +function parsePipeConsumerDump( + data: FbsConsumer.ConsumerDump +): PipeConsumerDump { + const base = parseBaseConsumerDump(data.base()!); + const rtpStreams = fbsUtils.parseVector(data, 'rtpStreams', parseRtpStream); + + return { + ...base, + type: 'pipe', + rtpStreams, + }; +} + +function parseConsumerDumpResponse( + data: FbsConsumer.DumpResponse +): ConsumerDump { + const type = data.data()!.base()!.type(); + + switch (type) { + case FbsRtpParametersType.SIMPLE: { + const dump = new FbsConsumer.ConsumerDump(); + + data.data(dump); + + return parseSimpleConsumerDump(dump); + } + + case FbsRtpParametersType.SIMULCAST: { + const dump = new FbsConsumer.ConsumerDump(); + + data.data(dump); + + return parseSimulcastConsumerDump(dump); + } + + case FbsRtpParametersType.SVC: { + const dump = new FbsConsumer.ConsumerDump(); + + data.data(dump); + + return parseSvcConsumerDump(dump); + } + + case FbsRtpParametersType.PIPE: { + const dump = new FbsConsumer.ConsumerDump(); + + data.data(dump); + + return parsePipeConsumerDump(dump); + } + + default: { + throw new TypeError(`invalid Consumer type: ${type}`); + } + } +} + +function parseConsumerStats( + binary: FbsConsumer.GetStatsResponse +): (ConsumerStat | ProducerStat)[] { + return fbsUtils.parseVector(binary, 'stats', parseRtpStreamStats); } diff --git a/node/src/ConsumerTypes.ts b/node/src/ConsumerTypes.ts new file mode 100644 index 0000000000..2b1fbf7fd0 --- /dev/null +++ b/node/src/ConsumerTypes.ts @@ -0,0 +1,393 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { ProducerStat } from './ProducerTypes'; +import type { + MediaKind, + RtpCapabilities, + RtpEncodingParameters, + RtpParameters, +} from './rtpParametersTypes'; +import type { RtpStreamSendStats } from './rtpStreamStatsTypes'; +import type { AppData } from './types'; + +export type ConsumerOptions = { + /** + * The id of the Producer to consume. + */ + producerId: string; + + /** + * RTP capabilities of the consuming endpoint. + */ + rtpCapabilities: RtpCapabilities; + + /** + * Whether the consumer must start in paused mode. Default false. + * + * When creating a video Consumer, it's recommended to set paused to true, + * then transmit the Consumer parameters to the consuming endpoint and, once + * the consuming endpoint has created its local side Consumer, unpause the + * server side Consumer using the resume() method. This is an optimization + * to make it possible for the consuming endpoint to render the video as far + * as possible. If the server side Consumer was created with paused: false, + * mediasoup will immediately request a key frame to the remote Producer and + * suych a key frame may reach the consuming endpoint even before it's ready + * to consume it, generating “black” video until the device requests a keyframe + * by itself. + */ + paused?: boolean; + + /** + * The MID for the Consumer. If not specified, a sequentially growing + * number will be assigned. + */ + mid?: string; + + /** + * Preferred spatial and temporal layer for simulcast or SVC media sources. + * If unset, the highest ones are selected. + */ + preferredLayers?: ConsumerLayers; + + /** + * Whether this Consumer should enable RTP retransmissions, storing sent RTP + * and processing the incoming RTCP NACK from the remote Consumer. If not set + * it's true by default for video codecs and false for audio codecs. If set + * to true, NACK will be enabled if both endpoints (mediasoup and the remote + * Consumer) support NACK for this codec. When it comes to audio codecs, just + * OPUS supports NACK. + */ + enableRtx?: boolean; + + /** + * Whether this Consumer should ignore DTX packets (only valid for Opus codec). + * If set, DTX packets are not forwarded to the remote Consumer. + */ + ignoreDtx?: boolean; + + /** + * Whether this Consumer should consume all RTP streams generated by the + * Producer. + */ + pipe?: boolean; + + /** + * Custom application data. + */ + appData?: ConsumerAppData; +}; + +/** + * Consumer type. + */ +export type ConsumerType = 'simple' | 'simulcast' | 'svc' | 'pipe'; + +export type ConsumerScore = { + /** + * The score of the RTP stream of the consumer. + */ + score: number; + + /** + * The score of the currently selected RTP stream of the producer. + */ + producerScore: number; + + /** + * The scores of all RTP streams in the producer ordered by encoding (just + * useful when the producer uses simulcast). + */ + producerScores: number[]; +}; + +export type ConsumerLayers = { + /** + * The spatial layer index (from 0 to N). + */ + spatialLayer: number; + + /** + * The temporal layer index (from 0 to N). + */ + temporalLayer?: number; +}; + +export type ConsumerDump = + | SimpleConsumerDump + | SimulcastConsumerDump + | SvcConsumerDump + | PipeConsumerDump; + +export type SimpleConsumerDump = BaseConsumerDump & { + type: string; + rtpStream: RtpStreamDump; +}; + +export type SimulcastConsumerDump = BaseConsumerDump & { + type: string; + rtpStream: RtpStreamDump; + preferredSpatialLayer: number; + targetSpatialLayer: number; + currentSpatialLayer: number; + preferredTemporalLayer: number; + targetTemporalLayer: number; + currentTemporalLayer: number; +}; + +export type SvcConsumerDump = SimulcastConsumerDump; + +export type PipeConsumerDump = BaseConsumerDump & { + type: string; + rtpStreams: RtpStreamDump[]; +}; + +export type BaseConsumerDump = { + id: string; + producerId: string; + kind: MediaKind; + rtpParameters: RtpParameters; + consumableRtpEncodings?: RtpEncodingParameters[]; + supportedCodecPayloadTypes: number[]; + traceEventTypes: string[]; + paused: boolean; + producerPaused: boolean; + priority: number; +}; + +export type RtpStreamDump = { + params: RtpStreamParametersDump; + score: number; + rtxStream?: RtxStreamDump; +}; + +export type RtpStreamParametersDump = { + encodingIdx: number; + ssrc: number; + payloadType: number; + mimeType: string; + clockRate: number; + rid?: string; + cname: string; + rtxSsrc?: number; + rtxPayloadType?: number; + useNack: boolean; + usePli: boolean; + useFir: boolean; + useInBandFec: boolean; + useDtx: boolean; + spatialLayers: number; + temporalLayers: number; +}; + +export type RtxStreamDump = { + params: RtxStreamParameters; +}; + +export type RtxStreamParameters = { + ssrc: number; + payloadType: number; + mimeType: string; + clockRate: number; + rrid?: string; + cname: string; +}; + +export type ConsumerStat = RtpStreamSendStats; + +/** + * Valid types for 'trace' event. + */ +export type ConsumerTraceEventType = + | 'rtp' + | 'keyframe' + | 'nack' + | 'pli' + | 'fir'; + +/** + * 'trace' event data. + */ +export type ConsumerTraceEventData = { + /** + * Trace type. + */ + type: ConsumerTraceEventType; + + /** + * Event timestamp. + */ + timestamp: number; + + /** + * Event direction. + */ + direction: 'in' | 'out'; + + /** + * Per type information. + */ + info: any; +}; + +export type ConsumerEvents = { + transportclose: []; + producerclose: []; + producerpause: []; + producerresume: []; + score: [ConsumerScore]; + layerschange: [ConsumerLayers?]; + trace: [ConsumerTraceEventData]; + rtp: [Buffer]; + listenererror: [string, Error]; + // Private events. + '@close': []; + '@producerclose': []; +}; + +export type ConsumerObserver = EnhancedEventEmitter; + +export type ConsumerObserverEvents = { + close: []; + pause: []; + resume: []; + score: [ConsumerScore]; + layerschange: [ConsumerLayers?]; + trace: [ConsumerTraceEventData]; +}; + +export interface Consumer + extends EnhancedEventEmitter { + /** + * Consumer id. + */ + get id(): string; + + /** + * Associated Producer id. + */ + get producerId(): string; + + /** + * Whether the Consumer is closed. + */ + get closed(): boolean; + + /** + * Media kind. + */ + get kind(): MediaKind; + + /** + * RTP parameters. + */ + get rtpParameters(): RtpParameters; + + /** + * Consumer type. + */ + get type(): ConsumerType; + + /** + * Whether the Consumer is paused. + */ + get paused(): boolean; + + /** + * Whether the associate Producer is paused. + */ + get producerPaused(): boolean; + + /** + * Current priority. + */ + get priority(): number; + + /** + * Consumer score. + */ + get score(): ConsumerScore; + + /** + * Preferred video layers. + */ + get preferredLayers(): ConsumerLayers | undefined; + + /** + * Current video layers. + */ + get currentLayers(): ConsumerLayers | undefined; + + /** + * App custom data. + */ + get appData(): ConsumerAppData; + + /** + * App custom data setter. + */ + set appData(appData: ConsumerAppData); + + /** + * Observer. + */ + get observer(): ConsumerObserver; + + /** + * Close the Consumer. + */ + close(): void; + + /** + * Transport was closed. + * + * @private + */ + transportClosed(): void; + + /** + * Dump Consumer. + */ + dump(): Promise; + + /** + * Get Consumer stats. + */ + getStats(): Promise<(ConsumerStat | ProducerStat)[]>; + + /** + * Pause the Consumer. + */ + pause(): Promise; + + /** + * Resume the Consumer. + */ + resume(): Promise; + + /** + * Set preferred video layers. + */ + setPreferredLayers({ + spatialLayer, + temporalLayer, + }: ConsumerLayers): Promise; + + /** + * Set priority. + */ + setPriority(priority: number): Promise; + + /** + * Unset priority. + */ + unsetPriority(): Promise; + + /** + * Request a key frame to the Producer. + */ + requestKeyFrame(): Promise; + + /** + * Enable 'trace' event. + */ + enableTraceEvent(types?: ConsumerTraceEventType[]): Promise; +} diff --git a/node/src/DataConsumer.ts b/node/src/DataConsumer.ts index bad2dd11ad..3bcc932069 100644 --- a/node/src/DataConsumer.ts +++ b/node/src/DataConsumer.ts @@ -1,97 +1,44 @@ import { Logger } from './Logger'; -import { EnhancedEventEmitter } from './EnhancedEventEmitter'; +import { EnhancedEventEmitter } from './enhancedEvents'; +import type { + DataConsumer, + DataConsumerType, + DataConsumerDump, + DataConsumerStat, + DataConsumerEvents, + DataConsumerObserver, + DataConsumerObserverEvents, +} from './DataConsumerTypes'; import { Channel } from './Channel'; -import { PayloadChannel } from './PayloadChannel'; -import { TransportInternal } from './Transport'; -import { SctpStreamParameters } from './SctpParameters'; - -export type DataConsumerOptions = -{ - /** - * The id of the DataProducer to consume. - */ - dataProducerId: string; - - /** - * Just if consuming over SCTP. - * Whether data messages must be received in order. If true the messages will - * be sent reliably. Defaults to the value in the DataProducer if it has type - * 'sctp' or to true if it has type 'direct'. - */ - ordered?: boolean; - - /** - * Just if consuming over SCTP. - * When ordered is false indicates the time (in milliseconds) after which a - * SCTP packet will stop being retransmitted. Defaults to the value in the - * DataProducer if it has type 'sctp' or unset if it has type 'direct'. - */ - maxPacketLifeTime?: number; - - /** - * Just if consuming over SCTP. - * When ordered is false indicates the maximum number of times a packet will - * be retransmitted. Defaults to the value in the DataProducer if it has type - * 'sctp' or unset if it has type 'direct'. - */ - maxRetransmits?: number; - - /** - * Custom application data. - */ - appData?: Record; -}; - -export type DataConsumerStat = -{ - type: string; - timestamp: number; - label: string; - protocol: string; - messagesSent: number; - bytesSent: number; - bufferedAmount: number; -}; - -/** - * DataConsumer type. - */ -export type DataConsumerType = 'sctp' | 'direct'; - -export type DataConsumerEvents = -{ - transportclose: []; - dataproducerclose: []; - message: [Buffer, number]; - sctpsendbufferfull: []; - bufferedamountlow: [number]; - // Private events. - '@close': []; - '@dataproducerclose': []; -}; - -export type DataConsumerObserverEvents = -{ - close: []; -}; - -type DataConsumerInternal = TransportInternal & -{ +import type { TransportInternal } from './Transport'; +import type { SctpStreamParameters } from './sctpParametersTypes'; +import { parseSctpStreamParameters } from './sctpParametersFbsUtils'; +import type { AppData } from './types'; +import * as fbsUtils from './fbsUtils'; +import { Event, Notification } from './fbs/notification'; +import * as FbsTransport from './fbs/transport'; +import * as FbsRequest from './fbs/request'; +import * as FbsDataConsumer from './fbs/data-consumer'; +import * as FbsDataProducer from './fbs/data-producer'; + +type DataConsumerInternal = TransportInternal & { dataConsumerId: string; }; -type DataConsumerData = -{ +type DataConsumerData = { dataProducerId: string; type: DataConsumerType; sctpStreamParameters?: SctpStreamParameters; label: string; protocol: string; + bufferedAmountLowThreshold: number; }; const logger = new Logger('DataConsumer'); -export class DataConsumer extends EnhancedEventEmitter +export class DataConsumerImpl + extends EnhancedEventEmitter + implements DataConsumer { // Internal data. readonly #internal: DataConsumerInternal; @@ -102,38 +49,42 @@ export class DataConsumer extends EnhancedEventEmitter // Channel instance. readonly #channel: Channel; - // PayloadChannel instance. - readonly #payloadChannel: PayloadChannel; - // Closed flag. #closed = false; + // Paused flag. + #paused = false; + + // Associated DataProducer paused flag. + #dataProducerPaused = false; + + // Subchannels subscribed to. + #subchannels: number[]; + // Custom app data. - readonly #appData: Record; + #appData: DataConsumerAppData; // Observer instance. - readonly #observer = new EnhancedEventEmitter(); - - /** - * @private - */ - constructor( - { - internal, - data, - channel, - payloadChannel, - appData - }: - { - internal: DataConsumerInternal; - data: DataConsumerData; - channel: Channel; - payloadChannel: PayloadChannel; - appData?: Record; - } - ) - { + readonly #observer: DataConsumerObserver = + new EnhancedEventEmitter(); + + constructor({ + internal, + data, + channel, + paused, + dataProducerPaused, + subchannels, + appData, + }: { + internal: DataConsumerInternal; + data: DataConsumerData; + channel: Channel; + paused: boolean; + dataProducerPaused: boolean; + subchannels: number[]; + appData?: DataConsumerAppData; + }) { super(); logger.debug('constructor()'); @@ -141,99 +92,71 @@ export class DataConsumer extends EnhancedEventEmitter this.#internal = internal; this.#data = data; this.#channel = channel; - this.#payloadChannel = payloadChannel; - this.#appData = appData || {}; + this.#paused = paused; + this.#dataProducerPaused = dataProducerPaused; + this.#subchannels = subchannels; + this.#appData = appData ?? ({} as DataConsumerAppData); this.handleWorkerNotifications(); + this.handleListenerError(); } - /** - * DataConsumer id. - */ - get id(): string - { + get id(): string { return this.#internal.dataConsumerId; } - /** - * Associated DataProducer id. - */ - get dataProducerId(): string - { + get dataProducerId(): string { return this.#data.dataProducerId; } - /** - * Whether the DataConsumer is closed. - */ - get closed(): boolean - { + get closed(): boolean { return this.#closed; } - /** - * DataConsumer type. - */ - get type(): DataConsumerType - { + get type(): DataConsumerType { return this.#data.type; } - /** - * SCTP stream parameters. - */ - get sctpStreamParameters(): SctpStreamParameters | undefined - { + get sctpStreamParameters(): SctpStreamParameters | undefined { return this.#data.sctpStreamParameters; } - /** - * DataChannel label. - */ - get label(): string - { + get label(): string { return this.#data.label; } - /** - * DataChannel protocol. - */ - get protocol(): string - { + get protocol(): string { return this.#data.protocol; } - /** - * App custom data. - */ - get appData(): Record - { + get paused(): boolean { + return this.#paused; + } + + get dataProducerPaused(): boolean { + return this.#dataProducerPaused; + } + + get subchannels(): number[] { + return Array.from(this.#subchannels); + } + + get appData(): DataConsumerAppData { return this.#appData; } - /** - * Invalid setter. - */ - set appData(appData: Record) // eslint-disable-line no-unused-vars - { - throw new Error('cannot override appData object'); + set appData(appData: DataConsumerAppData) { + this.#appData = appData; } - /** - * Observer. - */ - get observer(): EnhancedEventEmitter - { + get observer(): DataConsumerObserver { return this.#observer; } - /** - * Close the DataConsumer. - */ - close(): void - { - if (this.#closed) + close(): void { + if (this.#closed) { return; + } logger.debug('close()'); @@ -241,11 +164,19 @@ export class DataConsumer extends EnhancedEventEmitter // Remove notification subscriptions. this.#channel.removeAllListeners(this.#internal.dataConsumerId); - this.#payloadChannel.removeAllListeners(this.#internal.dataConsumerId); - const reqData = { dataConsumerId: this.#internal.dataConsumerId }; - - this.#channel.request('transport.closeDataConsumer', this.#internal.transportId, reqData) + /* Build Request. */ + const requestOffset = new FbsTransport.CloseDataConsumerRequestT( + this.#internal.dataConsumerId + ).pack(this.#channel.bufferBuilder); + + this.#channel + .request( + FbsRequest.Method.TRANSPORT_CLOSE_DATACONSUMER, + FbsRequest.Body.Transport_CloseDataConsumerRequest, + requestOffset, + this.#internal.transportId + ) .catch(() => {}); this.emit('@close'); @@ -254,15 +185,10 @@ export class DataConsumer extends EnhancedEventEmitter this.#observer.safeEmit('close'); } - /** - * Transport was closed. - * - * @private - */ - transportClosed(): void - { - if (this.#closed) + transportClosed(): void { + if (this.#closed) { return; + } logger.debug('transportClosed()'); @@ -270,7 +196,6 @@ export class DataConsumer extends EnhancedEventEmitter // Remove notification subscriptions. this.#channel.removeAllListeners(this.#internal.dataConsumerId); - this.#payloadChannel.removeAllListeners(this.#internal.dataConsumerId); this.safeEmit('transportclose'); @@ -278,46 +203,119 @@ export class DataConsumer extends EnhancedEventEmitter this.#observer.safeEmit('close'); } - /** - * Dump DataConsumer. - */ - async dump(): Promise - { + async dump(): Promise { logger.debug('dump()'); - return this.#channel.request('dataConsumer.dump', this.#internal.dataConsumerId); + const response = await this.#channel.request( + FbsRequest.Method.DATACONSUMER_DUMP, + undefined, + undefined, + this.#internal.dataConsumerId + ); + + /* Decode Response. */ + const dumpResponse = new FbsDataConsumer.DumpResponse(); + + response.body(dumpResponse); + + return parseDataConsumerDumpResponse(dumpResponse); } - /** - * Get DataConsumer stats. - */ - async getStats(): Promise - { + async getStats(): Promise { logger.debug('getStats()'); - return this.#channel.request('dataConsumer.getStats', this.#internal.dataConsumerId); + const response = await this.#channel.request( + FbsRequest.Method.DATACONSUMER_GET_STATS, + undefined, + undefined, + this.#internal.dataConsumerId + ); + + /* Decode Response. */ + const data = new FbsDataConsumer.GetStatsResponse(); + + response.body(data); + + return [parseDataConsumerStats(data)]; + } + + async pause(): Promise { + logger.debug('pause()'); + + await this.#channel.request( + FbsRequest.Method.DATACONSUMER_PAUSE, + undefined, + undefined, + this.#internal.dataConsumerId + ); + + const wasPaused = this.#paused; + + this.#paused = true; + + // Emit observer event. + if (!wasPaused && !this.#dataProducerPaused) { + this.#observer.safeEmit('pause'); + } + } + + async resume(): Promise { + logger.debug('resume()'); + + await this.#channel.request( + FbsRequest.Method.DATACONSUMER_RESUME, + undefined, + undefined, + this.#internal.dataConsumerId + ); + + const wasPaused = this.#paused; + + this.#paused = false; + + // Emit observer event. + if (wasPaused && !this.#dataProducerPaused) { + this.#observer.safeEmit('resume'); + } } - /** - * Set buffered amount low threshold. - */ - async setBufferedAmountLowThreshold(threshold: number): Promise - { - logger.debug('setBufferedAmountLowThreshold() [threshold:%s]', threshold); + async setBufferedAmountLowThreshold(threshold: number): Promise { + logger.debug(`setBufferedAmountLowThreshold() [threshold:${threshold}]`); - const reqData = { threshold }; + /* Build Request. */ + const requestOffset = + FbsDataConsumer.SetBufferedAmountLowThresholdRequest.createSetBufferedAmountLowThresholdRequest( + this.#channel.bufferBuilder, + threshold + ); await this.#channel.request( - 'dataConsumer.setBufferedAmountLowThreshold', this.#internal.dataConsumerId, reqData); + FbsRequest.Method.DATACONSUMER_SET_BUFFERED_AMOUNT_LOW_THRESHOLD, + FbsRequest.Body.DataConsumer_SetBufferedAmountLowThresholdRequest, + requestOffset, + this.#internal.dataConsumerId + ); + } + + async getBufferedAmount(): Promise { + logger.debug('getBufferedAmount()'); + + const response = await this.#channel.request( + FbsRequest.Method.DATACONSUMER_GET_BUFFERED_AMOUNT, + undefined, + undefined, + this.#internal.dataConsumerId + ); + + const data = new FbsDataConsumer.GetBufferedAmountResponse(); + + response.body(data); + + return data.bufferedAmount(); } - /** - * Send data. - */ - async send(message: string | Buffer, ppid?: number): Promise - { - if (typeof message !== 'string' && !Buffer.isBuffer(message)) - { + async send(message: string | Buffer, ppid?: number): Promise { + if (typeof message !== 'string' && !Buffer.isBuffer(message)) { throw new TypeError('message must be a string or a Buffer'); } @@ -337,111 +335,299 @@ export class DataConsumer extends EnhancedEventEmitter * +-------------------------------+----------+ */ - if (typeof ppid !== 'number') - { - ppid = (typeof message === 'string') - ? message.length > 0 ? 51 : 56 - : message.length > 0 ? 53 : 57; + if (typeof ppid !== 'number') { + ppid = + typeof message === 'string' + ? message.length > 0 + ? 51 + : 56 + : message.length > 0 + ? 53 + : 57; } // Ensure we honor PPIDs. - if (ppid === 56) + if (ppid === 56) { message = ' '; - else if (ppid === 57) + } else if (ppid === 57) { message = Buffer.alloc(1); + } + + const builder = this.#channel.bufferBuilder; + + let dataOffset = 0; + + if (typeof message === 'string') { + message = Buffer.from(message); + } + + dataOffset = FbsDataConsumer.SendRequest.createDataVector(builder, message); + + const requestOffset = FbsDataConsumer.SendRequest.createSendRequest( + builder, + ppid, + dataOffset + ); - const requestData = String(ppid); + await this.#channel.request( + FbsRequest.Method.DATACONSUMER_SEND, + FbsRequest.Body.DataConsumer_SendRequest, + requestOffset, + this.#internal.dataConsumerId + ); + } + + async setSubchannels(subchannels: number[]): Promise { + logger.debug('setSubchannels()'); + + /* Build Request. */ + const requestOffset = new FbsDataConsumer.SetSubchannelsRequestT( + subchannels + ).pack(this.#channel.bufferBuilder); + + const response = await this.#channel.request( + FbsRequest.Method.DATACONSUMER_SET_SUBCHANNELS, + FbsRequest.Body.DataConsumer_SetSubchannelsRequest, + requestOffset, + this.#internal.dataConsumerId + ); - await this.#payloadChannel.request( - 'dataConsumer.send', this.#internal.dataConsumerId, requestData, message); + /* Decode Response. */ + const data = new FbsDataConsumer.SetSubchannelsResponse(); + + response.body(data); + + // Update subchannels. + this.#subchannels = fbsUtils.parseVector(data, 'subchannels'); } - /** - * Get buffered amount size. - */ - async getBufferedAmount(): Promise - { - logger.debug('getBufferedAmount()'); + async addSubchannel(subchannel: number): Promise { + logger.debug('addSubchannel()'); + + /* Build Request. */ + const requestOffset = + FbsDataConsumer.AddSubchannelRequest.createAddSubchannelRequest( + this.#channel.bufferBuilder, + subchannel + ); + + const response = await this.#channel.request( + FbsRequest.Method.DATACONSUMER_ADD_SUBCHANNEL, + FbsRequest.Body.DataConsumer_AddSubchannelRequest, + requestOffset, + this.#internal.dataConsumerId + ); + + /* Decode Response. */ + const data = new FbsDataConsumer.AddSubchannelResponse(); + + response.body(data); + + // Update subchannels. + this.#subchannels = fbsUtils.parseVector(data, 'subchannels'); + } + + async removeSubchannel(subchannel: number): Promise { + logger.debug('removeSubchannel()'); + + /* Build Request. */ + const requestOffset = + FbsDataConsumer.RemoveSubchannelRequest.createRemoveSubchannelRequest( + this.#channel.bufferBuilder, + subchannel + ); + + const response = await this.#channel.request( + FbsRequest.Method.DATACONSUMER_REMOVE_SUBCHANNEL, + FbsRequest.Body.DataConsumer_RemoveSubchannelRequest, + requestOffset, + this.#internal.dataConsumerId + ); + + /* Decode Response. */ + const data = new FbsDataConsumer.RemoveSubchannelResponse(); - const { bufferedAmount } = - await this.#channel.request('dataConsumer.getBufferedAmount', this.#internal.dataConsumerId); + response.body(data); - return bufferedAmount; + // Update subchannels. + this.#subchannels = fbsUtils.parseVector(data, 'subchannels'); } - private handleWorkerNotifications(): void - { - this.#channel.on(this.#internal.dataConsumerId, (event: string, data: any) => - { - switch (event) - { - case 'dataproducerclose': - { - if (this.#closed) + private handleWorkerNotifications(): void { + this.#channel.on( + this.#internal.dataConsumerId, + (event: Event, data?: Notification) => { + switch (event) { + case Event.DATACONSUMER_DATAPRODUCER_CLOSE: { + if (this.#closed) { + break; + } + + this.#closed = true; + + // Remove notification subscriptions. + this.#channel.removeAllListeners(this.#internal.dataConsumerId); + + this.emit('@dataproducerclose'); + this.safeEmit('dataproducerclose'); + + // Emit observer event. + this.#observer.safeEmit('close'); + break; + } - this.#closed = true; + case Event.DATACONSUMER_DATAPRODUCER_PAUSE: { + if (this.#dataProducerPaused) { + break; + } - // Remove notification subscriptions. - this.#channel.removeAllListeners(this.#internal.dataConsumerId); - this.#payloadChannel.removeAllListeners(this.#internal.dataConsumerId); + this.#dataProducerPaused = true; - this.emit('@dataproducerclose'); - this.safeEmit('dataproducerclose'); + this.safeEmit('dataproducerpause'); - // Emit observer event. - this.#observer.safeEmit('close'); + // Emit observer event. + if (!this.#paused) { + this.#observer.safeEmit('pause'); + } - break; - } + break; + } - case 'sctpsendbufferfull': - { - this.safeEmit('sctpsendbufferfull'); + case Event.DATACONSUMER_DATAPRODUCER_RESUME: { + if (!this.#dataProducerPaused) { + break; + } - break; - } + this.#dataProducerPaused = false; - case 'bufferedamountlow': - { - const { bufferedAmount } = data as { bufferedAmount: number }; + this.safeEmit('dataproducerresume'); - this.safeEmit('bufferedamountlow', bufferedAmount); + // Emit observer event. + if (!this.#paused) { + this.#observer.safeEmit('resume'); + } - break; - } + break; + } - default: - { - logger.error('ignoring unknown event "%s" in channel listener', event); - } - } - }); + case Event.DATACONSUMER_SCTP_SENDBUFFER_FULL: { + this.safeEmit('sctpsendbufferfull'); - this.#payloadChannel.on( - this.#internal.dataConsumerId, - (event: string, data: any | undefined, payload: Buffer) => - { - switch (event) - { - case 'message': - { - if (this.#closed) + break; + } + + case Event.DATACONSUMER_BUFFERED_AMOUNT_LOW: { + const notification = + new FbsDataConsumer.BufferedAmountLowNotification(); + + data!.body(notification); + + const bufferedAmount = notification.bufferedAmount(); + + this.safeEmit('bufferedamountlow', bufferedAmount); + + break; + } + + case Event.DATACONSUMER_MESSAGE: { + if (this.#closed) { break; + } + + const notification = new FbsDataConsumer.MessageNotification(); - const ppid = data.ppid as number; - const message = payload; + data!.body(notification); - this.safeEmit('message', message, ppid); + this.safeEmit( + 'message', + Buffer.from(notification.dataArray()!), + notification.ppid() + ); break; } - default: - { - logger.error('ignoring unknown event "%s" in payload channel listener', event); + default: { + logger.error(`ignoring unknown event "${event}"`); } } - }); + } + ); + } + + private handleListenerError(): void { + this.on('listenererror', (eventName, error) => { + logger.error( + `event listener threw an error [eventName:${eventName}]:`, + error + ); + }); } } + +export function dataConsumerTypeToFbs( + type: DataConsumerType +): FbsDataProducer.Type { + switch (type) { + case 'sctp': { + return FbsDataProducer.Type.SCTP; + } + + case 'direct': { + return FbsDataProducer.Type.DIRECT; + } + + default: { + throw new TypeError('invalid DataConsumerType: ${type}'); + } + } +} + +export function dataConsumerTypeFromFbs( + type: FbsDataProducer.Type +): DataConsumerType { + switch (type) { + case FbsDataProducer.Type.SCTP: { + return 'sctp'; + } + + case FbsDataProducer.Type.DIRECT: { + return 'direct'; + } + } +} + +export function parseDataConsumerDumpResponse( + data: FbsDataConsumer.DumpResponse +): DataConsumerDump { + return { + id: data.id()!, + dataProducerId: data.dataProducerId()!, + type: dataConsumerTypeFromFbs(data.type()), + sctpStreamParameters: + data.sctpStreamParameters() !== null + ? parseSctpStreamParameters(data.sctpStreamParameters()!) + : undefined, + label: data.label()!, + protocol: data.protocol()!, + bufferedAmountLowThreshold: data.bufferedAmountLowThreshold(), + paused: data.paused(), + dataProducerPaused: data.dataProducerPaused(), + subchannels: fbsUtils.parseVector(data, 'subchannels'), + }; +} + +function parseDataConsumerStats( + binary: FbsDataConsumer.GetStatsResponse +): DataConsumerStat { + return { + type: 'data-consumer', + timestamp: Number(binary.timestamp()), + label: binary.label()!, + protocol: binary.protocol()!, + messagesSent: Number(binary.messagesSent()), + bytesSent: Number(binary.bytesSent()), + bufferedAmount: binary.bufferedAmount(), + }; +} diff --git a/node/src/DataConsumerTypes.ts b/node/src/DataConsumerTypes.ts new file mode 100644 index 0000000000..5f0d06815c --- /dev/null +++ b/node/src/DataConsumerTypes.ts @@ -0,0 +1,233 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { SctpStreamParameters } from './sctpParametersTypes'; +import type { AppData } from './types'; + +export type DataConsumerOptions = + { + /** + * The id of the DataProducer to consume. + */ + dataProducerId: string; + + /** + * Just if consuming over SCTP. + * Whether data messages must be received in order. If true the messages will + * be sent reliably. Defaults to the value in the DataProducer if it has type + * 'sctp' or to true if it has type 'direct'. + */ + ordered?: boolean; + + /** + * Just if consuming over SCTP. + * When ordered is false indicates the time (in milliseconds) after which a + * SCTP packet will stop being retransmitted. Defaults to the value in the + * DataProducer if it has type 'sctp' or unset if it has type 'direct'. + */ + maxPacketLifeTime?: number; + + /** + * Just if consuming over SCTP. + * When ordered is false indicates the maximum number of times a packet will + * be retransmitted. Defaults to the value in the DataProducer if it has type + * 'sctp' or unset if it has type 'direct'. + */ + maxRetransmits?: number; + + /** + * Whether the data consumer must start in paused mode. Default false. + */ + paused?: boolean; + + /** + * Subchannels this data consumer initially subscribes to. + * Only used in case this data consumer receives messages from a local data + * producer that specifies subchannel(s) when calling send(). + */ + subchannels?: number[]; + + /** + * Custom application data. + */ + appData?: DataConsumerAppData; + }; + +/** + * DataConsumer type. + */ +export type DataConsumerType = 'sctp' | 'direct'; + +export type DataConsumerDump = { + id: string; + paused: boolean; + dataProducerPaused: boolean; + subchannels: number[]; + dataProducerId: string; + type: DataConsumerType; + sctpStreamParameters?: SctpStreamParameters; + label: string; + protocol: string; + bufferedAmountLowThreshold: number; +}; + +export type DataConsumerStat = { + type: string; + timestamp: number; + label: string; + protocol: string; + messagesSent: number; + bytesSent: number; + bufferedAmount: number; +}; + +export type DataConsumerEvents = { + transportclose: []; + dataproducerclose: []; + dataproducerpause: []; + dataproducerresume: []; + message: [Buffer, number]; + sctpsendbufferfull: []; + bufferedamountlow: [number]; + listenererror: [string, Error]; + // Private events. + '@close': []; + '@dataproducerclose': []; +}; + +export type DataConsumerObserver = + EnhancedEventEmitter; + +export type DataConsumerObserverEvents = { + close: []; + pause: []; + resume: []; +}; + +export interface DataConsumer + extends EnhancedEventEmitter { + /** + * DataConsumer id. + */ + get id(): string; + + /** + * Associated DataProducer id. + */ + get dataProducerId(): string; + + /** + * Whether the DataConsumer is closed. + */ + get closed(): boolean; + + /** + * DataConsumer type. + */ + get type(): DataConsumerType; + + /** + * SCTP stream parameters. + */ + get sctpStreamParameters(): SctpStreamParameters | undefined; + + /** + * DataChannel label. + */ + get label(): string; + + /** + * DataChannel protocol. + */ + get protocol(): string; + + /** + * Whether the DataConsumer is paused. + */ + get paused(): boolean; + + /** + * Whether the associate DataProducer is paused. + */ + get dataProducerPaused(): boolean; + + /** + * Get current subchannels this data consumer is subscribed to. + */ + get subchannels(): number[]; + + /** + * App custom data. + */ + get appData(): DataConsumerAppData; + + /** + * App custom data setter. + */ + set appData(appData: DataConsumerAppData); + + /** + * Observer. + */ + get observer(): DataConsumerObserver; + + /** + * Close the DataConsumer. + */ + close(): void; + + /** + * Transport was closed. + * + * @private + */ + transportClosed(): void; + + /** + * Dump DataConsumer. + */ + dump(): Promise; + + /** + * Get DataConsumer stats. + */ + getStats(): Promise; + + /** + * Pause the DataConsumer. + */ + pause(): Promise; + + /** + * Resume the DataConsumer. + */ + resume(): Promise; + + /** + * Set buffered amount low threshold. + */ + setBufferedAmountLowThreshold(threshold: number): Promise; + + /** + * Get buffered amount size. + */ + getBufferedAmount(): Promise; + + /** + * Send a message. + */ + send(message: string | Buffer, ppid?: number): Promise; + + /** + * Set subchannels. + */ + setSubchannels(subchannels: number[]): Promise; + + /** + * Add a subchannel. + */ + addSubchannel(subchannel: number): Promise; + + /** + * Remove a subchannel. + */ + removeSubchannel(subchannel: number): Promise; +} diff --git a/node/src/DataProducer.ts b/node/src/DataProducer.ts index 6a99288348..e1c2c0f6e1 100644 --- a/node/src/DataProducer.ts +++ b/node/src/DataProducer.ts @@ -1,73 +1,29 @@ import { Logger } from './Logger'; -import { EnhancedEventEmitter } from './EnhancedEventEmitter'; +import { EnhancedEventEmitter } from './enhancedEvents'; +import type { + DataProducer, + DataProducerType, + DataProducerDump, + DataProducerStat, + DataProducerEvents, + DataProducerObserver, + DataProducerObserverEvents, +} from './DataProducerTypes'; import { Channel } from './Channel'; -import { PayloadChannel } from './PayloadChannel'; -import { TransportInternal } from './Transport'; -import { SctpStreamParameters } from './SctpParameters'; - -export type DataProducerOptions = -{ - /** - * DataProducer id (just for Router.pipeToRouter() method). - */ - id?: string; - - /** - * SCTP parameters defining how the endpoint is sending the data. - * Just if messages are sent over SCTP. - */ - sctpStreamParameters?: SctpStreamParameters; - - /** - * A label which can be used to distinguish this DataChannel from others. - */ - label?: string; - - /** - * Name of the sub-protocol used by this DataChannel. - */ - protocol?: string; - - /** - * Custom application data. - */ - appData?: Record; -}; - -export type DataProducerStat = -{ - type: string; - timestamp: number; - label: string; - protocol: string; - messagesReceived: number; - bytesReceived: number; -}; - -/** - * DataProducer type. - */ -export type DataProducerType = 'sctp' | 'direct'; - -export type DataProducerEvents = -{ - transportclose: []; - // Private events. - '@close': []; -}; - -export type DataProducerObserverEvents = -{ - close: []; -}; - -type DataProducerInternal = TransportInternal & -{ +import type { TransportInternal } from './Transport'; +import type { SctpStreamParameters } from './sctpParametersTypes'; +import { parseSctpStreamParameters } from './sctpParametersFbsUtils'; +import type { AppData } from './types'; +import * as FbsTransport from './fbs/transport'; +import * as FbsNotification from './fbs/notification'; +import * as FbsRequest from './fbs/request'; +import * as FbsDataProducer from './fbs/data-producer'; + +type DataProducerInternal = TransportInternal & { dataProducerId: string; }; -type DataProducerData = -{ +type DataProducerData = { type: DataProducerType; sctpStreamParameters?: SctpStreamParameters; label: string; @@ -76,7 +32,9 @@ type DataProducerData = const logger = new Logger('DataProducer'); -export class DataProducer extends EnhancedEventEmitter +export class DataProducerImpl + extends EnhancedEventEmitter + implements DataProducer { // Internal data. readonly #internal: DataProducerInternal; @@ -87,38 +45,32 @@ export class DataProducer extends EnhancedEventEmitter // Channel instance. readonly #channel: Channel; - // PayloadChannel instance. - readonly #payloadChannel: PayloadChannel; - // Closed flag. #closed = false; + // Paused flag. + #paused = false; + // Custom app data. - readonly #appData: Record; + #appData: DataProducerAppData; // Observer instance. - readonly #observer = new EnhancedEventEmitter(); - - /** - * @private - */ - constructor( - { - internal, - data, - channel, - payloadChannel, - appData - }: - { - internal: DataProducerInternal; - data: DataProducerData; - channel: Channel; - payloadChannel: PayloadChannel; - appData?: Record; - } - ) - { + readonly #observer: DataProducerObserver = + new EnhancedEventEmitter(); + + constructor({ + internal, + data, + channel, + paused, + appData, + }: { + internal: DataProducerInternal; + data: DataProducerData; + channel: Channel; + paused: boolean; + appData?: DataProducerAppData; + }) { super(); logger.debug('constructor()'); @@ -126,91 +78,57 @@ export class DataProducer extends EnhancedEventEmitter this.#internal = internal; this.#data = data; this.#channel = channel; - this.#payloadChannel = payloadChannel; - this.#appData = appData || {}; + this.#paused = paused; + this.#appData = appData ?? ({} as DataProducerAppData); this.handleWorkerNotifications(); + this.handleListenerError(); } - /** - * DataProducer id. - */ - get id(): string - { + get id(): string { return this.#internal.dataProducerId; } - /** - * Whether the DataProducer is closed. - */ - get closed(): boolean - { + get closed(): boolean { return this.#closed; } - /** - * DataProducer type. - */ - get type(): DataProducerType - { + get type(): DataProducerType { return this.#data.type; } - /** - * SCTP stream parameters. - */ - get sctpStreamParameters(): SctpStreamParameters | undefined - { + get sctpStreamParameters(): SctpStreamParameters | undefined { return this.#data.sctpStreamParameters; } - /** - * DataChannel label. - */ - get label(): string - { + get label(): string { return this.#data.label; } - /** - * DataChannel protocol. - */ - get protocol(): string - { + get protocol(): string { return this.#data.protocol; } - /** - * App custom data. - */ - get appData(): Record - { + get paused(): boolean { + return this.#paused; + } + + get appData(): DataProducerAppData { return this.#appData; } - /** - * Invalid setter. - */ - set appData(appData: Record) // eslint-disable-line no-unused-vars - { - throw new Error('cannot override appData object'); + set appData(appData: DataProducerAppData) { + this.#appData = appData; } - /** - * Observer. - */ - get observer(): EnhancedEventEmitter - { + get observer(): DataProducerObserver { return this.#observer; } - /** - * Close the DataProducer. - */ - close(): void - { - if (this.#closed) + close(): void { + if (this.#closed) { return; + } logger.debug('close()'); @@ -218,11 +136,19 @@ export class DataProducer extends EnhancedEventEmitter // Remove notification subscriptions. this.#channel.removeAllListeners(this.#internal.dataProducerId); - this.#payloadChannel.removeAllListeners(this.#internal.dataProducerId); - const reqData = { dataProducerId: this.#internal.dataProducerId }; - - this.#channel.request('transport.closeDataProducer', this.#internal.transportId, reqData) + /* Build Request. */ + const requestOffset = new FbsTransport.CloseDataProducerRequestT( + this.#internal.dataProducerId + ).pack(this.#channel.bufferBuilder); + + this.#channel + .request( + FbsRequest.Method.TRANSPORT_CLOSE_DATAPRODUCER, + FbsRequest.Body.Transport_CloseDataProducerRequest, + requestOffset, + this.#internal.transportId + ) .catch(() => {}); this.emit('@close'); @@ -231,15 +157,10 @@ export class DataProducer extends EnhancedEventEmitter this.#observer.safeEmit('close'); } - /** - * Transport was closed. - * - * @private - */ - transportClosed(): void - { - if (this.#closed) + transportClosed(): void { + if (this.#closed) { return; + } logger.debug('transportClosed()'); @@ -247,7 +168,6 @@ export class DataProducer extends EnhancedEventEmitter // Remove notification subscriptions. this.#channel.removeAllListeners(this.#internal.dataProducerId); - this.#payloadChannel.removeAllListeners(this.#internal.dataProducerId); this.safeEmit('transportclose'); @@ -255,33 +175,89 @@ export class DataProducer extends EnhancedEventEmitter this.#observer.safeEmit('close'); } - /** - * Dump DataProducer. - */ - async dump(): Promise - { + async dump(): Promise { logger.debug('dump()'); - return this.#channel.request('dataProducer.dump', this.#internal.dataProducerId); + const response = await this.#channel.request( + FbsRequest.Method.DATAPRODUCER_DUMP, + undefined, + undefined, + this.#internal.dataProducerId + ); + + /* Decode Response. */ + const produceResponse = new FbsDataProducer.DumpResponse(); + + response.body(produceResponse); + + return parseDataProducerDumpResponse(produceResponse); } - /** - * Get DataProducer stats. - */ - async getStats(): Promise - { + async getStats(): Promise { logger.debug('getStats()'); - return this.#channel.request('dataProducer.getStats', this.#internal.dataProducerId); + const response = await this.#channel.request( + FbsRequest.Method.DATAPRODUCER_GET_STATS, + undefined, + undefined, + this.#internal.dataProducerId + ); + + /* Decode Response. */ + const data = new FbsDataProducer.GetStatsResponse(); + + response.body(data); + + return [parseDataProducerStats(data)]; } - /** - * Send data (just valid for DataProducers created on a DirectTransport). - */ - send(message: string | Buffer, ppid?: number): void - { - if (typeof message !== 'string' && !Buffer.isBuffer(message)) - { + async pause(): Promise { + logger.debug('pause()'); + + await this.#channel.request( + FbsRequest.Method.DATAPRODUCER_PAUSE, + undefined, + undefined, + this.#internal.dataProducerId + ); + + const wasPaused = this.#paused; + + this.#paused = true; + + // Emit observer event. + if (!wasPaused) { + this.#observer.safeEmit('pause'); + } + } + + async resume(): Promise { + logger.debug('resume()'); + + await this.#channel.request( + FbsRequest.Method.DATAPRODUCER_RESUME, + undefined, + undefined, + this.#internal.dataProducerId + ); + + const wasPaused = this.#paused; + + this.#paused = false; + + // Emit observer event. + if (wasPaused) { + this.#observer.safeEmit('resume'); + } + } + + send( + message: string | Buffer, + ppid?: number, + subchannels?: number[], + requiredSubchannel?: number + ): void { + if (typeof message !== 'string' && !Buffer.isBuffer(message)) { throw new TypeError('message must be a string or a Buffer'); } @@ -301,27 +277,131 @@ export class DataProducer extends EnhancedEventEmitter * +-------------------------------+----------+ */ - if (typeof ppid !== 'number') - { - ppid = (typeof message === 'string') - ? message.length > 0 ? 51 : 56 - : message.length > 0 ? 53 : 57; + if (typeof ppid !== 'number') { + ppid = + typeof message === 'string' + ? message.length > 0 + ? 51 + : 56 + : message.length > 0 + ? 53 + : 57; } // Ensure we honor PPIDs. - if (ppid === 56) + if (ppid === 56) { message = ' '; - else if (ppid === 57) + } else if (ppid === 57) { message = Buffer.alloc(1); + } + + const builder = this.#channel.bufferBuilder; - const notifData = String(ppid); + let dataOffset = 0; - this.#payloadChannel.notify( - 'dataProducer.send', this.#internal.dataProducerId, notifData, message); + const subchannelsOffset = + FbsDataProducer.SendNotification.createSubchannelsVector( + builder, + subchannels ?? [] + ); + + if (typeof message === 'string') { + message = Buffer.from(message); + } + + dataOffset = FbsDataProducer.SendNotification.createDataVector( + builder, + message + ); + + const notificationOffset = + FbsDataProducer.SendNotification.createSendNotification( + builder, + ppid, + dataOffset, + subchannelsOffset, + requiredSubchannel ?? null + ); + + this.#channel.notify( + FbsNotification.Event.DATAPRODUCER_SEND, + FbsNotification.Body.DataProducer_SendNotification, + notificationOffset, + this.#internal.dataProducerId + ); } - private handleWorkerNotifications(): void - { + private handleWorkerNotifications(): void { // No need to subscribe to any event. } + + private handleListenerError(): void { + this.on('listenererror', (eventName, error) => { + logger.error( + `event listener threw an error [eventName:${eventName}]:`, + error + ); + }); + } +} + +export function dataProducerTypeToFbs( + type: DataProducerType +): FbsDataProducer.Type { + switch (type) { + case 'sctp': { + return FbsDataProducer.Type.SCTP; + } + + case 'direct': { + return FbsDataProducer.Type.DIRECT; + } + + default: { + throw new TypeError('invalid DataConsumerType: ${type}'); + } + } +} + +export function dataProducerTypeFromFbs( + type: FbsDataProducer.Type +): DataProducerType { + switch (type) { + case FbsDataProducer.Type.SCTP: { + return 'sctp'; + } + + case FbsDataProducer.Type.DIRECT: { + return 'direct'; + } + } +} + +export function parseDataProducerDumpResponse( + data: FbsDataProducer.DumpResponse +): DataProducerDump { + return { + id: data.id()!, + type: dataProducerTypeFromFbs(data.type()), + sctpStreamParameters: + data.sctpStreamParameters() !== null + ? parseSctpStreamParameters(data.sctpStreamParameters()!) + : undefined, + label: data.label()!, + protocol: data.protocol()!, + paused: data.paused(), + }; +} + +function parseDataProducerStats( + binary: FbsDataProducer.GetStatsResponse +): DataProducerStat { + return { + type: 'data-producer', + timestamp: Number(binary.timestamp()), + label: binary.label()!, + protocol: binary.protocol()!, + messagesReceived: Number(binary.messagesReceived()), + bytesReceived: Number(binary.bytesReceived()), + }; } diff --git a/node/src/DataProducerTypes.ts b/node/src/DataProducerTypes.ts new file mode 100644 index 0000000000..efb64d35bf --- /dev/null +++ b/node/src/DataProducerTypes.ts @@ -0,0 +1,171 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { SctpStreamParameters } from './sctpParametersTypes'; +import type { AppData } from './types'; + +export type DataProducerOptions = + { + /** + * DataProducer id (just for Router.pipeToRouter() method). + */ + id?: string; + + /** + * SCTP parameters defining how the endpoint is sending the data. + * Just if messages are sent over SCTP. + */ + sctpStreamParameters?: SctpStreamParameters; + + /** + * A label which can be used to distinguish this DataChannel from others. + */ + label?: string; + + /** + * Name of the sub-protocol used by this DataChannel. + */ + protocol?: string; + + /** + * Whether the data producer must start in paused mode. Default false. + */ + paused?: boolean; + + /** + * Custom application data. + */ + appData?: DataProducerAppData; + }; + +/** + * DataProducer type. + */ +export type DataProducerType = 'sctp' | 'direct'; + +export type DataProducerDump = { + id: string; + paused: boolean; + type: DataProducerType; + sctpStreamParameters?: SctpStreamParameters; + label: string; + protocol: string; +}; + +export type DataProducerStat = { + type: string; + timestamp: number; + label: string; + protocol: string; + messagesReceived: number; + bytesReceived: number; +}; + +export type DataProducerEvents = { + transportclose: []; + listenererror: [string, Error]; + // Private events. + '@close': []; +}; + +export type DataProducerObserver = + EnhancedEventEmitter; + +export type DataProducerObserverEvents = { + close: []; + pause: []; + resume: []; +}; + +export interface DataProducer + extends EnhancedEventEmitter { + /** + * DataProducer id. + */ + get id(): string; + + /** + * Whether the DataProducer is closed. + */ + get closed(): boolean; + + /** + * DataProducer type. + */ + get type(): DataProducerType; + + /** + * SCTP stream parameters. + */ + get sctpStreamParameters(): SctpStreamParameters | undefined; + + /** + * DataChannel label. + */ + get label(): string; + + /** + * DataChannel protocol. + */ + get protocol(): string; + + /** + * Whether the DataProducer is paused. + */ + get paused(): boolean; + + /** + * App custom data. + */ + get appData(): DataProducerAppData; + + /** + * App custom data setter. + */ + set appData(appData: DataProducerAppData); + + /** + * Observer. + */ + get observer(): DataProducerObserver; + + /** + * Close the DataProducer. + */ + close(): void; + + /** + * Transport was closed. + * + * @private + */ + transportClosed(): void; + + /** + * Dump DataProducer. + */ + dump(): Promise; + + /** + * Get DataProducer stats. + */ + getStats(): Promise; + + /** + * Pause the DataProducer. + */ + pause(): Promise; + + /** + * Resume the DataProducer. + */ + resume(): Promise; + + /** + * Send data (just valid for DataProducers created on a DirectTransport). + */ + send( + message: string | Buffer, + ppid?: number, + subchannels?: number[], + requiredSubchannel?: number + ): void; +} diff --git a/node/src/DirectTransport.ts b/node/src/DirectTransport.ts index 0b7f7ea865..a75d4b18bb 100644 --- a/node/src/DirectTransport.ts +++ b/node/src/DirectTransport.ts @@ -1,229 +1,248 @@ import { Logger } from './Logger'; -import { UnsupportedError } from './errors'; +import { EnhancedEventEmitter } from './enhancedEvents'; +import type { + DirectTransport, + DirectTransportDump, + DirectTransportStat, + DirectTransportEvents, + DirectTransportObserver, + DirectTransportObserverEvents, +} from './DirectTransportTypes'; +import type { Transport, BaseTransportDump } from './TransportTypes'; import { - Transport, - TransportTraceEventData, - TransportEvents, - TransportObserverEvents, - TransportConstructorOptions + TransportImpl, + TransportConstructorOptions, + parseBaseTransportDump, + parseBaseTransportStats, + parseTransportTraceEventData, } from './Transport'; -import { SctpParameters } from './SctpParameters'; - -export type DirectTransportOptions = -{ - /** - * Maximum allowed size for direct messages sent from DataProducers. - * Default 262144. - */ - maxMessageSize: number; - - /** - * Custom application data. - */ - appData?: Record; -}; - -export type DirectTransportStat = -{ - // Common to all Transports. - type: string; - transportId: string; - timestamp: number; - bytesReceived: number; - recvBitrate: number; - bytesSent: number; - sendBitrate: number; - rtpBytesReceived: number; - rtpRecvBitrate: number; - rtpBytesSent: number; - rtpSendBitrate: number; - rtxBytesReceived: number; - rtxRecvBitrate: number; - rtxBytesSent: number; - rtxSendBitrate: number; - probationBytesSent: number; - probationSendBitrate: number; - availableOutgoingBitrate?: number; - availableIncomingBitrate?: number; - maxIncomingBitrate?: number; -}; - -export type DirectTransportEvents = TransportEvents & -{ - rtcp: [Buffer]; -}; - -export type DirectTransportObserverEvents = TransportObserverEvents & -{ - rtcp: [Buffer]; -}; - -type DirectTransportConstructorOptions = TransportConstructorOptions & -{ - data: DirectTransportData; -}; - -export type DirectTransportData = -{ +import type { SctpParameters } from './sctpParametersTypes'; +import type { AppData } from './types'; +import { UnsupportedError } from './errors'; +import { Event, Notification } from './fbs/notification'; +import * as FbsDirectTransport from './fbs/direct-transport'; +import * as FbsTransport from './fbs/transport'; +import * as FbsNotification from './fbs/notification'; +import * as FbsRequest from './fbs/request'; + +type DirectTransportConstructorOptions = + TransportConstructorOptions & { + data: DirectTransportData; + }; + +export type DirectTransportData = { sctpParameters?: SctpParameters; }; const logger = new Logger('DirectTransport'); -export class DirectTransport extends - Transport +export class DirectTransportImpl< + DirectTransportAppData extends AppData = AppData, + > + extends TransportImpl< + DirectTransportAppData, + DirectTransportEvents, + DirectTransportObserver + > + implements Transport, DirectTransport { // DirectTransport data. + // eslint-disable-next-line no-unused-private-class-members readonly #data: DirectTransportData; - /** - * @private - */ - constructor(options: DirectTransportConstructorOptions) - { - super(options); + constructor( + options: DirectTransportConstructorOptions + ) { + const observer: DirectTransportObserver = + new EnhancedEventEmitter(); + + super(options, observer); logger.debug('constructor()'); - this.#data = - { + this.#data = { // Nothing. }; this.handleWorkerNotifications(); + this.handleListenerError(); + } + + get type(): 'direct' { + return 'direct'; } - /** - * Close the DirectTransport. - * - * @override - */ - close(): void - { - if (this.closed) + get observer(): DirectTransportObserver { + return super.observer; + } + + close(): void { + if (this.closed) { return; + } super.close(); } - /** - * Router was closed. - * - * @private - * @override - */ - routerClosed(): void - { - if (this.closed) + routerClosed(): void { + if (this.closed) { return; + } super.routerClosed(); } - /** - * Get DirectTransport stats. - * - * @override - */ - async getStats(): Promise - { + async dump(): Promise { + logger.debug('dump()'); + + const response = await this.channel.request( + FbsRequest.Method.TRANSPORT_DUMP, + undefined, + undefined, + this.internal.transportId + ); + + /* Decode Response. */ + const data = new FbsDirectTransport.DumpResponse(); + + response.body(data); + + return parseDirectTransportDumpResponse(data); + } + + async getStats(): Promise { logger.debug('getStats()'); - return this.channel.request('transport.getStats', this.internal.transportId); + const response = await this.channel.request( + FbsRequest.Method.TRANSPORT_GET_STATS, + undefined, + undefined, + this.internal.transportId + ); + + /* Decode Response. */ + const data = new FbsDirectTransport.GetStatsResponse(); + + response.body(data); + + return [parseGetStatsResponse(data)]; } - /** - * NO-OP method in DirectTransport. - * - * @override - */ - async connect(): Promise - { + // eslint-disable-next-line @typescript-eslint/require-await + async connect(): Promise { logger.debug('connect()'); } - /** - * @override - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async setMaxIncomingBitrate(bitrate: number): Promise - { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/require-await + async setMaxIncomingBitrate(bitrate: number): Promise { + throw new UnsupportedError( + 'setMaxIncomingBitrate() not implemented in DirectTransport' + ); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/require-await + async setMaxOutgoingBitrate(bitrate: number): Promise { throw new UnsupportedError( - 'setMaxIncomingBitrate() not implemented in DirectTransport'); + 'setMaxOutgoingBitrate() not implemented in DirectTransport' + ); } - /** - * @override - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async setMaxOutgoingBitrate(bitrate: number): Promise - { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/require-await + async setMinOutgoingBitrate(bitrate: number): Promise { throw new UnsupportedError( - 'setMaxOutgoingBitrate() not implemented in DirectTransport'); + 'setMinOutgoingBitrate() not implemented in DirectTransport' + ); } - /** - * Send RTCP packet. - */ - sendRtcp(rtcpPacket: Buffer) - { - if (!Buffer.isBuffer(rtcpPacket)) - { + sendRtcp(rtcpPacket: Buffer): void { + if (!Buffer.isBuffer(rtcpPacket)) { throw new TypeError('rtcpPacket must be a Buffer'); } - this.payloadChannel.notify( - 'transport.sendRtcp', this.internal.transportId, undefined, rtcpPacket); + const builder = this.channel.bufferBuilder; + const dataOffset = FbsTransport.SendRtcpNotification.createDataVector( + builder, + rtcpPacket + ); + const notificationOffset = + FbsTransport.SendRtcpNotification.createSendRtcpNotification( + builder, + dataOffset + ); + + this.channel.notify( + FbsNotification.Event.TRANSPORT_SEND_RTCP, + FbsNotification.Body.Transport_SendRtcpNotification, + notificationOffset, + this.internal.transportId + ); } - private handleWorkerNotifications(): void - { - this.channel.on(this.internal.transportId, (event: string, data?: any) => - { - switch (event) - { - case 'trace': - { - const trace = data as TransportTraceEventData; + private handleWorkerNotifications(): void { + this.channel.on( + this.internal.transportId, + (event: Event, data?: Notification) => { + switch (event) { + case Event.TRANSPORT_TRACE: { + const notification = new FbsTransport.TraceNotification(); - this.safeEmit('trace', trace); + data!.body(notification); - // Emit observer event. - this.observer.safeEmit('trace', trace); + const trace = parseTransportTraceEventData(notification); - break; - } + this.safeEmit('trace', trace); - default: - { - logger.error('ignoring unknown event "%s"', event); - } - } - }); + // Emit observer event. + this.observer.safeEmit('trace', trace); - this.payloadChannel.on( - this.internal.transportId, - (event: string, data: any | undefined, payload: Buffer) => - { - switch (event) - { - case 'rtcp': - { - if (this.closed) + break; + } + + case Event.DIRECTTRANSPORT_RTCP: { + if (this.closed) { break; + } - const packet = payload; + const notification = new FbsDirectTransport.RtcpNotification(); - this.safeEmit('rtcp', packet); + data!.body(notification); + + this.safeEmit('rtcp', Buffer.from(notification.dataArray()!)); break; } - default: - { - logger.error('ignoring unknown event "%s"', event); + default: { + logger.error(`ignoring unknown event "${event}"`); } } - }); + } + ); + } + + private handleListenerError(): void { + this.on('listenererror', (eventName, error) => { + logger.error( + `event listener threw an error [eventName:${eventName}]:`, + error + ); + }); } } + +export function parseDirectTransportDumpResponse( + binary: FbsDirectTransport.DumpResponse +): BaseTransportDump { + return parseBaseTransportDump(binary.base()!); +} + +function parseGetStatsResponse( + binary: FbsDirectTransport.GetStatsResponse +): DirectTransportStat { + const base = parseBaseTransportStats(binary.base()!); + + return { + ...base, + type: 'direct-transport', + }; +} diff --git a/node/src/DirectTransportTypes.ts b/node/src/DirectTransportTypes.ts new file mode 100644 index 0000000000..85bb824341 --- /dev/null +++ b/node/src/DirectTransportTypes.ts @@ -0,0 +1,89 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { + Transport, + BaseTransportDump, + BaseTransportStats, + TransportEvents, + TransportObserverEvents, +} from './TransportTypes'; +import type { AppData } from './types'; + +export type DirectTransportOptions< + DirectTransportAppData extends AppData = AppData, +> = { + /** + * Maximum allowed size for direct messages sent from DataProducers. + * Default 262144. + */ + maxMessageSize: number; + + /** + * Custom application data. + */ + appData?: DirectTransportAppData; +}; + +export type DirectTransportDump = BaseTransportDump; + +export type DirectTransportStat = BaseTransportStats & { + type: string; +}; + +export type DirectTransportEvents = TransportEvents & { + rtcp: [Buffer]; +}; + +export type DirectTransportObserver = + EnhancedEventEmitter; + +export type DirectTransportObserverEvents = TransportObserverEvents & { + rtcp: [Buffer]; +}; + +export interface DirectTransport< + DirectTransportAppData extends AppData = AppData, +> extends Transport< + DirectTransportAppData, + DirectTransportEvents, + DirectTransportObserver + > { + /** + * Transport type. + * + * @override + */ + get type(): 'direct'; + + /** + * Observer. + * + * @override + */ + get observer(): DirectTransportObserver; + + /** + * Dump DirectTransport. + * + * @override + */ + dump(): Promise; + + /** + * Get DirectTransport stats. + * + * @override + */ + getStats(): Promise; + + /** + * NO-OP method in DirectTransport. + * + * @override + */ + connect(): Promise; + + /** + * Send RTCP packet. + */ + sendRtcp(rtcpPacket: Buffer): void; +} diff --git a/node/src/Logger.ts b/node/src/Logger.ts index b5a199d989..1dd0b30a75 100644 --- a/node/src/Logger.ts +++ b/node/src/Logger.ts @@ -1,23 +1,41 @@ import debug from 'debug'; +import type { EnhancedEventEmitter } from './enhancedEvents'; const APP_NAME = 'mediasoup'; -export class Logger -{ +export type LoggerEmitterEvents = { + debuglog: [string, string]; + warnlog: [string, string]; + errorlog: [string, string, Error?]; +}; + +export type LoggerEmitter = EnhancedEventEmitter; + +export class Logger { + private static debugLogEmitter?: LoggerEmitter; + private static warnLogEmitter?: LoggerEmitter; + private static errorLogEmitter?: LoggerEmitter; + readonly #debug: debug.Debugger; readonly #warn: debug.Debugger; readonly #error: debug.Debugger; - constructor(prefix?: string) - { - if (prefix) - { + static setEmitters( + debugLogEmitter?: LoggerEmitter, + warnLogEmitter?: LoggerEmitter, + errorLogEmitter?: LoggerEmitter + ): void { + Logger.debugLogEmitter = debugLogEmitter; + Logger.warnLogEmitter = warnLogEmitter; + Logger.errorLogEmitter = errorLogEmitter; + } + + constructor(prefix?: string) { + if (prefix) { this.#debug = debug(`${APP_NAME}:${prefix}`); this.#warn = debug(`${APP_NAME}:WARN:${prefix}`); this.#error = debug(`${APP_NAME}:ERROR:${prefix}`); - } - else - { + } else { this.#debug = debug(APP_NAME); this.#warn = debug(`${APP_NAME}:WARN`); this.#error = debug(`${APP_NAME}:ERROR`); @@ -30,18 +48,26 @@ export class Logger /* eslint-enable no-console */ } - get debug(): debug.Debugger - { - return this.#debug; + debug(log: string): void { + this.#debug(log); + + Logger.debugLogEmitter?.safeEmit('debuglog', this.#debug.namespace, log); } - get warn(): debug.Debugger - { - return this.#warn; + warn(log: string): void { + this.#warn(log); + + Logger.warnLogEmitter?.safeEmit('warnlog', this.#warn.namespace, log); } - get error(): debug.Debugger - { - return this.#error; + error(log: string, error?: Error): void { + this.#error(log, error); + + Logger.errorLogEmitter?.safeEmit( + 'errorlog', + this.#error.namespace, + log, + error + ); } } diff --git a/node/src/PayloadChannel.ts b/node/src/PayloadChannel.ts deleted file mode 100644 index 2b5d83b412..0000000000 --- a/node/src/PayloadChannel.ts +++ /dev/null @@ -1,384 +0,0 @@ -import * as os from 'os'; -import { Duplex } from 'stream'; -import { Logger } from './Logger'; -import { EnhancedEventEmitter } from './EnhancedEventEmitter'; -import { InvalidStateError } from './errors'; - -const littleEndian = os.endianness() == 'LE'; -const logger = new Logger('PayloadChannel'); - -type Sent = -{ - id: number; - method: string; - resolve: (data?: any) => void; - reject: (error: Error) => void; - close: () => void; -}; - -// Binary length for a 4194304 bytes payload. -const MESSAGE_MAX_LEN = 4194308; -const PAYLOAD_MAX_LEN = 4194304; - -export class PayloadChannel extends EnhancedEventEmitter -{ - // Closed flag. - #closed = false; - - // Unix Socket instance for sending messages to the worker process. - readonly #producerSocket: Duplex; - - // Unix Socket instance for receiving messages to the worker process. - readonly #consumerSocket: Duplex; - - // Next id for messages sent to the worker process. - #nextId = 0; - - // Map of pending sent requests. - readonly #sents: Map = new Map(); - - // Buffer for reading messages from the worker. - #recvBuffer = Buffer.alloc(0); - - // Ongoing notification (waiting for its payload). - #ongoingNotification?: { targetId: string; event: string; data?: any }; - - /** - * @private - */ - constructor( - { - producerSocket, - consumerSocket - }: - { - producerSocket: any; - consumerSocket: any; - }) - { - super(); - - logger.debug('constructor()'); - - this.#producerSocket = producerSocket as Duplex; - this.#consumerSocket = consumerSocket as Duplex; - - // Read PayloadChannel notifications from the worker. - this.#consumerSocket.on('data', (buffer: Buffer) => - { - if (!this.#recvBuffer.length) - { - this.#recvBuffer = buffer; - } - else - { - this.#recvBuffer = Buffer.concat( - [ this.#recvBuffer, buffer ], - this.#recvBuffer.length + buffer.length); - } - - if (this.#recvBuffer.length > PAYLOAD_MAX_LEN) - { - logger.error('receiving buffer is full, discarding all data in it'); - - // Reset the buffer and exit. - this.#recvBuffer = Buffer.alloc(0); - - return; - } - - let msgStart = 0; - - while (true) // eslint-disable-line no-constant-condition - { - const readLen = this.#recvBuffer.length - msgStart; - - if (readLen < 4) - { - // Incomplete data. - break; - } - - const dataView = new DataView( - this.#recvBuffer.buffer, - this.#recvBuffer.byteOffset + msgStart); - const msgLen = dataView.getUint32(0, littleEndian); - - if (readLen < 4 + msgLen) - { - // Incomplete data. - break; - } - - const payload = this.#recvBuffer.subarray(msgStart + 4, msgStart + 4 + msgLen); - - msgStart += 4 + msgLen; - - this.processData(payload); - } - - if (msgStart != 0) - { - this.#recvBuffer = this.#recvBuffer.slice(msgStart); - } - }); - - this.#consumerSocket.on('end', () => ( - logger.debug('Consumer PayloadChannel ended by the worker process') - )); - - this.#consumerSocket.on('error', (error) => ( - logger.error('Consumer PayloadChannel error: %s', String(error)) - )); - - this.#producerSocket.on('end', () => ( - logger.debug('Producer PayloadChannel ended by the worker process') - )); - - this.#producerSocket.on('error', (error) => ( - logger.error('Producer PayloadChannel error: %s', String(error)) - )); - } - - /** - * @private - */ - close(): void - { - if (this.#closed) - return; - - logger.debug('close()'); - - this.#closed = true; - - // Remove event listeners but leave a fake 'error' handler to avoid - // propagation. - this.#consumerSocket.removeAllListeners('end'); - this.#consumerSocket.removeAllListeners('error'); - this.#consumerSocket.on('error', () => {}); - - this.#producerSocket.removeAllListeners('end'); - this.#producerSocket.removeAllListeners('error'); - this.#producerSocket.on('error', () => {}); - - // Destroy the socket after a while to allow pending incoming messages. - setTimeout(() => - { - try { this.#producerSocket.destroy(); } - catch (error) {} - try { this.#consumerSocket.destroy(); } - catch (error) {} - }, 200); - } - - /** - * @private - */ - notify( - event: string, - handlerId: string, - data: string | undefined, - payload: string | Buffer - ): void - { - logger.debug('notify() [event:%s]', event); - - if (this.#closed) - throw new InvalidStateError('PayloadChannel closed'); - - const notification = `n:${event}:${handlerId}:${data}`; - - if (Buffer.byteLength(notification) > MESSAGE_MAX_LEN) - throw new Error('PayloadChannel notification too big'); - else if (Buffer.byteLength(payload) > MESSAGE_MAX_LEN) - throw new Error('PayloadChannel payload too big'); - - try - { - // This may throw if closed or remote side ended. - this.#producerSocket.write( - Buffer.from(Uint32Array.of(Buffer.byteLength(notification)).buffer)); - this.#producerSocket.write(notification); - } - catch (error) - { - logger.warn('notify() | sending notification failed: %s', String(error)); - - return; - } - - try - { - // This may throw if closed or remote side ended. - this.#producerSocket.write( - Buffer.from(Uint32Array.of(Buffer.byteLength(payload)).buffer)); - this.#producerSocket.write(payload); - } - catch (error) - { - logger.warn('notify() | sending payload failed: %s', String(error)); - - return; - } - } - - /** - * @private - */ - async request( - method: string, - handlerId: string, - data: string, - payload: string | Buffer): Promise - { - this.#nextId < 4294967295 ? ++this.#nextId : (this.#nextId = 1); - - const id = this.#nextId; - - logger.debug('request() [method:%s, id:%s]', method, id); - - if (this.#closed) - throw new InvalidStateError('PayloadChannel closed'); - - const request = `r:${id}:${method}:${handlerId}:${data}`; - - if (Buffer.byteLength(request) > MESSAGE_MAX_LEN) - throw new Error('PayloadChannel request too big'); - else if (Buffer.byteLength(payload) > MESSAGE_MAX_LEN) - throw new Error('PayloadChannel payload too big'); - - // This may throw if closed or remote side ended. - this.#producerSocket.write( - Buffer.from(Uint32Array.of(Buffer.byteLength(request)).buffer)); - this.#producerSocket.write(request); - this.#producerSocket.write( - Buffer.from(Uint32Array.of(Buffer.byteLength(payload)).buffer)); - this.#producerSocket.write(payload); - - return new Promise((pResolve, pReject) => - { - const sent: Sent = - { - id : id, - method : method, - resolve : (data2) => - { - if (!this.#sents.delete(id)) - return; - - pResolve(data2); - }, - reject : (error) => - { - if (!this.#sents.delete(id)) - return; - - pReject(error); - }, - close : () => - { - pReject(new InvalidStateError('PayloadChannel closed')); - } - }; - - // Add sent stuff to the map. - this.#sents.set(id, sent); - }); - } - - private processData(data: Buffer): void - { - if (!this.#ongoingNotification) - { - let msg; - - try - { - msg = JSON.parse(data.toString('utf8')); - } - catch (error) - { - logger.error( - 'received invalid data from the worker process: %s', - String(error)); - - return; - } - - // If a response, retrieve its associated request. - if (msg.id) - { - const sent = this.#sents.get(msg.id); - - if (!sent) - { - logger.error( - 'received response does not match any sent request [id:%s]', msg.id); - - return; - } - - if (msg.accepted) - { - logger.debug( - 'request succeeded [method:%s, id:%s]', sent.method, sent.id); - - sent.resolve(msg.data); - } - else if (msg.error) - { - logger.warn( - 'request failed [method:%s, id:%s]: %s', - sent.method, sent.id, msg.reason); - - switch (msg.error) - { - case 'TypeError': - sent.reject(new TypeError(msg.reason)); - break; - - default: - sent.reject(new Error(msg.reason)); - } - } - else - { - logger.error( - 'received response is not accepted nor rejected [method:%s, id:%s]', - sent.method, sent.id); - } - } - // If a notification, create the ongoing notification instance. - else if (msg.targetId && msg.event) - { - this.#ongoingNotification = - { - targetId : String(msg.targetId), - event : msg.event, - data : msg.data - }; - } - else - { - logger.error('received data is not a notification nor a response'); - - return; - } - } - else - { - const payload = data as Buffer; - - // Emit the corresponding event. - this.emit( - this.#ongoingNotification.targetId, - this.#ongoingNotification.event, - this.#ongoingNotification.data, - payload); - - // Unset ongoing notification. - this.#ongoingNotification = undefined; - } - } -} diff --git a/node/src/PipeTransport.ts b/node/src/PipeTransport.ts index e9fadbd6e5..266c0d50d4 100644 --- a/node/src/PipeTransport.ts +++ b/node/src/PipeTransport.ts @@ -1,133 +1,55 @@ -import { v4 as uuidv4 } from 'uuid'; +import * as flatbuffers from 'flatbuffers'; import { Logger } from './Logger'; +import { EnhancedEventEmitter } from './enhancedEvents'; import * as ortc from './ortc'; +import type { + PipeTransport, + PipeConsumerOptions, + PipeTransportDump, + PipeTransportStat, + PipeTransportEvents, + PipeTransportObserver, + PipeTransportObserverEvents, +} from './PipeTransportTypes'; +import type { Transport, TransportTuple, SctpState } from './TransportTypes'; import { - Transport, - TransportListenIp, - TransportTuple, - TransportTraceEventData, - TransportEvents, - TransportObserverEvents, + TransportImpl, TransportConstructorOptions, - SctpState + parseBaseTransportDump, + parseBaseTransportStats, + parseSctpState, + parseTuple, + parseTransportTraceEventData, } from './Transport'; -import { Consumer, ConsumerType } from './Consumer'; -import { SctpParameters, NumSctpStreams } from './SctpParameters'; -import { SrtpParameters } from './SrtpParameters'; - -export type PipeTransportOptions = -{ - /** - * Listening IP address. - */ - listenIp: TransportListenIp | string; - - /** - * Fixed port to listen on instead of selecting automatically from Worker's port - * range. - */ - port?: number; - - /** - * Create a SCTP association. Default false. - */ - enableSctp?: boolean; - - /** - * SCTP streams number. - */ - numSctpStreams?: NumSctpStreams; - - /** - * Maximum allowed size for SCTP messages sent by DataProducers. - * Default 268435456. - */ - maxSctpMessageSize?: number; - - /** - * Maximum SCTP send buffer used by DataConsumers. - * Default 268435456. - */ - sctpSendBufferSize?: number; - - /** - * Enable RTX and NACK for RTP retransmission. Useful if both Routers are - * located in different hosts and there is packet lost in the link. For this - * to work, both PipeTransports must enable this setting. Default false. - */ - enableRtx?: boolean; - - /** - * Enable SRTP. Useful to protect the RTP and RTCP traffic if both Routers - * are located in different hosts. For this to work, connect() must be called - * with remote SRTP parameters. Default false. - */ - enableSrtp?: boolean; - - /** - * Custom application data. - */ - appData?: Record; -}; - -export type PipeTransportStat = -{ - // Common to all Transports. - type: string; - transportId: string; - timestamp: number; - sctpState?: SctpState; - bytesReceived: number; - recvBitrate: number; - bytesSent: number; - sendBitrate: number; - rtpBytesReceived: number; - rtpRecvBitrate: number; - rtpBytesSent: number; - rtpSendBitrate: number; - rtxBytesReceived: number; - rtxRecvBitrate: number; - rtxBytesSent: number; - rtxSendBitrate: number; - probationBytesSent: number; - probationSendBitrate: number; - availableOutgoingBitrate?: number; - availableIncomingBitrate?: number; - maxIncomingBitrate?: number; - // PipeTransport specific. - tuple: TransportTuple; -}; - -export type PipeConsumerOptions = -{ - /** - * The id of the Producer to consume. - */ - producerId: string; - - /** - * Custom application data. - */ - appData?: Record; -}; - -export type PipeTransportEvents = TransportEvents & -{ - sctpstatechange: [SctpState]; -}; - -export type PipeTransportObserverEvents = TransportObserverEvents & -{ - sctpstatechange: [SctpState]; -}; - -type PipeTransportConstructorOptions = TransportConstructorOptions & -{ - data: PipeTransportData; -}; - -export type PipeTransportData = -{ +import type { Producer } from './ProducerTypes'; +import type { Consumer, ConsumerType } from './ConsumerTypes'; +import { ConsumerImpl } from './Consumer'; +import type { RtpParameters } from './rtpParametersTypes'; +import { + serializeRtpEncodingParameters, + serializeRtpParameters, +} from './rtpParametersFbsUtils'; +import type { SctpParameters } from './sctpParametersTypes'; +import type { SrtpParameters } from './srtpParametersTypes'; +import { + parseSrtpParameters, + serializeSrtpParameters, +} from './srtpParametersFbsUtils'; +import type { AppData } from './types'; +import { generateUUIDv4 } from './utils'; +import { MediaKind as FbsMediaKind } from './fbs/rtp-parameters/media-kind'; +import * as FbsRtpParameters from './fbs/rtp-parameters'; +import { Event, Notification } from './fbs/notification'; +import * as FbsRequest from './fbs/request'; +import * as FbsTransport from './fbs/transport'; +import * as FbsPipeTransport from './fbs/pipe-transport'; + +type PipeTransportConstructorOptions = + TransportConstructorOptions & { + data: PipeTransportData; + }; + +export type PipeTransportData = { tuple: TransportTuple; sctpParameters?: SctpParameters; sctpState?: SctpState; @@ -137,199 +59,225 @@ export type PipeTransportData = const logger = new Logger('PipeTransport'); -export class PipeTransport - extends Transport +export class PipeTransportImpl + extends TransportImpl< + PipeTransportAppData, + PipeTransportEvents, + PipeTransportObserver + > + implements Transport, PipeTransport { // PipeTransport data. readonly #data: PipeTransportData; - /** - * @private - */ - constructor(options: PipeTransportConstructorOptions) - { - super(options); + constructor(options: PipeTransportConstructorOptions) { + const observer: PipeTransportObserver = + new EnhancedEventEmitter(); + + super(options, observer); logger.debug('constructor()'); const { data } = options; - this.#data = - { - tuple : data.tuple, - sctpParameters : data.sctpParameters, - sctpState : data.sctpState, - rtx : data.rtx, - srtpParameters : data.srtpParameters + this.#data = { + tuple: data.tuple, + sctpParameters: data.sctpParameters, + sctpState: data.sctpState, + rtx: data.rtx, + srtpParameters: data.srtpParameters, }; this.handleWorkerNotifications(); + this.handleListenerError(); + } + + get type(): 'pipe' { + return 'pipe'; } - /** - * Transport tuple. - */ - get tuple(): TransportTuple - { + get observer(): PipeTransportObserver { + return super.observer; + } + + get tuple(): TransportTuple { return this.#data.tuple; } - /** - * SCTP parameters. - */ - get sctpParameters(): SctpParameters | undefined - { + get sctpParameters(): SctpParameters | undefined { return this.#data.sctpParameters; } - /** - * SCTP state. - */ - get sctpState(): SctpState | undefined - { + get sctpState(): SctpState | undefined { return this.#data.sctpState; } - /** - * SRTP parameters. - */ - get srtpParameters(): SrtpParameters | undefined - { + get srtpParameters(): SrtpParameters | undefined { return this.#data.srtpParameters; } - /** - * Close the PipeTransport. - * - * @override - */ - close(): void - { - if (this.closed) + close(): void { + if (this.closed) { return; + } - if (this.#data.sctpState) + if (this.#data.sctpState) { this.#data.sctpState = 'closed'; + } super.close(); } - /** - * Router was closed. - * - * @private - * @override - */ - routerClosed(): void - { - if (this.closed) + routerClosed(): void { + if (this.closed) { return; + } - if (this.#data.sctpState) + if (this.#data.sctpState) { this.#data.sctpState = 'closed'; + } super.routerClosed(); } - /** - * Get PipeTransport stats. - * - * @override - */ - async getStats(): Promise - { + async dump(): Promise { + logger.debug('dump()'); + + const response = await this.channel.request( + FbsRequest.Method.TRANSPORT_DUMP, + undefined, + undefined, + this.internal.transportId + ); + + /* Decode Response. */ + const data = new FbsPipeTransport.DumpResponse(); + + response.body(data); + + return parsePipeTransportDumpResponse(data); + } + + async getStats(): Promise { logger.debug('getStats()'); - return this.channel.request('transport.getStats', this.internal.transportId); + const response = await this.channel.request( + FbsRequest.Method.TRANSPORT_GET_STATS, + undefined, + undefined, + this.internal.transportId + ); + + /* Decode Response. */ + const data = new FbsPipeTransport.GetStatsResponse(); + + response.body(data); + + return [parseGetStatsResponse(data)]; } - /** - * Provide the PipeTransport remote parameters. - * - * @override - */ - async connect( - { + async connect({ + ip, + port, + srtpParameters, + }: { + ip: string; + port: number; + srtpParameters?: SrtpParameters; + }): Promise { + logger.debug('connect()'); + + const requestOffset = createConnectRequest({ + builder: this.channel.bufferBuilder, ip, port, - srtpParameters - }: - { - ip: string; - port: number; - srtpParameters?: SrtpParameters; - } - ): Promise - { - logger.debug('connect()'); + srtpParameters, + }); + + // Wait for response. + const response = await this.channel.request( + FbsRequest.Method.PIPETRANSPORT_CONNECT, + FbsRequest.Body.PipeTransport_ConnectRequest, + requestOffset, + this.internal.transportId + ); - const reqData = { ip, port, srtpParameters }; + /* Decode Response. */ + const data = new FbsPipeTransport.ConnectResponse(); - const data = - await this.channel.request('transport.connect', this.internal.transportId, reqData); + response.body(data); // Update data. - this.#data.tuple = data.tuple; + if (data.tuple()) { + this.#data.tuple = parseTuple(data.tuple()!); + } } - /** - * Create a pipe Consumer. - * - * @override - */ - async consume({ producerId, appData }: PipeConsumerOptions): Promise - { + async consume({ + producerId, + appData, + }: PipeConsumerOptions): Promise> { logger.debug('consume()'); - if (!producerId || typeof producerId !== 'string') + if (!producerId || typeof producerId !== 'string') { throw new TypeError('missing producerId'); - else if (appData && typeof appData !== 'object') + } else if (appData && typeof appData !== 'object') { throw new TypeError('if given, appData must be an object'); + } const producer = this.getProducerById(producerId); - if (!producer) + if (!producer) { throw Error(`Producer with id "${producerId}" not found`); + } // This may throw. - const rtpParameters = ortc.getPipeConsumerRtpParameters( - producer.consumableRtpParameters, this.#data.rtx); + const rtpParameters = ortc.getPipeConsumerRtpParameters({ + consumableRtpParameters: producer.consumableRtpParameters, + enableRtx: this.#data.rtx, + }); - const reqData = - { - consumerId : uuidv4(), - producerId, - kind : producer.kind, + const consumerId = generateUUIDv4(); + + const consumeRequestOffset = createConsumeRequest({ + builder: this.channel.bufferBuilder, + consumerId, + producer, rtpParameters, - type : 'pipe', - consumableRtpEncodings : producer.consumableRtpParameters.encodings - }; + }); + + const response = await this.channel.request( + FbsRequest.Method.TRANSPORT_CONSUME, + FbsRequest.Body.Transport_ConsumeRequest, + consumeRequestOffset, + this.internal.transportId + ); + + /* Decode Response. */ + const consumeResponse = new FbsTransport.ConsumeResponse(); - const status = - await this.channel.request('transport.consume', this.internal.transportId, reqData); + response.body(consumeResponse); - const data = - { + const status = consumeResponse.unpack(); + + const data = { producerId, - kind : producer.kind, + kind: producer.kind, rtpParameters, - type : 'pipe' as ConsumerType + type: 'pipe' as ConsumerType, }; - const consumer = new Consumer( - { - internal : - { - ...this.internal, - consumerId : reqData.consumerId - }, - data, - channel : this.channel, - payloadChannel : this.payloadChannel, - appData, - paused : status.paused, - producerPaused : status.producerPaused - }); + const consumer: Consumer = new ConsumerImpl({ + internal: { + ...this.internal, + consumerId, + }, + data, + channel: this.channel, + appData, + paused: status.paused, + producerPaused: status.producerPaused, + }); this.consumers.set(consumer.id, consumer); consumer.on('@close', () => this.consumers.delete(consumer.id)); @@ -341,43 +289,183 @@ export class PipeTransport return consumer; } - private handleWorkerNotifications(): void - { - this.channel.on(this.internal.transportId, (event: string, data?: any) => - { - switch (event) - { - case 'sctpstatechange': - { - const sctpState = data.sctpState as SctpState; + private handleWorkerNotifications(): void { + this.channel.on( + this.internal.transportId, + (event: Event, data?: Notification) => { + switch (event) { + case Event.TRANSPORT_SCTP_STATE_CHANGE: { + const notification = new FbsTransport.SctpStateChangeNotification(); - this.#data.sctpState = sctpState; + data!.body(notification); - this.safeEmit('sctpstatechange', sctpState); + const sctpState = parseSctpState(notification.sctpState()); - // Emit observer event. - this.observer.safeEmit('sctpstatechange', sctpState); + this.#data.sctpState = sctpState; - break; - } + this.safeEmit('sctpstatechange', sctpState); - case 'trace': - { - const trace = data as TransportTraceEventData; + // Emit observer event. + this.observer.safeEmit('sctpstatechange', sctpState); - this.safeEmit('trace', trace); + break; + } - // Emit observer event. - this.observer.safeEmit('trace', trace); + case Event.TRANSPORT_TRACE: { + const notification = new FbsTransport.TraceNotification(); - break; - } + data!.body(notification); + + const trace = parseTransportTraceEventData(notification); + + this.safeEmit('trace', trace); - default: - { - logger.error('ignoring unknown event "%s"', event); + // Emit observer event. + this.observer.safeEmit('trace', trace); + + break; + } + + default: { + logger.error(`ignoring unknown event "${event}"`); + } } } + ); + } + + private handleListenerError(): void { + this.on('listenererror', (eventName, error) => { + logger.error( + `event listener threw an error [eventName:${eventName}]:`, + error + ); }); } } + +/* + * flatbuffers helpers. + */ + +export function parsePipeTransportDumpResponse( + binary: FbsPipeTransport.DumpResponse +): PipeTransportDump { + // Retrieve BaseTransportDump. + const baseTransportDump = parseBaseTransportDump(binary.base()!); + // Retrieve RTP Tuple. + const tuple = parseTuple(binary.tuple()!); + + // Retrieve SRTP Parameters. + let srtpParameters: SrtpParameters | undefined; + + if (binary.srtpParameters()) { + srtpParameters = parseSrtpParameters(binary.srtpParameters()!); + } + + return { + ...baseTransportDump, + tuple: tuple, + rtx: binary.rtx(), + srtpParameters: srtpParameters, + }; +} + +function parseGetStatsResponse( + binary: FbsPipeTransport.GetStatsResponse +): PipeTransportStat { + const base = parseBaseTransportStats(binary.base()!); + + return { + ...base, + type: 'pipe-transport', + tuple: parseTuple(binary.tuple()!), + }; +} + +function createConsumeRequest({ + builder, + consumerId, + producer, + rtpParameters, +}: { + builder: flatbuffers.Builder; + consumerId: string; + producer: Producer; + rtpParameters: RtpParameters; +}): number { + // Build the request. + const producerIdOffset = builder.createString(producer.id); + const consumerIdOffset = builder.createString(consumerId); + const rtpParametersOffset = serializeRtpParameters(builder, rtpParameters); + let consumableRtpEncodingsOffset: number | undefined; + + if (producer.consumableRtpParameters.encodings) { + consumableRtpEncodingsOffset = serializeRtpEncodingParameters( + builder, + producer.consumableRtpParameters.encodings + ); + } + + const ConsumeRequest = FbsTransport.ConsumeRequest; + + // Create Consume Request. + ConsumeRequest.startConsumeRequest(builder); + ConsumeRequest.addConsumerId(builder, consumerIdOffset); + ConsumeRequest.addProducerId(builder, producerIdOffset); + ConsumeRequest.addKind( + builder, + producer.kind === 'audio' ? FbsMediaKind.AUDIO : FbsMediaKind.VIDEO + ); + ConsumeRequest.addRtpParameters(builder, rtpParametersOffset); + ConsumeRequest.addType(builder, FbsRtpParameters.Type.PIPE); + + if (consumableRtpEncodingsOffset) { + ConsumeRequest.addConsumableRtpEncodings( + builder, + consumableRtpEncodingsOffset + ); + } + + return ConsumeRequest.endConsumeRequest(builder); +} + +function createConnectRequest({ + builder, + ip, + port, + srtpParameters, +}: { + builder: flatbuffers.Builder; + ip?: string; + port?: number; + srtpParameters?: SrtpParameters; +}): number { + let ipOffset = 0; + let srtpParametersOffset = 0; + + if (ip) { + ipOffset = builder.createString(ip); + } + + // Serialize SrtpParameters. + if (srtpParameters) { + srtpParametersOffset = serializeSrtpParameters(builder, srtpParameters); + } + + // Create PlainTransportConnectData. + FbsPipeTransport.ConnectRequest.startConnectRequest(builder); + FbsPipeTransport.ConnectRequest.addIp(builder, ipOffset); + + if (typeof port === 'number') { + FbsPipeTransport.ConnectRequest.addPort(builder, port); + } + if (srtpParameters) { + FbsPipeTransport.ConnectRequest.addSrtpParameters( + builder, + srtpParametersOffset + ); + } + + return FbsPipeTransport.ConnectRequest.endConnectRequest(builder); +} diff --git a/node/src/PipeTransportTypes.ts b/node/src/PipeTransportTypes.ts new file mode 100644 index 0000000000..4bc9734eb8 --- /dev/null +++ b/node/src/PipeTransportTypes.ts @@ -0,0 +1,200 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { + Transport, + TransportListenInfo, + TransportListenIp, + TransportTuple, + SctpState, + BaseTransportDump, + BaseTransportStats, + TransportEvents, + TransportObserverEvents, +} from './TransportTypes'; +import type { Consumer } from './ConsumerTypes'; +import type { SrtpParameters } from './srtpParametersTypes'; +import type { SctpParameters, NumSctpStreams } from './sctpParametersTypes'; +import type { Either, AppData } from './types'; + +export type PipeTransportOptions< + PipeTransportAppData extends AppData = AppData, +> = { + /** + * Create a SCTP association. Default false. + */ + enableSctp?: boolean; + + /** + * SCTP streams number. + */ + numSctpStreams?: NumSctpStreams; + + /** + * Maximum allowed size for SCTP messages sent by DataProducers. + * Default 268435456. + */ + maxSctpMessageSize?: number; + + /** + * Maximum SCTP send buffer used by DataConsumers. + * Default 268435456. + */ + sctpSendBufferSize?: number; + + /** + * Enable RTX and NACK for RTP retransmission. Useful if both Routers are + * located in different hosts and there is packet lost in the link. For this + * to work, both PipeTransports must enable this setting. Default false. + */ + enableRtx?: boolean; + + /** + * Enable SRTP. Useful to protect the RTP and RTCP traffic if both Routers + * are located in different hosts. For this to work, connect() must be called + * with remote SRTP parameters. Default false. + */ + enableSrtp?: boolean; + + /** + * Custom application data. + */ + appData?: PipeTransportAppData; +} & PipeTransportListen; + +type PipeTransportListen = Either< + PipeTransportListenInfo, + PipeTransportListenIp +>; + +type PipeTransportListenInfo = { + /** + * Listening info. + */ + listenInfo: TransportListenInfo; +}; + +type PipeTransportListenIp = { + /** + * Listening IP address. + */ + listenIp: TransportListenIp | string; + + /** + * Fixed port to listen on instead of selecting automatically from Worker's + * port range. + */ + port?: number; +}; + +export type PipeConsumerOptions = { + /** + * The id of the Producer to consume. + */ + producerId: string; + + /** + * Custom application data. + */ + appData?: ConsumerAppData; +}; + +export type PipeTransportDump = BaseTransportDump & { + tuple: TransportTuple; + rtx: boolean; + srtpParameters?: SrtpParameters; +}; + +export type PipeTransportStat = BaseTransportStats & { + type: string; + tuple: TransportTuple; +}; + +export type PipeTransportEvents = TransportEvents & { + sctpstatechange: [SctpState]; +}; + +export type PipeTransportObserver = + EnhancedEventEmitter; + +export type PipeTransportObserverEvents = TransportObserverEvents & { + sctpstatechange: [SctpState]; +}; + +export interface PipeTransport + extends Transport< + PipeTransportAppData, + PipeTransportEvents, + PipeTransportObserver + > { + /** + * Transport type. + * + * @override + */ + get type(): 'pipe'; + + /** + * Observer. + * + * @override + */ + get observer(): PipeTransportObserver; + + /** + * PipeTransport tuple. + */ + get tuple(): TransportTuple; + + /** + * SCTP parameters. + */ + get sctpParameters(): SctpParameters | undefined; + + /** + * SCTP state. + */ + get sctpState(): SctpState | undefined; + + /** + * SRTP parameters. + */ + get srtpParameters(): SrtpParameters | undefined; + + /** + * Dump PipeTransport. + * + * @override + */ + dump(): Promise; + + /** + * Get PipeTransport stats. + * + * @override + */ + getStats(): Promise; + + /** + * Provide the PipeTransport remote parameters. + * + * @override + */ + connect({ + ip, + port, + srtpParameters, + }: { + ip: string; + port: number; + srtpParameters?: SrtpParameters; + }): Promise; + + /** + * Create a pipe Consumer. + * + * @override + */ + consume({ + producerId, + appData, + }: PipeConsumerOptions): Promise>; +} diff --git a/node/src/PlainTransport.ts b/node/src/PlainTransport.ts index 4286d34c95..5e595152c1 100644 --- a/node/src/PlainTransport.ts +++ b/node/src/PlainTransport.ts @@ -1,135 +1,42 @@ +import * as flatbuffers from 'flatbuffers'; import { Logger } from './Logger'; +import { EnhancedEventEmitter } from './enhancedEvents'; +import type { + PlainTransport, + PlainTransportDump, + PlainTransportStat, + PlainTransportEvents, + PlainTransportObserver, + PlainTransportObserverEvents, +} from './PlainTransportTypes'; +import type { Transport, TransportTuple, SctpState } from './TransportTypes'; import { - Transport, - TransportListenIp, - TransportTuple, - TransportTraceEventData, - TransportEvents, - TransportObserverEvents, + TransportImpl, TransportConstructorOptions, - SctpState + parseSctpState, + parseTuple, + parseBaseTransportDump, + parseBaseTransportStats, + parseTransportTraceEventData, } from './Transport'; -import { SctpParameters, NumSctpStreams } from './SctpParameters'; -import { SrtpParameters, SrtpCryptoSuite } from './SrtpParameters'; - -export type PlainTransportOptions = -{ - /** - * Listening IP address. - */ - listenIp: TransportListenIp | string; - - /** - * Fixed port to listen on instead of selecting automatically from Worker's port - * range. - */ - port?: number; - - /** - * Use RTCP-mux (RTP and RTCP in the same port). Default true. - */ - rtcpMux?: boolean; - - /** - * Whether remote IP:port should be auto-detected based on first RTP/RTCP - * packet received. If enabled, connect() method must not be called unless - * SRTP is enabled. If so, it must be called with just remote SRTP parameters. - * Default false. - */ - comedia?: boolean; - - /** - * Create a SCTP association. Default false. - */ - enableSctp?: boolean; - - /** - * SCTP streams number. - */ - numSctpStreams?: NumSctpStreams; - - /** - * Maximum allowed size for SCTP messages sent by DataProducers. - * Default 262144. - */ - maxSctpMessageSize?: number; - - /** - * Maximum SCTP send buffer used by DataConsumers. - * Default 262144. - */ - sctpSendBufferSize?: number; - - /** - * Enable SRTP. For this to work, connect() must be called - * with remote SRTP parameters. Default false. - */ - enableSrtp?: boolean; - - /** - * The SRTP crypto suite to be used if enableSrtp is set. Default - * 'AES_CM_128_HMAC_SHA1_80'. - */ - srtpCryptoSuite?: SrtpCryptoSuite; - - /** - * Custom application data. - */ - appData?: Record; -}; - -export type PlainTransportStat = -{ - // Common to all Transports. - type: string; - transportId: string; - timestamp: number; - sctpState?: SctpState; - bytesReceived: number; - recvBitrate: number; - bytesSent: number; - sendBitrate: number; - rtpBytesReceived: number; - rtpRecvBitrate: number; - rtpBytesSent: number; - rtpSendBitrate: number; - rtxBytesReceived: number; - rtxRecvBitrate: number; - rtxBytesSent: number; - rtxSendBitrate: number; - probationBytesSent: number; - probationSendBitrate: number; - availableOutgoingBitrate?: number; - availableIncomingBitrate?: number; - maxIncomingBitrate?: number; - // PlainTransport specific. - rtcpMux: boolean; - comedia: boolean; - tuple: TransportTuple; - rtcpTuple?: TransportTuple; -}; - -export type PlainTransportEvents = TransportEvents & -{ - tuple: [TransportTuple]; - rtcptuple: [TransportTuple]; - sctpstatechange: [SctpState]; -}; - -export type PlainTransportObserverEvents = TransportObserverEvents & -{ - tuple: [TransportTuple]; - rtcptuple: [TransportTuple]; - sctpstatechange: [SctpState]; -}; - -type PlainTransportConstructorOptions = TransportConstructorOptions & -{ - data: PlainTransportData; -}; - -export type PlainTransportData = -{ +import type { SctpParameters } from './sctpParametersTypes'; +import type { SrtpParameters } from './srtpParametersTypes'; +import { + parseSrtpParameters, + serializeSrtpParameters, +} from './srtpParametersFbsUtils'; +import type { AppData } from './types'; +import { Event, Notification } from './fbs/notification'; +import * as FbsRequest from './fbs/request'; +import * as FbsTransport from './fbs/transport'; +import * as FbsPlainTransport from './fbs/plain-transport'; + +type PlainTransportConstructorOptions = + TransportConstructorOptions & { + data: PlainTransportData; + }; + +export type PlainTransportData = { rtcpMux?: boolean; comedia?: boolean; tuple: TransportTuple; @@ -141,224 +48,356 @@ export type PlainTransportData = const logger = new Logger('PlainTransport'); -export class PlainTransport extends - Transport +export class PlainTransportImpl + extends TransportImpl< + PlainTransportAppData, + PlainTransportEvents, + PlainTransportObserver + > + implements Transport, PlainTransport { // PlainTransport data. readonly #data: PlainTransportData; - /** - * @private - */ - constructor(options: PlainTransportConstructorOptions) - { - super(options); + constructor( + options: PlainTransportConstructorOptions + ) { + const observer: PlainTransportObserver = + new EnhancedEventEmitter(); + + super(options, observer); logger.debug('constructor()'); const { data } = options; - this.#data = - { - rtcpMux : data.rtcpMux, - comedia : data.comedia, - tuple : data.tuple, - rtcpTuple : data.rtcpTuple, - sctpParameters : data.sctpParameters, - sctpState : data.sctpState, - srtpParameters : data.srtpParameters + this.#data = { + rtcpMux: data.rtcpMux, + comedia: data.comedia, + tuple: data.tuple, + rtcpTuple: data.rtcpTuple, + sctpParameters: data.sctpParameters, + sctpState: data.sctpState, + srtpParameters: data.srtpParameters, }; this.handleWorkerNotifications(); + this.handleListenerError(); + } + + get type(): 'plain' { + return 'plain'; } - /** - * Transport tuple. - */ - get tuple(): TransportTuple - { + get observer(): PlainTransportObserver { + return super.observer; + } + + get tuple(): TransportTuple { return this.#data.tuple; } - /** - * Transport RTCP tuple. - */ - get rtcpTuple(): TransportTuple | undefined - { + get rtcpTuple(): TransportTuple | undefined { return this.#data.rtcpTuple; } - /** - * SCTP parameters. - */ - get sctpParameters(): SctpParameters | undefined - { + get sctpParameters(): SctpParameters | undefined { return this.#data.sctpParameters; } - /** - * SCTP state. - */ - get sctpState(): SctpState | undefined - { + get sctpState(): SctpState | undefined { return this.#data.sctpState; } - /** - * SRTP parameters. - */ - get srtpParameters(): SrtpParameters | undefined - { + get srtpParameters(): SrtpParameters | undefined { return this.#data.srtpParameters; } - /** - * Close the PlainTransport. - * - * @override - */ - close(): void - { - if (this.closed) + close(): void { + if (this.closed) { return; + } - if (this.#data.sctpState) + if (this.#data.sctpState) { this.#data.sctpState = 'closed'; + } super.close(); } - /** - * Router was closed. - * - * @private - * @override - */ - routerClosed(): void - { - if (this.closed) + routerClosed(): void { + if (this.closed) { return; + } - if (this.#data.sctpState) + if (this.#data.sctpState) { this.#data.sctpState = 'closed'; + } super.routerClosed(); } - /** - * Get PlainTransport stats. - * - * @override - */ - async getStats(): Promise - { + async dump(): Promise { + logger.debug('dump()'); + + const response = await this.channel.request( + FbsRequest.Method.TRANSPORT_DUMP, + undefined, + undefined, + this.internal.transportId + ); + + /* Decode Response. */ + const data = new FbsPlainTransport.DumpResponse(); + + response.body(data); + + return parsePlainTransportDumpResponse(data); + } + + async getStats(): Promise { logger.debug('getStats()'); - return this.channel.request('transport.getStats', this.internal.transportId); + const response = await this.channel.request( + FbsRequest.Method.TRANSPORT_GET_STATS, + undefined, + undefined, + this.internal.transportId + ); + + /* Decode Response. */ + const data = new FbsPlainTransport.GetStatsResponse(); + + response.body(data); + + return [parseGetStatsResponse(data)]; } - /** - * Provide the PlainTransport remote parameters. - * - * @override - */ - async connect( - { + async connect({ + ip, + port, + rtcpPort, + srtpParameters, + }: { + ip?: string; + port?: number; + rtcpPort?: number; + srtpParameters?: SrtpParameters; + }): Promise { + logger.debug('connect()'); + + const requestOffset = createConnectRequest({ + builder: this.channel.bufferBuilder, ip, port, rtcpPort, - srtpParameters - }: - { - ip?: string; - port?: number; - rtcpPort?: number; - srtpParameters?: SrtpParameters; - } - ): Promise - { - logger.debug('connect()'); + srtpParameters, + }); - const reqData = { ip, port, rtcpPort, srtpParameters }; + // Wait for response. + const response = await this.channel.request( + FbsRequest.Method.PLAINTRANSPORT_CONNECT, + FbsRequest.Body.PlainTransport_ConnectRequest, + requestOffset, + this.internal.transportId + ); - const data = - await this.channel.request('transport.connect', this.internal.transportId, reqData); + /* Decode Response. */ + const data = new FbsPlainTransport.ConnectResponse(); + + response.body(data); // Update data. - if (data.tuple) - this.#data.tuple = data.tuple; + if (data.tuple()) { + this.#data.tuple = parseTuple(data.tuple()!); + } - if (data.rtcpTuple) - this.#data.rtcpTuple = data.rtcpTuple; + if (data.rtcpTuple()) { + this.#data.rtcpTuple = parseTuple(data.rtcpTuple()!); + } - this.#data.srtpParameters = data.srtpParameters; + if (data.srtpParameters()) { + this.#data.srtpParameters = parseSrtpParameters(data.srtpParameters()!); + } } - private handleWorkerNotifications(): void - { - this.channel.on(this.internal.transportId, (event: string, data?: any) => - { - switch (event) - { - case 'tuple': - { - const tuple = data.tuple as TransportTuple; + private handleWorkerNotifications(): void { + this.channel.on( + this.internal.transportId, + (event: Event, data?: Notification) => { + switch (event) { + case Event.PLAINTRANSPORT_TUPLE: { + const notification = new FbsPlainTransport.TupleNotification(); - this.#data.tuple = tuple; + data!.body(notification); - this.safeEmit('tuple', tuple); + const tuple = parseTuple(notification.tuple()!); - // Emit observer event. - this.observer.safeEmit('tuple', tuple); + this.#data.tuple = tuple; - break; - } + this.safeEmit('tuple', tuple); - case 'rtcptuple': - { - const rtcpTuple = data.rtcpTuple as TransportTuple; + // Emit observer event. + this.observer.safeEmit('tuple', tuple); - this.#data.rtcpTuple = rtcpTuple; + break; + } - this.safeEmit('rtcptuple', rtcpTuple); + case Event.PLAINTRANSPORT_RTCP_TUPLE: { + const notification = new FbsPlainTransport.RtcpTupleNotification(); - // Emit observer event. - this.observer.safeEmit('rtcptuple', rtcpTuple); + data!.body(notification); - break; - } + const rtcpTuple = parseTuple(notification.tuple()!); - case 'sctpstatechange': - { - const sctpState = data.sctpState as SctpState; + this.#data.rtcpTuple = rtcpTuple; - this.#data.sctpState = sctpState; + this.safeEmit('rtcptuple', rtcpTuple); - this.safeEmit('sctpstatechange', sctpState); + // Emit observer event. + this.observer.safeEmit('rtcptuple', rtcpTuple); - // Emit observer event. - this.observer.safeEmit('sctpstatechange', sctpState); + break; + } - break; - } + case Event.TRANSPORT_SCTP_STATE_CHANGE: { + const notification = new FbsTransport.SctpStateChangeNotification(); - case 'trace': - { - const trace = data as TransportTraceEventData; + data!.body(notification); - this.safeEmit('trace', trace); + const sctpState = parseSctpState(notification.sctpState()); - // Emit observer event. - this.observer.safeEmit('trace', trace); + this.#data.sctpState = sctpState; - break; - } + this.safeEmit('sctpstatechange', sctpState); + + // Emit observer event. + this.observer.safeEmit('sctpstatechange', sctpState); - default: - { - logger.error('ignoring unknown event "%s"', event); + break; + } + + case Event.TRANSPORT_TRACE: { + const notification = new FbsTransport.TraceNotification(); + + data!.body(notification); + + const trace = parseTransportTraceEventData(notification); + + this.safeEmit('trace', trace); + + // Emit observer event. + this.observer.safeEmit('trace', trace); + + break; + } + + default: { + logger.error(`ignoring unknown event "${event}"`); + } } } + ); + } + + private handleListenerError(): void { + this.on('listenererror', (eventName, error) => { + logger.error( + `event listener threw an error [eventName:${eventName}]:`, + error + ); }); } } + +export function parsePlainTransportDumpResponse( + binary: FbsPlainTransport.DumpResponse +): PlainTransportDump { + // Retrieve BaseTransportDump. + const baseTransportDump = parseBaseTransportDump(binary.base()!); + // Retrieve RTP Tuple. + const tuple = parseTuple(binary.tuple()!); + + // Retrieve RTCP Tuple. + let rtcpTuple: TransportTuple | undefined; + + if (binary.rtcpTuple()) { + rtcpTuple = parseTuple(binary.rtcpTuple()!); + } + + // Retrieve SRTP Parameters. + let srtpParameters: SrtpParameters | undefined; + + if (binary.srtpParameters()) { + srtpParameters = parseSrtpParameters(binary.srtpParameters()!); + } + + return { + ...baseTransportDump, + rtcpMux: binary.rtcpMux(), + comedia: binary.comedia(), + tuple: tuple, + rtcpTuple: rtcpTuple, + srtpParameters: srtpParameters, + }; +} + +function parseGetStatsResponse( + binary: FbsPlainTransport.GetStatsResponse +): PlainTransportStat { + const base = parseBaseTransportStats(binary.base()!); + + return { + ...base, + type: 'plain-rtp-transport', + rtcpMux: binary.rtcpMux(), + comedia: binary.comedia(), + tuple: parseTuple(binary.tuple()!), + rtcpTuple: binary.rtcpTuple() ? parseTuple(binary.rtcpTuple()!) : undefined, + }; +} + +function createConnectRequest({ + builder, + ip, + port, + rtcpPort, + srtpParameters, +}: { + builder: flatbuffers.Builder; + ip?: string; + port?: number; + rtcpPort?: number; + srtpParameters?: SrtpParameters; +}): number { + let ipOffset = 0; + let srtpParametersOffset = 0; + + if (ip) { + ipOffset = builder.createString(ip); + } + + // Serialize SrtpParameters. + if (srtpParameters) { + srtpParametersOffset = serializeSrtpParameters(builder, srtpParameters); + } + + // Create PlainTransportConnectData. + FbsPlainTransport.ConnectRequest.startConnectRequest(builder); + FbsPlainTransport.ConnectRequest.addIp(builder, ipOffset); + + if (typeof port === 'number') { + FbsPlainTransport.ConnectRequest.addPort(builder, port); + } + if (typeof rtcpPort === 'number') { + FbsPlainTransport.ConnectRequest.addRtcpPort(builder, rtcpPort); + } + if (srtpParameters) { + FbsPlainTransport.ConnectRequest.addSrtpParameters( + builder, + srtpParametersOffset + ); + } + + return FbsPlainTransport.ConnectRequest.endConnectRequest(builder); +} diff --git a/node/src/PlainTransportTypes.ts b/node/src/PlainTransportTypes.ts new file mode 100644 index 0000000000..383a29c04a --- /dev/null +++ b/node/src/PlainTransportTypes.ts @@ -0,0 +1,209 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { + Transport, + TransportListenInfo, + TransportListenIp, + TransportTuple, + SctpState, + BaseTransportDump, + BaseTransportStats, + TransportEvents, + TransportObserverEvents, +} from './TransportTypes'; +import type { SrtpParameters, SrtpCryptoSuite } from './srtpParametersTypes'; +import type { SctpParameters, NumSctpStreams } from './sctpParametersTypes'; +import type { Either, AppData } from './types'; + +export type PlainTransportOptions< + PlainTransportAppData extends AppData = AppData, +> = { + /** + * Use RTCP-mux (RTP and RTCP in the same port). Default true. + */ + rtcpMux?: boolean; + + /** + * Whether remote IP:port should be auto-detected based on first RTP/RTCP + * packet received. If enabled, connect() method must not be called unless + * SRTP is enabled. If so, it must be called with just remote SRTP parameters. + * Default false. + */ + comedia?: boolean; + + /** + * Create a SCTP association. Default false. + */ + enableSctp?: boolean; + + /** + * SCTP streams number. + */ + numSctpStreams?: NumSctpStreams; + + /** + * Maximum allowed size for SCTP messages sent by DataProducers. + * Default 262144. + */ + maxSctpMessageSize?: number; + + /** + * Maximum SCTP send buffer used by DataConsumers. + * Default 262144. + */ + sctpSendBufferSize?: number; + + /** + * Enable SRTP. For this to work, connect() must be called + * with remote SRTP parameters. Default false. + */ + enableSrtp?: boolean; + + /** + * The SRTP crypto suite to be used if enableSrtp is set. Default + * 'AES_CM_128_HMAC_SHA1_80'. + */ + srtpCryptoSuite?: SrtpCryptoSuite; + + /** + * Custom application data. + */ + appData?: PlainTransportAppData; +} & PlainTransportListen; + +type PlainTransportListen = Either< + PlainTransportListenInfo, + PlainTransportListenIp +>; + +type PlainTransportListenInfo = { + /** + * Listening info. + */ + listenInfo: TransportListenInfo; + + /** + * Optional listening info for RTCP. + */ + rtcpListenInfo?: TransportListenInfo; +}; + +type PlainTransportListenIp = { + /** + * Listening IP address. + */ + listenIp: TransportListenIp | string; + + /** + * Fixed port to listen on instead of selecting automatically from Worker's + * port range. + */ + port?: number; +}; + +export type PlainTransportDump = BaseTransportDump & { + rtcpMux: boolean; + comedia: boolean; + tuple: TransportTuple; + rtcpTuple?: TransportTuple; + srtpParameters?: SrtpParameters; +}; + +export type PlainTransportStat = BaseTransportStats & { + type: string; + rtcpMux: boolean; + comedia: boolean; + tuple: TransportTuple; + rtcpTuple?: TransportTuple; +}; + +export type PlainTransportEvents = TransportEvents & { + tuple: [TransportTuple]; + rtcptuple: [TransportTuple]; + sctpstatechange: [SctpState]; +}; + +export type PlainTransportObserver = + EnhancedEventEmitter; + +export type PlainTransportObserverEvents = TransportObserverEvents & { + tuple: [TransportTuple]; + rtcptuple: [TransportTuple]; + sctpstatechange: [SctpState]; +}; + +export interface PlainTransport + extends Transport< + PlainTransportAppData, + PlainTransportEvents, + PlainTransportObserver + > { + /** + * Transport type. + * + * @override + */ + get type(): 'plain'; + + /** + * Observer. + * + * @override + */ + get observer(): PlainTransportObserver; + + /** + * PlainTransport tuple. + */ + get tuple(): TransportTuple; + + /** + * PlainTransport RTCP tuple. + */ + get rtcpTuple(): TransportTuple | undefined; + + /** + * SCTP parameters. + */ + get sctpParameters(): SctpParameters | undefined; + + /** + * SCTP state. + */ + get sctpState(): SctpState | undefined; + + /** + * SRTP parameters. + */ + get srtpParameters(): SrtpParameters | undefined; + + /** + * Dump PlainTransport. + * + * @override + */ + dump(): Promise; + + /** + * Get PlainTransport stats. + * + * @override + */ + getStats(): Promise; + + /** + * Provide the PlainTransport remote parameters. + * + * @override + */ + connect({ + ip, + port, + rtcpPort, + srtpParameters, + }: { + ip?: string; + port?: number; + rtcpPort?: number; + srtpParameters?: SrtpParameters; + }): Promise; +} diff --git a/node/src/Producer.ts b/node/src/Producer.ts index 95ec93d265..9420b8f8eb 100644 --- a/node/src/Producer.ts +++ b/node/src/Producer.ts @@ -1,182 +1,50 @@ import { Logger } from './Logger'; -import { EnhancedEventEmitter } from './EnhancedEventEmitter'; +import { EnhancedEventEmitter } from './enhancedEvents'; +import type { + Producer, + ProducerType, + ProducerScore, + ProducerVideoOrientation, + ProducerDump, + ProducerStat, + ProducerTraceEventType, + ProducerTraceEventData, + ProducerEvents, + ProducerObserver, + ProducerObserverEvents, +} from './ProducerTypes'; import { Channel } from './Channel'; -import { PayloadChannel } from './PayloadChannel'; -import { TransportInternal } from './Transport'; -import { MediaKind, RtpParameters } from './RtpParameters'; - -export type ProducerOptions = -{ - /** - * Producer id (just for Router.pipeToRouter() method). - */ - id?: string; - - /** - * Media kind ('audio' or 'video'). - */ - kind: MediaKind; - - /** - * RTP parameters defining what the endpoint is sending. - */ - rtpParameters: RtpParameters; - - /** - * Whether the producer must start in paused mode. Default false. - */ - paused?: boolean; - - /** - * Just for video. Time (in ms) before asking the sender for a new key frame - * after having asked a previous one. Default 0. - */ - keyFrameRequestDelay?: number; - - /** - * Custom application data. - */ - appData?: Record; -}; - -/** - * Valid types for 'trace' event. - */ -export type ProducerTraceEventType = 'rtp' | 'keyframe' | 'nack' | 'pli' | 'fir'; - -/** - * 'trace' event data. - */ -export type ProducerTraceEventData = -{ - /** - * Trace type. - */ - type: ProducerTraceEventType; - - /** - * Event timestamp. - */ - timestamp: number; - - /** - * Event direction. - */ - direction: 'in' | 'out'; - - /** - * Per type information. - */ - info: any; -}; - -export type ProducerScore = -{ - /** - * SSRC of the RTP stream. - */ - ssrc: number; - - /** - * RID of the RTP stream. - */ - rid?: string; - - /** - * The score of the RTP stream. - */ - score: number; -}; - -export type ProducerVideoOrientation = -{ - /** - * Whether the source is a video camera. - */ - camera: boolean; - - /** - * Whether the video source is flipped. - */ - flip: boolean; - - /** - * Rotation degrees (0, 90, 180 or 270). - */ - rotation: number; -}; - -export type ProducerStat = -{ - // Common to all RtpStreams. - type: string; - timestamp: number; - ssrc: number; - rtxSsrc?: number; - rid?: string; - kind: string; - mimeType: string; - packetsLost: number; - fractionLost: number; - packetsDiscarded: number; - packetsRetransmitted: number; - packetsRepaired: number; - nackCount: number; - nackPacketCount: number; - pliCount: number; - firCount: number; - score: number; - packetCount: number; - byteCount: number; - bitrate: number; - roundTripTime?: number; - rtxPacketsDiscarded?: number; - // RtpStreamRecv specific. - jitter: number; - bitrateByLayer?: any; -}; - -/** - * Producer type. - */ -export type ProducerType = 'simple' | 'simulcast' | 'svc'; - -export type ProducerEvents = -{ - transportclose: []; - score: [ProducerScore[]]; - videoorientationchange: [ProducerVideoOrientation]; - trace: [ProducerTraceEventData]; - // Private events. - '@close': []; -}; - -export type ProducerObserverEvents = -{ - close: []; - pause: []; - resume: []; - score: [ProducerScore[]]; - videoorientationchange: [ProducerVideoOrientation]; - trace: [ProducerTraceEventData]; -}; - -type ProducerInternal = TransportInternal & -{ +import type { TransportInternal } from './Transport'; +import type { MediaKind, RtpParameters } from './rtpParametersTypes'; +import { parseRtpParameters } from './rtpParametersFbsUtils'; +import { parseRtpStreamRecvStats } from './rtpStreamStatsFbsUtils'; +import type { AppData } from './types'; +import * as fbsUtils from './fbsUtils'; +import { Event, Notification } from './fbs/notification'; +import { TraceDirection as FbsTraceDirection } from './fbs/common'; +import * as FbsNotification from './fbs/notification'; +import * as FbsRequest from './fbs/request'; +import * as FbsTransport from './fbs/transport'; +import * as FbsProducer from './fbs/producer'; +import * as FbsProducerTraceInfo from './fbs/producer/trace-info'; +import * as FbsRtpParameters from './fbs/rtp-parameters'; + +type ProducerInternal = TransportInternal & { producerId: string; }; -type ProducerData = -{ +const logger = new Logger('Producer'); + +type ProducerData = { kind: MediaKind; rtpParameters: RtpParameters; type: ProducerType; consumableRtpParameters: RtpParameters; }; -const logger = new Logger('Producer'); - -export class Producer extends EnhancedEventEmitter +export class ProducerImpl + extends EnhancedEventEmitter + implements Producer { // Internal data. readonly #internal: ProducerInternal; @@ -187,46 +55,35 @@ export class Producer extends EnhancedEventEmitter // Channel instance. readonly #channel: Channel; - // PayloadChannel instance. - readonly #payloadChannel: PayloadChannel; - // Closed flag. #closed = false; - // Custom app data. - readonly #appData: Record; - // Paused flag. #paused = false; + // Custom app data. + #appData: ProducerAppData; + // Current score. #score: ProducerScore[] = []; // Observer instance. - readonly #observer = new EnhancedEventEmitter(); - - /** - * @private - */ - constructor( - { - internal, - data, - channel, - payloadChannel, - appData, - paused - }: - { - internal: ProducerInternal; - data: ProducerData; - channel: Channel; - payloadChannel: PayloadChannel; - appData?: Record; - paused: boolean; - } - ) - { + readonly #observer: ProducerObserver = + new EnhancedEventEmitter(); + + constructor({ + internal, + data, + channel, + appData, + paused, + }: { + internal: ProducerInternal; + data: ProducerData; + channel: Channel; + appData?: ProducerAppData; + paused: boolean; + }) { super(); logger.debug('constructor()'); @@ -234,119 +91,70 @@ export class Producer extends EnhancedEventEmitter this.#internal = internal; this.#data = data; this.#channel = channel; - this.#payloadChannel = payloadChannel; - this.#appData = appData || {}; this.#paused = paused; + this.#appData = appData ?? ({} as ProducerAppData); this.handleWorkerNotifications(); + this.handleListenerError(); } - /** - * Producer id. - */ - get id(): string - { + get id(): string { return this.#internal.producerId; } - /** - * Whether the Producer is closed. - */ - get closed(): boolean - { + get closed(): boolean { return this.#closed; } - /** - * Media kind. - */ - get kind(): MediaKind - { + get kind(): MediaKind { return this.#data.kind; } - /** - * RTP parameters. - */ - get rtpParameters(): RtpParameters - { + get rtpParameters(): RtpParameters { return this.#data.rtpParameters; } - /** - * Producer type. - */ - get type(): ProducerType - { + get type(): ProducerType { return this.#data.type; } - /** - * Consumable RTP parameters. - * - * @private - */ - get consumableRtpParameters(): RtpParameters - { + get consumableRtpParameters(): RtpParameters { return this.#data.consumableRtpParameters; } - /** - * Whether the Producer is paused. - */ - get paused(): boolean - { + get paused(): boolean { return this.#paused; } - /** - * Producer score list. - */ - get score(): ProducerScore[] - { + get score(): ProducerScore[] { return this.#score; } - /** - * App custom data. - */ - get appData(): Record - { + get appData(): ProducerAppData { return this.#appData; } - /** - * Invalid setter. - */ - set appData(appData: Record) // eslint-disable-line no-unused-vars - { - throw new Error('cannot override appData object'); + set appData(appData: ProducerAppData) { + this.#appData = appData; } - /** - * Observer. - */ - get observer(): EnhancedEventEmitter - { + get observer(): ProducerObserver { return this.#observer; } /** - * @private * Just for testing purposes. + * + * @private */ - get channelForTesting(): Channel - { + get channelForTesting(): Channel { return this.#channel; } - /** - * Close the Producer. - */ - close(): void - { - if (this.#closed) + close(): void { + if (this.#closed) { return; + } logger.debug('close()'); @@ -354,11 +162,19 @@ export class Producer extends EnhancedEventEmitter // Remove notification subscriptions. this.#channel.removeAllListeners(this.#internal.producerId); - this.#payloadChannel.removeAllListeners(this.#internal.producerId); - - const reqData = { producerId: this.#internal.producerId }; - this.#channel.request('transport.closeProducer', this.#internal.transportId, reqData) + /* Build Request. */ + const requestOffset = new FbsTransport.CloseProducerRequestT( + this.#internal.producerId + ).pack(this.#channel.bufferBuilder); + + this.#channel + .request( + FbsRequest.Method.TRANSPORT_CLOSE_PRODUCER, + FbsRequest.Body.Transport_CloseProducerRequest, + requestOffset, + this.#internal.transportId + ) .catch(() => {}); this.emit('@close'); @@ -367,15 +183,10 @@ export class Producer extends EnhancedEventEmitter this.#observer.safeEmit('close'); } - /** - * Transport was closed. - * - * @private - */ - transportClosed(): void - { - if (this.#closed) + transportClosed(): void { + if (this.#closed) { return; + } logger.debug('transportClosed()'); @@ -383,7 +194,6 @@ export class Producer extends EnhancedEventEmitter // Remove notification subscriptions. this.#channel.removeAllListeners(this.#internal.producerId); - this.#payloadChannel.removeAllListeners(this.#internal.producerId); this.safeEmit('transportclose'); @@ -391,138 +201,379 @@ export class Producer extends EnhancedEventEmitter this.#observer.safeEmit('close'); } - /** - * Dump Producer. - */ - async dump(): Promise - { + async dump(): Promise { logger.debug('dump()'); - return this.#channel.request('producer.dump', this.#internal.producerId); + const response = await this.#channel.request( + FbsRequest.Method.PRODUCER_DUMP, + undefined, + undefined, + this.#internal.producerId + ); + + /* Decode Response. */ + const dumpResponse = new FbsProducer.DumpResponse(); + + response.body(dumpResponse); + + return parseProducerDump(dumpResponse); } - /** - * Get Producer stats. - */ - async getStats(): Promise - { + async getStats(): Promise { logger.debug('getStats()'); - return this.#channel.request('producer.getStats', this.#internal.producerId); + const response = await this.#channel.request( + FbsRequest.Method.PRODUCER_GET_STATS, + undefined, + undefined, + this.#internal.producerId + ); + + /* Decode Response. */ + const data = new FbsProducer.GetStatsResponse(); + + response.body(data); + + return parseProducerStats(data); } - /** - * Pause the Producer. - */ - async pause(): Promise - { + async pause(): Promise { logger.debug('pause()'); - const wasPaused = this.#paused; + await this.#channel.request( + FbsRequest.Method.PRODUCER_PAUSE, + undefined, + undefined, + this.#internal.producerId + ); - await this.#channel.request('producer.pause', this.#internal.producerId); + const wasPaused = this.#paused; this.#paused = true; // Emit observer event. - if (!wasPaused) + if (!wasPaused) { this.#observer.safeEmit('pause'); + } } - /** - * Resume the Producer. - */ - async resume(): Promise - { + async resume(): Promise { logger.debug('resume()'); - const wasPaused = this.#paused; + await this.#channel.request( + FbsRequest.Method.PRODUCER_RESUME, + undefined, + undefined, + this.#internal.producerId + ); - await this.#channel.request('producer.resume', this.#internal.producerId); + const wasPaused = this.#paused; this.#paused = false; // Emit observer event. - if (wasPaused) + if (wasPaused) { this.#observer.safeEmit('resume'); + } } - /** - * Enable 'trace' event. - */ - async enableTraceEvent(types: ProducerTraceEventType[] = []): Promise - { + async enableTraceEvent(types: ProducerTraceEventType[] = []): Promise { logger.debug('enableTraceEvent()'); - const reqData = { types }; + if (!Array.isArray(types)) { + throw new TypeError('types must be an array'); + } + if (types.find(type => typeof type !== 'string')) { + throw new TypeError('every type must be a string'); + } + + // Convert event types. + const fbsEventTypes: FbsProducer.TraceEventType[] = []; + + for (const eventType of types) { + try { + fbsEventTypes.push(producerTraceEventTypeToFbs(eventType)); + } catch (error) { + logger.warn('enableTraceEvent() | [error:${error}]'); + } + } + + /* Build Request. */ + const requestOffset = new FbsProducer.EnableTraceEventRequestT( + fbsEventTypes + ).pack(this.#channel.bufferBuilder); await this.#channel.request( - 'producer.enableTraceEvent', this.#internal.producerId, reqData); + FbsRequest.Method.PRODUCER_ENABLE_TRACE_EVENT, + FbsRequest.Body.Producer_EnableTraceEventRequest, + requestOffset, + this.#internal.producerId + ); } - /** - * Send RTP packet (just valid for Producers created on a DirectTransport). - */ - send(rtpPacket: Buffer) - { - if (!Buffer.isBuffer(rtpPacket)) - { + send(rtpPacket: Buffer): void { + if (!Buffer.isBuffer(rtpPacket)) { throw new TypeError('rtpPacket must be a Buffer'); } - this.#payloadChannel.notify( - 'producer.send', this.#internal.producerId, undefined, rtpPacket); + const builder = this.#channel.bufferBuilder; + const dataOffset = FbsProducer.SendNotification.createDataVector( + builder, + rtpPacket + ); + const notificationOffset = + FbsProducer.SendNotification.createSendNotification(builder, dataOffset); + + this.#channel.notify( + FbsNotification.Event.PRODUCER_SEND, + FbsNotification.Body.Producer_SendNotification, + notificationOffset, + this.#internal.producerId + ); } - private handleWorkerNotifications(): void - { - this.#channel.on(this.#internal.producerId, (event: string, data?: any) => - { - switch (event) - { - case 'score': - { - const score = data as ProducerScore[]; + private handleWorkerNotifications(): void { + this.#channel.on( + this.#internal.producerId, + (event: Event, data?: Notification) => { + switch (event) { + case Event.PRODUCER_SCORE: { + const notification = new FbsProducer.ScoreNotification(); - this.#score = score; + data!.body(notification); - this.safeEmit('score', score); + const score: ProducerScore[] = fbsUtils.parseVector( + notification, + 'scores', + parseProducerScore + ); - // Emit observer event. - this.#observer.safeEmit('score', score); + this.#score = score; - break; - } + this.safeEmit('score', score); - case 'videoorientationchange': - { - const videoOrientation = data as ProducerVideoOrientation; + // Emit observer event. + this.#observer.safeEmit('score', score); - this.safeEmit('videoorientationchange', videoOrientation); + break; + } - // Emit observer event. - this.#observer.safeEmit('videoorientationchange', videoOrientation); + case Event.PRODUCER_VIDEO_ORIENTATION_CHANGE: { + const notification = + new FbsProducer.VideoOrientationChangeNotification(); - break; - } + data!.body(notification); - case 'trace': - { - const trace = data as ProducerTraceEventData; + const videoOrientation: ProducerVideoOrientation = + notification.unpack(); - this.safeEmit('trace', trace); + this.safeEmit('videoorientationchange', videoOrientation); - // Emit observer event. - this.#observer.safeEmit('trace', trace); + // Emit observer event. + this.#observer.safeEmit('videoorientationchange', videoOrientation); - break; - } + break; + } + + case Event.PRODUCER_TRACE: { + const notification = new FbsProducer.TraceNotification(); - default: - { - logger.error('ignoring unknown event "%s"', event); + data!.body(notification); + + const trace: ProducerTraceEventData = + parseTraceEventData(notification); + + this.safeEmit('trace', trace); + + // Emit observer event. + this.#observer.safeEmit('trace', trace); + + break; + } + + default: { + logger.error(`ignoring unknown event "${event}"`); + } } } + ); + } + + private handleListenerError(): void { + this.on('listenererror', (eventName, error) => { + logger.error( + `event listener threw an error [eventName:${eventName}]:`, + error + ); }); } } + +export function producerTypeFromFbs(type: FbsRtpParameters.Type): ProducerType { + switch (type) { + case FbsRtpParameters.Type.SIMPLE: { + return 'simple'; + } + + case FbsRtpParameters.Type.SIMULCAST: { + return 'simulcast'; + } + + case FbsRtpParameters.Type.SVC: { + return 'svc'; + } + + default: { + throw new TypeError(`invalid FbsRtpParameters.Type: ${type}`); + } + } +} + +export function producerTypeToFbs(type: ProducerType): FbsRtpParameters.Type { + switch (type) { + case 'simple': { + return FbsRtpParameters.Type.SIMPLE; + } + + case 'simulcast': { + return FbsRtpParameters.Type.SIMULCAST; + } + + case 'svc': { + return FbsRtpParameters.Type.SVC; + } + + default: { + throw new TypeError(`invalid ProducerType: ${type}`); + } + } +} + +function producerTraceEventTypeToFbs( + eventType: ProducerTraceEventType +): FbsProducer.TraceEventType { + switch (eventType) { + case 'keyframe': { + return FbsProducer.TraceEventType.KEYFRAME; + } + + case 'fir': { + return FbsProducer.TraceEventType.FIR; + } + + case 'nack': { + return FbsProducer.TraceEventType.NACK; + } + + case 'pli': { + return FbsProducer.TraceEventType.PLI; + } + + case 'rtp': { + return FbsProducer.TraceEventType.RTP; + } + + case 'sr': { + return FbsProducer.TraceEventType.SR; + } + + default: { + throw new TypeError(`invalid ProducerTraceEventType: ${eventType}`); + } + } +} + +function producerTraceEventTypeFromFbs( + eventType: FbsProducer.TraceEventType +): ProducerTraceEventType { + switch (eventType) { + case FbsProducer.TraceEventType.KEYFRAME: { + return 'keyframe'; + } + + case FbsProducer.TraceEventType.FIR: { + return 'fir'; + } + + case FbsProducer.TraceEventType.NACK: { + return 'nack'; + } + + case FbsProducer.TraceEventType.PLI: { + return 'pli'; + } + + case FbsProducer.TraceEventType.RTP: { + return 'rtp'; + } + + case FbsProducer.TraceEventType.SR: { + return 'sr'; + } + } +} + +export function parseProducerDump( + data: FbsProducer.DumpResponse +): ProducerDump { + return { + id: data.id()!, + kind: data.kind() === FbsRtpParameters.MediaKind.AUDIO ? 'audio' : 'video', + type: producerTypeFromFbs(data.type()), + rtpParameters: parseRtpParameters(data.rtpParameters()!), + // NOTE: optional values are represented with null instead of undefined. + // TODO: Make flatbuffers TS return undefined instead of null. + rtpMapping: data.rtpMapping() ? data.rtpMapping()!.unpack() : undefined, + // NOTE: optional values are represented with null instead of undefined. + // TODO: Make flatbuffers TS return undefined instead of null. + rtpStreams: + data.rtpStreamsLength() > 0 + ? fbsUtils.parseVector(data, 'rtpStreams', (rtpStream: any) => + rtpStream.unpack() + ) + : undefined, + traceEventTypes: fbsUtils.parseVector( + data, + 'traceEventTypes', + producerTraceEventTypeFromFbs + ), + paused: data.paused(), + }; +} + +function parseProducerStats( + binary: FbsProducer.GetStatsResponse +): ProducerStat[] { + return fbsUtils.parseVector(binary, 'stats', parseRtpStreamRecvStats); +} + +function parseProducerScore(binary: FbsProducer.Score): ProducerScore { + return { + encodingIdx: binary.encodingIdx(), + ssrc: binary.ssrc(), + rid: binary.rid() ?? undefined, + score: binary.score(), + }; +} + +function parseTraceEventData( + trace: FbsProducer.TraceNotification +): ProducerTraceEventData { + let info: any; + + if (trace.infoType() !== FbsProducer.TraceInfo.NONE) { + const accessor = trace.info.bind(trace); + + info = FbsProducerTraceInfo.unionToTraceInfo(trace.infoType(), accessor); + + trace.info(info); + } + + return { + type: producerTraceEventTypeFromFbs(trace.type()), + timestamp: Number(trace.timestamp()), + direction: + trace.direction() === FbsTraceDirection.DIRECTION_IN ? 'in' : 'out', + info: info ? info.unpack() : undefined, + }; +} diff --git a/node/src/ProducerTypes.ts b/node/src/ProducerTypes.ts new file mode 100644 index 0000000000..989acf72c0 --- /dev/null +++ b/node/src/ProducerTypes.ts @@ -0,0 +1,253 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { MediaKind, RtpParameters } from './rtpParametersTypes'; +import type { RtpStreamRecvStats } from './rtpStreamStatsTypes'; +import type { AppData } from './types'; + +export type ProducerOptions = { + /** + * Producer id (just for Router.pipeToRouter() method). + */ + id?: string; + + /** + * Media kind ('audio' or 'video'). + */ + kind: MediaKind; + + /** + * RTP parameters defining what the endpoint is sending. + */ + rtpParameters: RtpParameters; + + /** + * Whether the producer must start in paused mode. Default false. + */ + paused?: boolean; + + /** + * Just for video. Time (in ms) before asking the sender for a new key frame + * after having asked a previous one. Default 0. + */ + keyFrameRequestDelay?: number; + + /** + * Custom application data. + */ + appData?: ProducerAppData; +}; + +/** + * Producer type. + */ +export type ProducerType = 'simple' | 'simulcast' | 'svc'; + +export type ProducerScore = { + /** + * Index of the RTP stream in the rtpParameters.encodings array. + */ + encodingIdx: number; + + /** + * SSRC of the RTP stream. + */ + ssrc: number; + + /** + * RID of the RTP stream. + */ + rid?: string; + + /** + * The score of the RTP stream. + */ + score: number; +}; + +export type ProducerVideoOrientation = { + /** + * Whether the source is a video camera. + */ + camera: boolean; + + /** + * Whether the video source is flipped. + */ + flip: boolean; + + /** + * Rotation degrees (0, 90, 180 or 270). + */ + rotation: number; +}; + +export type ProducerDump = { + id: string; + kind: string; + type: ProducerType; + rtpParameters: RtpParameters; + rtpMapping: any; + rtpStreams: any; + traceEventTypes: string[]; + paused: boolean; +}; + +export type ProducerStat = RtpStreamRecvStats; + +/** + * Valid types for 'trace' event. + */ +export type ProducerTraceEventType = + | 'rtp' + | 'keyframe' + | 'nack' + | 'pli' + | 'fir' + | 'sr'; + +/** + * 'trace' event data. + */ +export type ProducerTraceEventData = { + /** + * Trace type. + */ + type: ProducerTraceEventType; + + /** + * Event timestamp. + */ + timestamp: number; + + /** + * Event direction. + */ + direction: 'in' | 'out'; + + /** + * Per type information. + */ + info: any; +}; + +export type ProducerEvents = { + transportclose: []; + score: [ProducerScore[]]; + videoorientationchange: [ProducerVideoOrientation]; + trace: [ProducerTraceEventData]; + listenererror: [string, Error]; + // Private events. + '@close': []; +}; + +export type ProducerObserver = EnhancedEventEmitter; + +export type ProducerObserverEvents = { + close: []; + pause: []; + resume: []; + score: [ProducerScore[]]; + videoorientationchange: [ProducerVideoOrientation]; + trace: [ProducerTraceEventData]; +}; + +export interface Producer + extends EnhancedEventEmitter { + /** + * Producer id. + */ + get id(): string; + + /** + * Whether the Producer is closed. + */ + get closed(): boolean; + + /** + * Media kind. + */ + get kind(): MediaKind; + + /** + * RTP parameters. + */ + get rtpParameters(): RtpParameters; + + /** + * Producer type. + */ + get type(): ProducerType; + + /** + * Consumable RTP parameters. + * + * @private + */ + get consumableRtpParameters(): RtpParameters; + + /** + * Whether the Producer is paused. + */ + get paused(): boolean; + + /** + * Producer score list. + */ + get score(): ProducerScore[]; + + /** + * App custom data. + */ + get appData(): ProducerAppData; + + /** + * App custom data setter. + */ + set appData(appData: ProducerAppData); + + /** + * Observer. + */ + get observer(): ProducerObserver; + + /** + * Close the Producer. + */ + close(): void; + + /** + * Transport was closed. + * + * @private + */ + transportClosed(): void; + + /** + * Dump Producer. + */ + dump(): Promise; + + /** + * Get Producer stats. + */ + getStats(): Promise; + + /** + * Pause the Producer. + */ + pause(): Promise; + + /** + * Resume the Producer. + */ + resume(): Promise; + + /** + * Enable 'trace' event. + */ + enableTraceEvent(types?: ProducerTraceEventType[]): Promise; + + /** + * Send RTP packet (just valid for Producers created on a DirectTransport). + */ + send(rtpPacket: Buffer): void; +} diff --git a/node/src/Router.ts b/node/src/Router.ts index 0c5bff2ddd..ebe6a3f1a7 100644 --- a/node/src/Router.ts +++ b/node/src/Router.ts @@ -1,152 +1,114 @@ -import { v4 as uuidv4 } from 'uuid'; import { Logger } from './Logger'; -import { EnhancedEventEmitter } from './EnhancedEventEmitter'; +import { EnhancedEventEmitter } from './enhancedEvents'; import * as ortc from './ortc'; import { InvalidStateError } from './errors'; -import { Channel } from './Channel'; -import { PayloadChannel } from './PayloadChannel'; -import { Transport, TransportListenIp } from './Transport'; -import { WebRtcTransport, WebRtcTransportOptions } from './WebRtcTransport'; -import { PlainTransport, PlainTransportOptions } from './PlainTransport'; -import { PipeTransport, PipeTransportOptions } from './PipeTransport'; -import { DirectTransport, DirectTransportOptions } from './DirectTransport'; -import { Producer } from './Producer'; -import { Consumer } from './Consumer'; -import { DataProducer } from './DataProducer'; -import { DataConsumer } from './DataConsumer'; -import { RtpObserver } from './RtpObserver'; -import { ActiveSpeakerObserver, ActiveSpeakerObserverOptions } from './ActiveSpeakerObserver'; -import { AudioLevelObserver, AudioLevelObserverOptions } from './AudioLevelObserver'; -import { RtpCapabilities, RtpCodecCapability } from './RtpParameters'; -import { NumSctpStreams } from './SctpParameters'; - -export type RouterOptions = -{ - /** - * Router media codecs. - */ - mediaCodecs?: RtpCodecCapability[]; - - /** - * Custom application data. - */ - appData?: Record; -}; - -export type PipeToRouterOptions = -{ - /** - * The id of the Producer to consume. - */ - producerId?: string; - - /** - * The id of the DataProducer to consume. - */ - dataProducerId?: string; - - /** - * Target Router instance. - */ - router: Router; - - /** - * IP used in the PipeTransport pair. Default '127.0.0.1'. - */ - listenIp?: TransportListenIp | string; - - /** - * Create a SCTP association. Default true. - */ - enableSctp?: boolean; - - /** - * SCTP streams number. - */ - numSctpStreams?: NumSctpStreams; - - /** - * Enable RTX and NACK for RTP retransmission. - */ - enableRtx?: boolean; - - /** - * Enable SRTP. - */ - enableSrtp?: boolean; -}; - -export type PipeToRouterResult = -{ - /** - * The Consumer created in the current Router. - */ - pipeConsumer?: Consumer; - - /** - * The Producer created in the target Router. - */ - pipeProducer?: Producer; - - /** - * The DataConsumer created in the current Router. - */ - pipeDataConsumer?: DataConsumer; - - /** - * The DataProducer created in the target Router. - */ - pipeDataProducer?: DataProducer; -}; - -type PipeTransportPair = -{ - [key: string]: PipeTransport; -}; - -export type RouterEvents = -{ - workerclose: []; - // Private events. - '@close': []; -}; - -export type RouterObserverEvents = -{ - close: []; - newtransport: [Transport]; - newrtpobserver: [RtpObserver]; +import type { Channel } from './Channel'; +import type { + Router, + PipeToRouterOptions, + PipeToRouterResult, + PipeTransportPair, + RouterDump, + RouterEvents, + RouterObserver, + RouterObserverEvents, +} from './RouterTypes'; +import type { + Transport, + TransportListenIp, + TransportProtocol, +} from './TransportTypes'; +import { portRangeToFbs, socketFlagsToFbs } from './Transport'; +import type { + WebRtcTransport, + WebRtcTransportOptions, +} from './WebRtcTransportTypes'; +import { + WebRtcTransportImpl, + parseWebRtcTransportDumpResponse, +} from './WebRtcTransport'; +import type { + PlainTransport, + PlainTransportOptions, +} from './PlainTransportTypes'; +import { + PlainTransportImpl, + parsePlainTransportDumpResponse, +} from './PlainTransport'; +import type { PipeTransport, PipeTransportOptions } from './PipeTransportTypes'; +import { + PipeTransportImpl, + parsePipeTransportDumpResponse, +} from './PipeTransport'; +import type { + DirectTransport, + DirectTransportOptions, +} from './DirectTransportTypes'; +import { + DirectTransportImpl, + parseDirectTransportDumpResponse, +} from './DirectTransport'; +import type { Producer } from './ProducerTypes'; +import type { Consumer } from './ConsumerTypes'; +import type { DataProducer } from './DataProducerTypes'; +import type { DataConsumer } from './DataConsumerTypes'; +import type { RtpObserver } from './RtpObserverTypes'; +import type { + ActiveSpeakerObserver, + ActiveSpeakerObserverOptions, +} from './ActiveSpeakerObserverTypes'; +import { ActiveSpeakerObserverImpl } from './ActiveSpeakerObserver'; +import type { + AudioLevelObserver, + AudioLevelObserverOptions, +} from './AudioLevelObserverTypes'; +import { AudioLevelObserverImpl } from './AudioLevelObserver'; +import type { RtpCapabilities } from './rtpParametersTypes'; +import { cryptoSuiteToFbs } from './srtpParametersFbsUtils'; +import type { AppData } from './types'; +import * as utils from './utils'; +import * as fbsUtils from './fbsUtils'; +import * as FbsActiveSpeakerObserver from './fbs/active-speaker-observer'; +import * as FbsAudioLevelObserver from './fbs/audio-level-observer'; +import * as FbsRequest from './fbs/request'; +import * as FbsWorker from './fbs/worker'; +import * as FbsRouter from './fbs/router'; +import * as FbsTransport from './fbs/transport'; +import { Protocol as FbsTransportProtocol } from './fbs/transport/protocol'; +import * as FbsWebRtcTransport from './fbs/web-rtc-transport'; +import * as FbsPlainTransport from './fbs/plain-transport'; +import * as FbsPipeTransport from './fbs/pipe-transport'; +import * as FbsDirectTransport from './fbs/direct-transport'; +import * as FbsSctpParameters from './fbs/sctp-parameters'; + +export type RouterInternal = { + routerId: string; }; -export type RouterInternal = -{ - routerId: string; +type RouterData = { + rtpCapabilities: RtpCapabilities; }; const logger = new Logger('Router'); -export class Router extends EnhancedEventEmitter +export class RouterImpl + extends EnhancedEventEmitter + implements Router { // Internal data. readonly #internal: RouterInternal; // Router data. - readonly #data: - { - rtpCapabilities: RtpCapabilities; - }; + readonly #data: RouterData; // Channel instance. readonly #channel: Channel; - // PayloadChannel instance. - readonly #payloadChannel: PayloadChannel; - // Closed flag. #closed = false; // Custom app data. - readonly #appData: Record; + #appData: RouterAppData; // Transports map. readonly #transports: Map = new Map(); @@ -162,32 +124,26 @@ export class Router extends EnhancedEventEmitter // Map of PipeTransport pair Promises indexed by the id of the Router in // which pipeToRouter() was called. - readonly #mapRouterPairPipeTransportPairPromise: - Map> = new Map(); + readonly #mapRouterPairPipeTransportPairPromise: Map< + string, + Promise + > = new Map(); // Observer instance. - readonly #observer = new EnhancedEventEmitter(); - - /** - * @private - */ - constructor( - { - internal, - data, - channel, - payloadChannel, - appData - }: - { - internal: RouterInternal; - data: any; - channel: Channel; - payloadChannel: PayloadChannel; - appData?: Record; - } - ) - { + readonly #observer: RouterObserver = + new EnhancedEventEmitter(); + + constructor({ + internal, + data, + channel, + appData, + }: { + internal: RouterInternal; + data: RouterData; + channel: Channel; + appData?: RouterAppData; + }) { super(); logger.debug('constructor()'); @@ -195,87 +151,67 @@ export class Router extends EnhancedEventEmitter this.#internal = internal; this.#data = data; this.#channel = channel; - this.#payloadChannel = payloadChannel; - this.#appData = appData || {}; + this.#appData = appData ?? ({} as RouterAppData); + + this.handleListenerError(); } - /** - * Router id. - */ - get id(): string - { + get id(): string { return this.#internal.routerId; } - /** - * Whether the Router is closed. - */ - get closed(): boolean - { + get closed(): boolean { return this.#closed; } - /** - * RTP capabilities of the Router. - */ - get rtpCapabilities(): RtpCapabilities - { + get rtpCapabilities(): RtpCapabilities { return this.#data.rtpCapabilities; } - /** - * App custom data. - */ - get appData(): Record - { + get appData(): RouterAppData { return this.#appData; } - /** - * Invalid setter. - */ - set appData(appData: Record) // eslint-disable-line no-unused-vars - { - throw new Error('cannot override appData object'); + set appData(appData: RouterAppData) { + this.#appData = appData; } - /** - * Observer. - */ - get observer(): EnhancedEventEmitter - { + get observer(): RouterObserver { return this.#observer; } /** - * @private * Just for testing purposes. + * + * @private */ - get transportsForTesting(): Map - { + get transportsForTesting(): Map { return this.#transports; } - /** - * Close the Router. - */ - close(): void - { - if (this.#closed) + close(): void { + if (this.#closed) { return; + } logger.debug('close()'); this.#closed = true; - const reqData = { routerId: this.#internal.routerId }; + const requestOffset = new FbsWorker.CloseRouterRequestT( + this.#internal.routerId + ).pack(this.#channel.bufferBuilder); - this.#channel.request('worker.closeRouter', undefined, reqData) + this.#channel + .request( + FbsRequest.Method.WORKER_CLOSE_ROUTER, + FbsRequest.Body.Worker_CloseRouterRequest, + requestOffset + ) .catch(() => {}); // Close every Transport. - for (const transport of this.#transports.values()) - { + for (const transport of this.#transports.values()) { transport.routerClosed(); } this.#transports.clear(); @@ -284,8 +220,7 @@ export class Router extends EnhancedEventEmitter this.#producers.clear(); // Close every RtpObserver. - for (const rtpObserver of this.#rtpObservers.values()) - { + for (const rtpObserver of this.#rtpObservers.values()) { rtpObserver.routerClosed(); } this.#rtpObservers.clear(); @@ -299,23 +234,17 @@ export class Router extends EnhancedEventEmitter this.#observer.safeEmit('close'); } - /** - * Worker was closed. - * - * @private - */ - workerClosed(): void - { - if (this.#closed) + workerClosed(): void { + if (this.#closed) { return; + } logger.debug('workerClosed()'); this.#closed = true; // Close every Transport. - for (const transport of this.#transports.values()) - { + for (const transport of this.#transports.values()) { transport.routerClosed(); } this.#transports.clear(); @@ -324,8 +253,7 @@ export class Router extends EnhancedEventEmitter this.#producers.clear(); // Close every RtpObserver. - for (const rtpObserver of this.#rtpObservers.values()) - { + for (const rtpObserver of this.#rtpObservers.values()) { rtpObserver.routerClosed(); } this.#rtpObservers.clear(); @@ -339,222 +267,414 @@ export class Router extends EnhancedEventEmitter this.#observer.safeEmit('close'); } - /** - * Dump Router. - */ - async dump(): Promise - { + async dump(): Promise { logger.debug('dump()'); - return this.#channel.request('router.dump', this.#internal.routerId); + // Send the request and wait for the response. + const response = await this.#channel.request( + FbsRequest.Method.ROUTER_DUMP, + undefined, + undefined, + this.#internal.routerId + ); + + /* Decode Response. */ + const dump = new FbsRouter.DumpResponse(); + + response.body(dump); + + return parseRouterDumpResponse(dump); } - /** - * Create a WebRtcTransport. - */ - async createWebRtcTransport( - { - webRtcServer, - listenIps, - port, - enableUdp = true, - enableTcp = false, - preferUdp = false, - preferTcp = false, - initialAvailableOutgoingBitrate = 600000, - enableSctp = false, - numSctpStreams = { OS: 1024, MIS: 1024 }, - maxSctpMessageSize = 262144, - sctpSendBufferSize = 262144, - appData - }: WebRtcTransportOptions - ): Promise - { + async createWebRtcTransport< + WebRtcTransportAppData extends AppData = AppData, + >({ + webRtcServer, + listenInfos, + listenIps, + port, + enableUdp, + enableTcp, + preferUdp = false, + preferTcp = false, + initialAvailableOutgoingBitrate = 600000, + enableSctp = false, + numSctpStreams = { OS: 1024, MIS: 1024 }, + maxSctpMessageSize = 262144, + sctpSendBufferSize = 262144, + iceConsentTimeout = 30, + appData, + }: WebRtcTransportOptions): Promise< + WebRtcTransport + > { logger.debug('createWebRtcTransport()'); - if (!webRtcServer && !Array.isArray(listenIps)) - throw new TypeError('missing webRtcServer and listenIps (one of them is mandatory)'); - else if (appData && typeof appData !== 'object') + if ( + !webRtcServer && + !Array.isArray(listenInfos) && + !Array.isArray(listenIps) + ) { + throw new TypeError( + 'missing webRtcServer, listenInfos and listenIps (one of them is mandatory)' + ); + } else if (webRtcServer && listenInfos && listenIps) { + throw new TypeError( + 'only one of webRtcServer, listenInfos and listenIps must be given' + ); + } else if ( + numSctpStreams && + (typeof numSctpStreams.OS !== 'number' || + typeof numSctpStreams.MIS !== 'number') + ) { + throw new TypeError('if given, numSctpStreams must contain OS and MIS'); + } else if (appData && typeof appData !== 'object') { throw new TypeError('if given, appData must be an object'); + } - if (listenIps) - { - listenIps = listenIps.map((listenIp) => - { - if (typeof listenIp === 'string' && listenIp) - { + // If webRtcServer is given, then do not force default values for enableUdp + // and enableTcp. Otherwise set them if unset. + if (webRtcServer) { + enableUdp ??= true; + enableTcp ??= true; + } else { + enableUdp ??= true; + enableTcp ??= false; + } + + // Convert deprecated TransportListenIps to TransportListenInfos. + if (listenIps) { + // Normalize IP strings to TransportListenIp objects. + listenIps = listenIps.map(listenIp => { + if (typeof listenIp === 'string') { return { ip: listenIp }; + } else { + return listenIp; + } + }); + + listenInfos = []; + + const orderedProtocols: TransportProtocol[] = []; + + if (enableUdp && (preferUdp || !enableTcp || !preferTcp)) { + orderedProtocols.push('udp'); + + if (enableTcp) { + orderedProtocols.push('tcp'); } - else if (typeof listenIp === 'object') - { - return { - ip : listenIp.ip, - announcedIp : listenIp.announcedIp || undefined - }; + } else if (enableTcp && ((preferTcp && !preferUdp) || !enableUdp)) { + orderedProtocols.push('tcp'); + + if (enableUdp) { + orderedProtocols.push('udp'); } - else - { - throw new TypeError('wrong listenIp'); + } + + for (const listenIp of listenIps as TransportListenIp[]) { + for (const protocol of orderedProtocols) { + listenInfos.push({ + protocol: protocol, + ip: listenIp.ip, + announcedAddress: listenIp.announcedIp, + port: port, + }); } - }); + } } - const reqData = - { - transportId : uuidv4(), - webRtcServerId : webRtcServer ? webRtcServer.id : undefined, - listenIps, - port, - enableUdp, - enableTcp, - preferUdp, - preferTcp, + const transportId = utils.generateUUIDv4(); + + /* Build Request. */ + let webRtcTransportListenServer: + | FbsWebRtcTransport.ListenServerT + | undefined; + let webRtcTransportListenIndividual: + | FbsWebRtcTransport.ListenIndividualT + | undefined; + + if (webRtcServer) { + webRtcTransportListenServer = new FbsWebRtcTransport.ListenServerT( + webRtcServer.id + ); + } else { + const fbsListenInfos: FbsTransport.ListenInfoT[] = []; + + for (const listenInfo of listenInfos!) { + fbsListenInfos.push( + new FbsTransport.ListenInfoT( + listenInfo.protocol === 'udp' + ? FbsTransportProtocol.UDP + : FbsTransportProtocol.TCP, + listenInfo.ip, + listenInfo.announcedAddress ?? listenInfo.announcedIp, + listenInfo.port, + portRangeToFbs(listenInfo.portRange), + socketFlagsToFbs(listenInfo.flags), + listenInfo.sendBufferSize, + listenInfo.recvBufferSize + ) + ); + } + + webRtcTransportListenIndividual = + new FbsWebRtcTransport.ListenIndividualT(fbsListenInfos); + } + + const baseTransportOptions = new FbsTransport.OptionsT( + undefined /* direct */, + undefined /* maxMessageSize */, initialAvailableOutgoingBitrate, enableSctp, - numSctpStreams, + new FbsSctpParameters.NumSctpStreamsT( + numSctpStreams.OS, + numSctpStreams.MIS + ), maxSctpMessageSize, sctpSendBufferSize, - isDataChannel : true - }; - - const data = webRtcServer - ? await this.#channel.request('router.createWebRtcTransportWithServer', this.#internal.routerId, reqData) - : await this.#channel.request('router.createWebRtcTransport', this.#internal.routerId, reqData); - - const transport = new WebRtcTransport( - { - internal : - { + true /* isDataChannel */ + ); + + const webRtcTransportOptions = + new FbsWebRtcTransport.WebRtcTransportOptionsT( + baseTransportOptions, + webRtcServer + ? FbsWebRtcTransport.Listen.ListenServer + : FbsWebRtcTransport.Listen.ListenIndividual, + webRtcServer + ? webRtcTransportListenServer + : webRtcTransportListenIndividual, + enableUdp, + enableTcp, + preferUdp, + preferTcp, + iceConsentTimeout + ); + + const requestOffset = new FbsRouter.CreateWebRtcTransportRequestT( + transportId, + webRtcTransportOptions + ).pack(this.#channel.bufferBuilder); + + const response = await this.#channel.request( + webRtcServer + ? FbsRequest.Method.ROUTER_CREATE_WEBRTCTRANSPORT_WITH_SERVER + : FbsRequest.Method.ROUTER_CREATE_WEBRTCTRANSPORT, + FbsRequest.Body.Router_CreateWebRtcTransportRequest, + requestOffset, + this.#internal.routerId + ); + + /* Decode Response. */ + const data = new FbsWebRtcTransport.DumpResponse(); + + response.body(data); + + const webRtcTransportData = parseWebRtcTransportDumpResponse(data); + + const transport: WebRtcTransport = + new WebRtcTransportImpl({ + internal: { ...this.#internal, - transportId : reqData.transportId + transportId: transportId, }, - data, - channel : this.#channel, - payloadChannel : this.#payloadChannel, + data: webRtcTransportData, + channel: this.#channel, appData, - getRouterRtpCapabilities : (): RtpCapabilities => this.#data.rtpCapabilities, - getProducerById : (producerId: string): Producer | undefined => ( - this.#producers.get(producerId) - ), - getDataProducerById : (dataProducerId: string): DataProducer | undefined => ( - this.#dataProducers.get(dataProducerId) - ) + getRouterRtpCapabilities: (): RtpCapabilities => + this.#data.rtpCapabilities, + getProducerById: (producerId: string): Producer | undefined => + this.#producers.get(producerId), + getDataProducerById: ( + dataProducerId: string + ): DataProducer | undefined => this.#dataProducers.get(dataProducerId), }); this.#transports.set(transport.id, transport); transport.on('@close', () => this.#transports.delete(transport.id)); - transport.on('@listenserverclose', () => this.#transports.delete(transport.id)); - transport.on('@newproducer', (producer: Producer) => this.#producers.set(producer.id, producer)); - transport.on('@producerclose', (producer: Producer) => this.#producers.delete(producer.id)); - transport.on('@newdataproducer', (dataProducer: DataProducer) => ( + transport.on('@listenserverclose', () => + this.#transports.delete(transport.id) + ); + transport.on('@newproducer', (producer: Producer) => + this.#producers.set(producer.id, producer) + ); + transport.on('@producerclose', (producer: Producer) => + this.#producers.delete(producer.id) + ); + transport.on('@newdataproducer', (dataProducer: DataProducer) => this.#dataProducers.set(dataProducer.id, dataProducer) - )); - transport.on('@dataproducerclose', (dataProducer: DataProducer) => ( + ); + transport.on('@dataproducerclose', (dataProducer: DataProducer) => this.#dataProducers.delete(dataProducer.id) - )); - - if (webRtcServer) - webRtcServer.handleWebRtcTransport(transport); + ); // Emit observer event. this.#observer.safeEmit('newtransport', transport); + if (webRtcServer) { + webRtcServer.handleWebRtcTransport(transport); + } + return transport; } - /** - * Create a PlainTransport. - */ - async createPlainTransport( - { - listenIp, - port, - rtcpMux = true, - comedia = false, - enableSctp = false, - numSctpStreams = { OS: 1024, MIS: 1024 }, - maxSctpMessageSize = 262144, - sctpSendBufferSize = 262144, - enableSrtp = false, - srtpCryptoSuite = 'AES_CM_128_HMAC_SHA1_80', - appData - }: PlainTransportOptions - ): Promise - { + async createPlainTransport({ + listenInfo, + rtcpListenInfo, + listenIp, + port, + rtcpMux = true, + comedia = false, + enableSctp = false, + numSctpStreams = { OS: 1024, MIS: 1024 }, + maxSctpMessageSize = 262144, + sctpSendBufferSize = 262144, + enableSrtp = false, + srtpCryptoSuite = 'AES_CM_128_HMAC_SHA1_80', + appData, + }: PlainTransportOptions): Promise< + PlainTransport + > { logger.debug('createPlainTransport()'); - if (!listenIp) - throw new TypeError('missing listenIp'); - else if (appData && typeof appData !== 'object') + if (!listenInfo && !listenIp) { + throw new TypeError( + 'missing listenInfo and listenIp (one of them is mandatory)' + ); + } else if (listenInfo && listenIp) { + throw new TypeError('only one of listenInfo and listenIp must be given'); + } else if (appData && typeof appData !== 'object') { throw new TypeError('if given, appData must be an object'); + } - if (typeof listenIp === 'string' && listenIp) - { - listenIp = { ip: listenIp }; + // If rtcpMux is enabled, ignore rtcpListenInfo. + if (rtcpMux && rtcpListenInfo) { + logger.warn( + 'createPlainTransport() | ignoring rtcpMux since rtcpListenInfo is given' + ); + + rtcpMux = false; } - else if (typeof listenIp === 'object') - { - listenIp = - { - ip : listenIp.ip, - announcedIp : listenIp.announcedIp || undefined + + // Convert deprecated TransportListenIps to TransportListenInfos. + if (listenIp) { + // Normalize IP string to TransportListenIp object. + if (typeof listenIp === 'string') { + listenIp = { ip: listenIp }; + } + + listenInfo = { + protocol: 'udp', + ip: listenIp.ip, + announcedAddress: listenIp.announcedIp, + port: port, }; } - else - { - throw new TypeError('wrong listenIp'); - } - const reqData = - { - transportId : uuidv4(), - listenIp, - port, - rtcpMux, - comedia, + const transportId = utils.generateUUIDv4(); + + /* Build Request. */ + const baseTransportOptions = new FbsTransport.OptionsT( + undefined /* direct */, + undefined /* maxMessageSize */, + undefined /* initialAvailableOutgoingBitrate */, enableSctp, - numSctpStreams, + new FbsSctpParameters.NumSctpStreamsT( + numSctpStreams.OS, + numSctpStreams.MIS + ), maxSctpMessageSize, sctpSendBufferSize, - isDataChannel : false, + false /* isDataChannel */ + ); + + const plainTransportOptions = new FbsPlainTransport.PlainTransportOptionsT( + baseTransportOptions, + new FbsTransport.ListenInfoT( + listenInfo!.protocol === 'udp' + ? FbsTransportProtocol.UDP + : FbsTransportProtocol.TCP, + listenInfo!.ip, + listenInfo!.announcedAddress ?? listenInfo!.announcedIp, + listenInfo!.port, + portRangeToFbs(listenInfo!.portRange), + socketFlagsToFbs(listenInfo!.flags), + listenInfo!.sendBufferSize, + listenInfo!.recvBufferSize + ), + rtcpListenInfo + ? new FbsTransport.ListenInfoT( + rtcpListenInfo.protocol === 'udp' + ? FbsTransportProtocol.UDP + : FbsTransportProtocol.TCP, + rtcpListenInfo.ip, + rtcpListenInfo.announcedAddress ?? rtcpListenInfo.announcedIp, + rtcpListenInfo.port, + portRangeToFbs(rtcpListenInfo.portRange), + socketFlagsToFbs(rtcpListenInfo.flags), + rtcpListenInfo.sendBufferSize, + rtcpListenInfo.recvBufferSize + ) + : undefined, + rtcpMux, + comedia, enableSrtp, - srtpCryptoSuite - }; + cryptoSuiteToFbs(srtpCryptoSuite) + ); + + const requestOffset = new FbsRouter.CreatePlainTransportRequestT( + transportId, + plainTransportOptions + ).pack(this.#channel.bufferBuilder); + + const response = await this.#channel.request( + FbsRequest.Method.ROUTER_CREATE_PLAINTRANSPORT, + FbsRequest.Body.Router_CreatePlainTransportRequest, + requestOffset, + this.#internal.routerId + ); + + /* Decode Response. */ + const data = new FbsPlainTransport.DumpResponse(); + + response.body(data); - const data = - await this.#channel.request('router.createPlainTransport', this.#internal.routerId, reqData); + const plainTransportData = parsePlainTransportDumpResponse(data); - const transport = new PlainTransport( - { - internal : - { + const transport: PlainTransport = + new PlainTransportImpl({ + internal: { ...this.#internal, - transportId : reqData.transportId + transportId: transportId, }, - data, - channel : this.#channel, - payloadChannel : this.#payloadChannel, + data: plainTransportData, + channel: this.#channel, appData, - getRouterRtpCapabilities : (): RtpCapabilities => this.#data.rtpCapabilities, - getProducerById : (producerId: string): Producer | undefined => ( - this.#producers.get(producerId) - ), - getDataProducerById : (dataProducerId: string): DataProducer | undefined => ( - this.#dataProducers.get(dataProducerId) - ) + getRouterRtpCapabilities: (): RtpCapabilities => + this.#data.rtpCapabilities, + getProducerById: (producerId: string): Producer | undefined => + this.#producers.get(producerId), + getDataProducerById: ( + dataProducerId: string + ): DataProducer | undefined => this.#dataProducers.get(dataProducerId), }); this.#transports.set(transport.id, transport); transport.on('@close', () => this.#transports.delete(transport.id)); - transport.on('@listenserverclose', () => this.#transports.delete(transport.id)); - transport.on('@newproducer', (producer: Producer) => this.#producers.set(producer.id, producer)); - transport.on('@producerclose', (producer: Producer) => this.#producers.delete(producer.id)); - transport.on('@newdataproducer', (dataProducer: DataProducer) => ( + transport.on('@listenserverclose', () => + this.#transports.delete(transport.id) + ); + transport.on('@newproducer', (producer: Producer) => + this.#producers.set(producer.id, producer) + ); + transport.on('@producerclose', (producer: Producer) => + this.#producers.delete(producer.id) + ); + transport.on('@newdataproducer', (dataProducer: DataProducer) => this.#dataProducers.set(dataProducer.id, dataProducer) - )); - transport.on('@dataproducerclose', (dataProducer: DataProducer) => ( + ); + transport.on('@dataproducerclose', (dataProducer: DataProducer) => this.#dataProducers.delete(dataProducer.id) - )); + ); // Emit observer event. this.#observer.safeEmit('newtransport', transport); @@ -562,95 +682,136 @@ export class Router extends EnhancedEventEmitter return transport; } - /** - * Create a PipeTransport. - */ - async createPipeTransport( - { - listenIp, - port, - enableSctp = false, - numSctpStreams = { OS: 1024, MIS: 1024 }, - maxSctpMessageSize = 268435456, - sctpSendBufferSize = 268435456, - enableRtx = false, - enableSrtp = false, - appData - }: PipeTransportOptions - ): Promise - { + async createPipeTransport({ + listenInfo, + listenIp, + port, + enableSctp = false, + numSctpStreams = { OS: 1024, MIS: 1024 }, + maxSctpMessageSize = 268435456, + sctpSendBufferSize = 268435456, + enableRtx = false, + enableSrtp = false, + appData, + }: PipeTransportOptions): Promise< + PipeTransport + > { logger.debug('createPipeTransport()'); - if (!listenIp) - throw new TypeError('missing listenIp'); - else if (appData && typeof appData !== 'object') + if (!listenInfo && !listenIp) { + throw new TypeError( + 'missing listenInfo and listenIp (one of them is mandatory)' + ); + } else if (listenInfo && listenIp) { + throw new TypeError('only one of listenInfo and listenIp must be given'); + } else if (appData && typeof appData !== 'object') { throw new TypeError('if given, appData must be an object'); - - if (typeof listenIp === 'string' && listenIp) - { - listenIp = { ip: listenIp }; } - else if (typeof listenIp === 'object') - { - listenIp = - { - ip : listenIp.ip, - announcedIp : listenIp.announcedIp || undefined + + // Convert deprecated TransportListenIps to TransportListenInfos. + if (listenIp) { + // Normalize IP string to TransportListenIp object. + if (typeof listenIp === 'string') { + listenIp = { ip: listenIp }; + } + + listenInfo = { + protocol: 'udp', + ip: listenIp.ip, + announcedAddress: listenIp.announcedIp, + port: port, }; } - else - { - throw new TypeError('wrong listenIp'); - } - const reqData = - { - transportId : uuidv4(), - listenIp, - port, + const transportId = utils.generateUUIDv4(); + + /* Build Request. */ + const baseTransportOptions = new FbsTransport.OptionsT( + undefined /* direct */, + undefined /* maxMessageSize */, + undefined /* initialAvailableOutgoingBitrate */, enableSctp, - numSctpStreams, + new FbsSctpParameters.NumSctpStreamsT( + numSctpStreams.OS, + numSctpStreams.MIS + ), maxSctpMessageSize, sctpSendBufferSize, - isDataChannel : false, + false /* isDataChannel */ + ); + + const pipeTransportOptions = new FbsPipeTransport.PipeTransportOptionsT( + baseTransportOptions, + new FbsTransport.ListenInfoT( + listenInfo!.protocol === 'udp' + ? FbsTransportProtocol.UDP + : FbsTransportProtocol.TCP, + listenInfo!.ip, + listenInfo!.announcedAddress ?? listenInfo!.announcedIp, + listenInfo!.port, + portRangeToFbs(listenInfo!.portRange), + socketFlagsToFbs(listenInfo!.flags), + listenInfo!.sendBufferSize, + listenInfo!.recvBufferSize + ), enableRtx, enableSrtp - }; + ); + + const requestOffset = new FbsRouter.CreatePipeTransportRequestT( + transportId, + pipeTransportOptions + ).pack(this.#channel.bufferBuilder); + + const response = await this.#channel.request( + FbsRequest.Method.ROUTER_CREATE_PIPETRANSPORT, + FbsRequest.Body.Router_CreatePipeTransportRequest, + requestOffset, + this.#internal.routerId + ); + + /* Decode Response. */ + const data = new FbsPipeTransport.DumpResponse(); - const data = - await this.#channel.request('router.createPipeTransport', this.#internal.routerId, reqData); + response.body(data); - const transport = new PipeTransport( - { - internal : - { + const plainTransportData = parsePipeTransportDumpResponse(data); + + const transport: PipeTransport = + new PipeTransportImpl({ + internal: { ...this.#internal, - transportId : reqData.transportId + transportId, }, - data, - channel : this.#channel, - payloadChannel : this.#payloadChannel, + data: plainTransportData, + channel: this.#channel, appData, - getRouterRtpCapabilities : (): RtpCapabilities => this.#data.rtpCapabilities, - getProducerById : (producerId: string): Producer | undefined => ( - this.#producers.get(producerId) - ), - getDataProducerById : (dataProducerId: string): DataProducer | undefined => ( - this.#dataProducers.get(dataProducerId) - ) + getRouterRtpCapabilities: (): RtpCapabilities => + this.#data.rtpCapabilities, + getProducerById: (producerId: string): Producer | undefined => + this.#producers.get(producerId), + getDataProducerById: ( + dataProducerId: string + ): DataProducer | undefined => this.#dataProducers.get(dataProducerId), }); this.#transports.set(transport.id, transport); transport.on('@close', () => this.#transports.delete(transport.id)); - transport.on('@listenserverclose', () => this.#transports.delete(transport.id)); - transport.on('@newproducer', (producer: Producer) => this.#producers.set(producer.id, producer)); - transport.on('@producerclose', (producer: Producer) => this.#producers.delete(producer.id)); - transport.on('@newdataproducer', (dataProducer: DataProducer) => ( + transport.on('@listenserverclose', () => + this.#transports.delete(transport.id) + ); + transport.on('@newproducer', (producer: Producer) => + this.#producers.set(producer.id, producer) + ); + transport.on('@producerclose', (producer: Producer) => + this.#producers.delete(producer.id) + ); + transport.on('@newdataproducer', (dataProducer: DataProducer) => this.#dataProducers.set(dataProducer.id, dataProducer) - )); - transport.on('@dataproducerclose', (dataProducer: DataProducer) => ( + ); + transport.on('@dataproducerclose', (dataProducer: DataProducer) => this.#dataProducers.delete(dataProducer.id) - )); + ); // Emit observer event. this.#observer.safeEmit('newtransport', transport); @@ -658,62 +819,93 @@ export class Router extends EnhancedEventEmitter return transport; } - /** - * Create a DirectTransport. - */ - async createDirectTransport( + async createDirectTransport( { maxMessageSize = 262144, - appData - }: DirectTransportOptions = - { - maxMessageSize : 262144 + appData, + }: DirectTransportOptions = { + maxMessageSize: 262144, } - ): Promise - { + ): Promise> { logger.debug('createDirectTransport()'); - const reqData = - { - transportId : uuidv4(), - direct : true, - maxMessageSize - }; - - const data = - await this.#channel.request('router.createDirectTransport', this.#internal.routerId, reqData); - - const transport = new DirectTransport( - { - internal : - { + if (typeof maxMessageSize !== 'number' || maxMessageSize < 0) { + throw new TypeError('if given, maxMessageSize must be a positive number'); + } else if (appData && typeof appData !== 'object') { + throw new TypeError('if given, appData must be an object'); + } + + const transportId = utils.generateUUIDv4(); + + /* Build Request. */ + const baseTransportOptions = new FbsTransport.OptionsT( + true /* direct */, + maxMessageSize, + undefined /* initialAvailableOutgoingBitrate */, + undefined /* enableSctp */, + undefined /* numSctpStreams */, + undefined /* maxSctpMessageSize */, + undefined /* sctpSendBufferSize */, + undefined /* isDataChannel */ + ); + + const directTransportOptions = + new FbsDirectTransport.DirectTransportOptionsT(baseTransportOptions); + + const requestOffset = new FbsRouter.CreateDirectTransportRequestT( + transportId, + directTransportOptions + ).pack(this.#channel.bufferBuilder); + + const response = await this.#channel.request( + FbsRequest.Method.ROUTER_CREATE_DIRECTTRANSPORT, + FbsRequest.Body.Router_CreateDirectTransportRequest, + requestOffset, + this.#internal.routerId + ); + + /* Decode Response. */ + const data = new FbsDirectTransport.DumpResponse(); + + response.body(data); + + const directTransportData = parseDirectTransportDumpResponse(data); + + const transport: DirectTransport = + new DirectTransportImpl({ + internal: { ...this.#internal, - transportId : reqData.transportId + transportId: transportId, }, - data, - channel : this.#channel, - payloadChannel : this.#payloadChannel, + data: directTransportData, + channel: this.#channel, appData, - getRouterRtpCapabilities : (): RtpCapabilities => this.#data.rtpCapabilities, - getProducerById : (producerId: string): Producer | undefined => ( - this.#producers.get(producerId) - ), - getDataProducerById : (dataProducerId: string): DataProducer | undefined => ( - this.#dataProducers.get(dataProducerId) - ) + getRouterRtpCapabilities: (): RtpCapabilities => + this.#data.rtpCapabilities, + getProducerById: (producerId: string): Producer | undefined => + this.#producers.get(producerId), + getDataProducerById: ( + dataProducerId: string + ): DataProducer | undefined => this.#dataProducers.get(dataProducerId), }); this.#transports.set(transport.id, transport); transport.on('@close', () => this.#transports.delete(transport.id)); - transport.on('@listenserverclose', () => this.#transports.delete(transport.id)); - transport.on('@newproducer', (producer: Producer) => this.#producers.set(producer.id, producer)); - transport.on('@producerclose', (producer: Producer) => this.#producers.delete(producer.id)); - transport.on('@newdataproducer', (dataProducer: DataProducer) => ( + transport.on('@listenserverclose', () => + this.#transports.delete(transport.id) + ); + transport.on('@newproducer', (producer: Producer) => + this.#producers.set(producer.id, producer) + ); + transport.on('@producerclose', (producer: Producer) => + this.#producers.delete(producer.id) + ); + transport.on('@newdataproducer', (dataProducer: DataProducer) => this.#dataProducers.set(dataProducer.id, dataProducer) - )); - transport.on('@dataproducerclose', (dataProducer: DataProducer) => ( + ); + transport.on('@dataproducerclose', (dataProducer: DataProducer) => this.#dataProducers.delete(dataProducer.id) - )); + ); // Emit observer event. this.#observer.safeEmit('newtransport', transport); @@ -721,49 +913,67 @@ export class Router extends EnhancedEventEmitter return transport; } - /** - * Pipes the given Producer or DataProducer into another Router in same host. - */ - async pipeToRouter( - { - producerId, - dataProducerId, - router, - listenIp = '127.0.0.1', - enableSctp = true, - numSctpStreams = { OS: 1024, MIS: 1024 }, - enableRtx = false, - enableSrtp = false - }: PipeToRouterOptions - ): Promise - { + async pipeToRouter({ + producerId, + dataProducerId, + router, + listenInfo, + listenIp, + enableSctp = true, + numSctpStreams = { OS: 1024, MIS: 1024 }, + enableRtx = false, + enableSrtp = false, + }: PipeToRouterOptions): Promise { logger.debug('pipeToRouter()'); - if (!producerId && !dataProducerId) + if (!listenInfo && !listenIp) { + listenInfo = { + protocol: 'udp', + ip: '127.0.0.1', + }; + } + + if (listenInfo && listenIp) { + throw new TypeError('only one of listenInfo and listenIp must be given'); + } else if (!producerId && !dataProducerId) { throw new TypeError('missing producerId or dataProducerId'); - else if (producerId && dataProducerId) + } else if (producerId && dataProducerId) { throw new TypeError('just producerId or dataProducerId can be given'); - else if (!router) + } else if (!router) { throw new TypeError('Router not found'); - else if (router === this) + } else if (router === this) { throw new TypeError('cannot use this Router as destination'); + } + + // Convert deprecated TransportListenIps to TransportListenInfos. + if (listenIp) { + // Normalize IP string to TransportListenIp object. + if (typeof listenIp === 'string') { + listenIp = { ip: listenIp }; + } + + listenInfo = { + protocol: 'udp', + ip: listenIp.ip, + announcedAddress: listenIp.announcedIp, + }; + } let producer: Producer | undefined; let dataProducer: DataProducer | undefined; - if (producerId) - { + if (producerId) { producer = this.#producers.get(producerId); - if (!producer) + if (!producer) { throw new TypeError('Producer not found'); - } - else if (dataProducerId) - { + } + } else if (dataProducerId) { dataProducer = this.#dataProducers.get(dataProducerId); - if (!dataProducer) + if (!dataProducer) { throw new TypeError('DataProducer not found'); + } } const pipeTransportPairKey = router.id; @@ -773,284 +983,278 @@ export class Router extends EnhancedEventEmitter let localPipeTransport: PipeTransport; let remotePipeTransport: PipeTransport; - if (pipeTransportPairPromise) - { + if (pipeTransportPairPromise) { pipeTransportPair = await pipeTransportPairPromise; localPipeTransport = pipeTransportPair[this.id]; remotePipeTransport = pipeTransportPair[router.id]; - } - else - { - pipeTransportPairPromise = new Promise((resolve, reject) => - { - Promise.all( - [ - this.createPipeTransport( - { listenIp, enableSctp, numSctpStreams, enableRtx, enableSrtp }), - router.createPipeTransport( - { listenIp, enableSctp, numSctpStreams, enableRtx, enableSrtp }) - ]) - .then((pipeTransports) => - { + } else { + pipeTransportPairPromise = new Promise((resolve, reject) => { + Promise.all([ + this.createPipeTransport({ + listenInfo: listenInfo!, + enableSctp, + numSctpStreams, + enableRtx, + enableSrtp, + }), + router.createPipeTransport({ + listenInfo: listenInfo!, + enableSctp, + numSctpStreams, + enableRtx, + enableSrtp, + }), + ]) + .then(pipeTransports => { localPipeTransport = pipeTransports[0]; remotePipeTransport = pipeTransports[1]; }) - .then(() => - { - return Promise.all( - [ - localPipeTransport.connect( - { - ip : remotePipeTransport.tuple.localIp, - port : remotePipeTransport.tuple.localPort, - srtpParameters : remotePipeTransport.srtpParameters - }), - remotePipeTransport.connect( - { - ip : localPipeTransport.tuple.localIp, - port : localPipeTransport.tuple.localPort, - srtpParameters : localPipeTransport.srtpParameters - }) - ]); + .then(() => { + return Promise.all([ + localPipeTransport.connect({ + ip: remotePipeTransport.tuple.localAddress, + port: remotePipeTransport.tuple.localPort, + srtpParameters: remotePipeTransport.srtpParameters, + }), + remotePipeTransport.connect({ + ip: localPipeTransport.tuple.localAddress, + port: localPipeTransport.tuple.localPort, + srtpParameters: localPipeTransport.srtpParameters, + }), + ]); }) - .then(() => - { - localPipeTransport.observer.on('close', () => - { + .then(() => { + localPipeTransport.observer.on('close', () => { remotePipeTransport.close(); this.#mapRouterPairPipeTransportPairPromise.delete( - pipeTransportPairKey); + pipeTransportPairKey + ); }); - remotePipeTransport.observer.on('close', () => - { + remotePipeTransport.observer.on('close', () => { localPipeTransport.close(); this.#mapRouterPairPipeTransportPairPromise.delete( - pipeTransportPairKey); + pipeTransportPairKey + ); }); - resolve( - { - [this.id] : localPipeTransport, - [router.id] : remotePipeTransport - }); + resolve({ + [this.id]: localPipeTransport, + [router.id]: remotePipeTransport, + }); }) - .catch((error) => - { + .catch(error => { logger.error( - 'pipeToRouter() | error creating PipeTransport pair:%o', - error); + 'pipeToRouter() | error creating PipeTransport pair:', + error + ); - if (localPipeTransport) + if (localPipeTransport) { localPipeTransport.close(); + } - if (remotePipeTransport) + if (remotePipeTransport) { remotePipeTransport.close(); + } - reject(error); + reject(error instanceof Error ? error : new Error(String(error))); }); }); this.#mapRouterPairPipeTransportPairPromise.set( - pipeTransportPairKey, pipeTransportPairPromise); + pipeTransportPairKey, + pipeTransportPairPromise + ); router.addPipeTransportPair(this.id, pipeTransportPairPromise); await pipeTransportPairPromise; } - if (producer) - { + if (producer) { let pipeConsumer: Consumer | undefined; let pipeProducer: Producer | undefined; - try - { - pipeConsumer = await localPipeTransport!.consume( - { - producerId : producerId! - }); + try { + pipeConsumer = await localPipeTransport!.consume({ + producerId: producerId!, + }); - pipeProducer = await remotePipeTransport!.produce( - { - id : producer.id, - kind : pipeConsumer!.kind, - rtpParameters : pipeConsumer!.rtpParameters, - paused : pipeConsumer!.producerPaused, - appData : producer.appData - }); + pipeProducer = await remotePipeTransport!.produce({ + id: producer.id, + kind: pipeConsumer.kind, + rtpParameters: pipeConsumer.rtpParameters, + paused: pipeConsumer.producerPaused, + appData: producer.appData, + }); // Ensure that the producer has not been closed in the meanwhile. - if (producer.closed) + if (producer.closed) { throw new InvalidStateError('original Producer closed'); + } // Ensure that producer.paused has not changed in the meanwhile and, if // so, sync the pipeProducer. - if (pipeProducer.paused !== producer.paused) - { - if (producer.paused) + if (pipeProducer.paused !== producer.paused) { + if (producer.paused) { await pipeProducer.pause(); - else + } else { await pipeProducer.resume(); + } } // Pipe events from the pipe Consumer to the pipe Producer. - pipeConsumer!.observer.on('close', () => pipeProducer!.close()); - pipeConsumer!.observer.on('pause', () => pipeProducer!.pause()); - pipeConsumer!.observer.on('resume', () => pipeProducer!.resume()); + pipeConsumer.observer.on('close', () => pipeProducer!.close()); + pipeConsumer.observer.on('pause', () => void pipeProducer!.pause()); + pipeConsumer.observer.on('resume', () => void pipeProducer!.resume()); // Pipe events from the pipe Producer to the pipe Consumer. pipeProducer.observer.on('close', () => pipeConsumer!.close()); return { pipeConsumer, pipeProducer }; - } - catch (error) - { + } catch (error) { logger.error( - 'pipeToRouter() | error creating pipe Consumer/Producer pair:%o', - error); + 'pipeToRouter() | error creating pipe Consumer/Producer pair:', + error as Error + ); - if (pipeConsumer) + if (pipeConsumer) { pipeConsumer.close(); + } - if (pipeProducer) + if (pipeProducer) { pipeProducer.close(); + } throw error; } - } - else if (dataProducer) - { + } else if (dataProducer) { let pipeDataConsumer: DataConsumer | undefined; let pipeDataProducer: DataProducer | undefined; - try - { - pipeDataConsumer = await localPipeTransport!.consumeData( - { - dataProducerId : dataProducerId! - }); + try { + pipeDataConsumer = await localPipeTransport!.consumeData({ + dataProducerId: dataProducerId!, + }); - pipeDataProducer = await remotePipeTransport!.produceData( - { - id : dataProducer.id, - sctpStreamParameters : pipeDataConsumer!.sctpStreamParameters, - label : pipeDataConsumer!.label, - protocol : pipeDataConsumer!.protocol, - appData : dataProducer.appData - }); + pipeDataProducer = await remotePipeTransport!.produceData({ + id: dataProducer.id, + sctpStreamParameters: pipeDataConsumer.sctpStreamParameters, + label: pipeDataConsumer.label, + protocol: pipeDataConsumer.protocol, + appData: dataProducer.appData, + }); // Ensure that the dataProducer has not been closed in the meanwhile. - if (dataProducer.closed) + if (dataProducer.closed) { throw new InvalidStateError('original DataProducer closed'); + } // Pipe events from the pipe DataConsumer to the pipe DataProducer. - pipeDataConsumer!.observer.on('close', () => pipeDataProducer!.close()); + pipeDataConsumer.observer.on('close', () => pipeDataProducer!.close()); // Pipe events from the pipe DataProducer to the pipe DataConsumer. pipeDataProducer.observer.on('close', () => pipeDataConsumer!.close()); return { pipeDataConsumer, pipeDataProducer }; - } - catch (error) - { + } catch (error) { logger.error( - 'pipeToRouter() | error creating pipe DataConsumer/DataProducer pair:%o', - error); + 'pipeToRouter() | error creating pipe DataConsumer/DataProducer pair:', + error as Error + ); - if (pipeDataConsumer) - pipeDataConsumer.close(); - - if (pipeDataProducer) - pipeDataProducer.close(); + pipeDataConsumer?.close(); + pipeDataProducer?.close(); throw error; } - } - else - { + } else { throw new Error('internal error'); } } - /** - * @private - */ addPipeTransportPair( pipeTransportPairKey: string, pipeTransportPairPromise: Promise - ): void - { - if (this.#mapRouterPairPipeTransportPairPromise.has(pipeTransportPairKey)) - { + ): void { + if (this.#mapRouterPairPipeTransportPairPromise.has(pipeTransportPairKey)) { throw new Error( - 'given pipeTransportPairKey already exists in this Router'); + 'given pipeTransportPairKey already exists in this Router' + ); } this.#mapRouterPairPipeTransportPairPromise.set( - pipeTransportPairKey, pipeTransportPairPromise); + pipeTransportPairKey, + pipeTransportPairPromise + ); pipeTransportPairPromise - .then((pipeTransportPair) => - { + .then(pipeTransportPair => { const localPipeTransport = pipeTransportPair[this.id]; // NOTE: No need to do any other cleanup here since that is done by the // Router calling this method on us. - localPipeTransport.observer.on('close', () => - { + localPipeTransport.observer.on('close', () => { this.#mapRouterPairPipeTransportPairPromise.delete( - pipeTransportPairKey); + pipeTransportPairKey + ); }); }) - .catch(() => - { + .catch(() => { this.#mapRouterPairPipeTransportPairPromise.delete( - pipeTransportPairKey); + pipeTransportPairKey + ); }); } - /** - * Create an ActiveSpeakerObserver - */ - async createActiveSpeakerObserver( - { - interval = 300, - appData - }: ActiveSpeakerObserverOptions = {} - ): Promise - { + async createActiveSpeakerObserver< + ActiveSpeakerObserverAppData extends AppData = AppData, + >({ + interval = 300, + appData, + }: ActiveSpeakerObserverOptions = {}): Promise< + ActiveSpeakerObserver + > { logger.debug('createActiveSpeakerObserver()'); - if (appData && typeof appData !== 'object') + if (typeof interval !== 'number') { + throw new TypeError('if given, interval must be an number'); + } else if (appData && typeof appData !== 'object') { throw new TypeError('if given, appData must be an object'); - - const reqData = - { - rtpObserverId : uuidv4(), - interval - }; + } + + const rtpObserverId = utils.generateUUIDv4(); + + /* Build Request. */ + const activeRtpObserverOptions = + new FbsActiveSpeakerObserver.ActiveSpeakerObserverOptionsT(interval); - await this.#channel.request('router.createActiveSpeakerObserver', this.#internal.routerId, reqData); + const requestOffset = new FbsRouter.CreateActiveSpeakerObserverRequestT( + rtpObserverId, + activeRtpObserverOptions + ).pack(this.#channel.bufferBuilder); - const activeSpeakerObserver = new ActiveSpeakerObserver( - { - internal : - { + await this.#channel.request( + FbsRequest.Method.ROUTER_CREATE_ACTIVESPEAKEROBSERVER, + FbsRequest.Body.Router_CreateActiveSpeakerObserverRequest, + requestOffset, + this.#internal.routerId + ); + + const activeSpeakerObserver: ActiveSpeakerObserver = + new ActiveSpeakerObserverImpl({ + internal: { ...this.#internal, - rtpObserverId : reqData.rtpObserverId + rtpObserverId: rtpObserverId, }, - channel : this.#channel, - payloadChannel : this.#payloadChannel, + channel: this.#channel, appData, - getProducerById : (producerId: string): Producer | undefined => ( - this.#producers.get(producerId) - ) + getProducerById: (producerId: string): Producer | undefined => + this.#producers.get(producerId), }); - + this.#rtpObservers.set(activeSpeakerObserver.id, activeSpeakerObserver); - activeSpeakerObserver.on('@close', () => - { + activeSpeakerObserver.on('@close', () => { this.#rtpObservers.delete(activeSpeakerObserver.id); }); @@ -1060,51 +1264,70 @@ export class Router extends EnhancedEventEmitter return activeSpeakerObserver; } - /** - * Create an AudioLevelObserver. - */ - async createAudioLevelObserver( - { - maxEntries = 1, - threshold = -80, - interval = 1000, - appData - }: AudioLevelObserverOptions = {} - ): Promise - { + async createAudioLevelObserver< + AudioLevelObserverAppData extends AppData = AppData, + >({ + maxEntries = 1, + threshold = -80, + interval = 1000, + appData, + }: AudioLevelObserverOptions = {}): Promise< + AudioLevelObserver + > { logger.debug('createAudioLevelObserver()'); - if (appData && typeof appData !== 'object') + if (typeof maxEntries !== 'number' || maxEntries <= 0) { + throw new TypeError('if given, maxEntries must be a positive number'); + } else if ( + typeof threshold !== 'number' || + threshold < -127 || + threshold > 0 + ) { + throw new TypeError( + 'if given, threshole must be a negative number greater than -127' + ); + } else if (typeof interval !== 'number') { + throw new TypeError('if given, interval must be an number'); + } else if (appData && typeof appData !== 'object') { throw new TypeError('if given, appData must be an object'); + } - const reqData = - { - rtpObserverId : uuidv4(), - maxEntries, - threshold, - interval - }; - - await this.#channel.request('router.createAudioLevelObserver', this.#internal.routerId, reqData); - - const audioLevelObserver = new AudioLevelObserver( - { - internal : - { + const rtpObserverId = utils.generateUUIDv4(); + + /* Build Request. */ + const audioLevelObserverOptions = + new FbsAudioLevelObserver.AudioLevelObserverOptionsT( + maxEntries, + threshold, + interval + ); + + const requestOffset = new FbsRouter.CreateAudioLevelObserverRequestT( + rtpObserverId, + audioLevelObserverOptions + ).pack(this.#channel.bufferBuilder); + + await this.#channel.request( + FbsRequest.Method.ROUTER_CREATE_AUDIOLEVELOBSERVER, + FbsRequest.Body.Router_CreateAudioLevelObserverRequest, + requestOffset, + this.#internal.routerId + ); + + const audioLevelObserver: AudioLevelObserver = + new AudioLevelObserverImpl({ + internal: { ...this.#internal, - rtpObserverId : reqData.rtpObserverId + rtpObserverId: rtpObserverId, }, - channel : this.#channel, - payloadChannel : this.#payloadChannel, + channel: this.#channel, appData, - getProducerById : (producerId: string): Producer | undefined => ( - this.#producers.get(producerId) - ) + getProducerById: (producerId: string): Producer | undefined => + this.#producers.get(producerId), }); this.#rtpObservers.set(audioLevelObserver.id, audioLevelObserver); - audioLevelObserver.on('@close', () => - { + audioLevelObserver.on('@close', () => { this.#rtpObservers.delete(audioLevelObserver.id); }); @@ -1114,39 +1337,72 @@ export class Router extends EnhancedEventEmitter return audioLevelObserver; } - /** - * Check whether the given RTP capabilities can consume the given Producer. - */ - canConsume( - { - producerId, - rtpCapabilities - }: - { - producerId: string; - rtpCapabilities: RtpCapabilities; - } - ): boolean - { + canConsume({ + producerId, + rtpCapabilities, + }: { + producerId: string; + rtpCapabilities: RtpCapabilities; + }): boolean { const producer = this.#producers.get(producerId); - if (!producer) - { - logger.error( - 'canConsume() | Producer with id "%s" not found', producerId); + if (!producer) { + logger.error(`canConsume() | Producer with id "${producerId}" not found`); return false; } - try - { - return ortc.canConsume(producer.consumableRtpParameters, rtpCapabilities); - } - catch (error) - { - logger.error('canConsume() | unexpected error: %s', String(error)); + // Clone given RTP capabilities to not modify input data. + const clonedRtpCapabilities = utils.clone(rtpCapabilities); + + try { + return ortc.canConsume( + producer.consumableRtpParameters, + clonedRtpCapabilities + ); + } catch (error) { + logger.error(`canConsume() | unexpected error: ${error}`); return false; } } + + private handleListenerError(): void { + this.on('listenererror', (eventName, error) => { + logger.error( + `event listener threw an error [eventName:${eventName}]:`, + error + ); + }); + } +} + +export function parseRouterDumpResponse( + binary: FbsRouter.DumpResponse +): RouterDump { + return { + id: binary.id()!, + transportIds: fbsUtils.parseVector(binary, 'transportIds'), + rtpObserverIds: fbsUtils.parseVector(binary, 'rtpObserverIds'), + mapProducerIdConsumerIds: fbsUtils.parseStringStringArrayVector( + binary, + 'mapProducerIdConsumerIds' + ), + mapConsumerIdProducerId: fbsUtils.parseStringStringVector( + binary, + 'mapConsumerIdProducerId' + ), + mapProducerIdObserverIds: fbsUtils.parseStringStringArrayVector( + binary, + 'mapProducerIdObserverIds' + ), + mapDataProducerIdDataConsumerIds: fbsUtils.parseStringStringArrayVector( + binary, + 'mapDataProducerIdDataConsumerIds' + ), + mapDataConsumerIdDataProducerId: fbsUtils.parseStringStringVector( + binary, + 'mapDataConsumerIdDataProducerId' + ), + }; } diff --git a/node/src/RouterTypes.ts b/node/src/RouterTypes.ts new file mode 100644 index 0000000000..5a7db9be1e --- /dev/null +++ b/node/src/RouterTypes.ts @@ -0,0 +1,291 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { + Transport, + TransportListenInfo, + TransportListenIp, +} from './TransportTypes'; +import type { + WebRtcTransport, + WebRtcTransportOptions, +} from './WebRtcTransportTypes'; +import type { + PlainTransport, + PlainTransportOptions, +} from './PlainTransportTypes'; +import type { PipeTransport, PipeTransportOptions } from './PipeTransportTypes'; +import type { + DirectTransport, + DirectTransportOptions, +} from './DirectTransportTypes'; +import type { Producer } from './ProducerTypes'; +import type { Consumer } from './ConsumerTypes'; +import type { DataProducer } from './DataProducerTypes'; +import type { DataConsumer } from './DataConsumerTypes'; +import type { RtpObserver } from './RtpObserverTypes'; +import type { + ActiveSpeakerObserver, + ActiveSpeakerObserverOptions, +} from './ActiveSpeakerObserverTypes'; +import type { + AudioLevelObserver, + AudioLevelObserverOptions, +} from './AudioLevelObserverTypes'; +import type { RtpCapabilities, RtpCodecCapability } from './rtpParametersTypes'; +import type { NumSctpStreams } from './sctpParametersTypes'; +import type { Either, AppData } from './types'; + +export type RouterOptions = { + /** + * Router media codecs. + */ + mediaCodecs?: RtpCodecCapability[]; + + /** + * Custom application data. + */ + appData?: RouterAppData; +}; + +export type PipeToRouterOptions = { + /** + * The id of the Producer to consume. + */ + producerId?: string; + + /** + * The id of the DataProducer to consume. + */ + dataProducerId?: string; + + /** + * Target Router instance. + */ + router: Router; + + /** + * Create a SCTP association. Default true. + */ + enableSctp?: boolean; + + /** + * SCTP streams number. + */ + numSctpStreams?: NumSctpStreams; + + /** + * Enable RTX and NACK for RTP retransmission. + */ + enableRtx?: boolean; + + /** + * Enable SRTP. + */ + enableSrtp?: boolean; +} & PipeToRouterListen; + +type PipeToRouterListen = Either; + +type PipeToRouterListenInfo = { + listenInfo: TransportListenInfo; +}; + +type PipeToRouterListenIp = { + /** + * IP used in the PipeTransport pair. Default '127.0.0.1'. + */ + listenIp?: TransportListenIp | string; +}; + +export type PipeToRouterResult = { + /** + * The Consumer created in the current Router. + */ + pipeConsumer?: Consumer; + + /** + * The Producer created in the target Router. + */ + pipeProducer?: Producer; + + /** + * The DataConsumer created in the current Router. + */ + pipeDataConsumer?: DataConsumer; + + /** + * The DataProducer created in the target Router. + */ + pipeDataProducer?: DataProducer; +}; + +export type PipeTransportPair = { + [key: string]: PipeTransport; +}; + +export type RouterDump = { + /** + * The Router id. + */ + id: string; + /** + * Id of Transports. + */ + transportIds: string[]; + /** + * Id of RtpObservers. + */ + rtpObserverIds: string[]; + /** + * Array of Producer id and its respective Consumer ids. + */ + mapProducerIdConsumerIds: { key: string; values: string[] }[]; + /** + * Array of Consumer id and its Producer id. + */ + mapConsumerIdProducerId: { key: string; value: string }[]; + /** + * Array of Producer id and its respective Observer ids. + */ + mapProducerIdObserverIds: { key: string; values: string[] }[]; + /** + * Array of Producer id and its respective DataConsumer ids. + */ + mapDataProducerIdDataConsumerIds: { key: string; values: string[] }[]; + /** + * Array of DataConsumer id and its DataProducer id. + */ + mapDataConsumerIdDataProducerId: { key: string; value: string }[]; +}; + +export type RouterEvents = { + workerclose: []; + listenererror: [string, Error]; + // Private events. + '@close': []; +}; + +export type RouterObserver = EnhancedEventEmitter; + +export type RouterObserverEvents = { + close: []; + newtransport: [Transport]; + newrtpobserver: [RtpObserver]; +}; + +export interface Router + extends EnhancedEventEmitter { + /** + * Router id. + */ + get id(): string; + + /** + * Whether the Router is closed. + */ + get closed(): boolean; + + /** + * RTP capabilities of the Router. + */ + get rtpCapabilities(): RtpCapabilities; + + /** + * App custom data. + */ + get appData(): RouterAppData; + + /** + * App custom data setter. + */ + set appData(appData: RouterAppData); + + /** + * Observer. + */ + get observer(): RouterObserver; + + /** + * Close the Router. + */ + close(): void; + + /** + * Worker was closed. + * + * @private + */ + workerClosed(): void; + + /** + * Dump Router. + */ + dump(): Promise; + + /** + * Create a WebRtcTransport. + */ + createWebRtcTransport( + options: WebRtcTransportOptions + ): Promise>; + + /** + * Create a PlainTransport. + */ + createPlainTransport( + options: PlainTransportOptions + ): Promise>; + + /** + * Create a PipeTransport. + */ + createPipeTransport( + options: PipeTransportOptions + ): Promise>; + + /** + * Create a DirectTransport. + */ + createDirectTransport( + options?: DirectTransportOptions + ): Promise>; + + /** + * Pipes the given Producer or DataProducer into another Router in same host. + */ + pipeToRouter(options: PipeToRouterOptions): Promise; + + /** + * @private + */ + addPipeTransportPair( + pipeTransportPairKey: string, + pipeTransportPairPromise: Promise + ): void; + + /** + * Create an ActiveSpeakerObserver + */ + createActiveSpeakerObserver< + ActiveSpeakerObserverAppData extends AppData = AppData, + >( + options?: ActiveSpeakerObserverOptions + ): Promise>; + + /** + * Create an AudioLevelObserver. + */ + createAudioLevelObserver( + options?: AudioLevelObserverOptions + ): Promise>; + + /** + * Check whether the given RTP capabilities can consume the given Producer. + */ + canConsume({ + producerId, + rtpCapabilities, + }: { + producerId: string; + rtpCapabilities: RtpCapabilities; + }): boolean; +} diff --git a/node/src/RtpObserver.ts b/node/src/RtpObserver.ts index d2cd7257d7..93c26e5231 100644 --- a/node/src/RtpObserver.ts +++ b/node/src/RtpObserver.ts @@ -1,62 +1,41 @@ import { Logger } from './Logger'; -import { EnhancedEventEmitter } from './EnhancedEventEmitter'; -import { Channel } from './Channel'; -import { PayloadChannel } from './PayloadChannel'; -import { RouterInternal } from './Router'; -import { Producer } from './Producer'; - -export type RtpObserverEvents = -{ - routerclose: []; - // Private events. - '@close': []; -}; - -export type RtpObserverObserverEvents = -{ - close: []; - pause: []; - resume: []; - addproducer: [Producer]; - removeproducer: [Producer]; -}; - -export type RtpObserverConstructorOptions = -{ +import { EnhancedEventEmitter } from './enhancedEvents'; +import type { + RtpObserverEvents, + RtpObserverObserver, +} from './RtpObserverTypes'; +import type { Channel } from './Channel'; +import type { RouterInternal } from './Router'; +import type { Producer } from './ProducerTypes'; +import type { AppData } from './types'; +import * as FbsRequest from './fbs/request'; +import * as FbsRouter from './fbs/router'; +import * as FbsRtpObserver from './fbs/rtp-observer'; + +export type RtpObserverConstructorOptions = { internal: RtpObserverObserverInternal; channel: Channel; - payloadChannel: PayloadChannel; - appData?: Record; + appData?: RtpObserverAppData; getProducerById: (producerId: string) => Producer | undefined; }; -export type RtpObserverObserverInternal = RouterInternal & -{ +type RtpObserverObserverInternal = RouterInternal & { rtpObserverId: string; }; const logger = new Logger('RtpObserver'); -export type RtpObserverAddRemoveProducerOptions = -{ - /** - * The id of the Producer to be added or removed. - */ - producerId: string; -}; - -export class RtpObserver - extends EnhancedEventEmitter -{ +export abstract class RtpObserverImpl< + RtpObserverAppData extends AppData = AppData, + Events extends RtpObserverEvents = RtpObserverEvents, + Observer extends RtpObserverObserver = RtpObserverObserver, +> extends EnhancedEventEmitter { // Internal data. protected readonly internal: RtpObserverObserverInternal; // Channel instance. protected readonly channel: Channel; - // PayloadChannel instance. - protected readonly payloadChannel: PayloadChannel; - // Closed flag. #closed = false; @@ -64,94 +43,64 @@ export class RtpObserver #paused = false; // Custom app data. - readonly #appData: Record; + #appData: RtpObserverAppData; // Method to retrieve a Producer. - protected readonly getProducerById: (producerId: string) => Producer | undefined; + protected readonly getProducerById: ( + producerId: string + ) => Producer | undefined; // Observer instance. - readonly #observer = new EnhancedEventEmitter(); + readonly #observer: Observer; - /** - * @private - * @interface - */ - constructor( + protected constructor( { internal, channel, - payloadChannel, appData, - getProducerById - }: RtpObserverConstructorOptions - ) - { + getProducerById, + }: RtpObserverConstructorOptions, + observer: Observer + ) { super(); logger.debug('constructor()'); this.internal = internal; this.channel = channel; - this.payloadChannel = payloadChannel; - this.#appData = appData || {}; + this.#appData = appData ?? ({} as RtpObserverAppData); this.getProducerById = getProducerById; + this.#observer = observer; } - /** - * RtpObserver id. - */ - get id(): string - { + get id(): string { return this.internal.rtpObserverId; } - /** - * Whether the RtpObserver is closed. - */ - get closed(): boolean - { + get closed(): boolean { return this.#closed; } - /** - * Whether the RtpObserver is paused. - */ - get paused(): boolean - { + get paused(): boolean { return this.#paused; } - /** - * App custom data. - */ - get appData(): Record - { + get appData(): RtpObserverAppData { return this.#appData; } - /** - * Invalid setter. - */ - set appData(appData: Record) // eslint-disable-line no-unused-vars - { - throw new Error('cannot override appData object'); + set appData(appData: RtpObserverAppData) { + this.#appData = appData; } - /** - * Observer. - */ - get observer(): EnhancedEventEmitter - { + get observer(): Observer { return this.#observer; } - /** - * Close the RtpObserver. - */ - close(): void - { - if (this.#closed) + close(): void { + if (this.#closed) { return; + } logger.debug('close()'); @@ -159,11 +108,19 @@ export class RtpObserver // Remove notification subscriptions. this.channel.removeAllListeners(this.internal.rtpObserverId); - this.payloadChannel.removeAllListeners(this.internal.rtpObserverId); - - const reqData = { rtpObserverId: this.internal.rtpObserverId }; - this.channel.request('router.closeRtpObserver', this.internal.routerId, reqData) + /* Build Request. */ + const requestOffset = new FbsRouter.CloseRtpObserverRequestT( + this.internal.rtpObserverId + ).pack(this.channel.bufferBuilder); + + this.channel + .request( + FbsRequest.Method.ROUTER_CLOSE_RTPOBSERVER, + FbsRequest.Body.Router_CloseRtpObserverRequest, + requestOffset, + this.internal.routerId + ) .catch(() => {}); this.emit('@close'); @@ -172,15 +129,10 @@ export class RtpObserver this.#observer.safeEmit('close'); } - /** - * Router was closed. - * - * @private - */ - routerClosed(): void - { - if (this.#closed) + routerClosed(): void { + if (this.#closed) { return; + } logger.debug('routerClosed()'); @@ -188,7 +140,6 @@ export class RtpObserver // Remove notification subscriptions. this.channel.removeAllListeners(this.internal.rtpObserverId); - this.payloadChannel.removeAllListeners(this.internal.rtpObserverId); this.safeEmit('routerclose'); @@ -196,77 +147,89 @@ export class RtpObserver this.#observer.safeEmit('close'); } - /** - * Pause the RtpObserver. - */ - async pause(): Promise - { + async pause(): Promise { logger.debug('pause()'); const wasPaused = this.#paused; - await this.channel.request('rtpObserver.pause', this.internal.rtpObserverId); + await this.channel.request( + FbsRequest.Method.RTPOBSERVER_PAUSE, + undefined, + undefined, + this.internal.rtpObserverId + ); this.#paused = true; // Emit observer event. - if (!wasPaused) + if (!wasPaused) { this.#observer.safeEmit('pause'); + } } - /** - * Resume the RtpObserver. - */ - async resume(): Promise - { + async resume(): Promise { logger.debug('resume()'); const wasPaused = this.#paused; - await this.channel.request('rtpObserver.resume', this.internal.rtpObserverId); + await this.channel.request( + FbsRequest.Method.RTPOBSERVER_RESUME, + undefined, + undefined, + this.internal.rtpObserverId + ); this.#paused = false; // Emit observer event. - if (wasPaused) + if (wasPaused) { this.#observer.safeEmit('resume'); + } } - /** - * Add a Producer to the RtpObserver. - */ - async addProducer({ producerId }: RtpObserverAddRemoveProducerOptions): Promise - { + async addProducer({ producerId }: { producerId: string }): Promise { logger.debug('addProducer()'); const producer = this.getProducerById(producerId); - if (!producer) + if (!producer) { throw Error(`Producer with id "${producerId}" not found`); + } - const reqData = { producerId }; + const requestOffset = new FbsRtpObserver.AddProducerRequestT( + producerId + ).pack(this.channel.bufferBuilder); - await this.channel.request('rtpObserver.addProducer', this.internal.rtpObserverId, reqData); + await this.channel.request( + FbsRequest.Method.RTPOBSERVER_ADD_PRODUCER, + FbsRequest.Body.RtpObserver_AddProducerRequest, + requestOffset, + this.internal.rtpObserverId + ); // Emit observer event. this.#observer.safeEmit('addproducer', producer); } - /** - * Remove a Producer from the RtpObserver. - */ - async removeProducer({ producerId }: RtpObserverAddRemoveProducerOptions): Promise - { + async removeProducer({ producerId }: { producerId: string }): Promise { logger.debug('removeProducer()'); const producer = this.getProducerById(producerId); - if (!producer) + if (!producer) { throw Error(`Producer with id "${producerId}" not found`); - - const reqData = { producerId }; - - await this.channel.request('rtpObserver.removeProducer', this.internal.rtpObserverId, reqData); + } + + const requestOffset = new FbsRtpObserver.RemoveProducerRequestT( + producerId + ).pack(this.channel.bufferBuilder); + + await this.channel.request( + FbsRequest.Method.RTPOBSERVER_REMOVE_PRODUCER, + FbsRequest.Body.RtpObserver_RemoveProducerRequest, + requestOffset, + this.internal.rtpObserverId + ); // Emit observer event. this.#observer.safeEmit('removeproducer', producer); diff --git a/node/src/RtpObserverTypes.ts b/node/src/RtpObserverTypes.ts new file mode 100644 index 0000000000..4e04f9d056 --- /dev/null +++ b/node/src/RtpObserverTypes.ts @@ -0,0 +1,105 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { Producer } from './ProducerTypes'; +import type { AppData } from './types'; + +/** + * RtpObserver type. + */ +export type RtpObserverType = 'audiolevel' | 'activespeaker'; + +export type RtpObserverEvents = { + routerclose: []; + listenererror: [string, Error]; + // Private events. + '@close': []; +}; + +export type RtpObserverObserver = + EnhancedEventEmitter; + +export type RtpObserverObserverEvents = { + close: []; + pause: []; + resume: []; + addproducer: [Producer]; + removeproducer: [Producer]; +}; + +export interface RtpObserver< + RtpObserverAppData extends AppData = AppData, + Events extends RtpObserverEvents = RtpObserverEvents, + Observer extends RtpObserverObserver = RtpObserverObserver, +> extends EnhancedEventEmitter { + /** + * RtpObserver id. + */ + get id(): string; + + /** + * Whether the RtpObserver is closed. + */ + get closed(): boolean; + + /** + * RtpObserver type. + * + * @virtual + * @privateRemarks + * - It's marked as virtual getter since each RtpObserver class overrides it. + */ + get type(): RtpObserverType; + + /** + * Whether the RtpObserver is paused. + */ + get paused(): boolean; + + /** + * App custom data. + */ + get appData(): RtpObserverAppData; + + /** + * App custom data setter. + */ + set appData(appData: RtpObserverAppData); + + /** + * Observer. + * + * @virtual + */ + get observer(): Observer; + + /** + * Close the RtpObserver. + */ + close(): void; + + /** + * Router was closed. + * + * @private + */ + routerClosed(): void; + + /** + * Pause the RtpObserver. + */ + pause(): Promise; + + /** + * Resume the RtpObserver. + */ + resume(): Promise; + + /** + * Add a Producer to the RtpObserver. + */ + addProducer({ producerId }: { producerId: string }): Promise; + + /** + * Remove a Producer from the RtpObserver. + */ + removeProducer({ producerId }: { producerId: string }): Promise; +} diff --git a/node/src/Transport.ts b/node/src/Transport.ts index 839af7c8d9..d298dc5f0b 100644 --- a/node/src/Transport.ts +++ b/node/src/Transport.ts @@ -1,143 +1,124 @@ -import { v4 as uuidv4 } from 'uuid'; +import * as flatbuffers from 'flatbuffers'; import { Logger } from './Logger'; -import { EnhancedEventEmitter } from './EnhancedEventEmitter'; -import * as utils from './utils'; +import { EnhancedEventEmitter } from './enhancedEvents'; import * as ortc from './ortc'; -import { Channel } from './Channel'; -import { PayloadChannel } from './PayloadChannel'; -import { RouterInternal } from './Router'; -import { WebRtcTransportData } from './WebRtcTransport'; -import { PlainTransportData } from './PlainTransport'; -import { PipeTransportData } from './PipeTransport'; -import { DirectTransportData } from './DirectTransport'; -import { Producer, ProducerOptions } from './Producer'; -import { Consumer, ConsumerOptions, ConsumerType } from './Consumer'; +import type { + Transport, + TransportType, + TransportProtocol, + TransportPortRange, + TransportSocketFlags, + TransportTuple, + SctpState, + RtpListenerDump, + SctpListenerDump, + RecvRtpHeaderExtensions, + BaseTransportDump, + BaseTransportStats, + TransportTraceEventType, + TransportTraceEventData, + TransportEvents, + TransportObserver, +} from './TransportTypes'; +import type { Channel } from './Channel'; +import type { RouterInternal } from './Router'; +import type { WebRtcTransportData } from './WebRtcTransport'; +import type { PlainTransportData } from './PlainTransport'; +import type { PipeTransportData } from './PipeTransport'; +import type { DirectTransportData } from './DirectTransport'; +import type { Producer, ProducerOptions } from './ProducerTypes'; import { + ProducerImpl, + producerTypeFromFbs, + producerTypeToFbs, +} from './Producer'; +import type { + Consumer, + ConsumerOptions, + ConsumerType, + ConsumerLayers, +} from './ConsumerTypes'; +import { ConsumerImpl } from './Consumer'; +import type { DataProducer, DataProducerOptions, - DataProducerType -} from './DataProducer'; + DataProducerType, +} from './DataProducerTypes'; import { + DataProducerImpl, + dataProducerTypeToFbs, + parseDataProducerDumpResponse, +} from './DataProducer'; +import type { DataConsumer, DataConsumerOptions, - DataConsumerType + DataConsumerType, +} from './DataConsumerTypes'; +import { + DataConsumerImpl, + dataConsumerTypeToFbs, + parseDataConsumerDumpResponse, } from './DataConsumer'; -import { RtpCapabilities } from './RtpParameters'; -import { SctpStreamParameters } from './SctpParameters'; - -export type TransportListenIp = -{ - /** - * Listening IPv4 or IPv6. - */ - ip: string; - - /** - * Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with - * private IP). - */ - announcedIp?: string; -}; - -/** - * Transport protocol. - */ -export type TransportProtocol = 'udp' | 'tcp'; - -export type TransportTuple = -{ - localIp: string; - localPort: number; - remoteIp?: string; - remotePort?: number; - protocol: TransportProtocol; -}; - -/** - * Valid types for 'trace' event. - */ -export type TransportTraceEventType = 'probation' | 'bwe'; - -/** - * 'trace' event data. - */ -export type TransportTraceEventData = -{ - /** - * Trace type. - */ - type: TransportTraceEventType; - - /** - * Event timestamp. - */ - timestamp: number; - - /** - * Event direction. - */ - direction: 'in' | 'out'; - - /** - * Per type information. - */ - info: any; -}; - -export type SctpState = 'new' | 'connecting' | 'connected' | 'failed' | 'closed'; - -export type TransportEvents = -{ - routerclose: []; - listenserverclose: []; - trace: [TransportTraceEventData]; - // Private events. - '@close': []; - '@newproducer': [Producer]; - '@producerclose': [Producer]; - '@newdataproducer': [DataProducer]; - '@dataproducerclose': [DataProducer]; - '@listenserverclose': []; -}; - -export type TransportObserverEvents = -{ - close: []; - newproducer: [Producer]; - newconsumer: [Consumer]; - newdataproducer: [DataProducer]; - newdataconsumer: [DataConsumer]; - trace: [TransportTraceEventData]; -}; - -export type TransportConstructorOptions = -{ +import type { + MediaKind, + RtpCapabilities, + RtpParameters, +} from './rtpParametersTypes'; +import { + serializeRtpEncodingParameters, + serializeRtpParameters, +} from './rtpParametersFbsUtils'; +import type { + SctpParameters, + SctpStreamParameters, +} from './sctpParametersTypes'; +import { + parseSctpParametersDump, + serializeSctpStreamParameters, +} from './sctpParametersFbsUtils'; +import type { AppData } from './types'; +import * as utils from './utils'; +import * as fbsUtils from './fbsUtils'; +import { TraceDirection as FbsTraceDirection } from './fbs/common'; +import * as FbsRequest from './fbs/request'; +import { MediaKind as FbsMediaKind } from './fbs/rtp-parameters/media-kind'; +import * as FbsConsumer from './fbs/consumer'; +import * as FbsDataConsumer from './fbs/data-consumer'; +import * as FbsDataProducer from './fbs/data-producer'; +import * as FbsTransport from './fbs/transport'; +import * as FbsRouter from './fbs/router'; +import * as FbsRtpParameters from './fbs/rtp-parameters'; +import { SctpState as FbsSctpState } from './fbs/sctp-association/sctp-state'; + +export type TransportConstructorOptions = { internal: TransportInternal; data: TransportData; channel: Channel; - payloadChannel: PayloadChannel; - appData?: Record; + appData?: TransportAppData; getRouterRtpCapabilities: () => RtpCapabilities; getProducerById: (producerId: string) => Producer | undefined; getDataProducerById: (dataProducerId: string) => DataProducer | undefined; }; -export type TransportInternal = RouterInternal & -{ +export type TransportInternal = RouterInternal & { transportId: string; }; type TransportData = - | WebRtcTransportData - | PlainTransportData - | PipeTransportData - | DirectTransportData; + | WebRtcTransportData + | PlainTransportData + | PipeTransportData + | DirectTransportData; const logger = new Logger('Transport'); -export class Transport +export abstract class TransportImpl< + TransportAppData extends AppData = AppData, + Events extends TransportEvents = TransportEvents, + Observer extends TransportObserver = TransportObserver, + > extends EnhancedEventEmitter + implements Transport { // Internal data. protected readonly internal: TransportInternal; @@ -148,24 +129,24 @@ export class Transport; + #appData: TransportAppData; // Method to retrieve Router RTP capabilities. readonly #getRouterRtpCapabilities: () => RtpCapabilities; // Method to retrieve a Producer. - protected readonly getProducerById: (producerId: string) => Producer | undefined; + protected readonly getProducerById: ( + producerId: string + ) => Producer | undefined; // Method to retrieve a DataProducer. - protected readonly getDataProducerById: - (dataProducerId: string) => DataProducer | undefined; + protected readonly getDataProducerById: ( + dataProducerId: string + ) => DataProducer | undefined; // Producers map. readonly #producers: Map = new Map(); @@ -192,25 +173,20 @@ export class Transport(); + readonly #observer: Observer; - /** - * @private - * @interface - */ - constructor( + protected constructor( { internal, data, channel, - payloadChannel, appData, getRouterRtpCapabilities, getProducerById, - getDataProducerById - }: TransportConstructorOptions - ) - { + getDataProducerById, + }: TransportConstructorOptions, + observer: Observer + ) { super(); logger.debug('constructor()'); @@ -218,69 +194,46 @@ export class Transport - { + abstract get type(): TransportType; + + get appData(): TransportAppData { return this.#appData; } - /** - * Invalid setter. - */ - set appData(appData: Record) // eslint-disable-line no-unused-vars - { - throw new Error('cannot override appData object'); + set appData(appData: TransportAppData) { + this.#appData = appData; } - /** - * Observer. - */ - get observer(): EnhancedEventEmitter - { + get observer(): Observer { return this.#observer; } /** - * @private * Just for testing purposes. */ - get channelForTesting(): Channel - { + get channelForTesting(): Channel { return this.channel; } - /** - * Close the Transport. - */ - close(): void - { - if (this.#closed) + close(): void { + if (this.#closed) { return; + } logger.debug('close()'); @@ -288,16 +241,23 @@ export class Transport {}); // Close every Producer. - for (const producer of this.#producers.values()) - { + for (const producer of this.#producers.values()) { producer.transportClosed(); // Must tell the Router. @@ -306,15 +266,13 @@ export class Transport - { - logger.debug('dump()'); + abstract dump(): Promise; - return this.channel.request('transport.dump', this.internal.transportId); - } + abstract getStats(): Promise; - /** - * Get Transport stats. - * - * @abstract - */ - async getStats(): Promise - { - // Should not happen. - throw new Error('method not implemented in the subclass'); - } + abstract connect(params: any): Promise; - /** - * Provide the Transport remote parameters. - * - * @abstract - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async connect(params: any): Promise - { - // Should not happen. - throw new Error('method not implemented in the subclass'); + async setMaxIncomingBitrate(bitrate: number): Promise { + logger.debug(`setMaxIncomingBitrate() [bitrate:${bitrate}]`); + + /* Build Request. */ + const requestOffset = + FbsTransport.SetMaxIncomingBitrateRequest.createSetMaxIncomingBitrateRequest( + this.channel.bufferBuilder, + bitrate + ); + + await this.channel.request( + FbsRequest.Method.TRANSPORT_SET_MAX_INCOMING_BITRATE, + FbsRequest.Body.Transport_SetMaxIncomingBitrateRequest, + requestOffset, + this.internal.transportId + ); } - /** - * Set maximum incoming bitrate for receiving media. - */ - async setMaxIncomingBitrate(bitrate: number): Promise - { - logger.debug('setMaxIncomingBitrate() [bitrate:%s]', bitrate); + async setMaxOutgoingBitrate(bitrate: number): Promise { + logger.debug(`setMaxOutgoingBitrate() [bitrate:${bitrate}]`); - const reqData = { bitrate }; + /* Build Request. */ + const requestOffset = new FbsTransport.SetMaxOutgoingBitrateRequestT( + bitrate + ).pack(this.channel.bufferBuilder); await this.channel.request( - 'transport.setMaxIncomingBitrate', this.internal.transportId, reqData); + FbsRequest.Method.TRANSPORT_SET_MAX_OUTGOING_BITRATE, + FbsRequest.Body.Transport_SetMaxOutgoingBitrateRequest, + requestOffset, + this.internal.transportId + ); } - /** - * Set maximum outgoing bitrate for sending media. - */ - async setMaxOutgoingBitrate(bitrate: number): Promise - { - logger.debug('setMaxOutgoingBitrate() [bitrate:%s]', bitrate); + async setMinOutgoingBitrate(bitrate: number): Promise { + logger.debug(`setMinOutgoingBitrate() [bitrate:${bitrate}]`); - const reqData = { bitrate }; + /* Build Request. */ + const requestOffset = new FbsTransport.SetMinOutgoingBitrateRequestT( + bitrate + ).pack(this.channel.bufferBuilder); await this.channel.request( - 'transport.setMaxOutgoingBitrate', this.internal.transportId, reqData); + FbsRequest.Method.TRANSPORT_SET_MIN_OUTGOING_BITRATE, + FbsRequest.Body.Transport_SetMinOutgoingBitrateRequest, + requestOffset, + this.internal.transportId + ); } - /** - * Create a Producer. - */ - async produce( - { - id = undefined, - kind, - rtpParameters, - paused = false, - keyFrameRequestDelay, - appData - }: ProducerOptions - ): Promise - { + async produce({ + id = undefined, + kind, + rtpParameters, + paused = false, + keyFrameRequestDelay, + appData, + }: ProducerOptions): Promise> { logger.debug('produce()'); - if (id && this.#producers.has(id)) + if (id && this.#producers.has(id)) { throw new TypeError(`a Producer with same id "${id}" already exists`); - else if (![ 'audio', 'video' ].includes(kind)) + } else if (!['audio', 'video'].includes(kind)) { throw new TypeError(`invalid kind "${kind}"`); - else if (appData && typeof appData !== 'object') + } else if (appData && typeof appData !== 'object') { throw new TypeError('if given, appData must be an object'); + } + + // Clone given RTP parameters to not modify input data. + const clonedRtpParameters = utils.clone(rtpParameters); // This may throw. - ortc.validateRtpParameters(rtpParameters); + ortc.validateRtpParameters(clonedRtpParameters); // If missing or empty encodings, add one. if ( - !rtpParameters.encodings || - !Array.isArray(rtpParameters.encodings) || - rtpParameters.encodings.length === 0 - ) - { - rtpParameters.encodings = [ {} ]; + !clonedRtpParameters.encodings || + !Array.isArray(clonedRtpParameters.encodings) || + clonedRtpParameters.encodings.length === 0 + ) { + clonedRtpParameters.encodings = [{}]; } // Don't do this in PipeTransports since there we must keep CNAME value in // each Producer. - if (this.constructor.name !== 'PipeTransport') - { + if (this.type !== 'pipe') { // If CNAME is given and we don't have yet a CNAME for Producers in this // Transport, take it. - if (!this.#cnameForProducers && rtpParameters.rtcp && rtpParameters.rtcp.cname) - { - this.#cnameForProducers = rtpParameters.rtcp.cname; + if (!this.#cnameForProducers && clonedRtpParameters.rtcp?.cname) { + this.#cnameForProducers = clonedRtpParameters.rtcp.cname; } - // Otherwise if we don't have yet a CNAME for Producers and the RTP parameters - // do not include CNAME, create a random one. - else if (!this.#cnameForProducers) - { - this.#cnameForProducers = uuidv4().substr(0, 8); + // Otherwise if we don't have yet a CNAME for Producers and the RTP + // parameters do not include CNAME, create a random one. + else if (!this.#cnameForProducers) { + this.#cnameForProducers = utils.generateUUIDv4().substr(0, 8); } // Override Producer's CNAME. - rtpParameters.rtcp = rtpParameters.rtcp || {}; - rtpParameters.rtcp.cname = this.#cnameForProducers; + clonedRtpParameters.rtcp = clonedRtpParameters.rtcp ?? {}; + clonedRtpParameters.rtcp.cname = this.#cnameForProducers; } const routerRtpCapabilities = this.#getRouterRtpCapabilities(); // This may throw. const rtpMapping = ortc.getProducerRtpParametersMapping( - rtpParameters, routerRtpCapabilities); + clonedRtpParameters, + routerRtpCapabilities + ); // This may throw. const consumableRtpParameters = ortc.getConsumableRtpParameters( - kind, rtpParameters, routerRtpCapabilities, rtpMapping); - - const reqData = - { - producerId : id || uuidv4(), kind, - rtpParameters, + clonedRtpParameters, + routerRtpCapabilities, + rtpMapping + ); + + const producerId = id ?? utils.generateUUIDv4(); + const requestOffset = createProduceRequest({ + builder: this.channel.bufferBuilder, + producerId, + kind, + rtpParameters: clonedRtpParameters, rtpMapping, keyFrameRequestDelay, - paused - }; + paused, + }); - const status = - await this.channel.request('transport.produce', this.internal.transportId, reqData); + const response = await this.channel.request( + FbsRequest.Method.TRANSPORT_PRODUCE, + FbsRequest.Body.Transport_ProduceRequest, + requestOffset, + this.internal.transportId + ); - const data = - { + /* Decode Response. */ + const produceResponse = new FbsTransport.ProduceResponse(); + + response.body(produceResponse); + + const status = produceResponse.unpack(); + + const data = { kind, - rtpParameters, - type : status.type, - consumableRtpParameters + rtpParameters: clonedRtpParameters, + type: producerTypeFromFbs(status.type), + consumableRtpParameters, }; - const producer = new Producer( - { - internal : - { - ...this.internal, - producerId : reqData.producerId - }, - data, - channel : this.channel, - payloadChannel : this.payloadChannel, - appData, - paused - }); + const producer: Producer = new ProducerImpl({ + internal: { + ...this.internal, + producerId, + }, + data, + channel: this.channel, + appData, + paused, + }); this.#producers.set(producer.id, producer); - producer.on('@close', () => - { + producer.on('@close', () => { this.#producers.delete(producer.id); this.emit('@producerclose', producer); }); @@ -635,107 +574,121 @@ export class Transport - { + async consume({ + producerId, + rtpCapabilities, + paused = false, + mid, + preferredLayers, + ignoreDtx = false, + enableRtx, + pipe = false, + appData, + }: ConsumerOptions): Promise> { logger.debug('consume()'); - if (!producerId || typeof producerId !== 'string') + if (!producerId || typeof producerId !== 'string') { throw new TypeError('missing producerId'); - else if (appData && typeof appData !== 'object') + } else if (appData && typeof appData !== 'object') { throw new TypeError('if given, appData must be an object'); - else if (mid && (typeof mid !== 'string' || mid.length === 0)) + } else if (mid && (typeof mid !== 'string' || mid.length === 0)) { throw new TypeError('if given, mid must be non empty string'); + } + + // Clone given RTP capabilities to not modify input data. + const clonedRtpCapabilities = utils.clone(rtpCapabilities); // This may throw. - ortc.validateRtpCapabilities(rtpCapabilities!); + ortc.validateRtpCapabilities(clonedRtpCapabilities); const producer = this.getProducerById(producerId); - if (!producer) + if (!producer) { throw Error(`Producer with id "${producerId}" not found`); + } + + // If enableRtx is not given, set it to true if video and false if audio. + if (enableRtx === undefined) { + enableRtx = producer.kind === 'video'; + } // This may throw. - const rtpParameters = ortc.getConsumerRtpParameters( - producer.consumableRtpParameters, rtpCapabilities!, pipe); + const rtpParameters = ortc.getConsumerRtpParameters({ + consumableRtpParameters: producer.consumableRtpParameters, + remoteRtpCapabilities: clonedRtpCapabilities, + pipe, + enableRtx, + }); // Set MID. - if (!pipe) - { - if (mid) - { + if (!pipe) { + if (mid) { rtpParameters.mid = mid; - } - else - { + } else { rtpParameters.mid = `${this.#nextMidForConsumers++}`; // We use up to 8 bytes for MID (string). - if (this.#nextMidForConsumers === 100000000) - { + if (this.#nextMidForConsumers === 100000000) { logger.error( - `consume() | reaching max MID value "${this.#nextMidForConsumers}"`); + `consume() | reaching max MID value "${this.#nextMidForConsumers}"` + ); this.#nextMidForConsumers = 0; } } } - const reqData = - { - consumerId : uuidv4(), - producerId, - kind : producer.kind, + const consumerId = utils.generateUUIDv4(); + const requestOffset = createConsumeRequest({ + builder: this.channel.bufferBuilder, + producer, + consumerId, rtpParameters, - type : pipe ? 'pipe' : producer.type, - consumableRtpEncodings : producer.consumableRtpParameters.encodings, paused, preferredLayers, - ignoreDtx - }; + ignoreDtx, + pipe, + }); - const status = - await this.channel.request('transport.consume', this.internal.transportId, reqData); + const response = await this.channel.request( + FbsRequest.Method.TRANSPORT_CONSUME, + FbsRequest.Body.Transport_ConsumeRequest, + requestOffset, + this.internal.transportId + ); - const data = - { + /* Decode Response. */ + const consumeResponse = new FbsTransport.ConsumeResponse(); + + response.body(consumeResponse); + + const status = consumeResponse.unpack(); + + const data = { producerId, - kind : producer.kind, + kind: producer.kind, rtpParameters, - type : pipe ? 'pipe' : producer.type as ConsumerType + type: pipe ? 'pipe' : (producer.type as ConsumerType), }; - const consumer = new Consumer( - { - internal : - { - ...this.internal, - consumerId : reqData.consumerId - }, - data, - channel : this.channel, - payloadChannel : this.payloadChannel, - appData, - paused : status.paused, - producerPaused : status.producerPaused, - score : status.score, - preferredLayers : status.preferredLayers - }); + const consumer: Consumer = new ConsumerImpl({ + internal: { + ...this.internal, + consumerId, + }, + data, + channel: this.channel, + appData, + paused: status.paused, + producerPaused: status.producerPaused, + score: status.score ?? undefined, + preferredLayers: status.preferredLayers + ? { + spatialLayer: status.preferredLayers.spatialLayer, + temporalLayer: status.preferredLayers.temporalLayer ?? undefined, + } + : undefined, + }); this.consumers.set(consumer.id, consumer); consumer.on('@close', () => this.consumers.delete(consumer.id)); @@ -747,76 +700,95 @@ export class Transport - { + async produceData({ + id = undefined, + sctpStreamParameters, + label = '', + protocol = '', + paused = false, + appData, + }: DataProducerOptions = {}): Promise< + DataProducer + > { logger.debug('produceData()'); - if (id && this.dataProducers.has(id)) + if (id && this.dataProducers.has(id)) { throw new TypeError(`a DataProducer with same id "${id}" already exists`); - else if (appData && typeof appData !== 'object') + } else if (appData && typeof appData !== 'object') { throw new TypeError('if given, appData must be an object'); + } let type: DataProducerType; + // Clone given SCTP stream parameters to not modify input data. + let clonedSctpStreamParameters = utils.clone< + SctpStreamParameters | undefined + >(sctpStreamParameters); + // If this is not a DirectTransport, sctpStreamParameters are required. - if (this.constructor.name !== 'DirectTransport') - { + if (this.type !== 'direct') { type = 'sctp'; // This may throw. - ortc.validateSctpStreamParameters(sctpStreamParameters!); + ortc.validateSctpStreamParameters(clonedSctpStreamParameters!); } // If this is a DirectTransport, sctpStreamParameters must not be given. - else - { + else { type = 'direct'; - if (sctpStreamParameters) - { + if (sctpStreamParameters) { logger.warn( - 'produceData() | sctpStreamParameters are ignored when producing data on a DirectTransport'); + 'produceData() | sctpStreamParameters are ignored when producing data on a DirectTransport' + ); + + clonedSctpStreamParameters = undefined; } } - const reqData = - { - dataProducerId : id || uuidv4(), + const dataProducerId = id ?? utils.generateUUIDv4(); + const requestOffset = createProduceDataRequest({ + builder: this.channel.bufferBuilder, + dataProducerId, type, - sctpStreamParameters, + sctpStreamParameters: clonedSctpStreamParameters, label, - protocol - }; + protocol, + paused, + }); + + const response = await this.channel.request( + FbsRequest.Method.TRANSPORT_PRODUCE_DATA, + FbsRequest.Body.Transport_ProduceDataRequest, + requestOffset, + this.internal.transportId + ); + + /* Decode Response. */ + const produceDataResponse = new FbsDataProducer.DumpResponse(); + + response.body(produceDataResponse); - const data = - await this.channel.request('transport.produceData', this.internal.transportId, reqData); + const dump = parseDataProducerDumpResponse(produceDataResponse); - const dataProducer = new DataProducer( - { - internal : - { + const dataProducer: DataProducer = + new DataProducerImpl({ + internal: { ...this.internal, - dataProducerId : reqData.dataProducerId + dataProducerId, }, - data, - channel : this.channel, - payloadChannel : this.payloadChannel, - appData + data: { + type: dump.type, + sctpStreamParameters: dump.sctpStreamParameters, + label: dump.label, + protocol: dump.protocol, + }, + channel: this.channel, + paused, + appData, }); this.dataProducers.set(dataProducer.id, dataProducer); - dataProducer.on('@close', () => - { + dataProducer.on('@close', () => { this.dataProducers.delete(dataProducer.id); this.emit('@dataproducerclose', dataProducer); }); @@ -829,30 +801,30 @@ export class Transport - { + async consumeData({ + dataProducerId, + ordered, + maxPacketLifeTime, + maxRetransmits, + paused = false, + subchannels, + appData, + }: DataConsumerOptions): Promise< + DataConsumer + > { logger.debug('consumeData()'); - if (!dataProducerId || typeof dataProducerId !== 'string') + if (!dataProducerId || typeof dataProducerId !== 'string') { throw new TypeError('missing dataProducerId'); - else if (appData && typeof appData !== 'object') + } else if (appData && typeof appData !== 'object') { throw new TypeError('if given, appData must be an object'); + } const dataProducer = this.getDataProducerById(dataProducerId); - if (!dataProducer) + if (!dataProducer) { throw Error(`DataProducer with id "${dataProducerId}" not found`); + } let type: DataConsumerType; let sctpStreamParameters: SctpStreamParameters | undefined; @@ -860,21 +832,26 @@ export class Transport( + dataProducer.sctpStreamParameters + ) ?? ({} as SctpStreamParameters); // Override if given. - if (ordered !== undefined) + if (ordered !== undefined) { sctpStreamParameters.ordered = ordered; + } - if (maxPacketLifeTime !== undefined) + if (maxPacketLifeTime !== undefined) { sctpStreamParameters.maxPacketLifeTime = maxPacketLifeTime; + } - if (maxRetransmits !== undefined) + if (maxRetransmits !== undefined) { sctpStreamParameters.maxRetransmits = maxRetransmits; + } // This may throw. sctpStreamId = this.getNextSctpStreamId(); @@ -883,63 +860,84 @@ export class Transport = + new DataConsumerImpl({ + internal: { ...this.internal, - dataConsumerId : reqData.dataConsumerId + dataConsumerId, + }, + data: { + dataProducerId: dump.dataProducerId, + type: dump.type, + sctpStreamParameters: dump.sctpStreamParameters, + label: dump.label, + protocol: dump.protocol, + bufferedAmountLowThreshold: dump.bufferedAmountLowThreshold, }, - data, - channel : this.channel, - payloadChannel : this.payloadChannel, - appData + channel: this.channel, + paused: dump.paused, + subchannels: dump.subchannels, + dataProducerPaused: dump.dataProducerPaused, + appData, }); this.dataConsumers.set(dataConsumer.id, dataConsumer); - dataConsumer.on('@close', () => - { + dataConsumer.on('@close', () => { this.dataConsumers.delete(dataConsumer.id); - if (this.#sctpStreamIds) + if (this.#sctpStreamIds) { this.#sctpStreamIds[sctpStreamId] = 0; + } }); - dataConsumer.on('@dataproducerclose', () => - { + dataConsumer.on('@dataproducerclose', () => { this.dataConsumers.delete(dataConsumer.id); - if (this.#sctpStreamIds) + if (this.#sctpStreamIds) { this.#sctpStreamIds[sctpStreamId] = 0; + } }); // Emit observer event. @@ -948,42 +946,60 @@ export class Transport - { - logger.debug('pause()'); + async enableTraceEvent(types: TransportTraceEventType[] = []): Promise { + logger.debug('enableTraceEvent()'); - const reqData = { types }; + if (!Array.isArray(types)) { + throw new TypeError('types must be an array'); + } else if (types.find(type => typeof type !== 'string')) { + throw new TypeError('every type must be a string'); + } + + // Convert event types. + const fbsEventTypes: FbsTransport.TraceEventType[] = []; + + for (const eventType of types) { + try { + fbsEventTypes.push(transportTraceEventTypeToFbs(eventType)); + } catch (error) { + // Ignore invalid event types. + } + } + + /* Build Request. */ + const requestOffset = new FbsTransport.EnableTraceEventRequestT( + fbsEventTypes + ).pack(this.channel.bufferBuilder); await this.channel.request( - 'transport.enableTraceEvent', this.internal.transportId, reqData); + FbsRequest.Method.TRANSPORT_ENABLE_TRACE_EVENT, + FbsRequest.Body.Transport_EnableTraceEventRequest, + requestOffset, + this.internal.transportId + ); } - private getNextSctpStreamId(): number - { + private getNextSctpStreamId(): number { if ( !this.#data.sctpParameters || typeof this.#data.sctpParameters.MIS !== 'number' - ) - { + ) { throw new TypeError('missing sctpParameters.MIS'); } const numStreams = this.#data.sctpParameters.MIS; - if (!this.#sctpStreamIds) + if (!this.#sctpStreamIds) { this.#sctpStreamIds = Buffer.alloc(numStreams, 0); + } let sctpStreamId; - for (let idx = 0; idx < this.#sctpStreamIds.length; ++idx) - { - sctpStreamId = (this.#nextSctpStreamId + idx) % this.#sctpStreamIds.length; + for (let idx = 0; idx < this.#sctpStreamIds.length; ++idx) { + sctpStreamId = + (this.#nextSctpStreamId + idx) % this.#sctpStreamIds.length; - if (!this.#sctpStreamIds[sctpStreamId]) - { + if (!this.#sctpStreamIds[sctpStreamId]) { this.#nextSctpStreamId = sctpStreamId + 1; return sctpStreamId; @@ -993,3 +1009,585 @@ export class Transport(binary, 'producerIds'); + // Retrieve consumerIds. + const consumerIds = fbsUtils.parseVector(binary, 'consumerIds'); + // Retrieve map SSRC consumerId. + const mapSsrcConsumerId = fbsUtils.parseUint32StringVector( + binary, + 'mapSsrcConsumerId' + ); + // Retrieve map RTX SSRC consumerId. + const mapRtxSsrcConsumerId = fbsUtils.parseUint32StringVector( + binary, + 'mapRtxSsrcConsumerId' + ); + // Retrieve dataProducerIds. + const dataProducerIds = fbsUtils.parseVector( + binary, + 'dataProducerIds' + ); + // Retrieve dataConsumerIds. + const dataConsumerIds = fbsUtils.parseVector( + binary, + 'dataConsumerIds' + ); + // Retrieve recvRtpHeaderExtesions. + const recvRtpHeaderExtensions = parseRecvRtpHeaderExtensions( + binary.recvRtpHeaderExtensions()! + ); + // Retrieve RtpListener. + const rtpListener = parseRtpListenerDump(binary.rtpListener()!); + + // Retrieve SctpParameters. + const fbsSctpParameters = binary.sctpParameters(); + let sctpParameters: SctpParameters | undefined; + + if (fbsSctpParameters) { + sctpParameters = parseSctpParametersDump(fbsSctpParameters); + } + + // Retrieve sctpState. + const sctpState = + binary.sctpState() === null + ? undefined + : parseSctpState(binary.sctpState()!); + + // Retrive sctpListener. + const sctpListener = binary.sctpListener() + ? parseSctpListenerDump(binary.sctpListener()!) + : undefined; + + // Retrieve traceEventTypes. + const traceEventTypes = fbsUtils.parseVector( + binary, + 'traceEventTypes', + transportTraceEventTypeFromFbs + ); + + return { + id: binary.id()!, + producerIds: producerIds, + consumerIds: consumerIds, + mapSsrcConsumerId: mapSsrcConsumerId, + mapRtxSsrcConsumerId: mapRtxSsrcConsumerId, + dataProducerIds: dataProducerIds, + dataConsumerIds: dataConsumerIds, + recvRtpHeaderExtensions: recvRtpHeaderExtensions, + rtpListener: rtpListener, + maxMessageSize: binary.maxMessageSize(), + sctpParameters: sctpParameters, + sctpState: sctpState, + sctpListener: sctpListener, + traceEventTypes: traceEventTypes, + }; +} + +export function parseBaseTransportStats( + binary: FbsTransport.Stats +): BaseTransportStats { + const sctpState = + binary.sctpState() === null + ? undefined + : parseSctpState(binary.sctpState()!); + + return { + transportId: binary.transportId()!, + timestamp: Number(binary.timestamp()), + sctpState, + bytesReceived: Number(binary.bytesReceived()), + recvBitrate: Number(binary.recvBitrate()), + bytesSent: Number(binary.bytesSent()), + sendBitrate: Number(binary.sendBitrate()), + rtpBytesReceived: Number(binary.rtpBytesReceived()), + rtpRecvBitrate: Number(binary.rtpRecvBitrate()), + rtpBytesSent: Number(binary.rtpBytesSent()), + rtpSendBitrate: Number(binary.rtpSendBitrate()), + rtxBytesReceived: Number(binary.rtxBytesReceived()), + rtxRecvBitrate: Number(binary.rtxRecvBitrate()), + rtxBytesSent: Number(binary.rtxBytesSent()), + rtxSendBitrate: Number(binary.rtxSendBitrate()), + probationBytesSent: Number(binary.probationBytesSent()), + probationSendBitrate: Number(binary.probationSendBitrate()), + availableOutgoingBitrate: + typeof binary.availableOutgoingBitrate() === 'number' + ? Number(binary.availableOutgoingBitrate()) + : undefined, + availableIncomingBitrate: + typeof binary.availableIncomingBitrate() === 'number' + ? Number(binary.availableIncomingBitrate()) + : undefined, + maxIncomingBitrate: + typeof binary.maxIncomingBitrate() === 'number' + ? Number(binary.maxIncomingBitrate()) + : undefined, + maxOutgoingBitrate: + typeof binary.maxOutgoingBitrate() === 'number' + ? Number(binary.maxOutgoingBitrate()) + : undefined, + minOutgoingBitrate: + typeof binary.minOutgoingBitrate() === 'number' + ? Number(binary.minOutgoingBitrate()) + : undefined, + rtpPacketLossReceived: + typeof binary.rtpPacketLossReceived() === 'number' + ? Number(binary.rtpPacketLossReceived()) + : undefined, + rtpPacketLossSent: + typeof binary.rtpPacketLossSent() === 'number' + ? Number(binary.rtpPacketLossSent()) + : undefined, + }; +} + +export function parseTransportTraceEventData( + trace: FbsTransport.TraceNotification +): TransportTraceEventData { + switch (trace.type()) { + case FbsTransport.TraceEventType.BWE: { + const info = new FbsTransport.BweTraceInfo(); + + trace.info(info); + + return { + type: 'bwe', + timestamp: Number(trace.timestamp()), + direction: + trace.direction() === FbsTraceDirection.DIRECTION_IN ? 'in' : 'out', + info: parseBweTraceInfo(info), + }; + } + + case FbsTransport.TraceEventType.PROBATION: { + return { + type: 'probation', + timestamp: Number(trace.timestamp()), + direction: + trace.direction() === FbsTraceDirection.DIRECTION_IN ? 'in' : 'out', + info: {}, + }; + } + } +} + +function parseRecvRtpHeaderExtensions( + binary: FbsTransport.RecvRtpHeaderExtensions +): RecvRtpHeaderExtensions { + return { + mid: binary.mid() !== null ? binary.mid()! : undefined, + rid: binary.rid() !== null ? binary.rid()! : undefined, + rrid: binary.rrid() !== null ? binary.rrid()! : undefined, + absSendTime: + binary.absSendTime() !== null ? binary.absSendTime()! : undefined, + transportWideCc01: + binary.transportWideCc01() !== null + ? binary.transportWideCc01()! + : undefined, + }; +} + +function transportTraceEventTypeToFbs( + eventType: TransportTraceEventType +): FbsTransport.TraceEventType { + switch (eventType) { + case 'probation': { + return FbsTransport.TraceEventType.PROBATION; + } + + case 'bwe': { + return FbsTransport.TraceEventType.BWE; + } + } +} + +function transportTraceEventTypeFromFbs( + eventType: FbsTransport.TraceEventType +): TransportTraceEventType { + switch (eventType) { + case FbsTransport.TraceEventType.PROBATION: { + return 'probation'; + } + + case FbsTransport.TraceEventType.BWE: { + return 'bwe'; + } + } +} + +function parseBweTraceInfo(binary: FbsTransport.BweTraceInfo): { + desiredBitrate: number; + effectiveDesiredBitrate: number; + minBitrate: number; + maxBitrate: number; + startBitrate: number; + maxPaddingBitrate: number; + availableBitrate: number; + bweType: 'transport-cc' | 'remb'; +} { + return { + desiredBitrate: binary.desiredBitrate(), + effectiveDesiredBitrate: binary.effectiveDesiredBitrate(), + minBitrate: binary.minBitrate(), + maxBitrate: binary.maxBitrate(), + startBitrate: binary.startBitrate(), + maxPaddingBitrate: binary.maxPaddingBitrate(), + availableBitrate: binary.availableBitrate(), + bweType: + binary.bweType() === FbsTransport.BweType.TRANSPORT_CC + ? 'transport-cc' + : 'remb', + }; +} + +function createConsumeRequest({ + builder, + producer, + consumerId, + rtpParameters, + paused, + preferredLayers, + ignoreDtx, + pipe, +}: { + builder: flatbuffers.Builder; + producer: Producer; + consumerId: string; + rtpParameters: RtpParameters; + paused: boolean; + preferredLayers?: ConsumerLayers; + ignoreDtx?: boolean; + pipe: boolean; +}): number { + const rtpParametersOffset = serializeRtpParameters(builder, rtpParameters); + const consumerIdOffset = builder.createString(consumerId); + const producerIdOffset = builder.createString(producer.id); + let consumableRtpEncodingsOffset: number | undefined; + let preferredLayersOffset: number | undefined; + + if (producer.consumableRtpParameters.encodings) { + consumableRtpEncodingsOffset = serializeRtpEncodingParameters( + builder, + producer.consumableRtpParameters.encodings + ); + } + + if (preferredLayers) { + FbsConsumer.ConsumerLayers.startConsumerLayers(builder); + FbsConsumer.ConsumerLayers.addSpatialLayer( + builder, + preferredLayers.spatialLayer + ); + + if (preferredLayers.temporalLayer !== undefined) { + FbsConsumer.ConsumerLayers.addTemporalLayer( + builder, + preferredLayers.temporalLayer + ); + } + + preferredLayersOffset = + FbsConsumer.ConsumerLayers.endConsumerLayers(builder); + } + + const ConsumeRequest = FbsTransport.ConsumeRequest; + + // Create Consume Request. + ConsumeRequest.startConsumeRequest(builder); + ConsumeRequest.addConsumerId(builder, consumerIdOffset); + ConsumeRequest.addProducerId(builder, producerIdOffset); + ConsumeRequest.addKind( + builder, + producer.kind === 'audio' ? FbsMediaKind.AUDIO : FbsMediaKind.VIDEO + ); + ConsumeRequest.addRtpParameters(builder, rtpParametersOffset); + ConsumeRequest.addType( + builder, + pipe ? FbsRtpParameters.Type.PIPE : producerTypeToFbs(producer.type) + ); + + if (consumableRtpEncodingsOffset) { + ConsumeRequest.addConsumableRtpEncodings( + builder, + consumableRtpEncodingsOffset + ); + } + + ConsumeRequest.addPaused(builder, paused); + + if (preferredLayersOffset) { + ConsumeRequest.addPreferredLayers(builder, preferredLayersOffset); + } + + ConsumeRequest.addIgnoreDtx(builder, Boolean(ignoreDtx)); + + return ConsumeRequest.endConsumeRequest(builder); +} + +function createProduceRequest({ + builder, + producerId, + kind, + rtpParameters, + rtpMapping, + keyFrameRequestDelay, + paused, +}: { + builder: flatbuffers.Builder; + producerId: string; + kind: MediaKind; + rtpParameters: RtpParameters; + rtpMapping: ortc.RtpCodecsEncodingsMapping; + keyFrameRequestDelay?: number; + paused: boolean; +}): number { + const producerIdOffset = builder.createString(producerId); + const rtpParametersOffset = serializeRtpParameters(builder, rtpParameters); + const rtpMappingOffset = ortc.serializeRtpMapping(builder, rtpMapping); + + FbsTransport.ProduceRequest.startProduceRequest(builder); + FbsTransport.ProduceRequest.addProducerId(builder, producerIdOffset); + FbsTransport.ProduceRequest.addKind( + builder, + kind === 'audio' ? FbsMediaKind.AUDIO : FbsMediaKind.VIDEO + ); + FbsTransport.ProduceRequest.addRtpParameters(builder, rtpParametersOffset); + FbsTransport.ProduceRequest.addRtpMapping(builder, rtpMappingOffset); + FbsTransport.ProduceRequest.addKeyFrameRequestDelay( + builder, + keyFrameRequestDelay ?? 0 + ); + FbsTransport.ProduceRequest.addPaused(builder, paused); + + return FbsTransport.ProduceRequest.endProduceRequest(builder); +} + +function createProduceDataRequest({ + builder, + dataProducerId, + type, + sctpStreamParameters, + label, + protocol, + paused, +}: { + builder: flatbuffers.Builder; + dataProducerId: string; + type: DataProducerType; + sctpStreamParameters?: SctpStreamParameters; + label: string; + protocol: string; + paused: boolean; +}): number { + const dataProducerIdOffset = builder.createString(dataProducerId); + const labelOffset = builder.createString(label); + const protocolOffset = builder.createString(protocol); + + let sctpStreamParametersOffset = 0; + + if (sctpStreamParameters) { + sctpStreamParametersOffset = serializeSctpStreamParameters( + builder, + sctpStreamParameters + ); + } + + FbsTransport.ProduceDataRequest.startProduceDataRequest(builder); + FbsTransport.ProduceDataRequest.addDataProducerId( + builder, + dataProducerIdOffset + ); + FbsTransport.ProduceDataRequest.addType(builder, dataProducerTypeToFbs(type)); + + if (sctpStreamParametersOffset) { + FbsTransport.ProduceDataRequest.addSctpStreamParameters( + builder, + sctpStreamParametersOffset + ); + } + + FbsTransport.ProduceDataRequest.addLabel(builder, labelOffset); + FbsTransport.ProduceDataRequest.addProtocol(builder, protocolOffset); + FbsTransport.ProduceDataRequest.addPaused(builder, paused); + + return FbsTransport.ProduceDataRequest.endProduceDataRequest(builder); +} + +function createConsumeDataRequest({ + builder, + dataConsumerId, + dataProducerId, + type, + sctpStreamParameters, + label, + protocol, + paused, + subchannels = [], +}: { + builder: flatbuffers.Builder; + dataConsumerId: string; + dataProducerId: string; + type: DataConsumerType; + sctpStreamParameters?: SctpStreamParameters; + label: string; + protocol: string; + paused: boolean; + subchannels?: number[]; +}): number { + const dataConsumerIdOffset = builder.createString(dataConsumerId); + const dataProducerIdOffset = builder.createString(dataProducerId); + const labelOffset = builder.createString(label); + const protocolOffset = builder.createString(protocol); + + let sctpStreamParametersOffset = 0; + + if (sctpStreamParameters) { + sctpStreamParametersOffset = serializeSctpStreamParameters( + builder, + sctpStreamParameters + ); + } + + const subchannelsOffset = + FbsTransport.ConsumeDataRequest.createSubchannelsVector( + builder, + subchannels + ); + + FbsTransport.ConsumeDataRequest.startConsumeDataRequest(builder); + FbsTransport.ConsumeDataRequest.addDataConsumerId( + builder, + dataConsumerIdOffset + ); + FbsTransport.ConsumeDataRequest.addDataProducerId( + builder, + dataProducerIdOffset + ); + FbsTransport.ConsumeDataRequest.addType(builder, dataConsumerTypeToFbs(type)); + + if (sctpStreamParametersOffset) { + FbsTransport.ConsumeDataRequest.addSctpStreamParameters( + builder, + sctpStreamParametersOffset + ); + } + + FbsTransport.ConsumeDataRequest.addLabel(builder, labelOffset); + FbsTransport.ConsumeDataRequest.addProtocol(builder, protocolOffset); + FbsTransport.ConsumeDataRequest.addPaused(builder, paused); + FbsTransport.ConsumeDataRequest.addSubchannels(builder, subchannelsOffset); + + return FbsTransport.ConsumeDataRequest.endConsumeDataRequest(builder); +} + +function parseRtpListenerDump( + binary: FbsTransport.RtpListener +): RtpListenerDump { + // Retrieve ssrcTable. + const ssrcTable = fbsUtils.parseUint32StringVector(binary, 'ssrcTable'); + // Retrieve midTable. + const midTable = fbsUtils.parseUint32StringVector(binary, 'midTable'); + // Retrieve ridTable. + const ridTable = fbsUtils.parseUint32StringVector(binary, 'ridTable'); + + return { + ssrcTable, + midTable, + ridTable, + }; +} + +function parseSctpListenerDump( + binary: FbsTransport.SctpListener +): SctpListenerDump { + // Retrieve streamIdTable. + const streamIdTable = fbsUtils.parseUint32StringVector( + binary, + 'streamIdTable' + ); + + return { streamIdTable }; +} diff --git a/node/src/TransportTypes.ts b/node/src/TransportTypes.ts new file mode 100644 index 0000000000..26aac8cbd5 --- /dev/null +++ b/node/src/TransportTypes.ts @@ -0,0 +1,399 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { Producer, ProducerOptions } from './ProducerTypes'; +import type { Consumer, ConsumerOptions } from './ConsumerTypes'; +import type { DataProducer, DataProducerOptions } from './DataProducerTypes'; +import type { DataConsumer, DataConsumerOptions } from './DataConsumerTypes'; +import type { SctpParameters } from './sctpParametersTypes'; +import type { AppData } from './types'; + +/** + * Transport type. + */ +export type TransportType = 'webrtc' | 'plain' | 'pipe' | 'direct'; + +export type TransportListenInfo = { + /** + * Network protocol. + */ + protocol: TransportProtocol; + + /** + * Listening IPv4 or IPv6. + */ + ip: string; + + /** + * @deprecated Use |announcedAddress| instead. + * + * Announced IPv4, IPv6 or hostname (useful when running mediasoup behind NAT + * with private IP). + */ + announcedIp?: string; + + /** + * Announced IPv4, IPv6 or hostname (useful when running mediasoup behind NAT + * with private IP). + */ + announcedAddress?: string; + + /** + * Listening port. + */ + port?: number; + + /** + * Listening port range. If given then |port| will be ignored. + */ + portRange?: TransportPortRange; + + /** + * Socket flags. + */ + flags?: TransportSocketFlags; + + /** + * Send buffer size (bytes). + */ + sendBufferSize?: number; + + /** + * Recv buffer size (bytes). + */ + recvBufferSize?: number; +}; + +/** + * Use TransportListenInfo instead. + * @deprecated + */ +export type TransportListenIp = { + /** + * Listening IPv4 or IPv6. + */ + ip: string; + + /** + * Announced IPv4, IPv6 or hostname (useful when running mediasoup behind NAT + * with private IP). + */ + announcedIp?: string; +}; + +/** + * Transport protocol. + */ +export type TransportProtocol = 'udp' | 'tcp'; + +/** + * Port range.. + */ +export type TransportPortRange = { + /** + * Lowest port in the range. + */ + min: number; + /** + * Highest port in the range. + */ + max: number; +}; + +/** + * UDP/TCP socket flags. + */ +export type TransportSocketFlags = { + /** + * Disable dual-stack support so only IPv6 is used (only if |ip| is IPv6). + */ + ipv6Only?: boolean; + /** + * Make different transports bind to the same IP and port (only for UDP). + * Useful for multicast scenarios with plain transport. Use with caution. + */ + udpReusePort?: boolean; +}; + +export type TransportTuple = { + // @deprecated Use localAddress instead. + localIp: string; + localAddress: string; + localPort: number; + remoteIp?: string; + remotePort?: number; + protocol: TransportProtocol; +}; + +export type SctpState = + | 'new' + | 'connecting' + | 'connected' + | 'failed' + | 'closed'; + +export type RtpListenerDump = { + ssrcTable: { key: number; value: string }[]; + midTable: { key: number; value: string }[]; + ridTable: { key: number; value: string }[]; +}; + +export type SctpListenerDump = { + streamIdTable: { key: number; value: string }[]; +}; + +export type RecvRtpHeaderExtensions = { + mid?: number; + rid?: number; + rrid?: number; + absSendTime?: number; + transportWideCc01?: number; +}; + +export type BaseTransportDump = { + id: string; + producerIds: string[]; + consumerIds: string[]; + mapSsrcConsumerId: { key: number; value: string }[]; + mapRtxSsrcConsumerId: { key: number; value: string }[]; + recvRtpHeaderExtensions: RecvRtpHeaderExtensions; + rtpListener: RtpListenerDump; + maxMessageSize: number; + dataProducerIds: string[]; + dataConsumerIds: string[]; + sctpParameters?: SctpParameters; + sctpState?: SctpState; + sctpListener?: SctpListenerDump; + traceEventTypes?: string[]; +}; + +export type BaseTransportStats = { + transportId: string; + timestamp: number; + sctpState?: SctpState; + bytesReceived: number; + recvBitrate: number; + bytesSent: number; + sendBitrate: number; + rtpBytesReceived: number; + rtpRecvBitrate: number; + rtpBytesSent: number; + rtpSendBitrate: number; + rtxBytesReceived: number; + rtxRecvBitrate: number; + rtxBytesSent: number; + rtxSendBitrate: number; + probationBytesSent: number; + probationSendBitrate: number; + availableOutgoingBitrate?: number; + availableIncomingBitrate?: number; + maxIncomingBitrate?: number; + maxOutgoingBitrate?: number; + minOutgoingBitrate?: number; + rtpPacketLossReceived?: number; + rtpPacketLossSent?: number; +}; + +/** + * Valid types for 'trace' event. + */ +export type TransportTraceEventType = 'probation' | 'bwe'; + +/** + * 'trace' event data. + */ +export type TransportTraceEventData = { + /** + * Trace type. + */ + type: TransportTraceEventType; + + /** + * Event timestamp. + */ + timestamp: number; + + /** + * Event direction. + */ + direction: 'in' | 'out'; + + /** + * Per type information. + */ + info: unknown; +}; + +export type TransportEvents = { + routerclose: []; + listenserverclose: []; + trace: [TransportTraceEventData]; + listenererror: [string, Error]; + // Private events. + '@close': []; + '@newproducer': [Producer]; + '@producerclose': [Producer]; + '@newdataproducer': [DataProducer]; + '@dataproducerclose': [DataProducer]; + '@listenserverclose': []; +}; + +export type TransportObserver = EnhancedEventEmitter; + +export type TransportObserverEvents = { + close: []; + newproducer: [Producer]; + newconsumer: [Consumer]; + newdataproducer: [DataProducer]; + newdataconsumer: [DataConsumer]; + trace: [TransportTraceEventData]; +}; + +export interface Transport< + TransportAppData extends AppData = AppData, + Events extends TransportEvents = TransportEvents, + Observer extends TransportObserver = TransportObserver, +> extends EnhancedEventEmitter { + /** + * Transport id. + */ + get id(): string; + + /** + * Whether the Transport is closed. + */ + get closed(): boolean; + + /** + * Transport type. + * + * @virtual + * @privateRemarks + * - It's marked as virtual getter since each Transport class overrides it. + */ + get type(): TransportType; + + /** + * App custom data. + */ + get appData(): TransportAppData; + + /** + * App custom data setter. + */ + set appData(appData: TransportAppData); + + /** + * Observer. + * + * @virtual + */ + get observer(): Observer; + + /** + * Close the Transport. + * + * @virtual + */ + close(): void; + + /** + * Router was closed. + * + * @private + * @virtual + */ + routerClosed(): void; + + /** + * Listen server was closed (this just happens in WebRtcTransports when their + * associated WebRtcServer is closed). + * + * @private + * @virtual + */ + listenServerClosed(): void; + + /** + * Dump Transport. + * + * @abstract + */ + dump(): Promise; + + /** + * Get Transport stats. + * + * @abstract + */ + getStats(): Promise; + + /** + * Provide the Transport remote parameters. + * + * @abstract + */ + connect(params: unknown): Promise; + + /** + * Set maximum incoming bitrate for receiving media. + * + * @virtual + * @privateRemarks + * - It's marked as virtual method because DirectTransport overrides it. + */ + setMaxIncomingBitrate(bitrate: number): Promise; + + /** + * Set maximum outgoing bitrate for sending media. + * + * @virtual + * @privateRemarks + * - It's marked as virtual method because DirectTransport overrides it. + */ + setMaxOutgoingBitrate(bitrate: number): Promise; + + /** + * Set minimum outgoing bitrate for sending media. + * + * @virtual + * @privateRemarks + * - It's marked as virtual method because DirectTransport overrides it. + */ + setMinOutgoingBitrate(bitrate: number): Promise; + + /** + * Create a Producer. + */ + produce( + options: ProducerOptions + ): Promise>; + + /** + * Create a Consumer. + * + * @virtual + * @privateRemarks + * - It's marked as virtual method because PipeTransport overrides it. + */ + consume( + options: ConsumerOptions + ): Promise>; + + /** + * Create a DataProducer. + */ + produceData( + options?: DataProducerOptions + ): Promise>; + + /** + * Create a DataConsumer. + */ + consumeData( + options: DataConsumerOptions + ): Promise>; + + /** + * Enable 'trace' event. + */ + enableTraceEvent(types?: TransportTraceEventType[]): Promise; +} diff --git a/node/src/WebRtcServer.ts b/node/src/WebRtcServer.ts index 2ba6951546..1a04379e2a 100644 --- a/node/src/WebRtcServer.ts +++ b/node/src/WebRtcServer.ts @@ -1,68 +1,32 @@ import { Logger } from './Logger'; -import { EnhancedEventEmitter } from './EnhancedEventEmitter'; -import { Channel } from './Channel'; -import { TransportProtocol } from './Transport'; -import { WebRtcTransport } from './WebRtcTransport'; - -export type WebRtcServerListenInfo = -{ - /** - * Network protocol. - */ - protocol: TransportProtocol; - - /** - * Listening IPv4 or IPv6. - */ - ip: string; - - /** - * Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with - * private IP). - */ - announcedIp?: string; - - /** - * Listening port. - */ - port?: number; -}; - -export type WebRtcServerOptions = -{ - /** - * Listen infos. - */ - listenInfos: WebRtcServerListenInfo[]; - - /** - * Custom application data. - */ - appData?: Record; -}; - -export type WebRtcServerEvents = -{ - workerclose: []; - // Private events. - '@close': []; -}; - -export type WebRtcServerObserverEvents = -{ - close: []; - webrtctransporthandled: [WebRtcTransport]; - webrtctransportunhandled: [WebRtcTransport]; -}; - -type WebRtcServerInternal = -{ +import { EnhancedEventEmitter } from './enhancedEvents'; +import type { Channel } from './Channel'; +import type { + WebRtcServer, + IpPort, + IceUserNameFragment, + TupleHash, + WebRtcServerDump, + WebRtcServerEvents, + WebRtcServerObserver, + WebRtcServerObserverEvents, +} from './WebRtcServerTypes'; +import type { WebRtcTransport } from './WebRtcTransportTypes'; +import type { AppData } from './types'; +import * as fbsUtils from './fbsUtils'; +import { Body as RequestBody, Method } from './fbs/request'; +import * as FbsWorker from './fbs/worker'; +import * as FbsWebRtcServer from './fbs/web-rtc-server'; + +type WebRtcServerInternal = { webRtcServerId: string; }; const logger = new Logger('WebRtcServer'); -export class WebRtcServer extends EnhancedEventEmitter +export class WebRtcServerImpl + extends EnhancedEventEmitter + implements WebRtcServer { // Internal data. readonly #internal: WebRtcServerInternal; @@ -74,108 +38,86 @@ export class WebRtcServer extends EnhancedEventEmitter #closed = false; // Custom app data. - readonly #appData: Record; + #appData: WebRtcServerAppData; // Transports map. readonly #webRtcTransports: Map = new Map(); // Observer instance. - readonly #observer = new EnhancedEventEmitter(); - - /** - * @private - */ - constructor( - { - internal, - channel, - appData - }: - { - internal: WebRtcServerInternal; - channel: Channel; - appData?: Record; - } - ) - { + readonly #observer: WebRtcServerObserver = + new EnhancedEventEmitter(); + + constructor({ + internal, + channel, + appData, + }: { + internal: WebRtcServerInternal; + channel: Channel; + appData?: WebRtcServerAppData; + }) { super(); logger.debug('constructor()'); this.#internal = internal; this.#channel = channel; - this.#appData = appData || {}; + this.#appData = appData ?? ({} as WebRtcServerAppData); + + this.handleListenerError(); } - /** - * WebRtcServer id. - */ - get id(): string - { + get id(): string { return this.#internal.webRtcServerId; } - /** - * Whether the WebRtcServer is closed. - */ - get closed(): boolean - { + get closed(): boolean { return this.#closed; } - /** - * App custom data. - */ - get appData(): Record - { + get appData(): WebRtcServerAppData { return this.#appData; } - /** - * Invalid setter. - */ - set appData(appData: Record) // eslint-disable-line no-unused-vars - { - throw new Error('cannot override appData object'); + set appData(appData: WebRtcServerAppData) { + this.#appData = appData; } - /** - * Observer. - */ - get observer(): EnhancedEventEmitter - { + get observer(): WebRtcServerObserver { return this.#observer; } /** - * @private * Just for testing purposes. */ - get webRtcTransportsForTesting(): Map - { + get webRtcTransportsForTesting(): Map { return this.#webRtcTransports; } - /** - * Close the WebRtcServer. - */ - close(): void - { - if (this.#closed) + close(): void { + if (this.#closed) { return; + } logger.debug('close()'); this.#closed = true; - const reqData = { webRtcServerId: this.#internal.webRtcServerId }; - - this.#channel.request('worker.closeWebRtcServer', undefined, reqData) + // Build the request. + const requestOffset = new FbsWorker.CloseWebRtcServerRequestT( + this.#internal.webRtcServerId + ).pack(this.#channel.bufferBuilder); + + this.#channel + .request( + Method.WORKER_WEBRTCSERVER_CLOSE, + RequestBody.Worker_CloseWebRtcServerRequest, + requestOffset + ) .catch(() => {}); // Close every WebRtcTransport. - for (const webRtcTransport of this.#webRtcTransports.values()) - { + for (const webRtcTransport of this.#webRtcTransports.values()) { webRtcTransport.listenServerClosed(); // Emit observer event. @@ -189,15 +131,10 @@ export class WebRtcServer extends EnhancedEventEmitter this.#observer.safeEmit('close'); } - /** - * Worker was closed. - * - * @private - */ - workerClosed(): void - { - if (this.#closed) + workerClosed(): void { + if (this.#closed) { return; + } logger.debug('workerClosed()'); @@ -213,32 +150,84 @@ export class WebRtcServer extends EnhancedEventEmitter this.#observer.safeEmit('close'); } - /** - * Dump WebRtcServer. - */ - async dump(): Promise - { + async dump(): Promise { logger.debug('dump()'); - return this.#channel.request('webRtcServer.dump', this.#internal.webRtcServerId); + const response = await this.#channel.request( + Method.WEBRTCSERVER_DUMP, + undefined, + undefined, + this.#internal.webRtcServerId + ); + + /* Decode Response. */ + const dump = new FbsWebRtcServer.DumpResponse(); + + response.body(dump); + + return parseWebRtcServerDump(dump); } - /** - * @private - */ - handleWebRtcTransport(webRtcTransport: WebRtcTransport): void - { + handleWebRtcTransport(webRtcTransport: WebRtcTransport): void { this.#webRtcTransports.set(webRtcTransport.id, webRtcTransport); // Emit observer event. this.#observer.safeEmit('webrtctransporthandled', webRtcTransport); - webRtcTransport.on('@close', () => - { + webRtcTransport.on('@close', () => { this.#webRtcTransports.delete(webRtcTransport.id); // Emit observer event. this.#observer.safeEmit('webrtctransportunhandled', webRtcTransport); }); } + + private handleListenerError(): void { + this.on('listenererror', (eventName, error) => { + logger.error( + `event listener threw an error [eventName:${eventName}]:`, + error + ); + }); + } +} + +function parseIpPort(binary: FbsWebRtcServer.IpPort): IpPort { + return { + ip: binary.ip()!, + port: binary.port(), + }; +} + +function parseIceUserNameFragment( + binary: FbsWebRtcServer.IceUserNameFragment +): IceUserNameFragment { + return { + localIceUsernameFragment: binary.localIceUsernameFragment()!, + webRtcTransportId: binary.webRtcTransportId()!, + }; +} + +function parseTupleHash(binary: FbsWebRtcServer.TupleHash): TupleHash { + return { + tupleHash: Number(binary.tupleHash()), + webRtcTransportId: binary.webRtcTransportId()!, + }; +} + +function parseWebRtcServerDump( + data: FbsWebRtcServer.DumpResponse +): WebRtcServerDump { + return { + id: data.id()!, + udpSockets: fbsUtils.parseVector(data, 'udpSockets', parseIpPort), + tcpServers: fbsUtils.parseVector(data, 'tcpServers', parseIpPort), + webRtcTransportIds: fbsUtils.parseVector(data, 'webRtcTransportIds'), + localIceUsernameFragments: fbsUtils.parseVector( + data, + 'localIceUsernameFragments', + parseIceUserNameFragment + ), + tupleHashes: fbsUtils.parseVector(data, 'tupleHashes', parseTupleHash), + }; } diff --git a/node/src/WebRtcServerTypes.ts b/node/src/WebRtcServerTypes.ts new file mode 100644 index 0000000000..936e4af7cd --- /dev/null +++ b/node/src/WebRtcServerTypes.ts @@ -0,0 +1,112 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { TransportListenInfo } from './TransportTypes'; +import type { WebRtcTransport } from './WebRtcTransportTypes'; +import type { AppData } from './types'; + +export type WebRtcServerOptions = + { + /** + * Listen infos. + */ + listenInfos: TransportListenInfo[]; + + /** + * Custom application data. + */ + appData?: WebRtcServerAppData; + }; + +/** + * @deprecated Use TransportListenInfo instead. + */ +export type WebRtcServerListenInfo = TransportListenInfo; + +export type IpPort = { + ip: string; + port: number; +}; + +export type IceUserNameFragment = { + localIceUsernameFragment: string; + webRtcTransportId: string; +}; + +export type TupleHash = { + tupleHash: number; + webRtcTransportId: string; +}; + +export type WebRtcServerDump = { + id: string; + udpSockets: IpPort[]; + tcpServers: IpPort[]; + webRtcTransportIds: string[]; + localIceUsernameFragments: IceUserNameFragment[]; + tupleHashes: TupleHash[]; +}; + +export type WebRtcServerEvents = { + workerclose: []; + listenererror: [string, Error]; + // Private events. + '@close': []; +}; + +export type WebRtcServerObserver = + EnhancedEventEmitter; + +export type WebRtcServerObserverEvents = { + close: []; + webrtctransporthandled: [WebRtcTransport]; + webrtctransportunhandled: [WebRtcTransport]; +}; + +export interface WebRtcServer + extends EnhancedEventEmitter { + /** + * WebRtcServer id. + */ + get id(): string; + + /** + * Whether the WebRtcServer is closed. + */ + get closed(): boolean; + + /** + * App custom data. + */ + get appData(): WebRtcServerAppData; + + /** + * App custom data setter. + */ + set appData(appData: WebRtcServerAppData); + + /** + * Observer. + */ + get observer(): WebRtcServerObserver; + + /** + * Close the WebRtcServer. + */ + close(): void; + + /** + * Worker was closed. + * + * @private + */ + workerClosed(): void; + + /** + * Dump WebRtcServer. + */ + dump(): Promise; + + /** + * @private + */ + handleWebRtcTransport(webRtcTransport: WebRtcTransport): void; +} diff --git a/node/src/WebRtcTransport.ts b/node/src/WebRtcTransport.ts index e8733374bc..522882cd65 100644 --- a/node/src/WebRtcTransport.ts +++ b/node/src/WebRtcTransport.ts @@ -1,198 +1,57 @@ +import * as flatbuffers from 'flatbuffers'; import { Logger } from './Logger'; +import { EnhancedEventEmitter } from './enhancedEvents'; +import type { + WebRtcTransport, + IceParameters, + IceCandidate, + DtlsParameters, + FingerprintAlgorithm, + DtlsFingerprint, + IceRole, + IceState, + IceCandidateType, + IceCandidateTcpType, + DtlsRole, + DtlsState, + WebRtcTransportDump, + WebRtcTransportStat, + WebRtcTransportEvents, + WebRtcTransportObserver, + WebRtcTransportObserverEvents, +} from './WebRtcTransportTypes'; +import type { Transport, TransportTuple, SctpState } from './TransportTypes'; import { - Transport, - TransportListenIp, - TransportProtocol, - TransportTuple, - TransportTraceEventData, - TransportEvents, - TransportObserverEvents, + TransportImpl, TransportConstructorOptions, - SctpState + parseSctpState, + parseBaseTransportDump, + parseBaseTransportStats, + parseProtocol, + parseTransportTraceEventData, + parseTuple, } from './Transport'; -import { WebRtcServer } from './WebRtcServer'; -import { SctpParameters, NumSctpStreams } from './SctpParameters'; -import { Either } from './utils'; - -export type WebRtcTransportListenIndividual = -{ - /** - * Listening IP address or addresses in order of preference (first one is the - * preferred one). Mandatory unless webRtcServer is given. - */ - listenIps: (TransportListenIp | string)[]; - - /** - * Fixed port to listen on instead of selecting automatically from Worker's port - * range. - */ - port?: number; -}; - -export type WebRtcTransportListenServer = -{ - /** - * Instance of WebRtcServer. Mandatory unless listenIps is given. - */ - webRtcServer: WebRtcServer; -}; - -export type WebRtcTransportListen = - Either; - -export type WebRtcTransportOptionsBase = -{ - /** - * Listen in UDP. Default true. - */ - enableUdp?: boolean; - - /** - * Listen in TCP. Default false. - */ - enableTcp?: boolean; - - /** - * Prefer UDP. Default false. - */ - preferUdp?: boolean; - - /** - * Prefer TCP. Default false. - */ - preferTcp?: boolean; - - /** - * Initial available outgoing bitrate (in bps). Default 600000. - */ - initialAvailableOutgoingBitrate?: number; - - /** - * Create a SCTP association. Default false. - */ - enableSctp?: boolean; - - /** - * SCTP streams number. - */ - numSctpStreams?: NumSctpStreams; - - /** - * Maximum allowed size for SCTP messages sent by DataProducers. - * Default 262144. - */ - maxSctpMessageSize?: number; - - /** - * Maximum SCTP send buffer used by DataConsumers. - * Default 262144. - */ - sctpSendBufferSize?: number; - - /** - * Custom application data. - */ - appData?: Record; -}; - -export type WebRtcTransportOptions = WebRtcTransportOptionsBase & WebRtcTransportListen; - -export type IceParameters = -{ - usernameFragment: string; - password: string; - iceLite?: boolean; -}; - -export type IceCandidate = -{ - foundation: string; - priority: number; - ip: string; - protocol: TransportProtocol; - port: number; - type: 'host'; - tcpType: 'passive' | undefined; -}; - -export type DtlsParameters = -{ - role?: DtlsRole; - fingerprints: DtlsFingerprint[]; -}; - -/** - * The hash function algorithm (as defined in the "Hash function Textual Names" - * registry initially specified in RFC 4572 Section 8) and its corresponding - * certificate fingerprint value (in lowercase hex string as expressed utilizing - * the syntax of "fingerprint" in RFC 4572 Section 5). - */ -export type DtlsFingerprint = -{ - algorithm: string; - value: string; -}; - -export type IceState = 'new' | 'connected' | 'completed' | 'disconnected' | 'closed'; - -export type DtlsRole = 'auto' | 'client' | 'server'; - -export type DtlsState = 'new' | 'connecting' | 'connected' | 'failed' | 'closed'; - -export type WebRtcTransportStat = -{ - // Common to all Transports. - type: string; - transportId: string; - timestamp: number; - sctpState?: SctpState; - bytesReceived: number; - recvBitrate: number; - bytesSent: number; - sendBitrate: number; - rtpBytesReceived: number; - rtpRecvBitrate: number; - rtpBytesSent: number; - rtpSendBitrate: number; - rtxBytesReceived: number; - rtxRecvBitrate: number; - rtxBytesSent: number; - rtxSendBitrate: number; - probationBytesSent: number; - probationSendBitrate: number; - availableOutgoingBitrate?: number; - availableIncomingBitrate?: number; - maxIncomingBitrate?: number; - // WebRtcTransport specific. - iceRole: string; - iceState: IceState; - iceSelectedTuple?: TransportTuple; - dtlsState: DtlsState; -}; - -export type WebRtcTransportEvents = TransportEvents & -{ - icestatechange: [IceState]; - iceselectedtuplechange: [TransportTuple]; - dtlsstatechange: [DtlsState]; - sctpstatechange: [SctpState]; -}; - -export type WebRtcTransportObserverEvents = TransportObserverEvents & -{ - icestatechange: [IceState]; - iceselectedtuplechange: [TransportTuple]; - dtlsstatechange: [DtlsState]; - sctpstatechange: [SctpState]; -}; - -type WebRtcTransportConstructorOptions = TransportConstructorOptions & -{ - data: WebRtcTransportData; -}; - -export type WebRtcTransportData = -{ +import type { SctpParameters } from './sctpParametersTypes'; +import type { AppData } from './types'; +import * as fbsUtils from './fbsUtils'; +import { Event, Notification } from './fbs/notification'; +import * as FbsRequest from './fbs/request'; +import * as FbsTransport from './fbs/transport'; +import * as FbsWebRtcTransport from './fbs/web-rtc-transport'; +import { DtlsState as FbsDtlsState } from './fbs/web-rtc-transport/dtls-state'; +import { DtlsRole as FbsDtlsRole } from './fbs/web-rtc-transport/dtls-role'; +import { FingerprintAlgorithm as FbsFingerprintAlgorithm } from './fbs/web-rtc-transport/fingerprint-algorithm'; +import { IceState as FbsIceState } from './fbs/web-rtc-transport/ice-state'; +import { IceRole as FbsIceRole } from './fbs/web-rtc-transport/ice-role'; +import { IceCandidateType as FbsIceCandidateType } from './fbs/web-rtc-transport/ice-candidate-type'; +import { IceCandidateTcpType as FbsIceCandidateTcpType } from './fbs/web-rtc-transport/ice-candidate-tcp-type'; + +type WebRtcTransportConstructorOptions = + TransportConstructorOptions & { + data: WebRtcTransportData; + }; + +export type WebRtcTransportData = { iceRole: 'controlled'; iceParameters: IceParameters; iceCandidates: IceCandidate[]; @@ -207,311 +66,657 @@ export type WebRtcTransportData = const logger = new Logger('WebRtcTransport'); -export class WebRtcTransport extends - Transport +export class WebRtcTransportImpl< + WebRtcTransportAppData extends AppData = AppData, + > + extends TransportImpl< + WebRtcTransportAppData, + WebRtcTransportEvents, + WebRtcTransportObserver + > + implements Transport, WebRtcTransport { // WebRtcTransport data. readonly #data: WebRtcTransportData; - /** - * @private - */ - constructor(options: WebRtcTransportConstructorOptions) - { - super(options); + constructor( + options: WebRtcTransportConstructorOptions + ) { + const observer: WebRtcTransportObserver = + new EnhancedEventEmitter(); + + super(options, observer); logger.debug('constructor()'); const { data } = options; - this.#data = - { - iceRole : data.iceRole, - iceParameters : data.iceParameters, - iceCandidates : data.iceCandidates, - iceState : data.iceState, - iceSelectedTuple : data.iceSelectedTuple, - dtlsParameters : data.dtlsParameters, - dtlsState : data.dtlsState, - dtlsRemoteCert : data.dtlsRemoteCert, - sctpParameters : data.sctpParameters, - sctpState : data.sctpState + this.#data = { + iceRole: data.iceRole, + iceParameters: data.iceParameters, + iceCandidates: data.iceCandidates, + iceState: data.iceState, + iceSelectedTuple: data.iceSelectedTuple, + dtlsParameters: data.dtlsParameters, + dtlsState: data.dtlsState, + dtlsRemoteCert: data.dtlsRemoteCert, + sctpParameters: data.sctpParameters, + sctpState: data.sctpState, }; this.handleWorkerNotifications(); + this.handleListenerError(); } - /** - * ICE role. - */ - get iceRole(): 'controlled' - { + get type(): 'webrtc' { + return 'webrtc'; + } + + get observer(): WebRtcTransportObserver { + return super.observer; + } + + get iceRole(): 'controlled' { return this.#data.iceRole; } - /** - * ICE parameters. - */ - get iceParameters(): IceParameters - { + get iceParameters(): IceParameters { return this.#data.iceParameters; } - /** - * ICE candidates. - */ - get iceCandidates(): IceCandidate[] - { + get iceCandidates(): IceCandidate[] { return this.#data.iceCandidates; } - /** - * ICE state. - */ - get iceState(): IceState - { + get iceState(): IceState { return this.#data.iceState; } - /** - * ICE selected tuple. - */ - get iceSelectedTuple(): TransportTuple | undefined - { + get iceSelectedTuple(): TransportTuple | undefined { return this.#data.iceSelectedTuple; } - /** - * DTLS parameters. - */ - get dtlsParameters(): DtlsParameters - { + get dtlsParameters(): DtlsParameters { return this.#data.dtlsParameters; } - /** - * DTLS state. - */ - get dtlsState(): DtlsState - { + get dtlsState(): DtlsState { return this.#data.dtlsState; } - /** - * Remote certificate in PEM format. - */ - get dtlsRemoteCert(): string | undefined - { + get dtlsRemoteCert(): string | undefined { return this.#data.dtlsRemoteCert; } - /** - * SCTP parameters. - */ - get sctpParameters(): SctpParameters | undefined - { + get sctpParameters(): SctpParameters | undefined { return this.#data.sctpParameters; } - /** - * SCTP state. - */ - get sctpState(): SctpState | undefined - { + get sctpState(): SctpState | undefined { return this.#data.sctpState; } - /** - * Close the WebRtcTransport. - * - * @override - */ - close(): void - { - if (this.closed) + close(): void { + if (this.closed) { return; + } this.#data.iceState = 'closed'; this.#data.iceSelectedTuple = undefined; this.#data.dtlsState = 'closed'; - if (this.#data.sctpState) + if (this.#data.sctpState) { this.#data.sctpState = 'closed'; + } super.close(); } - /** - * Router was closed. - * - * @private - * @override - */ - routerClosed(): void - { - if (this.closed) + routerClosed(): void { + if (this.closed) { return; + } this.#data.iceState = 'closed'; this.#data.iceSelectedTuple = undefined; this.#data.dtlsState = 'closed'; - if (this.#data.sctpState) + if (this.#data.sctpState) { this.#data.sctpState = 'closed'; + } super.routerClosed(); } - /** - * Called when closing the associated WebRtcServer. - * - * @private - */ - webRtcServerClosed(): void - { - if (this.closed) + listenServerClosed(): void { + if (this.closed) { return; + } this.#data.iceState = 'closed'; this.#data.iceSelectedTuple = undefined; this.#data.dtlsState = 'closed'; - if (this.#data.sctpState) + if (this.#data.sctpState) { this.#data.sctpState = 'closed'; + } super.listenServerClosed(); } - /** - * Get WebRtcTransport stats. - * - * @override - */ - async getStats(): Promise - { + async dump(): Promise { + logger.debug('dump()'); + + const response = await this.channel.request( + FbsRequest.Method.TRANSPORT_DUMP, + undefined, + undefined, + this.internal.transportId + ); + + /* Decode Response. */ + const data = new FbsWebRtcTransport.DumpResponse(); + + response.body(data); + + return parseWebRtcTransportDumpResponse(data); + } + + async getStats(): Promise { logger.debug('getStats()'); - return this.channel.request('transport.getStats', this.internal.transportId); + const response = await this.channel.request( + FbsRequest.Method.TRANSPORT_GET_STATS, + undefined, + undefined, + this.internal.transportId + ); + + /* Decode Response. */ + const data = new FbsWebRtcTransport.GetStatsResponse(); + + response.body(data); + + return [parseGetStatsResponse(data)]; } - /** - * Provide the WebRtcTransport remote parameters. - * - * @override - */ - async connect({ dtlsParameters }: { dtlsParameters: DtlsParameters }): Promise - { + async connect({ + dtlsParameters, + }: { + dtlsParameters: DtlsParameters; + }): Promise { logger.debug('connect()'); - const reqData = { dtlsParameters }; + const requestOffset = createConnectRequest({ + builder: this.channel.bufferBuilder, + dtlsParameters, + }); - const data = - await this.channel.request('transport.connect', this.internal.transportId, reqData); + // Wait for response. + const response = await this.channel.request( + FbsRequest.Method.WEBRTCTRANSPORT_CONNECT, + FbsRequest.Body.WebRtcTransport_ConnectRequest, + requestOffset, + this.internal.transportId + ); + + /* Decode Response. */ + const data = new FbsWebRtcTransport.ConnectResponse(); + + response.body(data); // Update data. - this.#data.dtlsParameters.role = data.dtlsLocalRole; + this.#data.dtlsParameters.role = dtlsRoleFromFbs(data.dtlsLocalRole()); } - /** - * Restart ICE. - */ - async restartIce(): Promise - { + async restartIce(): Promise { logger.debug('restartIce()'); - const data = - await this.channel.request('transport.restartIce', this.internal.transportId); + const response = await this.channel.request( + FbsRequest.Method.TRANSPORT_RESTART_ICE, + undefined, + undefined, + this.internal.transportId + ); + + /* Decode Response. */ + const restartIceResponse = new FbsTransport.RestartIceResponse(); + + response.body(restartIceResponse); - const { iceParameters } = data; + const iceParameters = { + usernameFragment: restartIceResponse.usernameFragment()!, + password: restartIceResponse.password()!, + iceLite: restartIceResponse.iceLite(), + }; this.#data.iceParameters = iceParameters; return iceParameters; } - private handleWorkerNotifications(): void - { - this.channel.on(this.internal.transportId, (event: string, data?: any) => - { - switch (event) - { - case 'icestatechange': - { - const iceState = data.iceState as IceState; + private handleWorkerNotifications(): void { + this.channel.on( + this.internal.transportId, + (event: Event, data?: Notification) => { + switch (event) { + case Event.WEBRTCTRANSPORT_ICE_STATE_CHANGE: { + const notification = + new FbsWebRtcTransport.IceStateChangeNotification(); - this.#data.iceState = iceState; + data!.body(notification); - this.safeEmit('icestatechange', iceState); + const iceState = iceStateFromFbs(notification.iceState()); - // Emit observer event. - this.observer.safeEmit('icestatechange', iceState); + this.#data.iceState = iceState; - break; - } + this.safeEmit('icestatechange', iceState); - case 'iceselectedtuplechange': - { - const iceSelectedTuple = data.iceSelectedTuple as TransportTuple; + // Emit observer event. + this.observer.safeEmit('icestatechange', iceState); - this.#data.iceSelectedTuple = iceSelectedTuple; + break; + } - this.safeEmit('iceselectedtuplechange', iceSelectedTuple); + case Event.WEBRTCTRANSPORT_ICE_SELECTED_TUPLE_CHANGE: { + const notification = + new FbsWebRtcTransport.IceSelectedTupleChangeNotification(); - // Emit observer event. - this.observer.safeEmit('iceselectedtuplechange', iceSelectedTuple); + data!.body(notification); - break; - } + const iceSelectedTuple = parseTuple(notification.tuple()!); - case 'dtlsstatechange': - { - const dtlsState = data.dtlsState as DtlsState; - const dtlsRemoteCert = data.dtlsRemoteCert as string; + this.#data.iceSelectedTuple = iceSelectedTuple; - this.#data.dtlsState = dtlsState; + this.safeEmit('iceselectedtuplechange', iceSelectedTuple); - if (dtlsState === 'connected') - this.#data.dtlsRemoteCert = dtlsRemoteCert; + // Emit observer event. + this.observer.safeEmit('iceselectedtuplechange', iceSelectedTuple); - this.safeEmit('dtlsstatechange', dtlsState); + break; + } - // Emit observer event. - this.observer.safeEmit('dtlsstatechange', dtlsState); + case Event.WEBRTCTRANSPORT_DTLS_STATE_CHANGE: { + const notification = + new FbsWebRtcTransport.DtlsStateChangeNotification(); - break; - } + data!.body(notification); - case 'sctpstatechange': - { - const sctpState = data.sctpState as SctpState; + const dtlsState = dtlsStateFromFbs(notification.dtlsState()); - this.#data.sctpState = sctpState; + this.#data.dtlsState = dtlsState; - this.safeEmit('sctpstatechange', sctpState); + if (dtlsState === 'connected') { + this.#data.dtlsRemoteCert = notification.remoteCert()!; + } - // Emit observer event. - this.observer.safeEmit('sctpstatechange', sctpState); + this.safeEmit('dtlsstatechange', dtlsState); - break; - } + // Emit observer event. + this.observer.safeEmit('dtlsstatechange', dtlsState); - case 'trace': - { - const trace = data as TransportTraceEventData; + break; + } - this.safeEmit('trace', trace); + case Event.TRANSPORT_SCTP_STATE_CHANGE: { + const notification = new FbsTransport.SctpStateChangeNotification(); - // Emit observer event. - this.observer.safeEmit('trace', trace); + data!.body(notification); - break; - } + const sctpState = parseSctpState(notification.sctpState()); + + this.#data.sctpState = sctpState; + + this.safeEmit('sctpstatechange', sctpState); + + // Emit observer event. + this.observer.safeEmit('sctpstatechange', sctpState); - default: - { - logger.error('ignoring unknown event "%s"', event); + break; + } + + case Event.TRANSPORT_TRACE: { + const notification = new FbsTransport.TraceNotification(); + + data!.body(notification); + + const trace = parseTransportTraceEventData(notification); + + this.safeEmit('trace', trace); + + // Emit observer event. + this.observer.safeEmit('trace', trace); + + break; + } + + default: { + logger.error(`ignoring unknown event "${event}"`); + } } } + ); + } + + private handleListenerError(): void { + this.on('listenererror', (eventName, error) => { + logger.error( + `event listener threw an error [eventName:${eventName}]:`, + error + ); }); } } + +function iceStateFromFbs(fbsIceState: FbsIceState): IceState { + switch (fbsIceState) { + case FbsIceState.NEW: { + return 'new'; + } + + case FbsIceState.CONNECTED: { + return 'connected'; + } + + case FbsIceState.COMPLETED: { + return 'completed'; + } + + case FbsIceState.DISCONNECTED: { + return 'disconnected'; + } + } +} + +function iceRoleFromFbs(role: FbsIceRole): IceRole { + switch (role) { + case FbsIceRole.CONTROLLED: { + return 'controlled'; + } + + case FbsIceRole.CONTROLLING: { + return 'controlling'; + } + } +} + +function iceCandidateTypeFromFbs(type: FbsIceCandidateType): IceCandidateType { + switch (type) { + case FbsIceCandidateType.HOST: { + return 'host'; + } + } +} + +function iceCandidateTcpTypeFromFbs( + type: FbsIceCandidateTcpType +): IceCandidateTcpType { + switch (type) { + case FbsIceCandidateTcpType.PASSIVE: { + return 'passive'; + } + } +} + +function dtlsStateFromFbs(fbsDtlsState: FbsDtlsState): DtlsState { + switch (fbsDtlsState) { + case FbsDtlsState.NEW: { + return 'new'; + } + + case FbsDtlsState.CONNECTING: { + return 'connecting'; + } + + case FbsDtlsState.CONNECTED: { + return 'connected'; + } + + case FbsDtlsState.FAILED: { + return 'failed'; + } + + case FbsDtlsState.CLOSED: { + return 'closed'; + } + } +} + +function dtlsRoleFromFbs(role: FbsDtlsRole): DtlsRole { + switch (role) { + case FbsDtlsRole.AUTO: { + return 'auto'; + } + + case FbsDtlsRole.CLIENT: { + return 'client'; + } + + case FbsDtlsRole.SERVER: { + return 'server'; + } + } +} + +function fingerprintAlgorithmsFromFbs( + algorithm: FbsFingerprintAlgorithm +): FingerprintAlgorithm { + switch (algorithm) { + case FbsFingerprintAlgorithm.SHA1: { + return 'sha-1'; + } + + case FbsFingerprintAlgorithm.SHA224: { + return 'sha-224'; + } + + case FbsFingerprintAlgorithm.SHA256: { + return 'sha-256'; + } + + case FbsFingerprintAlgorithm.SHA384: { + return 'sha-384'; + } + + case FbsFingerprintAlgorithm.SHA512: { + return 'sha-512'; + } + } +} + +function fingerprintAlgorithmToFbs( + algorithm: FingerprintAlgorithm +): FbsFingerprintAlgorithm { + switch (algorithm) { + case 'sha-1': { + return FbsFingerprintAlgorithm.SHA1; + } + + case 'sha-224': { + return FbsFingerprintAlgorithm.SHA224; + } + + case 'sha-256': { + return FbsFingerprintAlgorithm.SHA256; + } + + case 'sha-384': { + return FbsFingerprintAlgorithm.SHA384; + } + + case 'sha-512': { + return FbsFingerprintAlgorithm.SHA512; + } + + default: { + throw new TypeError(`invalid FingerprintAlgorithm: ${algorithm}`); + } + } +} + +function dtlsRoleToFbs(role: DtlsRole): FbsDtlsRole { + switch (role) { + case 'auto': { + return FbsDtlsRole.AUTO; + } + + case 'client': { + return FbsDtlsRole.CLIENT; + } + + case 'server': { + return FbsDtlsRole.SERVER; + } + + default: { + throw new TypeError(`invalid DtlsRole: ${role}`); + } + } +} + +export function parseWebRtcTransportDumpResponse( + binary: FbsWebRtcTransport.DumpResponse +): WebRtcTransportDump { + // Retrieve BaseTransportDump. + const baseTransportDump = parseBaseTransportDump(binary.base()!); + // Retrieve ICE candidates. + const iceCandidates = fbsUtils.parseVector( + binary, + 'iceCandidates', + parseIceCandidate + ); + // Retrieve ICE parameters. + const iceParameters = parseIceParameters(binary.iceParameters()!); + // Retrieve DTLS parameters. + const dtlsParameters = parseDtlsParameters(binary.dtlsParameters()!); + + return { + ...baseTransportDump, + sctpParameters: baseTransportDump.sctpParameters, + sctpState: baseTransportDump.sctpState, + iceRole: 'controlled', + iceParameters: iceParameters, + iceCandidates: iceCandidates, + iceState: iceStateFromFbs(binary.iceState()), + dtlsParameters: dtlsParameters, + dtlsState: dtlsStateFromFbs(binary.dtlsState()), + }; +} + +function createConnectRequest({ + builder, + dtlsParameters, +}: { + builder: flatbuffers.Builder; + dtlsParameters: DtlsParameters; +}): number { + // Serialize DtlsParameters. This can throw. + const dtlsParametersOffset = serializeDtlsParameters(builder, dtlsParameters); + + return FbsWebRtcTransport.ConnectRequest.createConnectRequest( + builder, + dtlsParametersOffset + ); +} + +function parseGetStatsResponse( + binary: FbsWebRtcTransport.GetStatsResponse +): WebRtcTransportStat { + const base = parseBaseTransportStats(binary.base()!); + + return { + ...base, + type: 'webrtc-transport', + iceRole: iceRoleFromFbs(binary.iceRole()), + iceState: iceStateFromFbs(binary.iceState()), + iceSelectedTuple: binary.iceSelectedTuple() + ? parseTuple(binary.iceSelectedTuple()!) + : undefined, + dtlsState: dtlsStateFromFbs(binary.dtlsState()), + }; +} + +function parseIceCandidate( + binary: FbsWebRtcTransport.IceCandidate +): IceCandidate { + return { + foundation: binary.foundation()!, + priority: binary.priority(), + ip: binary.address()!, + address: binary.address()!, + protocol: parseProtocol(binary.protocol()), + port: binary.port(), + type: iceCandidateTypeFromFbs(binary.type()), + tcpType: + binary.tcpType() === null + ? undefined + : iceCandidateTcpTypeFromFbs(binary.tcpType()!), + }; +} + +function parseIceParameters( + binary: FbsWebRtcTransport.IceParameters +): IceParameters { + return { + usernameFragment: binary.usernameFragment()!, + password: binary.password()!, + iceLite: binary.iceLite(), + }; +} + +function parseDtlsParameters( + binary: FbsWebRtcTransport.DtlsParameters +): DtlsParameters { + const fingerprints: DtlsFingerprint[] = []; + + for (let i = 0; i < binary.fingerprintsLength(); ++i) { + const fbsFingerprint = binary.fingerprints(i)!; + const fingerPrint: DtlsFingerprint = { + algorithm: fingerprintAlgorithmsFromFbs(fbsFingerprint.algorithm()), + value: fbsFingerprint.value()!, + }; + + fingerprints.push(fingerPrint); + } + + return { + fingerprints: fingerprints, + role: binary.role() === null ? undefined : dtlsRoleFromFbs(binary.role()), + }; +} + +function serializeDtlsParameters( + builder: flatbuffers.Builder, + dtlsParameters: DtlsParameters +): number { + const fingerprints: number[] = []; + + for (const fingerprint of dtlsParameters.fingerprints) { + const algorithm = fingerprintAlgorithmToFbs(fingerprint.algorithm); + const valueOffset = builder.createString(fingerprint.value); + const fingerprintOffset = FbsWebRtcTransport.Fingerprint.createFingerprint( + builder, + algorithm, + valueOffset + ); + + fingerprints.push(fingerprintOffset); + } + + const fingerprintsOffset = + FbsWebRtcTransport.DtlsParameters.createFingerprintsVector( + builder, + fingerprints + ); + + const role = + dtlsParameters.role !== undefined + ? dtlsRoleToFbs(dtlsParameters.role) + : FbsWebRtcTransport.DtlsRole.AUTO; + + return FbsWebRtcTransport.DtlsParameters.createDtlsParameters( + builder, + fingerprintsOffset, + role + ); +} diff --git a/node/src/WebRtcTransportTypes.ts b/node/src/WebRtcTransportTypes.ts new file mode 100644 index 0000000000..b466da87e1 --- /dev/null +++ b/node/src/WebRtcTransportTypes.ts @@ -0,0 +1,319 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { + Transport, + TransportListenInfo, + TransportListenIp, + TransportProtocol, + TransportTuple, + SctpState, + BaseTransportDump, + BaseTransportStats, + TransportEvents, + TransportObserverEvents, +} from './TransportTypes'; +import type { WebRtcServer } from './WebRtcServerTypes'; +import type { SctpParameters, NumSctpStreams } from './sctpParametersTypes'; +import type { Either, AppData } from './types'; + +export type WebRtcTransportOptions< + WebRtcTransportAppData extends AppData = AppData, +> = WebRtcTransportOptionsBase & WebRtcTransportListen; + +type WebRtcTransportOptionsBase = { + /** + * Listen in UDP. Default true. + */ + enableUdp?: boolean; + + /** + * Listen in TCP. Default true if webrtcServer is given, false otherwise. + */ + enableTcp?: boolean; + + /** + * Prefer UDP. Default false. + */ + preferUdp?: boolean; + + /** + * Prefer TCP. Default false. + */ + preferTcp?: boolean; + + /** + * ICE consent timeout (in seconds). If 0 it is disabled. Default 30. + */ + iceConsentTimeout?: number; + + /** + * Initial available outgoing bitrate (in bps). Default 600000. + */ + initialAvailableOutgoingBitrate?: number; + + /** + * Create a SCTP association. Default false. + */ + enableSctp?: boolean; + + /** + * SCTP streams number. + */ + numSctpStreams?: NumSctpStreams; + + /** + * Maximum allowed size for SCTP messages sent by DataProducers. + * Default 262144. + */ + maxSctpMessageSize?: number; + + /** + * Maximum SCTP send buffer used by DataConsumers. + * Default 262144. + */ + sctpSendBufferSize?: number; + + /** + * Custom application data. + */ + appData?: WebRtcTransportAppData; +}; + +type WebRtcTransportListen = Either< + Either< + WebRtcTransportListenIndividualListenInfo, + WebRtcTransportListenIndividualListenIp + >, + WebRtcTransportListenServer +>; + +type WebRtcTransportListenIndividualListenInfo = { + /** + * Listening info. + */ + listenInfos: TransportListenInfo[]; +}; + +type WebRtcTransportListenIndividualListenIp = { + /** + * Listening IP address or addresses in order of preference (first one is the + * preferred one). + */ + listenIps: (TransportListenIp | string)[]; + + /** + * Fixed port to listen on instead of selecting automatically from Worker's port + * range. + */ + port?: number; +}; + +type WebRtcTransportListenServer = { + /** + * Instance of WebRtcServer. + */ + webRtcServer: WebRtcServer; +}; + +export type IceParameters = { + usernameFragment: string; + password: string; + iceLite?: boolean; +}; + +export type IceCandidate = { + foundation: string; + priority: number; + // @deprecated Use |address| instead. + ip: string; + address: string; + protocol: TransportProtocol; + port: number; + type: IceCandidateType; + tcpType?: IceCandidateTcpType; +}; + +export type DtlsParameters = { + role?: DtlsRole; + fingerprints: DtlsFingerprint[]; +}; + +/** + * The hash function algorithm (as defined in the "Hash function Textual Names" + * registry initially specified in RFC 4572 Section 8). + */ +export type FingerprintAlgorithm = + | 'sha-1' + | 'sha-224' + | 'sha-256' + | 'sha-384' + | 'sha-512'; + +/** + * The hash function algorithm and its corresponding certificate fingerprint + * value (in lowercase hex string as expressed utilizing the syntax of + * "fingerprint" in RFC 4572 Section 5). + */ +export type DtlsFingerprint = { + algorithm: FingerprintAlgorithm; + value: string; +}; + +export type IceRole = 'controlled' | 'controlling'; + +export type IceState = + | 'new' + | 'connected' + | 'completed' + | 'disconnected' + | 'closed'; + +export type IceCandidateType = 'host'; + +export type IceCandidateTcpType = 'passive'; + +export type DtlsRole = 'auto' | 'client' | 'server'; + +export type DtlsState = + | 'new' + | 'connecting' + | 'connected' + | 'failed' + | 'closed'; + +export type WebRtcTransportDump = BaseTransportDump & { + iceRole: 'controlled'; + iceParameters: IceParameters; + iceCandidates: IceCandidate[]; + iceState: IceState; + iceSelectedTuple?: TransportTuple; + dtlsParameters: DtlsParameters; + dtlsState: DtlsState; + dtlsRemoteCert?: string; +}; + +export type WebRtcTransportStat = BaseTransportStats & { + type: string; + iceRole: string; + iceState: IceState; + iceSelectedTuple?: TransportTuple; + dtlsState: DtlsState; +}; + +export type WebRtcTransportEvents = TransportEvents & { + icestatechange: [IceState]; + iceselectedtuplechange: [TransportTuple]; + dtlsstatechange: [DtlsState]; + sctpstatechange: [SctpState]; +}; + +export type WebRtcTransportObserver = + EnhancedEventEmitter; + +export type WebRtcTransportObserverEvents = TransportObserverEvents & { + icestatechange: [IceState]; + iceselectedtuplechange: [TransportTuple]; + dtlsstatechange: [DtlsState]; + sctpstatechange: [SctpState]; +}; + +export interface WebRtcTransport< + WebRtcTransportAppData extends AppData = AppData, +> extends Transport< + WebRtcTransportAppData, + WebRtcTransportEvents, + WebRtcTransportObserver + > { + /** + * Transport type. + * + * @override + */ + get type(): 'webrtc'; + + /** + * Observer. + * + * @override + */ + get observer(): WebRtcTransportObserver; + + /** + * ICE role. + */ + get iceRole(): 'controlled'; + + /** + * ICE parameters. + */ + get iceParameters(): IceParameters; + + /** + * ICE candidates. + */ + get iceCandidates(): IceCandidate[]; + + /** + * ICE state. + */ + get iceState(): IceState; + + /** + * ICE selected tuple. + */ + get iceSelectedTuple(): TransportTuple | undefined; + + /** + * DTLS parameters. + */ + get dtlsParameters(): DtlsParameters; + + /** + * DTLS state. + */ + get dtlsState(): DtlsState; + + /** + * Remote certificate in PEM format. + */ + get dtlsRemoteCert(): string | undefined; + + /** + * SCTP parameters. + */ + get sctpParameters(): SctpParameters | undefined; + + /** + * SCTP state. + */ + get sctpState(): SctpState | undefined; + + /** + * Dump WebRtcTransport. + * + * @override + */ + dump(): Promise; + + /** + * Get WebRtcTransport stats. + * + * @override + */ + getStats(): Promise; + + /** + * Provide the WebRtcTransport remote parameters. + * + * @override + */ + connect({ + dtlsParameters, + }: { + dtlsParameters: DtlsParameters; + }): Promise; + + /** + * Restart ICE. + */ + restartIce(): Promise; +} diff --git a/node/src/Worker.ts b/node/src/Worker.ts index 74eec53483..b30c5d70ba 100644 --- a/node/src/Worker.ts +++ b/node/src/Worker.ts @@ -1,211 +1,70 @@ -import * as process from 'process'; -import * as path from 'path'; -import { spawn, ChildProcess } from 'child_process'; -import { v4 as uuidv4 } from 'uuid'; +import * as process from 'node:process'; +import * as path from 'node:path'; +import { spawn, ChildProcess } from 'node:child_process'; +import { version } from './'; import { Logger } from './Logger'; -import { EnhancedEventEmitter } from './EnhancedEventEmitter'; +import { EnhancedEventEmitter } from './enhancedEvents'; import * as ortc from './ortc'; +import type { + Worker, + WorkerSettings, + WorkerUpdateableSettings, + WorkerResourceUsage, + WorkerDump, + WorkerEvents, + WorkerObserver, + WorkerObserverEvents, +} from './WorkerTypes'; import { Channel } from './Channel'; -import { PayloadChannel } from './PayloadChannel'; -import { Router, RouterOptions } from './Router'; -import { WebRtcServer, WebRtcServerOptions } from './WebRtcServer'; - -export type WorkerLogLevel = 'debug' | 'warn' | 'error' | 'none'; - -export type WorkerLogTag = - | 'info' - | 'ice' - | 'dtls' - | 'rtp' - | 'srtp' - | 'rtcp' - | 'rtx' - | 'bwe' - | 'score' - | 'simulcast' - | 'svc' - | 'sctp' - | 'message'; - -export type WorkerSettings = -{ - /** - * Logging level for logs generated by the media worker subprocesses (check - * the Debugging documentation). Valid values are 'debug', 'warn', 'error' and - * 'none'. Default 'error'. - */ - logLevel?: WorkerLogLevel; - - /** - * Log tags for debugging. Check the meaning of each available tag in the - * Debugging documentation. - */ - logTags?: WorkerLogTag[]; - - /** - * Minimun RTC port for ICE, DTLS, RTP, etc. Default 10000. - */ - rtcMinPort?: number; - - /** - * Maximum RTC port for ICE, DTLS, RTP, etc. Default 59999. - */ - rtcMaxPort?: number; - - /** - * Path to the DTLS public certificate file in PEM format. If unset, a - * certificate is dynamically created. - */ - dtlsCertificateFile?: string; - - /** - * Path to the DTLS certificate private key file in PEM format. If unset, a - * certificate is dynamically created. - */ - dtlsPrivateKeyFile?: string; - - /** - * Field trials for libwebrtc. - * @private - * - * NOTE: For advanced users only. An invalid value will make the worker crash. - * Default value is - * "WebRTC-Bwe-AlrLimitedBackoff/Enabled/". - */ - libwebrtcFieldTrials?: string; - - /** - * Custom application data. - */ - appData?: Record; -}; - -export type WorkerUpdateableSettings = Pick; - -/** - * An object with the fields of the uv_rusage_t struct. - * - * - http://docs.libuv.org/en/v1.x/misc.html#c.uv_rusage_t - * - https://linux.die.net/man/2/getrusage - */ -export type WorkerResourceUsage = -{ - /* eslint-disable camelcase */ - - /** - * User CPU time used (in ms). - */ - ru_utime: number; - - /** - * System CPU time used (in ms). - */ - ru_stime: number; - - /** - * Maximum resident set size. - */ - ru_maxrss: number; - - /** - * Integral shared memory size. - */ - ru_ixrss: number; - - /** - * Integral unshared data size. - */ - ru_idrss: number; - - /** - * Integral unshared stack size. - */ - ru_isrss: number; - - /** - * Page reclaims (soft page faults). - */ - ru_minflt: number; - - /** - * Page faults (hard page faults). - */ - ru_majflt: number; - - /** - * Swaps. - */ - ru_nswap: number; - - /** - * Block input operations. - */ - ru_inblock: number; - - /** - * Block output operations. - */ - ru_oublock: number; - - /** - * IPC messages sent. - */ - ru_msgsnd: number; - - /** - * IPC messages received. - */ - ru_msgrcv: number; - - /** - * Signals received. - */ - ru_nsignals: number; - - /** - * Voluntary context switches. - */ - ru_nvcsw: number; - - /** - * Involuntary context switches. - */ - ru_nivcsw: number; - - /* eslint-enable camelcase */ -}; - -export type WorkerEvents = -{ - died: [Error]; - // Private events. - '@success': []; - '@failure': [Error]; -}; - -export type WorkerObserverEvents = -{ - close: []; - newwebrtcserver: [WebRtcServer]; - newrouter: [Router]; -}; +import type { WebRtcServer, WebRtcServerOptions } from './WebRtcServerTypes'; +import { WebRtcServerImpl } from './WebRtcServer'; +import type { Router, RouterOptions } from './RouterTypes'; +import { RouterImpl } from './Router'; +import { portRangeToFbs, socketFlagsToFbs } from './Transport'; +import type { RtpCodecCapability } from './rtpParametersTypes'; +import * as utils from './utils'; +import * as fbsUtils from './fbsUtils'; +import type { AppData } from './types'; +import { Event } from './fbs/notification'; +import * as FbsRequest from './fbs/request'; +import * as FbsWorker from './fbs/worker'; +import * as FbsTransport from './fbs/transport'; +import { Protocol as FbsTransportProtocol } from './fbs/transport/protocol'; // If env MEDIASOUP_WORKER_BIN is given, use it as worker binary. // Otherwise if env MEDIASOUP_BUILDTYPE is 'Debug' use the Debug binary. // Otherwise use the Release binary. -const workerBin = process.env.MEDIASOUP_WORKER_BIN +export const workerBin = process.env.MEDIASOUP_WORKER_BIN ? process.env.MEDIASOUP_WORKER_BIN : process.env.MEDIASOUP_BUILDTYPE === 'Debug' - ? path.join(__dirname, '..', '..', 'worker', 'out', 'Debug', 'mediasoup-worker') - : path.join(__dirname, '..', '..', 'worker', 'out', 'Release', 'mediasoup-worker'); + ? path.join( + __dirname, + '..', + '..', + 'worker', + 'out', + 'Debug', + 'mediasoup-worker' + ) + : path.join( + __dirname, + '..', + '..', + 'worker', + 'out', + 'Release', + 'mediasoup-worker' + ); const logger = new Logger('Worker'); const workerLogger = new Logger('Worker'); -export class Worker extends EnhancedEventEmitter +export class WorkerImpl + extends EnhancedEventEmitter + implements Worker { // mediasoup-worker child process. - #child?: ChildProcess; + #child: ChildProcess; // Worker process PID. readonly #pid: number; @@ -213,17 +72,17 @@ export class Worker extends EnhancedEventEmitter // Channel instance. readonly #channel: Channel; - // PayloadChannel instance. - readonly #payloadChannel: PayloadChannel; - // Closed flag. #closed = false; // Died dlag. #died = false; + // Worker subprocess closed flag. + #subprocessClosed = false; + // Custom app data. - readonly #appData: Record; + #appData: WorkerAppData; // WebRtcServers set. readonly #webRtcServers: Set = new Set(); @@ -232,23 +91,20 @@ export class Worker extends EnhancedEventEmitter readonly #routers: Set = new Set(); // Observer instance. - readonly #observer = new EnhancedEventEmitter(); - - /** - * @private - */ - constructor( - { - logLevel, - logTags, - rtcMinPort, - rtcMaxPort, - dtlsCertificateFile, - dtlsPrivateKeyFile, - libwebrtcFieldTrials, - appData - }: WorkerSettings) - { + readonly #observer: WorkerObserver = + new EnhancedEventEmitter(); + + constructor({ + logLevel, + logTags, + rtcMinPort, + rtcMaxPort, + dtlsCertificateFile, + dtlsPrivateKeyFile, + libwebrtcFieldTrials, + disableLiburing, + appData, + }: WorkerSettings) { super(); logger.debug('constructor()'); @@ -256,44 +112,53 @@ export class Worker extends EnhancedEventEmitter let spawnBin = workerBin; let spawnArgs: string[] = []; - if (process.env.MEDIASOUP_USE_VALGRIND === 'true') - { - spawnBin = process.env.MEDIASOUP_VALGRIND_BIN || 'valgrind'; + if (process.env.MEDIASOUP_USE_VALGRIND === 'true') { + spawnBin = process.env.MEDIASOUP_VALGRIND_BIN ?? 'valgrind'; - if (process.env.MEDIASOUP_VALGRIND_OPTIONS) - { - spawnArgs = spawnArgs.concat(process.env.MEDIASOUP_VALGRIND_OPTIONS.split(/\s+/)); + if (process.env.MEDIASOUP_VALGRIND_OPTIONS) { + spawnArgs = spawnArgs.concat( + process.env.MEDIASOUP_VALGRIND_OPTIONS.split(/\s+/) + ); } spawnArgs.push(workerBin); } - if (typeof logLevel === 'string' && logLevel) + if (typeof logLevel === 'string' && logLevel) { spawnArgs.push(`--logLevel=${logLevel}`); + } - for (const logTag of (Array.isArray(logTags) ? logTags : [])) - { - if (typeof logTag === 'string' && logTag) + for (const logTag of Array.isArray(logTags) ? logTags : []) { + if (typeof logTag === 'string' && logTag) { spawnArgs.push(`--logTag=${logTag}`); + } } - if (typeof rtcMinPort === 'number' && !Number.isNaN(rtcMinPort)) + if (typeof rtcMinPort === 'number' && !Number.isNaN(rtcMinPort)) { spawnArgs.push(`--rtcMinPort=${rtcMinPort}`); + } - if (typeof rtcMaxPort === 'number' && !Number.isNaN(rtcMaxPort)) + if (typeof rtcMaxPort === 'number' && !Number.isNaN(rtcMaxPort)) { spawnArgs.push(`--rtcMaxPort=${rtcMaxPort}`); + } - if (typeof dtlsCertificateFile === 'string' && dtlsCertificateFile) + if (typeof dtlsCertificateFile === 'string' && dtlsCertificateFile) { spawnArgs.push(`--dtlsCertificateFile=${dtlsCertificateFile}`); + } - if (typeof dtlsPrivateKeyFile === 'string' && dtlsPrivateKeyFile) + if (typeof dtlsPrivateKeyFile === 'string' && dtlsPrivateKeyFile) { spawnArgs.push(`--dtlsPrivateKeyFile=${dtlsPrivateKeyFile}`); + } - if (typeof libwebrtcFieldTrials === 'string' && libwebrtcFieldTrials) + if (typeof libwebrtcFieldTrials === 'string' && libwebrtcFieldTrials) { spawnArgs.push(`--libwebrtcFieldTrials=${libwebrtcFieldTrials}`); + } + + if (disableLiburing) { + spawnArgs.push(`--disableLiburing=true`); + } - logger.debug( - 'spawning worker process: %s %s', spawnBin, spawnArgs.join(' ')); + logger.debug(`spawning worker process: ${spawnBin} ${spawnArgs.join(' ')}`); this.#child = spawn( // command @@ -302,252 +167,207 @@ export class Worker extends EnhancedEventEmitter spawnArgs, // options { - env : - { - MEDIASOUP_VERSION : '__MEDIASOUP_VERSION__', + env: { + MEDIASOUP_VERSION: version, // Let the worker process inherit all environment variables, useful // if a custom and not in the path GCC is used so the user can set // LD_LIBRARY_PATH environment variable for runtime. - ...process.env + ...process.env, }, - detached : false, + detached: false, // fd 0 (stdin) : Just ignore it. // fd 1 (stdout) : Pipe it for 3rd libraries that log their own stuff. // fd 2 (stderr) : Same as stdout. // fd 3 (channel) : Producer Channel fd. // fd 4 (channel) : Consumer Channel fd. - // fd 5 (channel) : Producer PayloadChannel fd. - // fd 6 (channel) : Consumer PayloadChannel fd. - stdio : [ 'ignore', 'pipe', 'pipe', 'pipe', 'pipe', 'pipe', 'pipe' ], - windowsHide : true - }); + stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'], + windowsHide: true, + } + ); this.#pid = this.#child.pid!; - this.#channel = new Channel( - { - producerSocket : this.#child.stdio[3], - consumerSocket : this.#child.stdio[4], - pid : this.#pid - }); - - this.#payloadChannel = new PayloadChannel( - { - // NOTE: TypeScript does not like more than 5 fds. - // @ts-ignore - producerSocket : this.#child.stdio[5], - // @ts-ignore - consumerSocket : this.#child.stdio[6] - }); + this.#channel = new Channel({ + producerSocket: this.#child.stdio[3], + consumerSocket: this.#child.stdio[4], + pid: this.#pid, + }); - this.#appData = appData || {}; + this.#appData = appData ?? ({} as WorkerAppData); let spawnDone = false; // Listen for 'running' notification. - this.#channel.once(String(this.#pid), (event: string) => - { - if (!spawnDone && event === 'running') - { + this.#channel.once(String(this.#pid), (event: Event) => { + if (!spawnDone && event === Event.WORKER_RUNNING) { spawnDone = true; - logger.debug('worker process running [pid:%s]', this.#pid); + logger.debug(`worker process running [pid:${this.#pid}]`); this.emit('@success'); } }); - this.#child.on('exit', (code, signal) => - { - this.#child = undefined; + this.#child.on('exit', (code, signal) => { + // If killed by ourselves, do nothing. + if (this.#child.killed) { + return; + } - if (!spawnDone) - { + if (!spawnDone) { spawnDone = true; - if (code === 42) - { + if (code === 42) { logger.error( - 'worker process failed due to wrong settings [pid:%s]', this.#pid); + `worker process failed due to wrong settings [pid:${this.#pid}]` + ); this.close(); this.emit('@failure', new TypeError('wrong settings')); - } - else - { + } else { logger.error( - 'worker process failed unexpectedly [pid:%s, code:%s, signal:%s]', - this.#pid, code, signal); + `worker process failed unexpectedly [pid:${this.#pid}, code:${code}, signal:${signal}]` + ); this.close(); this.emit( '@failure', - new Error(`[pid:${this.#pid}, code:${code}, signal:${signal}]`)); + new Error(`[pid:${this.#pid}, code:${code}, signal:${signal}]`) + ); } - } - else - { + } else { logger.error( - 'worker process died unexpectedly [pid:%s, code:%s, signal:%s]', - this.#pid, code, signal); + `worker process died unexpectedly [pid:${this.#pid}, code:${code}, signal:${signal}]` + ); this.workerDied( - new Error(`[pid:${this.#pid}, code:${code}, signal:${signal}]`)); + new Error(`[pid:${this.#pid}, code:${code}, signal:${signal}]`) + ); } }); - this.#child.on('error', (error) => - { - this.#child = undefined; + this.#child.on('error', error => { + // If killed by ourselves, do nothing. + if (this.#child.killed) { + return; + } - if (!spawnDone) - { + if (!spawnDone) { spawnDone = true; logger.error( - 'worker process failed [pid:%s]: %s', this.#pid, error.message); + `worker process failed [pid:${this.#pid}]: ${error.message}` + ); this.close(); this.emit('@failure', error); - } - else - { + } else { logger.error( - 'worker process error [pid:%s]: %s', this.#pid, error.message); + `worker process error [pid:${this.#pid}]: ${error.message}` + ); this.workerDied(error); } }); + this.#child.on('close', (code, signal) => { + logger.debug( + `worker subprocess closed [pid:${this.#pid}, code:${code}, signal:${signal}]` + ); + + this.#subprocessClosed = true; + + this.safeEmit('subprocessclose'); + }); + // Be ready for 3rd party worker libraries logging to stdout. - this.#child.stdout!.on('data', (buffer) => - { - for (const line of buffer.toString('utf8').split('\n')) - { - if (line) + this.#child.stdout!.on('data', buffer => { + for (const line of buffer.toString('utf8').split('\n')) { + if (line) { workerLogger.debug(`(stdout) ${line}`); + } } }); // In case of a worker bug, mediasoup will log to stderr. - this.#child.stderr!.on('data', (buffer) => - { - for (const line of buffer.toString('utf8').split('\n')) - { - if (line) + this.#child.stderr!.on('data', buffer => { + for (const line of buffer.toString('utf8').split('\n')) { + if (line) { workerLogger.error(`(stderr) ${line}`); + } } }); + + this.handleListenerError(); } - /** - * Worker process identifier (PID). - */ - get pid(): number - { + get pid(): number { return this.#pid; } - /** - * Whether the Worker is closed. - */ - get closed(): boolean - { + get closed(): boolean { return this.#closed; } - /** - * Whether the Worker died. - */ - get died(): boolean - { + get died(): boolean { return this.#died; } - /** - * App custom data. - */ - get appData(): Record - { + get subprocessClosed(): boolean { + return this.#subprocessClosed; + } + + get appData(): WorkerAppData { return this.#appData; } - /** - * Invalid setter. - */ - set appData(appData: Record) // eslint-disable-line no-unused-vars - { - throw new Error('cannot override appData object'); + set appData(appData: WorkerAppData) { + this.#appData = appData; } - /** - * Observer. - */ - get observer(): EnhancedEventEmitter - { + get observer(): WorkerObserver { return this.#observer; } /** - * @private * Just for testing purposes. */ - get webRtcServersForTesting(): Set - { + get webRtcServersForTesting(): Set { return this.#webRtcServers; } /** - * @private * Just for testing purposes. */ - get routersForTesting(): Set - { + get routersForTesting(): Set { return this.#routers; } - /** - * Close the Worker. - */ - close(): void - { - if (this.#closed) + close(): void { + if (this.#closed) { return; + } logger.debug('close()'); this.#closed = true; // Kill the worker process. - if (this.#child) - { - // Remove event listeners but leave a fake 'error' hander to avoid - // propagation. - this.#child.removeAllListeners('exit'); - this.#child.removeAllListeners('error'); - this.#child.on('error', () => {}); - this.#child.kill('SIGTERM'); - this.#child = undefined; - } + this.#child.kill('SIGTERM'); // Close the Channel instance. this.#channel.close(); - // Close the PayloadChannel instance. - this.#payloadChannel.close(); - // Close every Router. - for (const router of this.#routers) - { + for (const router of this.#routers) { router.workerClosed(); } this.#routers.clear(); // Close every WebRtcServer. - for (const webRtcServer of this.#webRtcServers) - { + for (const webRtcServer of this.#webRtcServers) { webRtcServer.workerClosed(); } this.#webRtcServers.clear(); @@ -556,70 +376,124 @@ export class Worker extends EnhancedEventEmitter this.#observer.safeEmit('close'); } - /** - * Dump Worker. - */ - async dump(): Promise - { + async dump(): Promise { logger.debug('dump()'); - return this.#channel.request('worker.dump'); + // Send the request and wait for the response. + const response = await this.#channel.request(FbsRequest.Method.WORKER_DUMP); + + /* Decode Response. */ + const dump = new FbsWorker.DumpResponse(); + + response.body(dump); + + return parseWorkerDumpResponse(dump); } - /** - * Get mediasoup-worker process resource usage. - */ - async getResourceUsage(): Promise - { + async getResourceUsage(): Promise { logger.debug('getResourceUsage()'); - return this.#channel.request('worker.getResourceUsage'); + const response = await this.#channel.request( + FbsRequest.Method.WORKER_GET_RESOURCE_USAGE + ); + + /* Decode Response. */ + const resourceUsage = new FbsWorker.ResourceUsageResponse(); + + response.body(resourceUsage); + + const ru = resourceUsage.unpack(); + + return { + ru_utime: Number(ru.ruUtime), + ru_stime: Number(ru.ruStime), + ru_maxrss: Number(ru.ruMaxrss), + ru_ixrss: Number(ru.ruIxrss), + ru_idrss: Number(ru.ruIdrss), + ru_isrss: Number(ru.ruIsrss), + ru_minflt: Number(ru.ruMinflt), + ru_majflt: Number(ru.ruMajflt), + ru_nswap: Number(ru.ruNswap), + ru_inblock: Number(ru.ruInblock), + ru_oublock: Number(ru.ruOublock), + ru_msgsnd: Number(ru.ruMsgsnd), + ru_msgrcv: Number(ru.ruMsgrcv), + ru_nsignals: Number(ru.ruNsignals), + ru_nvcsw: Number(ru.ruNvcsw), + ru_nivcsw: Number(ru.ruNivcsw), + }; } - /** - * Update settings. - */ - async updateSettings( - { - logLevel, - logTags - }: WorkerUpdateableSettings = {} - ): Promise - { + async updateSettings({ + logLevel, + logTags, + }: WorkerUpdateableSettings = {}): Promise { logger.debug('updateSettings()'); - const reqData = { logLevel, logTags }; + // Build the request. + const requestOffset = new FbsWorker.UpdateSettingsRequestT( + logLevel, + logTags + ).pack(this.#channel.bufferBuilder); - await this.#channel.request('worker.updateSettings', undefined, reqData); + await this.#channel.request( + FbsRequest.Method.WORKER_UPDATE_SETTINGS, + FbsRequest.Body.Worker_UpdateSettingsRequest, + requestOffset + ); } - /** - * Create a WebRtcServer. - */ - async createWebRtcServer( - { - listenInfos, - appData - }: WebRtcServerOptions): Promise - { + async createWebRtcServer({ + listenInfos, + appData, + }: WebRtcServerOptions): Promise< + WebRtcServer + > { logger.debug('createWebRtcServer()'); - if (appData && typeof appData !== 'object') + if (appData && typeof appData !== 'object') { throw new TypeError('if given, appData must be an object'); + } - const reqData = - { - webRtcServerId : uuidv4(), - listenInfos - }; - - await this.#channel.request('worker.createWebRtcServer', undefined, reqData); + // Build the request. + const fbsListenInfos: FbsTransport.ListenInfoT[] = []; + + for (const listenInfo of listenInfos) { + fbsListenInfos.push( + new FbsTransport.ListenInfoT( + listenInfo.protocol === 'udp' + ? FbsTransportProtocol.UDP + : FbsTransportProtocol.TCP, + listenInfo.ip, + listenInfo.announcedAddress ?? listenInfo.announcedIp, + listenInfo.port, + portRangeToFbs(listenInfo.portRange), + socketFlagsToFbs(listenInfo.flags), + listenInfo.sendBufferSize, + listenInfo.recvBufferSize + ) + ); + } - const webRtcServer = new WebRtcServer( - { - internal : { webRtcServerId: reqData.webRtcServerId }, - channel : this.#channel, - appData + const webRtcServerId = utils.generateUUIDv4(); + + const createWebRtcServerRequestOffset = + new FbsWorker.CreateWebRtcServerRequestT( + webRtcServerId, + fbsListenInfos + ).pack(this.#channel.bufferBuilder); + + await this.#channel.request( + FbsRequest.Method.WORKER_CREATE_WEBRTCSERVER, + FbsRequest.Body.Worker_CreateWebRtcServerRequest, + createWebRtcServerRequestOffset + ); + + const webRtcServer: WebRtcServer = + new WebRtcServerImpl({ + internal: { webRtcServerId }, + channel: this.#channel, + appData, }); this.#webRtcServers.add(webRtcServer); @@ -631,39 +505,47 @@ export class Worker extends EnhancedEventEmitter return webRtcServer; } - /** - * Create a Router. - */ - async createRouter( - { - mediaCodecs, - appData - }: RouterOptions = {}): Promise - { + async createRouter({ + mediaCodecs, + appData, + }: RouterOptions = {}): Promise> { logger.debug('createRouter()'); - if (appData && typeof appData !== 'object') + if (appData && typeof appData !== 'object') { throw new TypeError('if given, appData must be an object'); + } + + // Clone given media codecs to not modify input data. + const clonedMediaCodecs = utils.clone( + mediaCodecs + ); // This may throw. - const rtpCapabilities = ortc.generateRouterRtpCapabilities(mediaCodecs); + const rtpCapabilities = + ortc.generateRouterRtpCapabilities(clonedMediaCodecs); + + const routerId = utils.generateUUIDv4(); - const reqData = { routerId: uuidv4() }; + // Get flatbuffer builder. + const createRouterRequestOffset = new FbsWorker.CreateRouterRequestT( + routerId + ).pack(this.#channel.bufferBuilder); - await this.#channel.request('worker.createRouter', undefined, reqData); + await this.#channel.request( + FbsRequest.Method.WORKER_CREATE_ROUTER, + FbsRequest.Body.Worker_CreateRouterRequest, + createRouterRequestOffset + ); const data = { rtpCapabilities }; - const router = new Router( - { - internal : - { - routerId : reqData.routerId - }, - data, - channel : this.#channel, - payloadChannel : this.#payloadChannel, - appData - }); + const router: Router = new RouterImpl({ + internal: { + routerId, + }, + data, + channel: this.#channel, + appData, + }); this.#routers.add(router); router.on('@close', () => this.#routers.delete(router)); @@ -674,12 +556,12 @@ export class Worker extends EnhancedEventEmitter return router; } - private workerDied(error: Error): void - { - if (this.#closed) + private workerDied(error: Error): void { + if (this.#closed) { return; + } - logger.debug(`died() [error:${error}]`); + logger.debug(`died() [error:${error.toString()}]`); this.#closed = true; this.#died = true; @@ -687,19 +569,14 @@ export class Worker extends EnhancedEventEmitter // Close the Channel instance. this.#channel.close(); - // Close the PayloadChannel instance. - this.#payloadChannel.close(); - // Close every Router. - for (const router of this.#routers) - { + for (const router of this.#routers) { router.workerClosed(); } this.#routers.clear(); // Close every WebRtcServer. - for (const webRtcServer of this.#webRtcServers) - { + for (const webRtcServer of this.#webRtcServers) { webRtcServer.workerClosed(); } this.#webRtcServers.clear(); @@ -709,4 +586,43 @@ export class Worker extends EnhancedEventEmitter // Emit observer event. this.#observer.safeEmit('close'); } + + private handleListenerError(): void { + this.on('listenererror', (eventName, error) => { + logger.error( + `event listener threw an error [eventName:${eventName}]:`, + error + ); + }); + } +} + +export function parseWorkerDumpResponse( + binary: FbsWorker.DumpResponse +): WorkerDump { + const dump: WorkerDump = { + pid: binary.pid(), + webRtcServerIds: fbsUtils.parseVector(binary, 'webRtcServerIds'), + routerIds: fbsUtils.parseVector(binary, 'routerIds'), + channelMessageHandlers: { + channelRequestHandlers: fbsUtils.parseVector( + binary.channelMessageHandlers()!, + 'channelRequestHandlers' + ), + channelNotificationHandlers: fbsUtils.parseVector( + binary.channelMessageHandlers()!, + 'channelNotificationHandlers' + ), + }, + }; + + if (binary.liburing()) { + dump.liburing = { + sqeProcessCount: Number(binary.liburing()!.sqeProcessCount()), + sqeMissCount: Number(binary.liburing()!.sqeMissCount()), + userDataMissCount: Number(binary.liburing()!.userDataMissCount()), + }; + } + + return dump; } diff --git a/node/src/WorkerTypes.ts b/node/src/WorkerTypes.ts new file mode 100644 index 0000000000..337b0c4ea8 --- /dev/null +++ b/node/src/WorkerTypes.ts @@ -0,0 +1,279 @@ +import type { EnhancedEventEmitter } from './enhancedEvents'; +import type { WebRtcServer, WebRtcServerOptions } from './WebRtcServerTypes'; +import type { Router, RouterOptions } from './RouterTypes'; +import type { AppData } from './types'; + +export type WorkerLogLevel = 'debug' | 'warn' | 'error' | 'none'; + +export type WorkerLogTag = + | 'info' + | 'ice' + | 'dtls' + | 'rtp' + | 'srtp' + | 'rtcp' + | 'rtx' + | 'bwe' + | 'score' + | 'simulcast' + | 'svc' + | 'sctp' + | 'message'; + +export type WorkerSettings = { + /** + * Logging level for logs generated by the media worker subprocesses (check + * the Debugging documentation). Valid values are 'debug', 'warn', 'error' and + * 'none'. Default 'error'. + */ + logLevel?: WorkerLogLevel; + + /** + * Log tags for debugging. Check the meaning of each available tag in the + * Debugging documentation. + */ + logTags?: WorkerLogTag[]; + + /** + * Minimun RTC port for ICE, DTLS, RTP, etc. Default 10000. + * @deprecated Use |portRange| in TransportListenInfo object instead. + */ + rtcMinPort?: number; + + /** + * Maximum RTC port for ICE, DTLS, RTP, etc. Default 59999. + * @deprecated Use |portRange| in TransportListenInfo object instead. + */ + rtcMaxPort?: number; + + /** + * Path to the DTLS public certificate file in PEM format. If unset, a + * certificate is dynamically created. + */ + dtlsCertificateFile?: string; + + /** + * Path to the DTLS certificate private key file in PEM format. If unset, a + * certificate is dynamically created. + */ + dtlsPrivateKeyFile?: string; + + /** + * Field trials for libwebrtc. + * @private + * + * NOTE: For advanced users only. An invalid value will make the worker crash. + * Default value is + * "WebRTC-Bwe-AlrLimitedBackoff/Enabled/". + */ + libwebrtcFieldTrials?: string; + + /** + * Disable liburing (io_uring) despite it's supported in current host. + */ + disableLiburing?: boolean; + + /** + * Custom application data. + */ + appData?: WorkerAppData; +}; + +export type WorkerUpdateableSettings = Pick< + WorkerSettings, + 'logLevel' | 'logTags' +>; + +/** + * An object with the fields of the uv_rusage_t struct. + * + * - http://docs.libuv.org/en/v1.x/misc.html#c.uv_rusage_t + * - https://linux.die.net/man/2/getrusage + */ +export type WorkerResourceUsage = { + /** + * User CPU time used (in ms). + */ + ru_utime: number; + + /** + * System CPU time used (in ms). + */ + ru_stime: number; + + /** + * Maximum resident set size. + */ + ru_maxrss: number; + + /** + * Integral shared memory size. + */ + ru_ixrss: number; + + /** + * Integral unshared data size. + */ + ru_idrss: number; + + /** + * Integral unshared stack size. + */ + ru_isrss: number; + + /** + * Page reclaims (soft page faults). + */ + ru_minflt: number; + + /** + * Page faults (hard page faults). + */ + ru_majflt: number; + + /** + * Swaps. + */ + ru_nswap: number; + + /** + * Block input operations. + */ + ru_inblock: number; + + /** + * Block output operations. + */ + ru_oublock: number; + + /** + * IPC messages sent. + */ + ru_msgsnd: number; + + /** + * IPC messages received. + */ + ru_msgrcv: number; + + /** + * Signals received. + */ + ru_nsignals: number; + + /** + * Voluntary context switches. + */ + ru_nvcsw: number; + + /** + * Involuntary context switches. + */ + ru_nivcsw: number; +}; + +export type WorkerDump = { + pid: number; + webRtcServerIds: string[]; + routerIds: string[]; + channelMessageHandlers: { + channelRequestHandlers: string[]; + channelNotificationHandlers: string[]; + }; + liburing?: { + sqeProcessCount: number; + sqeMissCount: number; + userDataMissCount: number; + }; +}; + +export type WorkerEvents = { + died: [Error]; + subprocessclose: []; + listenererror: [string, Error]; + // Private events. + '@success': []; + '@failure': [Error]; +}; + +export type WorkerObserver = EnhancedEventEmitter; + +export type WorkerObserverEvents = { + close: []; + newwebrtcserver: [WebRtcServer]; + newrouter: [Router]; +}; + +export interface Worker + extends EnhancedEventEmitter { + /** + * Worker process identifier (PID). + */ + get pid(): number; + + /** + * Whether the Worker is closed. + */ + get closed(): boolean; + + /** + * Whether the Worker died. + */ + get died(): boolean; + + /** + * Whether the Worker subprocess is closed. + */ + get subprocessClosed(): boolean; + + /** + * App custom data. + */ + get appData(): WorkerAppData; + + /** + * App custom data setter. + */ + set appData(appData: WorkerAppData); + + /** + * Observer. + */ + get observer(): WorkerObserver; + + /** + * Close the Worker. + */ + close(): void; + + /** + * Dump Worker. + */ + dump(): Promise; + + /** + * Get mediasoup-worker process resource usage. + */ + getResourceUsage(): Promise; + + /** + * Update settings. + */ + updateSettings( + options?: WorkerUpdateableSettings + ): Promise; + + /** + * Create a WebRtcServer. + */ + createWebRtcServer( + options: WebRtcServerOptions + ): Promise>; + + /** + * Create a Router. + */ + createRouter( + options?: RouterOptions + ): Promise>; +} diff --git a/node/src/EnhancedEventEmitter.ts b/node/src/enhancedEvents.ts similarity index 66% rename from node/src/EnhancedEventEmitter.ts rename to node/src/enhancedEvents.ts index b6b1dc0eab..8f7a5eb59a 100644 --- a/node/src/EnhancedEventEmitter.ts +++ b/node/src/enhancedEvents.ts @@ -1,49 +1,41 @@ -import { EventEmitter } from 'events'; -import { Logger } from './Logger'; - -const logger = new Logger('EnhancedEventEmitter'); +import { EventEmitter, once } from 'node:events'; type Events = Record; -export class EnhancedEventEmitter extends EventEmitter -{ - constructor() - { +export class EnhancedEventEmitter< + E extends Events = Events, +> extends EventEmitter { + constructor() { super(); + this.setMaxListeners(Infinity); } - emit(eventName: K, ...args: E[K]): boolean - { + emit(eventName: K, ...args: E[K]): boolean { return super.emit(eventName, ...args); } /** * Special addition to the EventEmitter API. */ - safeEmit(eventName: K, ...args: E[K]): boolean - { - const numListeners = super.listenerCount(eventName); - - try - { + safeEmit(eventName: K, ...args: E[K]): boolean { + try { return super.emit(eventName, ...args); - } - catch (error) - { - logger.error( - 'safeEmit() | event listener threw an error [eventName:%s]:%o', - eventName, error); - - return Boolean(numListeners); + } catch (error) { + try { + super.emit('listenererror', eventName, error); + } catch (error2) { + // Ignore it. + } + + return Boolean(super.listenerCount(eventName)); } } on( eventName: K, listener: (...args: E[K]) => void - ): this - { + ): this { super.on(eventName, listener as (...args: any[]) => void); return this; @@ -52,8 +44,7 @@ export class EnhancedEventEmitter extends EventEmitte off( eventName: K, listener: (...args: E[K]) => void - ): this - { + ): this { super.off(eventName, listener as (...args: any[]) => void); return this; @@ -62,8 +53,7 @@ export class EnhancedEventEmitter extends EventEmitte addListener( eventName: K, listener: (...args: E[K]) => void - ): this - { + ): this { super.on(eventName, listener as (...args: any[]) => void); return this; @@ -72,8 +62,7 @@ export class EnhancedEventEmitter extends EventEmitte prependListener( eventName: K, listener: (...args: E[K]) => void - ): this - { + ): this { super.prependListener(eventName, listener as (...args: any[]) => void); return this; @@ -82,8 +71,7 @@ export class EnhancedEventEmitter extends EventEmitte once( eventName: K, listener: (...args: E[K]) => void - ): this - { + ): this { super.once(eventName, listener as (...args: any[]) => void); return this; @@ -92,8 +80,7 @@ export class EnhancedEventEmitter extends EventEmitte prependOnceListener( eventName: K, listener: (...args: E[K]) => void - ): this - { + ): this { super.prependOnceListener(eventName, listener as (...args: any[]) => void); return this; @@ -102,32 +89,46 @@ export class EnhancedEventEmitter extends EventEmitte removeListener( eventName: K, listener: (...args: E[K]) => void - ): this - { + ): this { super.off(eventName, listener as (...args: any[]) => void); return this; } - removeAllListeners(eventName?: K): this - { + removeAllListeners(eventName?: K): this { super.removeAllListeners(eventName); return this; } - listenerCount(eventName: K): number - { + listenerCount(eventName: K): number { return super.listenerCount(eventName); } - listeners(eventName: K): Function[] - { + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + listeners(eventName: K): Function[] { return super.listeners(eventName); } - rawListeners(eventName: K): Function[] - { + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + rawListeners(eventName: K): Function[] { return super.rawListeners(eventName); } } + +/** + * TypeScript version of events.once(): + * https://nodejs.org/api/events.html#eventsonceemitter-name-options + * + * Usage example: + * ```ts + * await enhancedOnce(videoConsumer, 'producerpause'); + * ```` + */ +export async function enhancedOnce( + emmiter: EnhancedEventEmitter, + eventName: keyof E & string, + options?: any +): Promise { + return once(emmiter, eventName, options); +} diff --git a/node/src/errors.ts b/node/src/errors.ts index 0dff994d6e..e88967ff2f 100644 --- a/node/src/errors.ts +++ b/node/src/errors.ts @@ -1,35 +1,35 @@ /** * Error indicating not support for something. */ -export class UnsupportedError extends Error -{ - constructor(message: string) - { +export class UnsupportedError extends Error { + constructor(message: string) { super(message); this.name = 'UnsupportedError'; - if (Error.hasOwnProperty('captureStackTrace')) // Just in V8. + if (Error.hasOwnProperty('captureStackTrace')) { + // Just in V8. Error.captureStackTrace(this, UnsupportedError); - else - this.stack = (new Error(message)).stack; + } else { + this.stack = new Error(message).stack; + } } } /** * Error produced when calling a method in an invalid state. */ -export class InvalidStateError extends Error -{ - constructor(message: string) - { +export class InvalidStateError extends Error { + constructor(message: string) { super(message); this.name = 'InvalidStateError'; - if (Error.hasOwnProperty('captureStackTrace')) // Just in V8. + if (Error.hasOwnProperty('captureStackTrace')) { + // Just in V8. Error.captureStackTrace(this, InvalidStateError); - else - this.stack = (new Error(message)).stack; + } else { + this.stack = new Error(message).stack; + } } } diff --git a/node/src/extras.ts b/node/src/extras.ts new file mode 100644 index 0000000000..72ecee7755 --- /dev/null +++ b/node/src/extras.ts @@ -0,0 +1 @@ +export { EnhancedEventEmitter, enhancedOnce } from './enhancedEvents'; diff --git a/node/src/fbsUtils.ts b/node/src/fbsUtils.ts new file mode 100644 index 0000000000..502a9bc2a3 --- /dev/null +++ b/node/src/fbsUtils.ts @@ -0,0 +1,115 @@ +/** + * Parse flatbuffers vector into an array of the given T. + */ +export function parseVector( + binary: any, + methodName: string, + parseFn?: (binary2: any) => T +): T[] { + const array: T[] = []; + + for (let i = 0; i < binary[`${methodName}Length`](); ++i) { + if (parseFn) { + array.push(parseFn(binary[methodName](i))); + } else { + array.push(binary[methodName](i) as T); + } + } + + return array; +} + +/** + * Parse flatbuffers vector of StringString into the corresponding array. + */ +export function parseStringStringVector( + binary: any, + methodName: string +): { key: string; value: string }[] { + const array: { key: string; value: string }[] = []; + + for (let i = 0; i < binary[`${methodName}Length`](); ++i) { + const kv = binary[methodName](i)!; + + array.push({ key: kv.key(), value: kv.value() }); + } + + return array; +} + +/** + * Parse flatbuffers vector of StringUint8 into the corresponding array. + */ +export function parseStringUint8Vector( + binary: any, + methodName: string +): { key: string; value: number }[] { + const array: { key: string; value: number }[] = []; + + for (let i = 0; i < binary[`${methodName}Length`](); ++i) { + const kv = binary[methodName](i)!; + + array.push({ key: kv.key(), value: kv.value() }); + } + + return array; +} + +/** + * Parse flatbuffers vector of Uint16String into the corresponding array. + */ +export function parseUint16StringVector( + binary: any, + methodName: string +): { key: number; value: string }[] { + const array: { key: number; value: string }[] = []; + + for (let i = 0; i < binary[`${methodName}Length`](); ++i) { + const kv = binary[methodName](i)!; + + array.push({ key: kv.key(), value: kv.value() }); + } + + return array; +} + +/** + * Parse flatbuffers vector of Uint32String into the corresponding array. + */ +export function parseUint32StringVector( + binary: any, + methodName: string +): { key: number; value: string }[] { + const array: { key: number; value: string }[] = []; + + for (let i = 0; i < binary[`${methodName}Length`](); ++i) { + const kv = binary[methodName](i)!; + + array.push({ key: kv.key(), value: kv.value() }); + } + + return array; +} + +/** + * Parse flatbuffers vector of StringStringArray into the corresponding array. + */ +export function parseStringStringArrayVector( + binary: any, + methodName: string +): { key: string; values: string[] }[] { + const array: { key: string; values: string[] }[] = []; + + for (let i = 0; i < binary[`${methodName}Length`](); ++i) { + const kv = binary[methodName](i)!; + const values: string[] = []; + + for (let i2 = 0; i2 < kv.valuesLength(); ++i2) { + values.push(kv.values(i2)! as string); + } + + array.push({ key: kv.key(), values }); + } + + return array; +} diff --git a/node/src/index.ts b/node/src/index.ts index b3b5cf6965..241c928ab6 100644 --- a/node/src/index.ts +++ b/node/src/index.ts @@ -1,10 +1,11 @@ -import { Logger } from './Logger'; -import { EnhancedEventEmitter } from './EnhancedEventEmitter'; -import { Worker, WorkerSettings } from './Worker'; -import * as utils from './utils'; +import { Logger, LoggerEmitter } from './Logger'; +import { EnhancedEventEmitter } from './enhancedEvents'; +import type { Worker, WorkerSettings } from './WorkerTypes'; +import { WorkerImpl, workerBin } from './Worker'; import { supportedRtpCapabilities } from './supportedRtpCapabilities'; -import { RtpCapabilities } from './RtpParameters'; -import * as types from './types'; +import type { RtpCapabilities } from './rtpParametersTypes'; +import type * as types from './types'; +import * as utils from './utils'; /** * Expose all types. @@ -14,64 +15,116 @@ export { types }; /** * Expose mediasoup version. */ -export const version = '__MEDIASOUP_VERSION__'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +export const version: string = require('../../package.json').version; -/** - * Expose parseScalabilityMode() function. - */ -export { parse as parseScalabilityMode } from './scalabilityModes'; +export type Observer = EnhancedEventEmitter; -const logger = new Logger(); - -export type ObserverEvents = -{ +export type ObserverEvents = { newworker: [Worker]; }; -const observer = new EnhancedEventEmitter(); +const observer: Observer = new EnhancedEventEmitter(); /** * Observer. */ export { observer }; +/** + * Full path of the mediasoup-worker binary. + */ +export { workerBin }; + +const logger = new Logger(); + +/** + * Set event listeners for mediasoup generated logs. If called with no arguments + * then no events will be emitted. + * + * @example + * ```ts + * mediasoup.setLogEventListeners({ + * ondebug: undefined, + * onwarn: (namespace: string, log: string) => { + * MyEnterpriseLogger.warn(`${namespace} ${log}`); + * }, + * onerror: (namespace: string, log: string, error?: Error) => { + * if (error) { + * MyEnterpriseLogger.error(`${namespace} ${log}: ${error}`); + * } else { + * MyEnterpriseLogger.error(`${namespace} ${log}`); + * } + * } + * }); + * ``` + */ +export function setLogEventListeners( + listeners?: types.LogEventListeners +): void { + logger.debug('setLogEventListeners()'); + + let debugLogEmitter: LoggerEmitter | undefined; + let warnLogEmitter: LoggerEmitter | undefined; + let errorLogEmitter: LoggerEmitter | undefined; + + if (listeners?.ondebug) { + debugLogEmitter = new EnhancedEventEmitter(); + + debugLogEmitter.on('debuglog', listeners.ondebug); + } + + if (listeners?.onwarn) { + warnLogEmitter = new EnhancedEventEmitter(); + + warnLogEmitter.on('warnlog', listeners.onwarn); + } + + if (listeners?.onerror) { + errorLogEmitter = new EnhancedEventEmitter(); + + errorLogEmitter.on('errorlog', listeners.onerror); + } + + Logger.setEmitters(debugLogEmitter, warnLogEmitter, errorLogEmitter); +} + /** * Create a Worker. */ -export async function createWorker( - { - logLevel = 'error', - logTags, - rtcMinPort = 10000, - rtcMaxPort = 59999, - dtlsCertificateFile, - dtlsPrivateKeyFile, - libwebrtcFieldTrials, - appData - }: WorkerSettings = {} -): Promise -{ +export async function createWorker< + WorkerAppData extends types.AppData = types.AppData, +>({ + logLevel = 'error', + logTags, + rtcMinPort = 10000, + rtcMaxPort = 59999, + dtlsCertificateFile, + dtlsPrivateKeyFile, + libwebrtcFieldTrials, + disableLiburing, + appData, +}: WorkerSettings = {}): Promise> { logger.debug('createWorker()'); - if (appData && typeof appData !== 'object') + if (appData && typeof appData !== 'object') { throw new TypeError('if given, appData must be an object'); + } - const worker = new Worker( - { - logLevel, - logTags, - rtcMinPort, - rtcMaxPort, - dtlsCertificateFile, - dtlsPrivateKeyFile, - libwebrtcFieldTrials, - appData - }); + const worker: Worker = new WorkerImpl({ + logLevel, + logTags, + rtcMinPort, + rtcMaxPort, + dtlsCertificateFile, + dtlsPrivateKeyFile, + libwebrtcFieldTrials, + disableLiburing, + appData, + }); - return new Promise((resolve, reject) => - { - worker.on('@success', () => - { + return new Promise((resolve, reject) => { + worker.on('@success', () => { // Emit observer event. observer.safeEmit('newworker', worker); @@ -85,7 +138,16 @@ export async function createWorker( /** * Get a cloned copy of the mediasoup supported RTP capabilities. */ -export function getSupportedRtpCapabilities(): RtpCapabilities -{ - return utils.clone(supportedRtpCapabilities) as RtpCapabilities; +export function getSupportedRtpCapabilities(): RtpCapabilities { + return utils.clone(supportedRtpCapabilities); } + +/** + * Expose parseScalabilityMode() function. + */ +export { parseScalabilityMode } from './scalabilityModesUtils'; + +/** + * Expose extras module. + */ +export * as extras from './extras'; diff --git a/node/src/ortc.ts b/node/src/ortc.ts index 2677487934..be79bab7c2 100644 --- a/node/src/ortc.ts +++ b/node/src/ortc.ts @@ -1,9 +1,8 @@ import * as h264 from 'h264-profile-level-id'; -import * as utils from './utils'; -import { UnsupportedError } from './errors'; +import * as flatbuffers from 'flatbuffers'; import { supportedRtpCapabilities } from './supportedRtpCapabilities'; -import { parse as parseScalabilityMode } from './scalabilityModes'; -import { +import { parseScalabilityMode } from './scalabilityModesUtils'; +import type { RtpCapabilities, MediaKind, RtpCodecCapability, @@ -13,25 +12,19 @@ import { RtcpFeedback, RtpEncodingParameters, RtpHeaderExtensionParameters, - RtcpParameters -} from './RtpParameters'; -import { - SctpCapabilities, - NumSctpStreams, - SctpParameters, - SctpStreamParameters -} from './SctpParameters'; - -type RtpMapping = -{ - codecs: - { + RtcpParameters, +} from './rtpParametersTypes'; +import type { SctpStreamParameters } from './sctpParametersTypes'; +import * as utils from './utils'; +import { UnsupportedError } from './errors'; +import * as FbsRtpParameters from './fbs/rtp-parameters'; + +export type RtpCodecsEncodingsMapping = { + codecs: { payloadType: number; mappedPayloadType: number; }[]; - - encodings: - { + encodings: { ssrc?: number; rid?: string; scalabilityMode?: string; @@ -39,11 +32,10 @@ type RtpMapping = }[]; }; -const DynamicPayloadTypes = -[ - 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, - 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, - 122, 123, 124, 125, 126, 127, 96, 97, 98, 99 +const DynamicPayloadTypes = [ + 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, + 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 96, 97, 98, + 99, ]; /** @@ -51,727 +43,362 @@ const DynamicPayloadTypes = * fields with default values. * It throws if invalid. */ -export function validateRtpCapabilities(caps: RtpCapabilities): void -{ - if (typeof caps !== 'object') +export function validateRtpCapabilities(caps: RtpCapabilities): void { + if (typeof caps !== 'object') { throw new TypeError('caps is not an object'); + } // codecs is optional. If unset, fill with an empty array. - if (caps.codecs && !Array.isArray(caps.codecs)) + if (caps.codecs && !Array.isArray(caps.codecs)) { throw new TypeError('caps.codecs is not an array'); - else if (!caps.codecs) + } else if (!caps.codecs) { caps.codecs = []; + } - for (const codec of caps.codecs) - { + for (const codec of caps.codecs) { validateRtpCodecCapability(codec); } // headerExtensions is optional. If unset, fill with an empty array. - if (caps.headerExtensions && !Array.isArray(caps.headerExtensions)) + if (caps.headerExtensions && !Array.isArray(caps.headerExtensions)) { throw new TypeError('caps.headerExtensions is not an array'); - else if (!caps.headerExtensions) + } else if (!caps.headerExtensions) { caps.headerExtensions = []; - - for (const ext of caps.headerExtensions) - { - validateRtpHeaderExtension(ext); - } -} - -/** - * Validates RtpCodecCapability. It may modify given data by adding missing - * fields with default values. - * It throws if invalid. - */ -export function validateRtpCodecCapability(codec: RtpCodecCapability): void -{ - const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i'); - - if (typeof codec !== 'object') - throw new TypeError('codec is not an object'); - - // mimeType is mandatory. - if (!codec.mimeType || typeof codec.mimeType !== 'string') - throw new TypeError('missing codec.mimeType'); - - const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType); - - if (!mimeTypeMatch) - throw new TypeError('invalid codec.mimeType'); - - // Just override kind with media component of mimeType. - codec.kind = mimeTypeMatch[1].toLowerCase() as MediaKind; - - // preferredPayloadType is optional. - if (codec.preferredPayloadType && typeof codec.preferredPayloadType !== 'number') - throw new TypeError('invalid codec.preferredPayloadType'); - - // clockRate is mandatory. - if (typeof codec.clockRate !== 'number') - throw new TypeError('missing codec.clockRate'); - - // channels is optional. If unset, set it to 1 (just if audio). - if (codec.kind === 'audio') - { - if (typeof codec.channels !== 'number') - codec.channels = 1; - } - else - { - delete codec.channels; - } - - // parameters is optional. If unset, set it to an empty object. - if (!codec.parameters || typeof codec.parameters !== 'object') - codec.parameters = {}; - - for (const key of Object.keys(codec.parameters)) - { - let value = codec.parameters[key]; - - if (value === undefined) - { - codec.parameters[key] = ''; - value = ''; - } - - if (typeof value !== 'string' && typeof value !== 'number') - { - throw new TypeError( - `invalid codec parameter [key:${key}s, value:${value}]`); - } - - // Specific parameters validation. - if (key === 'apt') - { - if (typeof value !== 'number') - throw new TypeError('invalid codec apt parameter'); - } } - // rtcpFeedback is optional. If unset, set it to an empty array. - if (!codec.rtcpFeedback || !Array.isArray(codec.rtcpFeedback)) - codec.rtcpFeedback = []; - - for (const fb of codec.rtcpFeedback) - { - validateRtcpFeedback(fb); + for (const ext of caps.headerExtensions) { + validateRtpHeaderExtension(ext); } } -/** - * Validates RtcpFeedback. It may modify given data by adding missing - * fields with default values. - * It throws if invalid. - */ -export function validateRtcpFeedback(fb: RtcpFeedback): void -{ - if (typeof fb !== 'object') - throw new TypeError('fb is not an object'); - - // type is mandatory. - if (!fb.type || typeof fb.type !== 'string') - throw new TypeError('missing fb.type'); - - // parameter is optional. If unset set it to an empty string. - if (!fb.parameter || typeof fb.parameter !== 'string') - fb.parameter = ''; -} - -/** - * Validates RtpHeaderExtension. It may modify given data by adding missing - * fields with default values. - * It throws if invalid. - */ -export function validateRtpHeaderExtension(ext: RtpHeaderExtension): void -{ - - if (typeof ext !== 'object') - throw new TypeError('ext is not an object'); - - if (ext.kind !== 'audio' && ext.kind !== 'video') - throw new TypeError('invalid ext.kind'); - - // uri is mandatory. - if (!ext.uri || typeof ext.uri !== 'string') - throw new TypeError('missing ext.uri'); - - // preferredId is mandatory. - if (typeof ext.preferredId !== 'number') - throw new TypeError('missing ext.preferredId'); - - // preferredEncrypt is optional. If unset set it to false. - if (ext.preferredEncrypt && typeof ext.preferredEncrypt !== 'boolean') - throw new TypeError('invalid ext.preferredEncrypt'); - else if (!ext.preferredEncrypt) - ext.preferredEncrypt = false; - - // direction is optional. If unset set it to sendrecv. - if (ext.direction && typeof ext.direction !== 'string') - throw new TypeError('invalid ext.direction'); - else if (!ext.direction) - ext.direction = 'sendrecv'; -} - /** * Validates RtpParameters. It may modify given data by adding missing * fields with default values. * It throws if invalid. */ -export function validateRtpParameters(params: RtpParameters): void -{ - if (typeof params !== 'object') +export function validateRtpParameters(params: RtpParameters): void { + if (typeof params !== 'object') { throw new TypeError('params is not an object'); + } // mid is optional. - if (params.mid && typeof params.mid !== 'string') + if (params.mid && typeof params.mid !== 'string') { throw new TypeError('params.mid is not a string'); + } // codecs is mandatory. - if (!Array.isArray(params.codecs)) + if (!Array.isArray(params.codecs)) { throw new TypeError('missing params.codecs'); + } - for (const codec of params.codecs) - { + for (const codec of params.codecs) { validateRtpCodecParameters(codec); } // headerExtensions is optional. If unset, fill with an empty array. - if (params.headerExtensions && !Array.isArray(params.headerExtensions)) + if (params.headerExtensions && !Array.isArray(params.headerExtensions)) { throw new TypeError('params.headerExtensions is not an array'); - else if (!params.headerExtensions) + } else if (!params.headerExtensions) { params.headerExtensions = []; + } - for (const ext of params.headerExtensions) - { + for (const ext of params.headerExtensions) { validateRtpHeaderExtensionParameters(ext); } // encodings is optional. If unset, fill with an empty array. - if (params.encodings && !Array.isArray(params.encodings)) + if (params.encodings && !Array.isArray(params.encodings)) { throw new TypeError('params.encodings is not an array'); - else if (!params.encodings) + } else if (!params.encodings) { params.encodings = []; + } - for (const encoding of params.encodings) - { + for (const encoding of params.encodings) { validateRtpEncodingParameters(encoding); } // rtcp is optional. If unset, fill with an empty object. - if (params.rtcp && typeof params.rtcp !== 'object') + if (params.rtcp && typeof params.rtcp !== 'object') { throw new TypeError('params.rtcp is not an object'); - else if (!params.rtcp) + } else if (!params.rtcp) { params.rtcp = {}; + } validateRtcpParameters(params.rtcp); } /** - * Validates RtpCodecParameters. It may modify given data by adding missing + * Validates SctpStreamParameters. It may modify given data by adding missing * fields with default values. * It throws if invalid. */ -export function validateRtpCodecParameters(codec: RtpCodecParameters): void -{ - const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i'); - - if (typeof codec !== 'object') - throw new TypeError('codec is not an object'); +export function validateSctpStreamParameters( + params: SctpStreamParameters +): void { + if (typeof params !== 'object') { + throw new TypeError('params is not an object'); + } - // mimeType is mandatory. - if (!codec.mimeType || typeof codec.mimeType !== 'string') - throw new TypeError('missing codec.mimeType'); + // streamId is mandatory. + if (typeof params.streamId !== 'number') { + throw new TypeError('missing params.streamId'); + } - const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType); + // ordered is optional. + let orderedGiven = false; - if (!mimeTypeMatch) - throw new TypeError('invalid codec.mimeType'); + if (typeof params.ordered === 'boolean') { + orderedGiven = true; + } else { + params.ordered = true; + } - // payloadType is mandatory. - if (typeof codec.payloadType !== 'number') - throw new TypeError('missing codec.payloadType'); + // maxPacketLifeTime is optional. + if ( + params.maxPacketLifeTime && + typeof params.maxPacketLifeTime !== 'number' + ) { + throw new TypeError('invalid params.maxPacketLifeTime'); + } - // clockRate is mandatory. - if (typeof codec.clockRate !== 'number') - throw new TypeError('missing codec.clockRate'); + // maxRetransmits is optional. + if (params.maxRetransmits && typeof params.maxRetransmits !== 'number') { + throw new TypeError('invalid params.maxRetransmits'); + } - const kind = mimeTypeMatch[1].toLowerCase() as MediaKind; + if (params.maxPacketLifeTime && params.maxRetransmits) { + throw new TypeError( + 'cannot provide both maxPacketLifeTime and maxRetransmits' + ); + } - // channels is optional. If unset, set it to 1 (just if audio). - if (kind === 'audio') - { - if (typeof codec.channels !== 'number') - codec.channels = 1; + if ( + orderedGiven && + params.ordered && + (params.maxPacketLifeTime || params.maxRetransmits) + ) { + throw new TypeError( + 'cannot be ordered with maxPacketLifeTime or maxRetransmits' + ); + } else if ( + !orderedGiven && + (params.maxPacketLifeTime || params.maxRetransmits) + ) { + params.ordered = false; } - else - { - delete codec.channels; +} + +/** + * Generate RTP capabilities for the Router based on the given media codecs and + * mediasoup supported RTP capabilities. + */ +export function generateRouterRtpCapabilities( + mediaCodecs: RtpCodecCapability[] = [] +): RtpCapabilities { + // Normalize supported RTP capabilities. + validateRtpCapabilities(supportedRtpCapabilities); + + if (!Array.isArray(mediaCodecs)) { + throw new TypeError('mediaCodecs must be an Array'); } - // parameters is optional. If unset, set it to an empty object. - if (!codec.parameters || typeof codec.parameters !== 'object') - codec.parameters = {}; + const clonedSupportedRtpCapabilities = utils.clone( + supportedRtpCapabilities + ); + const dynamicPayloadTypes = utils.clone(DynamicPayloadTypes); + const caps: RtpCapabilities = { + codecs: [], + headerExtensions: clonedSupportedRtpCapabilities.headerExtensions, + }; - for (const key of Object.keys(codec.parameters)) - { - let value = codec.parameters[key]; + for (const mediaCodec of mediaCodecs) { + // This may throw. + validateRtpCodecCapability(mediaCodec); - if (value === undefined) - { - codec.parameters[key] = ''; - value = ''; - } + const matchedSupportedCodec = clonedSupportedRtpCapabilities.codecs!.find( + supportedCodec => + matchCodecs(mediaCodec, supportedCodec, { strict: false }) + ); - if (typeof value !== 'string' && typeof value !== 'number') - { - throw new TypeError( - `invalid codec parameter [key:${key}s, value:${value}]`); + if (!matchedSupportedCodec) { + throw new UnsupportedError( + `media codec not supported [mimeType:${mediaCodec.mimeType}]` + ); } - // Specific parameters validation. - if (key === 'apt') - { - if (typeof value !== 'number') - throw new TypeError('invalid codec apt parameter'); - } - } + // Clone the supported codec. + const codec = utils.clone(matchedSupportedCodec); - // rtcpFeedback is optional. If unset, set it to an empty array. - if (!codec.rtcpFeedback || !Array.isArray(codec.rtcpFeedback)) - codec.rtcpFeedback = []; + // If the given media codec has preferredPayloadType, keep it. + if (typeof mediaCodec.preferredPayloadType === 'number') { + codec.preferredPayloadType = mediaCodec.preferredPayloadType; - for (const fb of codec.rtcpFeedback) - { - validateRtcpFeedback(fb); - } -} + // Also remove the pt from the list of available dynamic values. + const idx = dynamicPayloadTypes.indexOf(codec.preferredPayloadType); -/** - * Validates RtpHeaderExtensionParameteters. It may modify given data by adding missing - * fields with default values. - * It throws if invalid. - */ -export function validateRtpHeaderExtensionParameters( - ext: RtpHeaderExtensionParameters -): void -{ + if (idx > -1) { + dynamicPayloadTypes.splice(idx, 1); + } + } + // Otherwise if the supported codec has preferredPayloadType, use it. + else if (typeof codec.preferredPayloadType === 'number') { + // No need to remove it from the list since it's not a dynamic value. + } + // Otherwise choose a dynamic one. + else { + // Take the first available pt and remove it from the list. + const pt = dynamicPayloadTypes.shift(); - if (typeof ext !== 'object') - throw new TypeError('ext is not an object'); + if (!pt) { + throw new Error('cannot allocate more dynamic codec payload types'); + } - // uri is mandatory. - if (!ext.uri || typeof ext.uri !== 'string') - throw new TypeError('missing ext.uri'); + codec.preferredPayloadType = pt; + } - // id is mandatory. - if (typeof ext.id !== 'number') - throw new TypeError('missing ext.id'); + // Ensure there is not duplicated preferredPayloadType values. + if ( + caps.codecs!.some( + c => c.preferredPayloadType === codec.preferredPayloadType + ) + ) { + throw new TypeError('duplicated codec.preferredPayloadType'); + } - // encrypt is optional. If unset set it to false. - if (ext.encrypt && typeof ext.encrypt !== 'boolean') - throw new TypeError('invalid ext.encrypt'); - else if (!ext.encrypt) - ext.encrypt = false; + // Merge the media codec parameters. + codec.parameters = { ...codec.parameters, ...mediaCodec.parameters }; - // parameters is optional. If unset, set it to an empty object. - if (!ext.parameters || typeof ext.parameters !== 'object') - ext.parameters = {}; + // Append to the codec list. + caps.codecs!.push(codec); - for (const key of Object.keys(ext.parameters)) - { - let value = ext.parameters[key]; + // Add a RTX video codec if video. + if (codec.kind === 'video') { + // Take the first available pt and remove it from the list. + const pt = dynamicPayloadTypes.shift(); - if (value === undefined) - { - ext.parameters[key] = ''; - value = ''; - } + if (!pt) { + throw new Error('cannot allocate more dynamic codec payload types'); + } - if (typeof value !== 'string' && typeof value !== 'number') - throw new TypeError('invalid header extension parameter'); + const rtxCodec: RtpCodecCapability = { + kind: codec.kind, + mimeType: `${codec.kind}/rtx`, + preferredPayloadType: pt, + clockRate: codec.clockRate, + parameters: { + apt: codec.preferredPayloadType, + }, + rtcpFeedback: [], + }; + + // Append to the codec list. + caps.codecs!.push(rtxCodec); + } } + + return caps; } /** - * Validates RtpEncodingParameters. It may modify given data by adding missing - * fields with default values. - * It throws if invalid. + * Get a mapping of codec payloads and encodings of the given Producer RTP + * parameters as values expected by the Router. + * + * It may throw if invalid or non supported RTP parameters are given. */ -export function validateRtpEncodingParameters(encoding: RtpEncodingParameters): void -{ - if (typeof encoding !== 'object') - throw new TypeError('encoding is not an object'); - - // ssrc is optional. - if (encoding.ssrc && typeof encoding.ssrc !== 'number') - throw new TypeError('invalid encoding.ssrc'); - - // rid is optional. - if (encoding.rid && typeof encoding.rid !== 'string') - throw new TypeError('invalid encoding.rid'); - - // rtx is optional. - if (encoding.rtx && typeof encoding.rtx !== 'object') - { - throw new TypeError('invalid encoding.rtx'); - } - else if (encoding.rtx) - { - // RTX ssrc is mandatory if rtx is present. - if (typeof encoding.rtx.ssrc !== 'number') - throw new TypeError('missing encoding.rtx.ssrc'); - } - - // dtx is optional. If unset set it to false. - if (!encoding.dtx || typeof encoding.dtx !== 'boolean') - encoding.dtx = false; - - // scalabilityMode is optional. - if (encoding.scalabilityMode && typeof encoding.scalabilityMode !== 'string') - throw new TypeError('invalid encoding.scalabilityMode'); -} - -/** - * Validates RtcpParameters. It may modify given data by adding missing - * fields with default values. - * It throws if invalid. - */ -export function validateRtcpParameters(rtcp: RtcpParameters): void -{ - if (typeof rtcp !== 'object') - throw new TypeError('rtcp is not an object'); - - // cname is optional. - if (rtcp.cname && typeof rtcp.cname !== 'string') - throw new TypeError('invalid rtcp.cname'); - - // reducedSize is optional. If unset set it to true. - if (!rtcp.reducedSize || typeof rtcp.reducedSize !== 'boolean') - rtcp.reducedSize = true; -} - -/** - * Validates SctpCapabilities. It may modify given data by adding missing - * fields with default values. - * It throws if invalid. - */ -export function validateSctpCapabilities(caps: SctpCapabilities): void -{ - if (typeof caps !== 'object') - throw new TypeError('caps is not an object'); - - // numStreams is mandatory. - if (!caps.numStreams || typeof caps.numStreams !== 'object') - throw new TypeError('missing caps.numStreams'); - - validateNumSctpStreams(caps.numStreams); -} - -/** - * Validates NumSctpStreams. It may modify given data by adding missing - * fields with default values. - * It throws if invalid. - */ -export function validateNumSctpStreams(numStreams: NumSctpStreams): void -{ - if (typeof numStreams !== 'object') - throw new TypeError('numStreams is not an object'); - - // OS is mandatory. - if (typeof numStreams.OS !== 'number') - throw new TypeError('missing numStreams.OS'); - - // MIS is mandatory. - if (typeof numStreams.MIS !== 'number') - throw new TypeError('missing numStreams.MIS'); -} - -/** - * Validates SctpParameters. It may modify given data by adding missing - * fields with default values. - * It throws if invalid. - */ -export function validateSctpParameters(params: SctpParameters): void -{ - if (typeof params !== 'object') - throw new TypeError('params is not an object'); - - // port is mandatory. - if (typeof params.port !== 'number') - throw new TypeError('missing params.port'); - - // OS is mandatory. - if (typeof params.OS !== 'number') - throw new TypeError('missing params.OS'); - - // MIS is mandatory. - if (typeof params.MIS !== 'number') - throw new TypeError('missing params.MIS'); - - // maxMessageSize is mandatory. - if (typeof params.maxMessageSize !== 'number') - throw new TypeError('missing params.maxMessageSize'); -} - -/** - * Validates SctpStreamParameters. It may modify given data by adding missing - * fields with default values. - * It throws if invalid. - */ -export function validateSctpStreamParameters(params: SctpStreamParameters): void -{ - if (typeof params !== 'object') - throw new TypeError('params is not an object'); - - // streamId is mandatory. - if (typeof params.streamId !== 'number') - throw new TypeError('missing params.streamId'); - - // ordered is optional. - let orderedGiven = false; - - if (typeof params.ordered === 'boolean') - orderedGiven = true; - else - params.ordered = true; - - // maxPacketLifeTime is optional. - if (params.maxPacketLifeTime && typeof params.maxPacketLifeTime !== 'number') - throw new TypeError('invalid params.maxPacketLifeTime'); - - // maxRetransmits is optional. - if (params.maxRetransmits && typeof params.maxRetransmits !== 'number') - throw new TypeError('invalid params.maxRetransmits'); - - if (params.maxPacketLifeTime && params.maxRetransmits) - throw new TypeError('cannot provide both maxPacketLifeTime and maxRetransmits'); - - if ( - orderedGiven && - params.ordered && - (params.maxPacketLifeTime || params.maxRetransmits) - ) - { - throw new TypeError('cannot be ordered with maxPacketLifeTime or maxRetransmits'); - } - else if (!orderedGiven && (params.maxPacketLifeTime || params.maxRetransmits)) - { - params.ordered = false; - } -} - -/** - * Generate RTP capabilities for the Router based on the given media codecs and - * mediasoup supported RTP capabilities. - */ -export function generateRouterRtpCapabilities( - mediaCodecs: RtpCodecCapability[] = [] -): RtpCapabilities -{ - // Normalize supported RTP capabilities. - validateRtpCapabilities(supportedRtpCapabilities); - - if (!Array.isArray(mediaCodecs)) - throw new TypeError('mediaCodecs must be an Array'); - - const clonedSupportedRtpCapabilities = - utils.clone(supportedRtpCapabilities) as RtpCapabilities; - const dynamicPayloadTypes = utils.clone(DynamicPayloadTypes) as number[]; - const caps: RtpCapabilities = - { - codecs : [], - headerExtensions : clonedSupportedRtpCapabilities.headerExtensions - }; - - for (const mediaCodec of mediaCodecs) - { - // This may throw. - validateRtpCodecCapability(mediaCodec); - - const matchedSupportedCodec = clonedSupportedRtpCapabilities - .codecs! - .find((supportedCodec) => ( - matchCodecs(mediaCodec, supportedCodec, { strict: false })) - ); - - if (!matchedSupportedCodec) - { - throw new UnsupportedError( - `media codec not supported [mimeType:${mediaCodec.mimeType}]`); - } - - // Clone the supported codec. - const codec = utils.clone(matchedSupportedCodec) as RtpCodecCapability; - - // If the given media codec has preferredPayloadType, keep it. - if (typeof mediaCodec.preferredPayloadType === 'number') - { - codec.preferredPayloadType = mediaCodec.preferredPayloadType; - - // Also remove the pt from the list of available dynamic values. - const idx = dynamicPayloadTypes.indexOf(codec.preferredPayloadType); - - if (idx > -1) - dynamicPayloadTypes.splice(idx, 1); - } - // Otherwise if the supported codec has preferredPayloadType, use it. - else if (typeof codec.preferredPayloadType === 'number') - { - // No need to remove it from the list since it's not a dynamic value. - } - // Otherwise choose a dynamic one. - else - { - // Take the first available pt and remove it from the list. - const pt = dynamicPayloadTypes.shift(); - - if (!pt) - throw new Error('cannot allocate more dynamic codec payload types'); - - codec.preferredPayloadType = pt; - } - - // Ensure there is not duplicated preferredPayloadType values. - if (caps.codecs!.some((c) => c.preferredPayloadType === codec.preferredPayloadType)) - throw new TypeError('duplicated codec.preferredPayloadType'); - - // Merge the media codec parameters. - codec.parameters = { ...codec.parameters, ...mediaCodec.parameters }; - - // Append to the codec list. - caps.codecs!.push(codec); - - // Add a RTX video codec if video. - if (codec.kind === 'video') - { - // Take the first available pt and remove it from the list. - const pt = dynamicPayloadTypes.shift(); - - if (!pt) - throw new Error('cannot allocate more dynamic codec payload types'); - - const rtxCodec: RtpCodecCapability = - { - kind : codec.kind, - mimeType : `${codec.kind}/rtx`, - preferredPayloadType : pt, - clockRate : codec.clockRate, - parameters : - { - apt : codec.preferredPayloadType - }, - rtcpFeedback : [] - }; - - // Append to the codec list. - caps.codecs!.push(rtxCodec); - } - } - - return caps; -} - -/** - * Get a mapping of codec payloads and encodings of the given Producer RTP - * parameters as values expected by the Router. - * - * It may throw if invalid or non supported RTP parameters are given. - */ -export function getProducerRtpParametersMapping( - params: RtpParameters, - caps: RtpCapabilities -): RtpMapping -{ - const rtpMapping: RtpMapping = - { - codecs : [], - encodings : [] - }; +export function getProducerRtpParametersMapping( + params: RtpParameters, + caps: RtpCapabilities +): RtpCodecsEncodingsMapping { + const rtpMapping: RtpCodecsEncodingsMapping = { + codecs: [], + encodings: [], + }; // Match parameters media codecs to capabilities media codecs. - const codecToCapCodec: Map = new Map(); + const codecToCapCodec: Map = + new Map(); - for (const codec of params.codecs) - { - if (isRtxCodec(codec)) + for (const codec of params.codecs) { + if (isRtxCodec(codec)) { continue; + } // Search for the same media codec in capabilities. - const matchedCapCodec = caps.codecs! - .find((capCodec) => ( - matchCodecs(codec, capCodec, { strict: true, modify: true })) - ); + const matchedCapCodec = caps.codecs!.find(capCodec => + matchCodecs(codec, capCodec, { strict: true, modify: true }) + ); - if (!matchedCapCodec) - { + if (!matchedCapCodec) { throw new UnsupportedError( - `unsupported codec [mimeType:${codec.mimeType}, payloadType:${codec.payloadType}]`); + `unsupported codec [mimeType:${codec.mimeType}, payloadType:${codec.payloadType}]` + ); } codecToCapCodec.set(codec, matchedCapCodec); } // Match parameters RTX codecs to capabilities RTX codecs. - for (const codec of params.codecs) - { - if (!isRtxCodec(codec)) + for (const codec of params.codecs) { + if (!isRtxCodec(codec)) { continue; + } // Search for the associated media codec. - const associatedMediaCodec = params.codecs - .find((mediaCodec) => mediaCodec.payloadType === codec.parameters.apt); + const associatedMediaCodec = params.codecs.find( + mediaCodec => mediaCodec.payloadType === codec.parameters.apt + ); - if (!associatedMediaCodec) - { + if (!associatedMediaCodec) { throw new TypeError( - `missing media codec found for RTX PT ${codec.payloadType}`); + `missing media codec found for RTX PT ${codec.payloadType}` + ); } const capMediaCodec = codecToCapCodec.get(associatedMediaCodec); // Ensure that the capabilities media codec has a RTX codec. - const associatedCapRtxCodec = caps.codecs! - .find((capCodec) => ( + const associatedCapRtxCodec = caps.codecs!.find( + capCodec => isRtxCodec(capCodec) && capCodec.parameters.apt === capMediaCodec!.preferredPayloadType - )); + ); - if (!associatedCapRtxCodec) - { + if (!associatedCapRtxCodec) { throw new UnsupportedError( - `no RTX codec for capability codec PT ${capMediaCodec!.preferredPayloadType}`); + `no RTX codec for capability codec PT ${ + capMediaCodec!.preferredPayloadType + }` + ); } codecToCapCodec.set(codec, associatedCapRtxCodec); } // Generate codecs mapping. - for (const [ codec, capCodec ] of codecToCapCodec) - { - rtpMapping.codecs.push( - { - payloadType : codec.payloadType, - mappedPayloadType : capCodec.preferredPayloadType! - }); + for (const [codec, capCodec] of codecToCapCodec) { + rtpMapping.codecs.push({ + payloadType: codec.payloadType, + mappedPayloadType: capCodec.preferredPayloadType!, + }); } // Generate encodings mapping. let mappedSsrc = utils.generateRandomNumber(); - for (const encoding of params.encodings!) - { + for (const encoding of params.encodings!) { const mappedEncoding: any = {}; mappedEncoding.mappedSsrc = mappedSsrc++; - if (encoding.rid) + if (encoding.rid) { mappedEncoding.rid = encoding.rid; - if (encoding.ssrc) + } + if (encoding.ssrc) { mappedEncoding.ssrc = encoding.ssrc; - if (encoding.scalabilityMode) + } + if (encoding.scalabilityMode) { mappedEncoding.scalabilityMode = encoding.scalabilityMode; + } rtpMapping.encodings.push(mappedEncoding); } @@ -787,90 +414,82 @@ export function getConsumableRtpParameters( kind: string, params: RtpParameters, caps: RtpCapabilities, - rtpMapping: RtpMapping -): RtpParameters -{ - const consumableParams: RtpParameters = - { - codecs : [], - headerExtensions : [], - encodings : [], - rtcp : {} + rtpMapping: RtpCodecsEncodingsMapping +): RtpParameters { + const consumableParams: RtpParameters = { + codecs: [], + headerExtensions: [], + encodings: [], + rtcp: {}, }; - for (const codec of params.codecs) - { - if (isRtxCodec(codec)) + for (const codec of params.codecs) { + if (isRtxCodec(codec)) { continue; + } - const consumableCodecPt = rtpMapping.codecs - .find((entry) => entry.payloadType === codec.payloadType)! - .mappedPayloadType; - - const matchedCapCodec = caps.codecs! - .find((capCodec) => capCodec.preferredPayloadType === consumableCodecPt)!; - - const consumableCodec: RtpCodecParameters = - { - mimeType : matchedCapCodec.mimeType, - payloadType : matchedCapCodec.preferredPayloadType!, - clockRate : matchedCapCodec.clockRate, - channels : matchedCapCodec.channels, - parameters : codec.parameters, // Keep the Producer codec parameters. - rtcpFeedback : matchedCapCodec.rtcpFeedback + const consumableCodecPt = rtpMapping.codecs.find( + entry => entry.payloadType === codec.payloadType + )!.mappedPayloadType; + + const matchedCapCodec = caps.codecs!.find( + capCodec => capCodec.preferredPayloadType === consumableCodecPt + )!; + + const consumableCodec: RtpCodecParameters = { + mimeType: matchedCapCodec.mimeType, + payloadType: matchedCapCodec.preferredPayloadType!, + clockRate: matchedCapCodec.clockRate, + channels: matchedCapCodec.channels, + parameters: codec.parameters, // Keep the Producer codec parameters. + rtcpFeedback: matchedCapCodec.rtcpFeedback, }; consumableParams.codecs.push(consumableCodec); - const consumableCapRtxCodec = caps.codecs! - .find((capRtxCodec) => ( + const consumableCapRtxCodec = caps.codecs!.find( + capRtxCodec => isRtxCodec(capRtxCodec) && capRtxCodec.parameters.apt === consumableCodec.payloadType - )); - - if (consumableCapRtxCodec) - { - const consumableRtxCodec: RtpCodecParameters = - { - mimeType : consumableCapRtxCodec.mimeType, - payloadType : consumableCapRtxCodec.preferredPayloadType!, - clockRate : consumableCapRtxCodec.clockRate, - parameters : consumableCapRtxCodec.parameters, - rtcpFeedback : consumableCapRtxCodec.rtcpFeedback + ); + + if (consumableCapRtxCodec) { + const consumableRtxCodec: RtpCodecParameters = { + mimeType: consumableCapRtxCodec.mimeType, + payloadType: consumableCapRtxCodec.preferredPayloadType!, + clockRate: consumableCapRtxCodec.clockRate, + parameters: consumableCapRtxCodec.parameters, + rtcpFeedback: consumableCapRtxCodec.rtcpFeedback, }; consumableParams.codecs.push(consumableRtxCodec); } } - for (const capExt of caps.headerExtensions!) - { - + for (const capExt of caps.headerExtensions!) { // Just take RTP header extension that can be used in Consumers. if ( capExt.kind !== kind || (capExt.direction !== 'sendrecv' && capExt.direction !== 'sendonly') - ) - { + ) { continue; } - const consumableExt = - { - uri : capExt.uri, - id : capExt.preferredId, - encrypt : capExt.preferredEncrypt, - parameters : {} + const consumableExt = { + uri: capExt.uri, + id: capExt.preferredId, + encrypt: capExt.preferredEncrypt, + parameters: {}, }; consumableParams.headerExtensions!.push(consumableExt); } // Clone Producer encodings since we'll mangle them. - const consumableEncodings = utils.clone(params.encodings) as RtpEncodingParameters[]; + const consumableEncodings = + utils.clone(params.encodings) ?? []; - for (let i = 0; i < consumableEncodings.length; ++i) - { + for (let i = 0; i < consumableEncodings.length; ++i) { const consumableEncoding = consumableEncodings[i]; const { mappedSsrc } = rtpMapping.encodings[i]; @@ -885,11 +504,9 @@ export function getConsumableRtpParameters( consumableParams.encodings!.push(consumableEncoding); } - consumableParams.rtcp = - { - cname : params.rtcp!.cname, - reducedSize : true, - mux : true + consumableParams.rtcp = { + cname: params.rtcp!.cname, + reducedSize: true, }; return consumableParams; @@ -901,27 +518,28 @@ export function getConsumableRtpParameters( export function canConsume( consumableParams: RtpParameters, caps: RtpCapabilities -): boolean -{ +): boolean { // This may throw. validateRtpCapabilities(caps); const matchingCodecs: RtpCodecParameters[] = []; - for (const codec of consumableParams.codecs) - { - const matchedCapCodec = caps.codecs! - .find((capCodec) => matchCodecs(capCodec, codec, { strict: true })); + for (const codec of consumableParams.codecs) { + const matchedCapCodec = caps.codecs!.find(capCodec => + matchCodecs(capCodec, codec, { strict: true }) + ); - if (!matchedCapCodec) + if (!matchedCapCodec) { continue; + } matchingCodecs.push(codec); } // Ensure there is at least one media codec. - if (matchingCodecs.length === 0 || isRtxCodec(matchingCodecs[0])) + if (matchingCodecs.length === 0 || isRtxCodec(matchingCodecs[0])) { return false; + } return true; } @@ -929,181 +547,190 @@ export function canConsume( /** * Generate RTP parameters for a specific Consumer. * - * It reduces encodings to just one and takes into account given RTP capabilities - * to reduce codecs, codecs' RTCP feedback and header extensions, and also enables - * or disabled RTX. + * It reduces encodings to just one and takes into account given RTP + * capabilities to reduce codecs, codecs' RTCP feedback and header extensions, + * and also enables or disables RTX. */ -export function getConsumerRtpParameters( - consumableParams: RtpParameters, - caps: RtpCapabilities, - pipe: boolean -): RtpParameters -{ - const consumerParams: RtpParameters = - { - codecs : [], - headerExtensions : [], - encodings : [], - rtcp : consumableParams.rtcp +export function getConsumerRtpParameters({ + consumableRtpParameters, + remoteRtpCapabilities, + pipe, + enableRtx, +}: { + consumableRtpParameters: RtpParameters; + remoteRtpCapabilities: RtpCapabilities; + pipe: boolean; + enableRtx: boolean; +}): RtpParameters { + const consumerParams: RtpParameters = { + codecs: [], + headerExtensions: [], + encodings: [], + rtcp: consumableRtpParameters.rtcp, }; - for (const capCodec of caps.codecs!) - { + for (const capCodec of remoteRtpCapabilities.codecs!) { validateRtpCodecCapability(capCodec); } const consumableCodecs = - utils.clone(consumableParams.codecs) as RtpCodecParameters[]; + utils.clone( + consumableRtpParameters.codecs + ) ?? []; let rtxSupported = false; - for (const codec of consumableCodecs) - { - const matchedCapCodec = caps.codecs! - .find((capCodec) => matchCodecs(capCodec, codec, { strict: true })); + for (const codec of consumableCodecs) { + if (!enableRtx && isRtxCodec(codec)) { + continue; + } - if (!matchedCapCodec) + const matchedCapCodec = remoteRtpCapabilities.codecs!.find(capCodec => + matchCodecs(capCodec, codec, { strict: true }) + ); + + if (!matchedCapCodec) { continue; + } - codec.rtcpFeedback = matchedCapCodec.rtcpFeedback; + codec.rtcpFeedback = matchedCapCodec.rtcpFeedback!.filter( + fb => enableRtx || fb.type !== 'nack' || fb.parameter + ); consumerParams.codecs.push(codec); } // Must sanitize the list of matched codecs by removing useless RTX codecs. - for (let idx = consumerParams.codecs.length -1; idx >= 0; --idx) - { + for (let idx = consumerParams.codecs.length - 1; idx >= 0; --idx) { const codec = consumerParams.codecs[idx]; - if (isRtxCodec(codec)) - { + if (isRtxCodec(codec)) { // Search for the associated media codec. - const associatedMediaCodec = consumerParams.codecs - .find((mediaCodec) => mediaCodec.payloadType === codec.parameters.apt); + const associatedMediaCodec = consumerParams.codecs.find( + mediaCodec => mediaCodec.payloadType === codec.parameters.apt + ); - if (associatedMediaCodec) + if (associatedMediaCodec) { rtxSupported = true; - else + } else { consumerParams.codecs.splice(idx, 1); + } } } // Ensure there is at least one media codec. - if (consumerParams.codecs.length === 0 || isRtxCodec(consumerParams.codecs[0])) - { + if ( + consumerParams.codecs.length === 0 || + isRtxCodec(consumerParams.codecs[0]) + ) { throw new UnsupportedError('no compatible media codecs'); } - consumerParams.headerExtensions = consumableParams.headerExtensions! - .filter((ext) => ( - caps.headerExtensions! - .some((capExt) => ( - capExt.preferredId === ext.id && - capExt.uri === ext.uri - )) - )); + consumerParams.headerExtensions = + consumableRtpParameters.headerExtensions!.filter(ext => + remoteRtpCapabilities.headerExtensions!.some( + capExt => capExt.preferredId === ext.id && capExt.uri === ext.uri + ) + ); // Reduce codecs' RTCP feedback. Use Transport-CC if available, REMB otherwise. if ( - consumerParams.headerExtensions.some((ext) => ( - ext.uri === 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01' - )) - ) - { - for (const codec of consumerParams.codecs) - { - codec.rtcpFeedback = codec.rtcpFeedback! - .filter((fb) => fb.type !== 'goog-remb'); + consumerParams.headerExtensions.some( + ext => + ext.uri === + 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01' + ) + ) { + for (const codec of consumerParams.codecs) { + codec.rtcpFeedback = codec.rtcpFeedback!.filter( + fb => fb.type !== 'goog-remb' + ); } - } - else if ( - consumerParams.headerExtensions.some((ext) => ( - ext.uri === 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time' - )) - ) - { - for (const codec of consumerParams.codecs) - { - codec.rtcpFeedback = codec.rtcpFeedback! - .filter((fb) => fb.type !== 'transport-cc'); + } else if ( + consumerParams.headerExtensions.some( + ext => + ext.uri === 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time' + ) + ) { + for (const codec of consumerParams.codecs) { + codec.rtcpFeedback = codec.rtcpFeedback!.filter( + fb => fb.type !== 'transport-cc' + ); } - } - else - { - for (const codec of consumerParams.codecs) - { - codec.rtcpFeedback = codec.rtcpFeedback! - .filter((fb) => ( - fb.type !== 'transport-cc' && - fb.type !== 'goog-remb' - )); + } else { + for (const codec of consumerParams.codecs) { + codec.rtcpFeedback = codec.rtcpFeedback!.filter( + fb => fb.type !== 'transport-cc' && fb.type !== 'goog-remb' + ); } } - if (!pipe) - { - const consumerEncoding: RtpEncodingParameters = - { - ssrc : utils.generateRandomNumber() + if (!pipe) { + const consumerEncoding: RtpEncodingParameters = { + ssrc: utils.generateRandomNumber(), }; - if (rtxSupported) + if (rtxSupported) { consumerEncoding.rtx = { ssrc: consumerEncoding.ssrc! + 1 }; + } - // If any of the consumableParams.encodings has scalabilityMode, process it - // (assume all encodings have the same value). - const encodingWithScalabilityMode = - consumableParams.encodings!.find((encoding) => encoding.scalabilityMode); + // If any of the consumableRtpParameters.encodings has scalabilityMode, + // process it (assume all encodings have the same value). + const encodingWithScalabilityMode = consumableRtpParameters.encodings!.find( + encoding => encoding.scalabilityMode + ); let scalabilityMode = encodingWithScalabilityMode ? encodingWithScalabilityMode.scalabilityMode : undefined; // If there is simulast, mangle spatial layers in scalabilityMode. - if (consumableParams.encodings!.length > 1) - { + if (consumableRtpParameters.encodings!.length > 1) { const { temporalLayers } = parseScalabilityMode(scalabilityMode); - scalabilityMode = `S${consumableParams.encodings!.length}T${temporalLayers}`; + scalabilityMode = `L${ + consumableRtpParameters.encodings!.length + }T${temporalLayers}`; } - if (scalabilityMode) + if (scalabilityMode) { consumerEncoding.scalabilityMode = scalabilityMode; + } // Use the maximum maxBitrate in any encoding and honor it in the Consumer's // encoding. - const maxEncodingMaxBitrate = - consumableParams.encodings!.reduce((maxBitrate, encoding) => ( + const maxEncodingMaxBitrate = consumableRtpParameters.encodings!.reduce( + (maxBitrate, encoding) => encoding.maxBitrate && encoding.maxBitrate > maxBitrate ? encoding.maxBitrate - : maxBitrate - ), 0); + : maxBitrate, + 0 + ); - if (maxEncodingMaxBitrate) - { + if (maxEncodingMaxBitrate) { consumerEncoding.maxBitrate = maxEncodingMaxBitrate; } // Set a single encoding for the Consumer. consumerParams.encodings!.push(consumerEncoding); - } - else - { + } else { const consumableEncodings = - utils.clone(consumableParams.encodings) as RtpEncodingParameters[]; + utils.clone( + consumableRtpParameters.encodings + ) ?? []; const baseSsrc = utils.generateRandomNumber(); const baseRtxSsrc = utils.generateRandomNumber(); - for (let i = 0; i < consumableEncodings.length; ++i) - { + for (let i = 0; i < consumableEncodings.length; ++i) { const encoding = consumableEncodings[i]; encoding.ssrc = baseSsrc + i; - if (rtxSupported) + if (rtxSupported) { encoding.rtx = { ssrc: baseRtxSsrc + i }; - else + } else { delete encoding.rtx; + } consumerParams.encodings!.push(encoding); } @@ -1118,60 +745,68 @@ export function getConsumerRtpParameters( * It keeps all original consumable encodings and removes support for BWE. If * enableRtx is false, it also removes RTX and NACK support. */ -export function getPipeConsumerRtpParameters( - consumableParams: RtpParameters, - enableRtx = false -): RtpParameters -{ - const consumerParams: RtpParameters = - { - codecs : [], - headerExtensions : [], - encodings : [], - rtcp : consumableParams.rtcp +export function getPipeConsumerRtpParameters({ + consumableRtpParameters, + enableRtx, +}: { + consumableRtpParameters: RtpParameters; + enableRtx: boolean; +}): RtpParameters { + const consumerParams: RtpParameters = { + codecs: [], + headerExtensions: [], + encodings: [], + rtcp: consumableRtpParameters.rtcp, }; const consumableCodecs = - utils.clone(consumableParams.codecs) as RtpCodecParameters[]; + utils.clone( + consumableRtpParameters.codecs + ) ?? []; - for (const codec of consumableCodecs) - { - if (!enableRtx && isRtxCodec(codec)) + for (const codec of consumableCodecs) { + if (!enableRtx && isRtxCodec(codec)) { continue; + } - codec.rtcpFeedback = codec.rtcpFeedback! - .filter((fb) => ( + codec.rtcpFeedback = codec.rtcpFeedback!.filter( + fb => (fb.type === 'nack' && fb.parameter === 'pli') || (fb.type === 'ccm' && fb.parameter === 'fir') || (enableRtx && fb.type === 'nack' && !fb.parameter) - )); + ); consumerParams.codecs.push(codec); } // Reduce RTP extensions by disabling transport MID and BWE related ones. - consumerParams.headerExtensions = consumableParams.headerExtensions! - .filter((ext) => ( - ext.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid' && - ext.uri !== 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time' && - ext.uri !== 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01' - )); + consumerParams.headerExtensions = + consumableRtpParameters.headerExtensions!.filter( + ext => + ext.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid' && + ext.uri !== + 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time' && + ext.uri !== + 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01' + ); const consumableEncodings = - utils.clone(consumableParams.encodings) as RtpEncodingParameters[]; + utils.clone( + consumableRtpParameters.encodings + ) ?? []; const baseSsrc = utils.generateRandomNumber(); const baseRtxSsrc = utils.generateRandomNumber(); - for (let i = 0; i < consumableEncodings.length; ++i) - { + for (let i = 0; i < consumableEncodings.length; ++i) { const encoding = consumableEncodings[i]; encoding.ssrc = baseSsrc + i; - if (enableRtx) + if (enableRtx) { encoding.rtx = { ssrc: baseRtxSsrc + i }; - else + } else { delete encoding.rtx; + } consumerParams.encodings!.push(encoding); } @@ -1179,8 +814,7 @@ export function getPipeConsumerRtpParameters( return consumerParams; } -function isRtxCodec(codec: RtpCodecCapability | RtpCodecParameters): boolean -{ +function isRtxCodec(codec: RtpCodecCapability | RtpCodecParameters): boolean { return /.+\/rtx$/i.test(codec.mimeType); } @@ -1188,87 +822,87 @@ function matchCodecs( aCodec: RtpCodecCapability | RtpCodecParameters, bCodec: RtpCodecCapability | RtpCodecParameters, { strict = false, modify = false } = {} -): boolean -{ +): boolean { const aMimeType = aCodec.mimeType.toLowerCase(); const bMimeType = bCodec.mimeType.toLowerCase(); - if (aMimeType !== bMimeType) + if (aMimeType !== bMimeType) { return false; + } - if (aCodec.clockRate !== bCodec.clockRate) + if (aCodec.clockRate !== bCodec.clockRate) { return false; + } - if (aCodec.channels !== bCodec.channels) + if (aCodec.channels !== bCodec.channels) { return false; + } // Per codec special checks. - switch (aMimeType) - { - case 'audio/multiopus': - { + switch (aMimeType) { + case 'audio/multiopus': { const aNumStreams = aCodec.parameters['num_streams']; const bNumStreams = bCodec.parameters['num_streams']; - if (aNumStreams !== bNumStreams) + if (aNumStreams !== bNumStreams) { return false; + } const aCoupledStreams = aCodec.parameters['coupled_streams']; const bCoupledStreams = bCodec.parameters['coupled_streams']; - if (aCoupledStreams !== bCoupledStreams) + if (aCoupledStreams !== bCoupledStreams) { return false; + } break; } case 'video/h264': - case 'video/h264-svc': - { - if (strict) - { + case 'video/h264-svc': { + if (strict) { const aPacketizationMode = aCodec.parameters['packetization-mode'] || 0; const bPacketizationMode = bCodec.parameters['packetization-mode'] || 0; - if (aPacketizationMode !== bPacketizationMode) + if (aPacketizationMode !== bPacketizationMode) { return false; + } - if (!h264.isSameProfile(aCodec.parameters, bCodec.parameters)) + if (!h264.isSameProfile(aCodec.parameters, bCodec.parameters)) { return false; + } let selectedProfileLevelId; - try - { - selectedProfileLevelId = - h264.generateProfileLevelIdForAnswer(aCodec.parameters, bCodec.parameters); - } - catch (error) - { + try { + selectedProfileLevelId = h264.generateProfileLevelIdStringForAnswer( + aCodec.parameters, + bCodec.parameters + ); + } catch (error) { return false; } - if (modify) - { - if (selectedProfileLevelId) + if (modify) { + if (selectedProfileLevelId) { aCodec.parameters['profile-level-id'] = selectedProfileLevelId; - else + } else { delete aCodec.parameters['profile-level-id']; + } } } break; } - case 'video/vp9': - { - if (strict) - { + case 'video/vp9': { + if (strict) { const aProfileId = aCodec.parameters['profile-id'] || 0; const bProfileId = bCodec.parameters['profile-id'] || 0; - if (aProfileId !== bProfileId) + if (aProfileId !== bProfileId) { return false; + } } break; @@ -1277,3 +911,387 @@ function matchCodecs( return true; } + +export function serializeRtpMapping( + builder: flatbuffers.Builder, + rtpMapping: RtpCodecsEncodingsMapping +): number { + const codecs: number[] = []; + + for (const codec of rtpMapping.codecs) { + codecs.push( + FbsRtpParameters.CodecMapping.createCodecMapping( + builder, + codec.payloadType, + codec.mappedPayloadType + ) + ); + } + const codecsOffset = FbsRtpParameters.RtpMapping.createCodecsVector( + builder, + codecs + ); + + const encodings: number[] = []; + + for (const encoding of rtpMapping.encodings) { + encodings.push( + FbsRtpParameters.EncodingMapping.createEncodingMapping( + builder, + builder.createString(encoding.rid), + encoding.ssrc ?? null, + builder.createString(encoding.scalabilityMode), + encoding.mappedSsrc + ) + ); + } + + const encodingsOffset = FbsRtpParameters.RtpMapping.createEncodingsVector( + builder, + encodings + ); + + return FbsRtpParameters.RtpMapping.createRtpMapping( + builder, + codecsOffset, + encodingsOffset + ); +} + +/** + * Validates RtpCodecCapability. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtpCodecCapability(codec: RtpCodecCapability): void { + const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i'); + + if (typeof codec !== 'object') { + throw new TypeError('codec is not an object'); + } + + // mimeType is mandatory. + if (!codec.mimeType || typeof codec.mimeType !== 'string') { + throw new TypeError('missing codec.mimeType'); + } + + const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType); + + if (!mimeTypeMatch) { + throw new TypeError('invalid codec.mimeType'); + } + + // Just override kind with media component of mimeType. + codec.kind = mimeTypeMatch[1].toLowerCase() as MediaKind; + + // preferredPayloadType is optional. + if ( + codec.preferredPayloadType && + typeof codec.preferredPayloadType !== 'number' + ) { + throw new TypeError('invalid codec.preferredPayloadType'); + } + + // clockRate is mandatory. + if (typeof codec.clockRate !== 'number') { + throw new TypeError('missing codec.clockRate'); + } + + // channels is optional. If unset, set it to 1 (just if audio). + if (codec.kind === 'audio') { + if (typeof codec.channels !== 'number') { + codec.channels = 1; + } + } else { + delete codec.channels; + } + + // parameters is optional. If unset, set it to an empty object. + if (!codec.parameters || typeof codec.parameters !== 'object') { + codec.parameters = {}; + } + + for (const key of Object.keys(codec.parameters)) { + let value = codec.parameters[key]; + + if (value === undefined) { + codec.parameters[key] = ''; + value = ''; + } + + if (typeof value !== 'string' && typeof value !== 'number') { + throw new TypeError( + `invalid codec parameter [key:${key}s, value:${value}]` + ); + } + + // Specific parameters validation. + if (key === 'apt') { + if (typeof value !== 'number') { + throw new TypeError('invalid codec apt parameter'); + } + } + } + + // rtcpFeedback is optional. If unset, set it to an empty array. + if (!codec.rtcpFeedback || !Array.isArray(codec.rtcpFeedback)) { + codec.rtcpFeedback = []; + } + + for (const fb of codec.rtcpFeedback) { + validateRtcpFeedback(fb); + } +} + +/** + * Validates RtcpFeedback. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtcpFeedback(fb: RtcpFeedback): void { + if (typeof fb !== 'object') { + throw new TypeError('fb is not an object'); + } + + // type is mandatory. + if (!fb.type || typeof fb.type !== 'string') { + throw new TypeError('missing fb.type'); + } + + // parameter is optional. If unset set it to an empty string. + if (!fb.parameter || typeof fb.parameter !== 'string') { + fb.parameter = ''; + } +} + +/** + * Validates RtpHeaderExtension. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtpHeaderExtension(ext: RtpHeaderExtension): void { + if (typeof ext !== 'object') { + throw new TypeError('ext is not an object'); + } + + if (ext.kind !== 'audio' && ext.kind !== 'video') { + throw new TypeError('invalid ext.kind'); + } + + // uri is mandatory. + if (!ext.uri || typeof ext.uri !== 'string') { + throw new TypeError('missing ext.uri'); + } + + // preferredId is mandatory. + if (typeof ext.preferredId !== 'number') { + throw new TypeError('missing ext.preferredId'); + } + + // preferredEncrypt is optional. If unset set it to false. + if (ext.preferredEncrypt && typeof ext.preferredEncrypt !== 'boolean') { + throw new TypeError('invalid ext.preferredEncrypt'); + } else if (!ext.preferredEncrypt) { + ext.preferredEncrypt = false; + } + + // direction is optional. If unset set it to sendrecv. + if (ext.direction && typeof ext.direction !== 'string') { + throw new TypeError('invalid ext.direction'); + } else if (!ext.direction) { + ext.direction = 'sendrecv'; + } +} + +/** + * Validates RtpCodecParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtpCodecParameters(codec: RtpCodecParameters): void { + const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i'); + + if (typeof codec !== 'object') { + throw new TypeError('codec is not an object'); + } + + // mimeType is mandatory. + if (!codec.mimeType || typeof codec.mimeType !== 'string') { + throw new TypeError('missing codec.mimeType'); + } + + const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType); + + if (!mimeTypeMatch) { + throw new TypeError('invalid codec.mimeType'); + } + + // payloadType is mandatory. + if (typeof codec.payloadType !== 'number') { + throw new TypeError('missing codec.payloadType'); + } + + // clockRate is mandatory. + if (typeof codec.clockRate !== 'number') { + throw new TypeError('missing codec.clockRate'); + } + + const kind = mimeTypeMatch[1].toLowerCase() as MediaKind; + + // channels is optional. If unset, set it to 1 (just if audio). + if (kind === 'audio') { + if (typeof codec.channels !== 'number') { + codec.channels = 1; + } + } else { + delete codec.channels; + } + + // parameters is optional. If unset, set it to an empty object. + if (!codec.parameters || typeof codec.parameters !== 'object') { + codec.parameters = {}; + } + + for (const key of Object.keys(codec.parameters)) { + let value = codec.parameters[key]; + + if (value === undefined) { + codec.parameters[key] = ''; + value = ''; + } + + if (typeof value !== 'string' && typeof value !== 'number') { + throw new TypeError( + `invalid codec parameter [key:${key}s, value:${value}]` + ); + } + + // Specific parameters validation. + if (key === 'apt') { + if (typeof value !== 'number') { + throw new TypeError('invalid codec apt parameter'); + } + } + } + + // rtcpFeedback is optional. If unset, set it to an empty array. + if (!codec.rtcpFeedback || !Array.isArray(codec.rtcpFeedback)) { + codec.rtcpFeedback = []; + } + + for (const fb of codec.rtcpFeedback) { + validateRtcpFeedback(fb); + } +} + +/** + * Validates RtpHeaderExtensionParameteters. It may modify given data by adding + * missing fields with default values. It throws if invalid. + */ +function validateRtpHeaderExtensionParameters( + ext: RtpHeaderExtensionParameters +): void { + if (typeof ext !== 'object') { + throw new TypeError('ext is not an object'); + } + + // uri is mandatory. + if (!ext.uri || typeof ext.uri !== 'string') { + throw new TypeError('missing ext.uri'); + } + + // id is mandatory. + if (typeof ext.id !== 'number') { + throw new TypeError('missing ext.id'); + } + + // encrypt is optional. If unset set it to false. + if (ext.encrypt && typeof ext.encrypt !== 'boolean') { + throw new TypeError('invalid ext.encrypt'); + } else if (!ext.encrypt) { + ext.encrypt = false; + } + + // parameters is optional. If unset, set it to an empty object. + if (!ext.parameters || typeof ext.parameters !== 'object') { + ext.parameters = {}; + } + + for (const key of Object.keys(ext.parameters)) { + let value = ext.parameters[key]; + + if (value === undefined) { + ext.parameters[key] = ''; + value = ''; + } + + if (typeof value !== 'string' && typeof value !== 'number') { + throw new TypeError('invalid header extension parameter'); + } + } +} + +/** + * Validates RtpEncodingParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtpEncodingParameters(encoding: RtpEncodingParameters): void { + if (typeof encoding !== 'object') { + throw new TypeError('encoding is not an object'); + } + + // ssrc is optional. + if (encoding.ssrc && typeof encoding.ssrc !== 'number') { + throw new TypeError('invalid encoding.ssrc'); + } + + // rid is optional. + if (encoding.rid && typeof encoding.rid !== 'string') { + throw new TypeError('invalid encoding.rid'); + } + + // rtx is optional. + if (encoding.rtx && typeof encoding.rtx !== 'object') { + throw new TypeError('invalid encoding.rtx'); + } else if (encoding.rtx) { + // RTX ssrc is mandatory if rtx is present. + if (typeof encoding.rtx.ssrc !== 'number') { + throw new TypeError('missing encoding.rtx.ssrc'); + } + } + + // dtx is optional. If unset set it to false. + if (!encoding.dtx || typeof encoding.dtx !== 'boolean') { + encoding.dtx = false; + } + + // scalabilityMode is optional. + if ( + encoding.scalabilityMode && + typeof encoding.scalabilityMode !== 'string' + ) { + throw new TypeError('invalid encoding.scalabilityMode'); + } +} + +/** + * Validates RtcpParameters. It may modify given data by adding missing + * fields with default values. + * It throws if invalid. + */ +function validateRtcpParameters(rtcp: RtcpParameters): void { + if (typeof rtcp !== 'object') { + throw new TypeError('rtcp is not an object'); + } + + // cname is optional. + if (rtcp.cname && typeof rtcp.cname !== 'string') { + throw new TypeError('invalid rtcp.cname'); + } + + // reducedSize is optional. If unset set it to true. + if (!rtcp.reducedSize || typeof rtcp.reducedSize !== 'boolean') { + rtcp.reducedSize = true; + } +} diff --git a/node/src/rtpParametersFbsUtils.ts b/node/src/rtpParametersFbsUtils.ts new file mode 100644 index 0000000000..727eb1cd1e --- /dev/null +++ b/node/src/rtpParametersFbsUtils.ts @@ -0,0 +1,544 @@ +import * as flatbuffers from 'flatbuffers'; +import type { + RtpParameters, + RtpCodecParameters, + RtcpFeedback, + RtpEncodingParameters, + RtpHeaderExtensionUri, + RtpHeaderExtensionParameters, + RtcpParameters, +} from './rtpParametersTypes'; +import * as fbsUtils from './fbsUtils'; +import { + Boolean as FbsBoolean, + Double as FbsDouble, + Integer32 as FbsInteger32, + Integer32Array as FbsInteger32Array, + String as FbsString, + Parameter as FbsParameter, + RtcpFeedback as FbsRtcpFeedback, + RtcpParameters as FbsRtcpParameters, + RtpCodecParameters as FbsRtpCodecParameters, + RtpEncodingParameters as FbsRtpEncodingParameters, + RtpHeaderExtensionParameters as FbsRtpHeaderExtensionParameters, + RtpHeaderExtensionUri as FbsRtpHeaderExtensionUri, + RtpParameters as FbsRtpParameters, + Rtx as FbsRtx, + Value as FbsValue, +} from './fbs/rtp-parameters'; + +export function serializeRtpParameters( + builder: flatbuffers.Builder, + rtpParameters: RtpParameters +): number { + const codecs: number[] = []; + const headerExtensions: number[] = []; + + for (const codec of rtpParameters.codecs) { + const mimeTypeOffset = builder.createString(codec.mimeType); + const parameters = serializeParameters(builder, codec.parameters); + const parametersOffset = FbsRtpCodecParameters.createParametersVector( + builder, + parameters + ); + + const rtcpFeedback: number[] = []; + + for (const rtcp of codec.rtcpFeedback ?? []) { + const typeOffset = builder.createString(rtcp.type); + const rtcpParametersOffset = builder.createString(rtcp.parameter); + + rtcpFeedback.push( + FbsRtcpFeedback.createRtcpFeedback( + builder, + typeOffset, + rtcpParametersOffset + ) + ); + } + const rtcpFeedbackOffset = FbsRtpCodecParameters.createRtcpFeedbackVector( + builder, + rtcpFeedback + ); + + codecs.push( + FbsRtpCodecParameters.createRtpCodecParameters( + builder, + mimeTypeOffset, + codec.payloadType, + codec.clockRate, + Number(codec.channels), + parametersOffset, + rtcpFeedbackOffset + ) + ); + } + const codecsOffset = FbsRtpParameters.createCodecsVector(builder, codecs); + + // RtpHeaderExtensionParameters. + for (const headerExtension of rtpParameters.headerExtensions ?? []) { + const uri = rtpHeaderExtensionUriToFbs(headerExtension.uri); + const parameters = serializeParameters(builder, headerExtension.parameters); + const parametersOffset = FbsRtpCodecParameters.createParametersVector( + builder, + parameters + ); + + headerExtensions.push( + FbsRtpHeaderExtensionParameters.createRtpHeaderExtensionParameters( + builder, + uri, + headerExtension.id, + Boolean(headerExtension.encrypt), + parametersOffset + ) + ); + } + const headerExtensionsOffset = FbsRtpParameters.createHeaderExtensionsVector( + builder, + headerExtensions + ); + + // RtpEncodingParameters. + const encodingsOffset = serializeRtpEncodingParameters( + builder, + rtpParameters.encodings ?? [] + ); + + // RtcpParameters. + const { cname, reducedSize } = rtpParameters.rtcp ?? { reducedSize: true }; + const cnameOffset = builder.createString(cname); + + const rtcpOffset = FbsRtcpParameters.createRtcpParameters( + builder, + cnameOffset, + Boolean(reducedSize) + ); + + const midOffset = builder.createString(rtpParameters.mid); + + FbsRtpParameters.startRtpParameters(builder); + FbsRtpParameters.addMid(builder, midOffset); + FbsRtpParameters.addCodecs(builder, codecsOffset); + + FbsRtpParameters.addHeaderExtensions(builder, headerExtensionsOffset); + FbsRtpParameters.addEncodings(builder, encodingsOffset); + FbsRtpParameters.addRtcp(builder, rtcpOffset); + + return FbsRtpParameters.endRtpParameters(builder); +} + +export function serializeRtpEncodingParameters( + builder: flatbuffers.Builder, + rtpEncodingParameters: RtpEncodingParameters[] = [] +): number { + const encodings: number[] = []; + + for (const encoding of rtpEncodingParameters) { + // Prepare Rid. + const ridOffset = builder.createString(encoding.rid); + + // Prepare Rtx. + let rtxOffset: number | undefined; + + if (encoding.rtx) { + rtxOffset = FbsRtx.createRtx(builder, encoding.rtx.ssrc); + } + + // Prepare scalability mode. + let scalabilityModeOffset: number | undefined; + + if (encoding.scalabilityMode) { + scalabilityModeOffset = builder.createString(encoding.scalabilityMode); + } + + // Start serialization. + FbsRtpEncodingParameters.startRtpEncodingParameters(builder); + + // Add SSRC. + if (encoding.ssrc) { + FbsRtpEncodingParameters.addSsrc(builder, encoding.ssrc); + } + + // Add Rid. + FbsRtpEncodingParameters.addRid(builder, ridOffset); + + // Add payload type. + if (encoding.codecPayloadType) { + FbsRtpEncodingParameters.addCodecPayloadType( + builder, + encoding.codecPayloadType + ); + } + + // Add RTX. + if (rtxOffset) { + FbsRtpEncodingParameters.addRtx(builder, rtxOffset); + } + + // Add DTX. + if (encoding.dtx !== undefined) { + FbsRtpEncodingParameters.addDtx(builder, encoding.dtx); + } + + // Add scalability ode. + if (scalabilityModeOffset) { + FbsRtpEncodingParameters.addScalabilityMode( + builder, + scalabilityModeOffset + ); + } + + // Add max bitrate. + if (encoding.maxBitrate !== undefined) { + FbsRtpEncodingParameters.addMaxBitrate(builder, encoding.maxBitrate); + } + + // End serialization. + encodings.push(FbsRtpEncodingParameters.endRtpEncodingParameters(builder)); + } + + return FbsRtpParameters.createEncodingsVector(builder, encodings); +} + +export function serializeParameters( + builder: flatbuffers.Builder, + parameters: any +): number[] { + const fbsParameters: number[] = []; + + for (const key of Object.keys(parameters)) { + const value = parameters[key]; + const keyOffset = builder.createString(key); + let parameterOffset: number; + + if (typeof value === 'boolean') { + parameterOffset = FbsParameter.createParameter( + builder, + keyOffset, + FbsValue.Boolean, + value === true ? 1 : 0 + ); + } else if (typeof value === 'number') { + // Integer. + if (value % 1 === 0) { + const valueOffset = FbsInteger32.createInteger32(builder, value); + + parameterOffset = FbsParameter.createParameter( + builder, + keyOffset, + FbsValue.Integer32, + valueOffset + ); + } + // Float. + else { + const valueOffset = FbsDouble.createDouble(builder, value); + + parameterOffset = FbsParameter.createParameter( + builder, + keyOffset, + FbsValue.Double, + valueOffset + ); + } + } else if (typeof value === 'string') { + const valueOffset = FbsString.createString( + builder, + builder.createString(value) + ); + + parameterOffset = FbsParameter.createParameter( + builder, + keyOffset, + FbsValue.String, + valueOffset + ); + } else if (Array.isArray(value)) { + const valueOffset = FbsInteger32Array.createValueVector(builder, value); + + parameterOffset = FbsParameter.createParameter( + builder, + keyOffset, + FbsValue.Integer32Array, + valueOffset + ); + } else { + throw new Error(`invalid parameter type [key:'${key}', value:${value}]`); + } + + fbsParameters.push(parameterOffset); + } + + return fbsParameters; +} + +export function parseRtcpFeedback(data: FbsRtcpFeedback): RtcpFeedback { + return { + type: data.type()!, + parameter: data.parameter() ?? undefined, + }; +} + +export function parseParameters(data: any): any { + const parameters: any = {}; + + for (let i = 0; i < data.parametersLength(); i++) { + const fbsParameter = data.parameters(i)!; + + switch (fbsParameter.valueType()) { + case FbsValue.Boolean: { + const value = new FbsBoolean(); + + fbsParameter.value(value); + parameters[String(fbsParameter.name())] = value.value(); + + break; + } + + case FbsValue.Integer32: { + const value = new FbsInteger32(); + + fbsParameter.value(value); + parameters[String(fbsParameter.name())] = value.value(); + + break; + } + + case FbsValue.Double: { + const value = new FbsDouble(); + + fbsParameter.value(value); + parameters[String(fbsParameter.name())] = value.value(); + + break; + } + + case FbsValue.String: { + const value = new FbsString(); + + fbsParameter.value(value); + parameters[String(fbsParameter.name())] = value.value(); + + break; + } + + case FbsValue.Integer32Array: { + const value = new FbsInteger32Array(); + + fbsParameter.value(value); + parameters[String(fbsParameter.name())] = value.valueArray(); + + break; + } + } + } + + return parameters; +} + +export function parseRtpCodecParameters( + data: FbsRtpCodecParameters +): RtpCodecParameters { + const parameters = parseParameters(data); + + let rtcpFeedback: RtcpFeedback[] = []; + + if (data.rtcpFeedbackLength() > 0) { + rtcpFeedback = fbsUtils.parseVector( + data, + 'rtcpFeedback', + parseRtcpFeedback + ); + } + + return { + mimeType: data.mimeType()!, + payloadType: data.payloadType(), + clockRate: data.clockRate(), + channels: data.channels() ?? undefined, + parameters, + rtcpFeedback, + }; +} + +export function rtpHeaderExtensionUriFromFbs( + uri: FbsRtpHeaderExtensionUri +): RtpHeaderExtensionUri { + switch (uri) { + case FbsRtpHeaderExtensionUri.Mid: { + return 'urn:ietf:params:rtp-hdrext:sdes:mid'; + } + + case FbsRtpHeaderExtensionUri.RtpStreamId: { + return 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id'; + } + + case FbsRtpHeaderExtensionUri.RepairRtpStreamId: { + return 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id'; + } + + case FbsRtpHeaderExtensionUri.FrameMarkingDraft07: { + return 'http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07'; + } + + case FbsRtpHeaderExtensionUri.FrameMarking: { + return 'urn:ietf:params:rtp-hdrext:framemarking'; + } + + case FbsRtpHeaderExtensionUri.AudioLevel: { + return 'urn:ietf:params:rtp-hdrext:ssrc-audio-level'; + } + + case FbsRtpHeaderExtensionUri.VideoOrientation: { + return 'urn:3gpp:video-orientation'; + } + + case FbsRtpHeaderExtensionUri.TimeOffset: { + return 'urn:ietf:params:rtp-hdrext:toffset'; + } + + case FbsRtpHeaderExtensionUri.TransportWideCcDraft01: { + return 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01'; + } + + case FbsRtpHeaderExtensionUri.AbsSendTime: { + return 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time'; + } + + case FbsRtpHeaderExtensionUri.AbsCaptureTime: { + return 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time'; + } + + case FbsRtpHeaderExtensionUri.PlayoutDelay: { + return 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay'; + } + } +} + +export function rtpHeaderExtensionUriToFbs( + uri: RtpHeaderExtensionUri +): FbsRtpHeaderExtensionUri { + switch (uri) { + case 'urn:ietf:params:rtp-hdrext:sdes:mid': { + return FbsRtpHeaderExtensionUri.Mid; + } + + case 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id': { + return FbsRtpHeaderExtensionUri.RtpStreamId; + } + + case 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id': { + return FbsRtpHeaderExtensionUri.RepairRtpStreamId; + } + + case 'http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07': { + return FbsRtpHeaderExtensionUri.FrameMarkingDraft07; + } + + case 'urn:ietf:params:rtp-hdrext:framemarking': { + return FbsRtpHeaderExtensionUri.FrameMarking; + } + + case 'urn:ietf:params:rtp-hdrext:ssrc-audio-level': { + return FbsRtpHeaderExtensionUri.AudioLevel; + } + + case 'urn:3gpp:video-orientation': { + return FbsRtpHeaderExtensionUri.VideoOrientation; + } + + case 'urn:ietf:params:rtp-hdrext:toffset': { + return FbsRtpHeaderExtensionUri.TimeOffset; + } + + case 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01': { + return FbsRtpHeaderExtensionUri.TransportWideCcDraft01; + } + + case 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time': { + return FbsRtpHeaderExtensionUri.AbsSendTime; + } + + case 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time': { + return FbsRtpHeaderExtensionUri.AbsCaptureTime; + } + + case 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay': { + return FbsRtpHeaderExtensionUri.PlayoutDelay; + } + + default: { + throw new TypeError(`invalid RtpHeaderExtensionUri: ${uri}`); + } + } +} + +export function parseRtpHeaderExtensionParameters( + data: FbsRtpHeaderExtensionParameters +): RtpHeaderExtensionParameters { + return { + uri: rtpHeaderExtensionUriFromFbs(data.uri()), + id: data.id(), + encrypt: data.encrypt(), + parameters: parseParameters(data), + }; +} + +export function parseRtpEncodingParameters( + data: FbsRtpEncodingParameters +): RtpEncodingParameters { + return { + ssrc: data.ssrc() ?? undefined, + rid: data.rid() ?? undefined, + codecPayloadType: + data.codecPayloadType() !== null ? data.codecPayloadType()! : undefined, + rtx: data.rtx() ? { ssrc: data.rtx()!.ssrc() } : undefined, + dtx: data.dtx(), + scalabilityMode: data.scalabilityMode() ?? undefined, + maxBitrate: data.maxBitrate() !== null ? data.maxBitrate()! : undefined, + }; +} + +export function parseRtpParameters(data: FbsRtpParameters): RtpParameters { + const codecs = fbsUtils.parseVector(data, 'codecs', parseRtpCodecParameters); + + let headerExtensions: RtpHeaderExtensionParameters[] = []; + + if (data.headerExtensionsLength() > 0) { + headerExtensions = fbsUtils.parseVector( + data, + 'headerExtensions', + parseRtpHeaderExtensionParameters + ); + } + + let encodings: RtpEncodingParameters[] = []; + + if (data.encodingsLength() > 0) { + encodings = fbsUtils.parseVector( + data, + 'encodings', + parseRtpEncodingParameters + ); + } + + let rtcp: RtcpParameters | undefined; + + if (data.rtcp()) { + const fbsRtcp = data.rtcp()!; + + rtcp = { + cname: fbsRtcp.cname() ?? undefined, + reducedSize: fbsRtcp.reducedSize(), + }; + } + + return { + mid: data.mid() ?? undefined, + codecs, + headerExtensions, + encodings, + rtcp, + }; +} diff --git a/node/src/RtpParameters.ts b/node/src/rtpParametersTypes.ts similarity index 88% rename from node/src/RtpParameters.ts rename to node/src/rtpParametersTypes.ts index c26cc86b3f..fa7e618863 100644 --- a/node/src/RtpParameters.ts +++ b/node/src/rtpParametersTypes.ts @@ -2,8 +2,7 @@ * The RTP capabilities define what mediasoup or an endpoint can receive at * media level. */ -export type RtpCapabilities = -{ +export type RtpCapabilities = { /** * Supported media and RTX codecs. */ @@ -37,8 +36,7 @@ export type MediaKind = 'audio' | 'video'; * require preferredPayloadType field (if unset, mediasoup will choose a random * one). If given, make sure it's in the 96-127 range. */ -export type RtpCodecCapability = -{ +export type RtpCodecCapability = { /** * Media kind. */ @@ -81,7 +79,11 @@ export type RtpCodecCapability = /** * Direction of RTP header extension. */ -export type RtpHeaderExtensionDirection = 'sendrecv' | 'sendonly' | 'recvonly' | 'inactive'; +export type RtpHeaderExtensionDirection = + | 'sendrecv' + | 'sendonly' + | 'recvonly' + | 'inactive'; /** * Provides information relating to supported header extensions. The list of @@ -93,8 +95,7 @@ export type RtpHeaderExtensionDirection = 'sendrecv' | 'sendonly' | 'recvonly' | * router.rtpCapabilities or mediasoup.getSupportedRtpCapabilities()). It's * ignored if present in endpoints' RTP capabilities. */ -export type RtpHeaderExtension = -{ +export type RtpHeaderExtension = { /** * Media kind. */ @@ -103,7 +104,7 @@ export type RtpHeaderExtension = /* * The URI of the RTP header extension, as defined in RFC 5285. */ - uri: string; + uri: RtpHeaderExtensionUri; /** * The preferred numeric identifier that goes in the RTP packet. Must be @@ -156,8 +157,7 @@ export type RtpHeaderExtension = * the associated producer. This applies even if the producer's encodings have * rid set. */ -export type RtpParameters = -{ +export type RtpParameters = { /** * The MID RTP extension value as defined in the BUNDLE specification. */ @@ -189,8 +189,7 @@ export type RtpParameters = * of media codecs supported by mediasoup and their settings is defined in the * supportedRtpCapabilities.ts file. */ -export type RtpCodecParameters = -{ +export type RtpCodecParameters = { /** * The codec MIME media type/subtype (e.g. 'audio/opus', 'video/VP8'). */ @@ -231,8 +230,7 @@ export type RtpCodecParameters = * messages. The list of RTCP feedbacks supported by mediasoup is defined in the * supportedRtpCapabilities.ts file. */ -export type RtcpFeedback = -{ +export type RtcpFeedback = { /** * RTCP feedback type. */ @@ -248,8 +246,7 @@ export type RtcpFeedback = * Provides information relating to an encoding, which represents a media RTP * stream and its associated RTX stream (if any). */ -export type RtpEncodingParameters = -{ +export type RtpEncodingParameters = { /** * The media SSRC. */ @@ -293,6 +290,20 @@ export type RtpEncodingParameters = maxBitrate?: number; }; +export type RtpHeaderExtensionUri = + | 'urn:ietf:params:rtp-hdrext:sdes:mid' + | 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id' + | 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id' + | 'http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07' + | 'urn:ietf:params:rtp-hdrext:framemarking' + | 'urn:ietf:params:rtp-hdrext:ssrc-audio-level' + | 'urn:3gpp:video-orientation' + | 'urn:ietf:params:rtp-hdrext:toffset' + | 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01' + | 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time' + | 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time' + | 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay'; + /** * Defines a RTP header extension within the RTP parameters. The list of RTP * header extensions supported by mediasoup is defined in the @@ -301,12 +312,11 @@ export type RtpEncodingParameters = * mediasoup does not currently support encrypted RTP header extensions and no * parameters are currently considered. */ -export type RtpHeaderExtensionParameters = -{ +export type RtpHeaderExtensionParameters = { /** * The URI of the RTP header extension, as defined in RFC 5285. */ - uri: string; + uri: RtpHeaderExtensionUri; /** * The numeric identifier that goes in the RTP packet. Must be unique. @@ -333,8 +343,7 @@ export type RtpHeaderExtensionParameters = * * mediasoup assumes reducedSize to always be true. */ -export type RtcpParameters = -{ +export type RtcpParameters = { /** * The Canonical Name (CNAME) used by RTCP (e.g. in SDES messages). */ @@ -345,9 +354,4 @@ export type RtcpParameters = * as specified in RFC 3550 (if false). Default true. */ reducedSize?: boolean; - - /** - * Whether RTCP-mux is used. Default true. - */ - mux?: boolean; }; diff --git a/node/src/rtpStreamStatsFbsUtils.ts b/node/src/rtpStreamStatsFbsUtils.ts new file mode 100644 index 0000000000..96d941e739 --- /dev/null +++ b/node/src/rtpStreamStatsFbsUtils.ts @@ -0,0 +1,105 @@ +import { + RtpStreamRecvStats, + RtpStreamSendStats, + BaseRtpStreamStats, + BitrateByLayer, +} from './rtpStreamStatsTypes'; +import * as FbsRtpStream from './fbs/rtp-stream'; +import * as FbsRtpParameters from './fbs/rtp-parameters'; + +export function parseRtpStreamStats( + binary: FbsRtpStream.Stats +): RtpStreamRecvStats | RtpStreamSendStats { + if (binary.dataType() === FbsRtpStream.StatsData.RecvStats) { + return parseRtpStreamRecvStats(binary); + } else { + return parseSendStreamStats(binary); + } +} + +export function parseRtpStreamRecvStats( + binary: FbsRtpStream.Stats +): RtpStreamRecvStats { + const recvStats = new FbsRtpStream.RecvStats(); + const baseStats = new FbsRtpStream.BaseStats(); + + binary.data(recvStats); + recvStats.base()!.data(baseStats); + + const base = parseBaseStreamStats(baseStats); + + return { + ...base, + type: 'inbound-rtp', + jitter: recvStats.jitter(), + byteCount: Number(recvStats.byteCount()), + packetCount: Number(recvStats.packetCount()), + bitrate: Number(recvStats.bitrate()), + bitrateByLayer: parseBitrateByLayer(recvStats), + }; +} + +export function parseSendStreamStats( + binary: FbsRtpStream.Stats +): RtpStreamSendStats { + const sendStats = new FbsRtpStream.SendStats(); + const baseStats = new FbsRtpStream.BaseStats(); + + binary.data(sendStats); + sendStats.base()!.data(baseStats); + + const base = parseBaseStreamStats(baseStats); + + return { + ...base, + type: 'outbound-rtp', + byteCount: Number(sendStats.byteCount()), + packetCount: Number(sendStats.packetCount()), + bitrate: Number(sendStats.bitrate()), + }; +} + +function parseBaseStreamStats( + binary: FbsRtpStream.BaseStats +): BaseRtpStreamStats { + return { + timestamp: Number(binary.timestamp()), + ssrc: binary.ssrc(), + rtxSsrc: binary.rtxSsrc() ?? undefined, + rid: binary.rid() ?? undefined, + kind: + binary.kind() === FbsRtpParameters.MediaKind.AUDIO ? 'audio' : 'video', + mimeType: binary.mimeType()!, + packetsLost: Number(binary.packetsLost()), + fractionLost: Number(binary.fractionLost()), + packetsDiscarded: Number(binary.packetsDiscarded()), + packetsRetransmitted: Number(binary.packetsRetransmitted()), + packetsRepaired: Number(binary.packetsRepaired()), + nackCount: Number(binary.nackCount()), + nackPacketCount: Number(binary.nackPacketCount()), + pliCount: Number(binary.pliCount()), + firCount: Number(binary.firCount()), + score: binary.score(), + roundTripTime: binary.roundTripTime(), + rtxPacketsDiscarded: binary.rtxPacketsDiscarded() + ? Number(binary.rtxPacketsDiscarded()) + : undefined, + }; +} + +function parseBitrateByLayer(binary: FbsRtpStream.RecvStats): BitrateByLayer { + if (binary.bitrateByLayerLength() === 0) { + return {}; + } + + const bitRateByLayer: { [key: string]: number } = {}; + + for (let i = 0; i < binary.bitrateByLayerLength(); ++i) { + const layer: string = binary.bitrateByLayer(i)!.layer()!; + const bitrate = binary.bitrateByLayer(i)!.bitrate(); + + bitRateByLayer[layer] = Number(bitrate); + } + + return bitRateByLayer; +} diff --git a/node/src/rtpStreamStatsTypes.ts b/node/src/rtpStreamStatsTypes.ts new file mode 100644 index 0000000000..aa806d6c7c --- /dev/null +++ b/node/src/rtpStreamStatsTypes.ts @@ -0,0 +1,38 @@ +export type RtpStreamRecvStats = BaseRtpStreamStats & { + type: string; + jitter: number; + packetCount: number; + byteCount: number; + bitrate: number; + bitrateByLayer: BitrateByLayer; +}; + +export type RtpStreamSendStats = BaseRtpStreamStats & { + type: string; + packetCount: number; + byteCount: number; + bitrate: number; +}; + +export type BaseRtpStreamStats = { + timestamp: number; + ssrc: number; + rtxSsrc?: number; + rid?: string; + kind: string; + mimeType: string; + packetsLost: number; + fractionLost: number; + packetsDiscarded: number; + packetsRetransmitted: number; + packetsRepaired: number; + nackCount: number; + nackPacketCount: number; + pliCount: number; + firCount: number; + score: number; + roundTripTime?: number; + rtxPacketsDiscarded?: number; +}; + +export type BitrateByLayer = { [key: string]: number }; diff --git a/node/src/scalabilityModes.ts b/node/src/scalabilityModes.ts deleted file mode 100644 index 2ebf56f6a3..0000000000 --- a/node/src/scalabilityModes.ts +++ /dev/null @@ -1,31 +0,0 @@ -const ScalabilityModeRegex = - new RegExp('^[LS]([1-9]\\d{0,1})T([1-9]\\d{0,1})(_KEY)?'); - -export type ScalabilityMode = -{ - spatialLayers: number; - temporalLayers: number; - ksvc: boolean; -}; - -export function parse(scalabilityMode?: string): ScalabilityMode -{ - const match = ScalabilityModeRegex.exec(scalabilityMode || ''); - - if (match) - { - return { - spatialLayers : Number(match[1]), - temporalLayers : Number(match[2]), - ksvc : Boolean(match[3]) - }; - } - else - { - return { - spatialLayers : 1, - temporalLayers : 1, - ksvc : false - }; - } -} diff --git a/node/src/scalabilityModesTypes.ts b/node/src/scalabilityModesTypes.ts new file mode 100644 index 0000000000..8d66dd8c0d --- /dev/null +++ b/node/src/scalabilityModesTypes.ts @@ -0,0 +1,5 @@ +export type ScalabilityMode = { + spatialLayers: number; + temporalLayers: number; + ksvc: boolean; +}; diff --git a/node/src/scalabilityModesUtils.ts b/node/src/scalabilityModesUtils.ts new file mode 100644 index 0000000000..3cbb70ab3e --- /dev/null +++ b/node/src/scalabilityModesUtils.ts @@ -0,0 +1,25 @@ +import { ScalabilityMode } from './scalabilityModesTypes'; + +const ScalabilityModeRegex = new RegExp( + '^[LS]([1-9]\\d{0,1})T([1-9]\\d{0,1})(_KEY)?' +); + +export function parseScalabilityMode( + scalabilityMode?: string +): ScalabilityMode { + const match = ScalabilityModeRegex.exec(scalabilityMode ?? ''); + + if (match) { + return { + spatialLayers: Number(match[1]), + temporalLayers: Number(match[2]), + ksvc: Boolean(match[3]), + }; + } else { + return { + spatialLayers: 1, + temporalLayers: 1, + ksvc: false, + }; + } +} diff --git a/node/src/sctpParametersFbsUtils.ts b/node/src/sctpParametersFbsUtils.ts new file mode 100644 index 0000000000..a99f86d88b --- /dev/null +++ b/node/src/sctpParametersFbsUtils.ts @@ -0,0 +1,54 @@ +import * as flatbuffers from 'flatbuffers'; +import type { + SctpStreamParameters, + SctpParametersDump, +} from './sctpParametersTypes'; +import * as FbsSctpParameters from './fbs/sctp-parameters'; + +export function parseSctpParametersDump( + binary: FbsSctpParameters.SctpParameters +): SctpParametersDump { + return { + port: binary.port(), + OS: binary.os(), + MIS: binary.mis(), + maxMessageSize: binary.maxMessageSize(), + sendBufferSize: binary.sendBufferSize(), + sctpBufferedAmount: binary.sctpBufferedAmount(), + isDataChannel: binary.isDataChannel(), + }; +} + +export function serializeSctpStreamParameters( + builder: flatbuffers.Builder, + parameters: SctpStreamParameters +): number { + return FbsSctpParameters.SctpStreamParameters.createSctpStreamParameters( + builder, + parameters.streamId, + parameters.ordered!, + typeof parameters.maxPacketLifeTime === 'number' + ? parameters.maxPacketLifeTime + : null, + typeof parameters.maxRetransmits === 'number' + ? parameters.maxRetransmits + : null + ); +} + +export function parseSctpStreamParameters( + parameters: FbsSctpParameters.SctpStreamParameters +): SctpStreamParameters { + return { + streamId: parameters.streamId(), + ordered: parameters.ordered()!, + maxPacketLifeTime: + parameters.maxPacketLifeTime() !== null + ? parameters.maxPacketLifeTime()! + : undefined, + maxRetransmits: + parameters.maxRetransmits() !== null + ? parameters.maxRetransmits()! + : undefined, + }; +} diff --git a/node/src/SctpParameters.ts b/node/src/sctpParametersTypes.ts similarity index 87% rename from node/src/SctpParameters.ts rename to node/src/sctpParametersTypes.ts index 82079d4516..3e8803408a 100644 --- a/node/src/SctpParameters.ts +++ b/node/src/sctpParametersTypes.ts @@ -1,6 +1,5 @@ -export type SctpCapabilities = -{ - numStreams: NumSctpStreams; +export type SctpCapabilities = { + numStreams: NumSctpStreams; }; /** @@ -25,8 +24,7 @@ export type SctpCapabilities = * mediasoup-client provides specific per browser/version OS and MIS values via * the device.sctpCapabilities getter. */ -export type NumSctpStreams = -{ +export type NumSctpStreams = { /** * Initially requested number of outgoing SCTP streams. */ @@ -38,8 +36,7 @@ export type NumSctpStreams = MIS: number; }; -export type SctpParameters = -{ +export type SctpParameters = { /** * Must always equal 5000. */ @@ -68,8 +65,7 @@ export type SctpParameters = * If ordered if false, only one of maxPacketLifeTime or maxRetransmits * can be true. */ -export type SctpStreamParameters = -{ +export type SctpStreamParameters = { /** * SCTP stream id. */ @@ -93,3 +89,13 @@ export type SctpStreamParameters = */ maxRetransmits?: number; }; + +export type SctpParametersDump = { + port: number; + OS: number; + MIS: number; + maxMessageSize: number; + sendBufferSize: number; + sctpBufferedAmount: number; + isDataChannel: boolean; +}; diff --git a/node/src/srtpParametersFbsUtils.ts b/node/src/srtpParametersFbsUtils.ts new file mode 100644 index 0000000000..b7b5f0dc48 --- /dev/null +++ b/node/src/srtpParametersFbsUtils.ts @@ -0,0 +1,73 @@ +import * as flatbuffers from 'flatbuffers'; +import type { SrtpParameters, SrtpCryptoSuite } from './srtpParametersTypes'; +import * as FbsSrtpParameters from './fbs/srtp-parameters'; + +export function cryptoSuiteFromFbs( + binary: FbsSrtpParameters.SrtpCryptoSuite +): SrtpCryptoSuite { + switch (binary) { + case FbsSrtpParameters.SrtpCryptoSuite.AEAD_AES_256_GCM: { + return 'AEAD_AES_256_GCM'; + } + + case FbsSrtpParameters.SrtpCryptoSuite.AEAD_AES_128_GCM: { + return 'AEAD_AES_128_GCM'; + } + + case FbsSrtpParameters.SrtpCryptoSuite.AES_CM_128_HMAC_SHA1_80: { + return 'AES_CM_128_HMAC_SHA1_80'; + } + + case FbsSrtpParameters.SrtpCryptoSuite.AES_CM_128_HMAC_SHA1_32: { + return 'AES_CM_128_HMAC_SHA1_32'; + } + } +} + +export function cryptoSuiteToFbs( + cryptoSuite: SrtpCryptoSuite +): FbsSrtpParameters.SrtpCryptoSuite { + switch (cryptoSuite) { + case 'AEAD_AES_256_GCM': { + return FbsSrtpParameters.SrtpCryptoSuite.AEAD_AES_256_GCM; + } + + case 'AEAD_AES_128_GCM': { + return FbsSrtpParameters.SrtpCryptoSuite.AEAD_AES_128_GCM; + } + + case 'AES_CM_128_HMAC_SHA1_80': { + return FbsSrtpParameters.SrtpCryptoSuite.AES_CM_128_HMAC_SHA1_80; + } + + case 'AES_CM_128_HMAC_SHA1_32': { + return FbsSrtpParameters.SrtpCryptoSuite.AES_CM_128_HMAC_SHA1_32; + } + + default: { + throw new TypeError(`invalid SrtpCryptoSuite: ${cryptoSuite}`); + } + } +} + +export function parseSrtpParameters( + binary: FbsSrtpParameters.SrtpParameters +): SrtpParameters { + return { + cryptoSuite: cryptoSuiteFromFbs(binary.cryptoSuite()), + keyBase64: binary.keyBase64()!, + }; +} + +export function serializeSrtpParameters( + builder: flatbuffers.Builder, + srtpParameters: SrtpParameters +): number { + const keyBase64Offset = builder.createString(srtpParameters.keyBase64); + + return FbsSrtpParameters.SrtpParameters.createSrtpParameters( + builder, + cryptoSuiteToFbs(srtpParameters.cryptoSuite), + keyBase64Offset + ); +} diff --git a/node/src/SrtpParameters.ts b/node/src/srtpParametersTypes.ts similarity index 92% rename from node/src/SrtpParameters.ts rename to node/src/srtpParametersTypes.ts index 1cbddce46c..d9150bbeb3 100644 --- a/node/src/SrtpParameters.ts +++ b/node/src/srtpParametersTypes.ts @@ -1,8 +1,7 @@ /** * SRTP parameters. */ -export type SrtpParameters = -{ +export type SrtpParameters = { /** * Encryption and authentication transforms to be used. */ diff --git a/node/src/supportedRtpCapabilities.ts b/node/src/supportedRtpCapabilities.ts index e91b92eaed..2a0d1f5788 100644 --- a/node/src/supportedRtpCapabilities.ts +++ b/node/src/supportedRtpCapabilities.ts @@ -1,388 +1,348 @@ -import { RtpCapabilities } from './RtpParameters'; +import type { RtpCapabilities } from './rtpParametersTypes'; -const supportedRtpCapabilities: RtpCapabilities = -{ - codecs : - [ - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 2, - rtcpFeedback : - [ - { type: 'transport-cc' } - ] - }, - { - kind : 'audio', - mimeType : 'audio/multiopus', - clockRate : 48000, - channels : 4, +const supportedRtpCapabilities: RtpCapabilities = { + codecs: [ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + rtcpFeedback: [{ type: 'nack' }, { type: 'transport-cc' }], + }, + { + kind: 'audio', + mimeType: 'audio/multiopus', + clockRate: 48000, + channels: 4, // Quad channel. - parameters : - { - 'channel_mapping' : '0,1,2,3', - 'num_streams' : 2, - 'coupled_streams' : 2 + parameters: { + channel_mapping: '0,1,2,3', + num_streams: 2, + coupled_streams: 2, }, - rtcpFeedback : - [ - { type: 'transport-cc' } - ] + rtcpFeedback: [{ type: 'nack' }, { type: 'transport-cc' }], }, { - kind : 'audio', - mimeType : 'audio/multiopus', - clockRate : 48000, - channels : 6, + kind: 'audio', + mimeType: 'audio/multiopus', + clockRate: 48000, + channels: 6, // 5.1. - parameters : - { - 'channel_mapping' : '0,4,1,2,3,5', - 'num_streams' : 4, - 'coupled_streams' : 2 + parameters: { + channel_mapping: '0,4,1,2,3,5', + num_streams: 4, + coupled_streams: 2, }, - rtcpFeedback : - [ - { type: 'transport-cc' } - ] + rtcpFeedback: [{ type: 'nack' }, { type: 'transport-cc' }], }, { - kind : 'audio', - mimeType : 'audio/multiopus', - clockRate : 48000, - channels : 8, + kind: 'audio', + mimeType: 'audio/multiopus', + clockRate: 48000, + channels: 8, // 7.1. - parameters : - { - 'channel_mapping' : '0,6,1,2,3,4,5,7', - 'num_streams' : 5, - 'coupled_streams' : 3 + parameters: { + channel_mapping: '0,6,1,2,3,4,5,7', + num_streams: 5, + coupled_streams: 3, }, - rtcpFeedback : - [ - { type: 'transport-cc' } - ] - }, - { - kind : 'audio', - mimeType : 'audio/PCMU', - preferredPayloadType : 0, - clockRate : 8000, - rtcpFeedback : - [ - { type: 'transport-cc' } - ] - }, - { - kind : 'audio', - mimeType : 'audio/PCMA', - preferredPayloadType : 8, - clockRate : 8000, - rtcpFeedback : - [ - { type: 'transport-cc' } - ] - }, + rtcpFeedback: [{ type: 'nack' }, { type: 'transport-cc' }], + }, + { + kind: 'audio', + mimeType: 'audio/PCMU', + preferredPayloadType: 0, + clockRate: 8000, + rtcpFeedback: [{ type: 'transport-cc' }], + }, + { + kind: 'audio', + mimeType: 'audio/PCMA', + preferredPayloadType: 8, + clockRate: 8000, + rtcpFeedback: [{ type: 'transport-cc' }], + }, { - kind : 'audio', - mimeType : 'audio/ISAC', - clockRate : 32000, - rtcpFeedback : - [ - { type: 'transport-cc' } - ] + kind: 'audio', + mimeType: 'audio/ISAC', + clockRate: 32000, + rtcpFeedback: [{ type: 'transport-cc' }], }, { - kind : 'audio', - mimeType : 'audio/ISAC', - clockRate : 16000, - rtcpFeedback : - [ - { type: 'transport-cc' } - ] + kind: 'audio', + mimeType: 'audio/ISAC', + clockRate: 16000, + rtcpFeedback: [{ type: 'transport-cc' }], }, { - kind : 'audio', - mimeType : 'audio/G722', - preferredPayloadType : 9, - clockRate : 8000, - rtcpFeedback : - [ - { type: 'transport-cc' } - ] + kind: 'audio', + mimeType: 'audio/G722', + preferredPayloadType: 9, + clockRate: 8000, + rtcpFeedback: [{ type: 'transport-cc' }], }, { - kind : 'audio', - mimeType : 'audio/iLBC', - clockRate : 8000, - rtcpFeedback : - [ - { type: 'transport-cc' } - ] + kind: 'audio', + mimeType: 'audio/iLBC', + clockRate: 8000, + rtcpFeedback: [{ type: 'transport-cc' }], }, { - kind : 'audio', - mimeType : 'audio/SILK', - clockRate : 24000, - rtcpFeedback : - [ - { type: 'transport-cc' } - ] + kind: 'audio', + mimeType: 'audio/SILK', + clockRate: 24000, + rtcpFeedback: [{ type: 'transport-cc' }], }, { - kind : 'audio', - mimeType : 'audio/SILK', - clockRate : 16000, - rtcpFeedback : - [ - { type: 'transport-cc' } - ] + kind: 'audio', + mimeType: 'audio/SILK', + clockRate: 16000, + rtcpFeedback: [{ type: 'transport-cc' }], }, { - kind : 'audio', - mimeType : 'audio/SILK', - clockRate : 12000, - rtcpFeedback : - [ - { type: 'transport-cc' } - ] + kind: 'audio', + mimeType: 'audio/SILK', + clockRate: 12000, + rtcpFeedback: [{ type: 'transport-cc' }], }, { - kind : 'audio', - mimeType : 'audio/SILK', - clockRate : 8000, - rtcpFeedback : - [ - { type: 'transport-cc' } - ] + kind: 'audio', + mimeType: 'audio/SILK', + clockRate: 8000, + rtcpFeedback: [{ type: 'transport-cc' }], }, { - kind : 'audio', - mimeType : 'audio/CN', - preferredPayloadType : 13, - clockRate : 32000 + kind: 'audio', + mimeType: 'audio/CN', + preferredPayloadType: 13, + clockRate: 32000, }, { - kind : 'audio', - mimeType : 'audio/CN', - preferredPayloadType : 13, - clockRate : 16000 + kind: 'audio', + mimeType: 'audio/CN', + preferredPayloadType: 13, + clockRate: 16000, }, { - kind : 'audio', - mimeType : 'audio/CN', - preferredPayloadType : 13, - clockRate : 8000 + kind: 'audio', + mimeType: 'audio/CN', + preferredPayloadType: 13, + clockRate: 8000, }, { - kind : 'audio', - mimeType : 'audio/telephone-event', - clockRate : 48000 + kind: 'audio', + mimeType: 'audio/telephone-event', + clockRate: 48000, }, { - kind : 'audio', - mimeType : 'audio/telephone-event', - clockRate : 32000 + kind: 'audio', + mimeType: 'audio/telephone-event', + clockRate: 32000, }, { - kind : 'audio', - mimeType : 'audio/telephone-event', - clockRate : 16000 + kind: 'audio', + mimeType: 'audio/telephone-event', + clockRate: 16000, }, { - kind : 'audio', - mimeType : 'audio/telephone-event', - clockRate : 8000 + kind: 'audio', + mimeType: 'audio/telephone-event', + clockRate: 8000, }, { - kind : 'video', - mimeType : 'video/VP8', - clockRate : 90000, - rtcpFeedback : - [ + kind: 'video', + mimeType: 'video/VP8', + clockRate: 90000, + rtcpFeedback: [ { type: 'nack' }, { type: 'nack', parameter: 'pli' }, { type: 'ccm', parameter: 'fir' }, { type: 'goog-remb' }, - { type: 'transport-cc' } - ] + { type: 'transport-cc' }, + ], }, { - kind : 'video', - mimeType : 'video/VP9', - clockRate : 90000, - rtcpFeedback : - [ + kind: 'video', + mimeType: 'video/VP9', + clockRate: 90000, + rtcpFeedback: [ { type: 'nack' }, { type: 'nack', parameter: 'pli' }, { type: 'ccm', parameter: 'fir' }, { type: 'goog-remb' }, - { type: 'transport-cc' } - ] + { type: 'transport-cc' }, + ], }, { - kind : 'video', - mimeType : 'video/H264', - clockRate : 90000, - parameters : - { - 'level-asymmetry-allowed' : 1 + kind: 'video', + mimeType: 'video/H264', + clockRate: 90000, + parameters: { + 'level-asymmetry-allowed': 1, }, - rtcpFeedback : - [ + rtcpFeedback: [ { type: 'nack' }, { type: 'nack', parameter: 'pli' }, { type: 'ccm', parameter: 'fir' }, { type: 'goog-remb' }, - { type: 'transport-cc' } - ] + { type: 'transport-cc' }, + ], }, { - kind : 'video', - mimeType : 'video/H264-SVC', - clockRate : 90000, - parameters : { - 'level-asymmetry-allowed' : 1 + kind: 'video', + mimeType: 'video/H264-SVC', + clockRate: 90000, + parameters: { + 'level-asymmetry-allowed': 1, }, - rtcpFeedback : [ + rtcpFeedback: [ { type: 'nack' }, { type: 'nack', parameter: 'pli' }, { type: 'ccm', parameter: 'fir' }, { type: 'goog-remb' }, - { type: 'transport-cc' } - ] + { type: 'transport-cc' }, + ], }, { - kind : 'video', - mimeType : 'video/H265', - clockRate : 90000, - parameters : - { - 'level-asymmetry-allowed' : 1 + kind: 'video', + mimeType: 'video/H265', + clockRate: 90000, + parameters: { + 'level-asymmetry-allowed': 1, }, - rtcpFeedback : - [ + rtcpFeedback: [ { type: 'nack' }, { type: 'nack', parameter: 'pli' }, { type: 'ccm', parameter: 'fir' }, { type: 'goog-remb' }, - { type: 'transport-cc' } - ] - } + { type: 'transport-cc' }, + ], + }, ], - headerExtensions : - [ + headerExtensions: [ { - kind : 'audio', - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - preferredId : 1, - preferredEncrypt : false, - direction : 'sendrecv' + kind: 'audio', + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + preferredId: 1, + preferredEncrypt: false, + direction: 'sendrecv', }, { - kind : 'video', - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - preferredId : 1, - preferredEncrypt : false, - direction : 'sendrecv' + kind: 'video', + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + preferredId: 1, + preferredEncrypt: false, + direction: 'sendrecv', }, { - kind : 'video', - uri : 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', - preferredId : 2, - preferredEncrypt : false, - direction : 'recvonly' + kind: 'video', + uri: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', + preferredId: 2, + preferredEncrypt: false, + direction: 'recvonly', }, { - kind : 'video', - uri : 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id', - preferredId : 3, - preferredEncrypt : false, - direction : 'recvonly' + kind: 'video', + uri: 'urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id', + preferredId: 3, + preferredEncrypt: false, + direction: 'recvonly', }, { - kind : 'audio', - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', - preferredId : 4, - preferredEncrypt : false, - direction : 'sendrecv' + kind: 'audio', + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + preferredId: 4, + preferredEncrypt: false, + direction: 'sendrecv', }, { - kind : 'video', - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', - preferredId : 4, - preferredEncrypt : false, - direction : 'sendrecv' + kind: 'video', + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + preferredId: 4, + preferredEncrypt: false, + direction: 'sendrecv', }, // NOTE: For audio we just enable transport-wide-cc-01 when receiving media. { - kind : 'audio', - uri : 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', - preferredId : 5, - preferredEncrypt : false, - direction : 'recvonly' + kind: 'audio', + uri: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', + preferredId: 5, + preferredEncrypt: false, + direction: 'recvonly', }, { - kind : 'video', - uri : 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', - preferredId : 5, - preferredEncrypt : false, - direction : 'sendrecv' + kind: 'video', + uri: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', + preferredId: 5, + preferredEncrypt: false, + direction: 'sendrecv', }, // NOTE: Remove this once framemarking draft becomes RFC. { - kind : 'video', - uri : 'http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07', - preferredId : 6, - preferredEncrypt : false, - direction : 'sendrecv' + kind: 'video', + uri: 'http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07', + preferredId: 6, + preferredEncrypt: false, + direction: 'sendrecv', + }, + { + kind: 'video', + uri: 'urn:ietf:params:rtp-hdrext:framemarking', + preferredId: 7, + preferredEncrypt: false, + direction: 'sendrecv', }, { - kind : 'video', - uri : 'urn:ietf:params:rtp-hdrext:framemarking', - preferredId : 7, - preferredEncrypt : false, - direction : 'sendrecv' + kind: 'audio', + uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + preferredId: 10, + preferredEncrypt: false, + direction: 'sendrecv', }, { - kind : 'audio', - uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', - preferredId : 10, - preferredEncrypt : false, - direction : 'sendrecv' + kind: 'video', + uri: 'urn:3gpp:video-orientation', + preferredId: 11, + preferredEncrypt: false, + direction: 'sendrecv', }, { - kind : 'video', - uri : 'urn:3gpp:video-orientation', - preferredId : 11, - preferredEncrypt : false, - direction : 'sendrecv' + kind: 'video', + uri: 'urn:ietf:params:rtp-hdrext:toffset', + preferredId: 12, + preferredEncrypt: false, + direction: 'sendrecv', }, { - kind : 'video', - uri : 'urn:ietf:params:rtp-hdrext:toffset', - preferredId : 12, - preferredEncrypt : false, - direction : 'sendrecv' + kind: 'audio', + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', + preferredId: 13, + preferredEncrypt: false, + direction: 'sendrecv', }, { - kind : 'audio', - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', - preferredId : 13, - preferredEncrypt : false, - direction : 'sendrecv' + kind: 'video', + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', + preferredId: 13, + preferredEncrypt: false, + direction: 'sendrecv', }, { - kind : 'video', - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', - preferredId : 13, - preferredEncrypt : false, - direction : 'sendrecv' - } - ] + kind: 'audio', + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay', + preferredId: 14, + preferredEncrypt: false, + direction: 'sendrecv', + }, + { + kind: 'video', + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay', + preferredId: 14, + preferredEncrypt: false, + direction: 'sendrecv', + }, + ], }; export { supportedRtpCapabilities }; diff --git a/node/src/tests/data/dtls-cert.pem b/node/src/test/data/dtls-cert.pem similarity index 100% rename from node/src/tests/data/dtls-cert.pem rename to node/src/test/data/dtls-cert.pem diff --git a/node/src/tests/data/dtls-key.pem b/node/src/test/data/dtls-key.pem similarity index 100% rename from node/src/tests/data/dtls-key.pem rename to node/src/test/data/dtls-key.pem diff --git a/node/src/test/test-ActiveSpeakerObserver.ts b/node/src/test/test-ActiveSpeakerObserver.ts new file mode 100644 index 0000000000..7467dac16a --- /dev/null +++ b/node/src/test/test-ActiveSpeakerObserver.ts @@ -0,0 +1,132 @@ +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { WorkerEvents, ActiveSpeakerObserverEvents } from '../types'; +import * as utils from '../utils'; + +type TestContext = { + mediaCodecs: mediasoup.types.RtpCodecCapability[]; + worker?: mediasoup.types.Worker; + router?: mediasoup.types.Router; +}; + +const ctx: TestContext = { + mediaCodecs: utils.deepFreeze([ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + parameters: { + useinbandfec: 1, + foo: 'bar', + }, + }, + ]), +}; + +beforeEach(async () => { + ctx.worker = await mediasoup.createWorker(); + ctx.router = await ctx.worker.createRouter({ mediaCodecs: ctx.mediaCodecs }); +}); + +afterEach(async () => { + ctx.worker?.close(); + + if (ctx.worker?.subprocessClosed === false) { + await enhancedOnce(ctx.worker, 'subprocessclose'); + } +}); + +test('router.createActiveSpeakerObserver() succeeds', async () => { + const onObserverNewRtpObserver = jest.fn(); + + ctx.router!.observer.once('newrtpobserver', onObserverNewRtpObserver); + + const activeSpeakerObserver = await ctx.router!.createActiveSpeakerObserver(); + + expect(onObserverNewRtpObserver).toHaveBeenCalledTimes(1); + expect(onObserverNewRtpObserver).toHaveBeenCalledWith(activeSpeakerObserver); + expect(typeof activeSpeakerObserver.id).toBe('string'); + expect(activeSpeakerObserver.closed).toBe(false); + expect(activeSpeakerObserver.type).toBe('activespeaker'); + expect(activeSpeakerObserver.paused).toBe(false); + expect(activeSpeakerObserver.appData).toEqual({}); + + await expect(ctx.router!.dump()).resolves.toMatchObject({ + rtpObserverIds: [activeSpeakerObserver.id], + }); +}, 2000); + +test('router.createActiveSpeakerObserver() with wrong arguments rejects with TypeError', async () => { + await expect( + ctx.router!.createActiveSpeakerObserver( + // @ts-expect-error --- Testing purposes. + { interval: false } + ) + ).rejects.toThrow(TypeError); + + await expect( + ctx.router!.createActiveSpeakerObserver( + // @ts-expect-error --- Testing purposes. + { appData: 'NOT-AN-OBJECT' } + ) + ).rejects.toThrow(TypeError); +}, 2000); + +test('activeSpeakerObserver.pause() and resume() succeed', async () => { + const activeSpeakerObserver = await ctx.router!.createActiveSpeakerObserver(); + + await activeSpeakerObserver.pause(); + + expect(activeSpeakerObserver.paused).toBe(true); + + await activeSpeakerObserver.resume(); + + expect(activeSpeakerObserver.paused).toBe(false); +}, 2000); + +test('activeSpeakerObserver.close() succeeds', async () => { + const activeSpeakerObserver = await ctx.router!.createActiveSpeakerObserver({ + interval: 500, + }); + + let dump = await ctx.router!.dump(); + + expect(dump.rtpObserverIds.length).toBe(1); + + activeSpeakerObserver.close(); + + expect(activeSpeakerObserver.closed).toBe(true); + + dump = await ctx.router!.dump(); + + expect(dump.rtpObserverIds.length).toBe(0); +}, 2000); + +test('ActiveSpeakerObserver emits "routerclose" if Router is closed', async () => { + const activeSpeakerObserver = await ctx.router!.createActiveSpeakerObserver(); + + const promise = enhancedOnce( + activeSpeakerObserver, + 'routerclose' + ); + + ctx.router!.close(); + await promise; + + expect(activeSpeakerObserver.closed).toBe(true); +}, 2000); + +test('ActiveSpeakerObserver emits "routerclose" if Worker is closed', async () => { + const activeSpeakerObserver = await ctx.router!.createActiveSpeakerObserver(); + + const promise = enhancedOnce( + activeSpeakerObserver, + 'routerclose' + ); + + ctx.worker!.close(); + await promise; + + expect(activeSpeakerObserver.closed).toBe(true); +}, 2000); diff --git a/node/src/test/test-AudioLevelObserver.ts b/node/src/test/test-AudioLevelObserver.ts new file mode 100644 index 0000000000..e10d1245a8 --- /dev/null +++ b/node/src/test/test-AudioLevelObserver.ts @@ -0,0 +1,141 @@ +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { WorkerEvents, AudioLevelObserverEvents } from '../types'; +import * as utils from '../utils'; + +type TestContext = { + mediaCodecs: mediasoup.types.RtpCodecCapability[]; + worker?: mediasoup.types.Worker; + router?: mediasoup.types.Router; +}; + +const ctx: TestContext = { + mediaCodecs: utils.deepFreeze([ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + parameters: { + useinbandfec: 1, + foo: 'bar', + }, + }, + ]), +}; + +beforeEach(async () => { + ctx.worker = await mediasoup.createWorker(); + ctx.router = await ctx.worker.createRouter({ mediaCodecs: ctx.mediaCodecs }); +}); + +afterEach(async () => { + ctx.worker?.close(); + + if (ctx.worker?.subprocessClosed === false) { + await enhancedOnce(ctx.worker, 'subprocessclose'); + } +}); + +test('router.createAudioLevelObserver() succeeds', async () => { + const onObserverNewRtpObserver = jest.fn(); + + ctx.router!.observer.once('newrtpobserver', onObserverNewRtpObserver); + + const audioLevelObserver = await ctx.router!.createAudioLevelObserver(); + + expect(onObserverNewRtpObserver).toHaveBeenCalledTimes(1); + expect(onObserverNewRtpObserver).toHaveBeenCalledWith(audioLevelObserver); + expect(typeof audioLevelObserver.id).toBe('string'); + expect(audioLevelObserver.closed).toBe(false); + expect(audioLevelObserver.type).toBe('audiolevel'); + expect(audioLevelObserver.paused).toBe(false); + expect(audioLevelObserver.appData).toEqual({}); + + await expect(ctx.router!.dump()).resolves.toMatchObject({ + rtpObserverIds: [audioLevelObserver.id], + }); +}, 2000); + +test('router.createAudioLevelObserver() with wrong arguments rejects with TypeError', async () => { + await expect( + ctx.router!.createAudioLevelObserver({ maxEntries: 0 }) + ).rejects.toThrow(TypeError); + + await expect( + ctx.router!.createAudioLevelObserver({ maxEntries: -10 }) + ).rejects.toThrow(TypeError); + + await expect( + // @ts-expect-error --- Testing purposes. + ctx.router!.createAudioLevelObserver({ threshold: 'foo' }) + ).rejects.toThrow(TypeError); + + await expect( + // @ts-expect-error --- Testing purposes. + ctx.router!.createAudioLevelObserver({ interval: false }) + ).rejects.toThrow(TypeError); + + await expect( + // @ts-expect-error --- Testing purposes. + ctx.router!.createAudioLevelObserver({ appData: 'NOT-AN-OBJECT' }) + ).rejects.toThrow(TypeError); +}, 2000); + +test('audioLevelObserver.pause() and resume() succeed', async () => { + const audioLevelObserver = await ctx.router!.createAudioLevelObserver(); + + await audioLevelObserver.pause(); + + expect(audioLevelObserver.paused).toBe(true); + + await audioLevelObserver.resume(); + + expect(audioLevelObserver.paused).toBe(false); +}, 2000); + +test('audioLevelObserver.close() succeeds', async () => { + const audioLevelObserver = await ctx.router!.createAudioLevelObserver({ + maxEntries: 8, + }); + + let dump = await ctx.router!.dump(); + + expect(dump.rtpObserverIds.length).toBe(1); + + audioLevelObserver.close(); + + expect(audioLevelObserver.closed).toBe(true); + + dump = await ctx.router!.dump(); + + expect(dump.rtpObserverIds.length).toBe(0); +}, 2000); + +test('AudioLevelObserver emits "routerclose" if Router is closed', async () => { + const audioLevelObserver = await ctx.router!.createAudioLevelObserver(); + + const promise = enhancedOnce( + audioLevelObserver, + 'routerclose' + ); + + ctx.router!.close(); + await promise; + + expect(audioLevelObserver.closed).toBe(true); +}, 2000); + +test('AudioLevelObserver emits "routerclose" if Worker is closed', async () => { + const audioLevelObserver = await ctx.router!.createAudioLevelObserver(); + + const promise = enhancedOnce( + audioLevelObserver, + 'routerclose' + ); + + ctx.worker!.close(); + await promise; + + expect(audioLevelObserver.closed).toBe(true); +}, 2000); diff --git a/node/src/test/test-Consumer.ts b/node/src/test/test-Consumer.ts new file mode 100644 index 0000000000..286076c27a --- /dev/null +++ b/node/src/test/test-Consumer.ts @@ -0,0 +1,1199 @@ +import * as flatbuffers from 'flatbuffers'; +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { WorkerEvents, ConsumerEvents } from '../types'; +import type { ConsumerImpl } from '../Consumer'; +import { UnsupportedError } from '../errors'; +import * as utils from '../utils'; +import { + Notification, + Body as NotificationBody, + Event, +} from '../fbs/notification'; +import * as FbsConsumer from '../fbs/consumer'; + +type TestContext = { + mediaCodecs: mediasoup.types.RtpCodecCapability[]; + audioProducerOptions: mediasoup.types.ProducerOptions; + videoProducerOptions: mediasoup.types.ProducerOptions; + consumerDeviceCapabilities: mediasoup.types.RtpCapabilities; + worker?: mediasoup.types.Worker; + router?: mediasoup.types.Router; + webRtcTransport1?: mediasoup.types.WebRtcTransport; + webRtcTransport2?: mediasoup.types.WebRtcTransport; + audioProducer?: mediasoup.types.Producer; + videoProducer?: mediasoup.types.Producer; +}; + +const ctx: TestContext = { + mediaCodecs: utils.deepFreeze([ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + parameters: { + foo: 'bar', + }, + }, + { + kind: 'video', + mimeType: 'video/VP8', + clockRate: 90000, + }, + { + kind: 'video', + mimeType: 'video/H264', + clockRate: 90000, + parameters: { + 'level-asymmetry-allowed': 1, + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + foo: 'bar', + }, + }, + ]), + audioProducerOptions: utils.deepFreeze({ + kind: 'audio', + rtpParameters: { + mid: 'AUDIO', + codecs: [ + { + mimeType: 'audio/opus', + payloadType: 111, + clockRate: 48000, + channels: 2, + parameters: { + useinbandfec: 1, + usedtx: 1, + foo: 222.222, + bar: '333', + }, + }, + ], + headerExtensions: [ + { + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + id: 10, + }, + { + uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + id: 12, + }, + ], + encodings: [{ ssrc: 11111111 }], + rtcp: { + cname: 'FOOBAR', + }, + }, + appData: { foo: 1, bar: '2' }, + }), + videoProducerOptions: utils.deepFreeze({ + kind: 'video', + rtpParameters: { + mid: 'VIDEO', + codecs: [ + { + mimeType: 'video/h264', + payloadType: 112, + clockRate: 90000, + parameters: { + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }, + rtcpFeedback: [ + { type: 'nack', parameter: '' }, + { type: 'nack', parameter: 'pli' }, + { type: 'goog-remb', parameter: '' }, + ], + }, + { + mimeType: 'video/rtx', + payloadType: 113, + clockRate: 90000, + parameters: { apt: 112 }, + }, + ], + headerExtensions: [ + { + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + id: 10, + }, + { + uri: 'urn:3gpp:video-orientation', + id: 13, + }, + ], + encodings: [ + { ssrc: 22222222, scalabilityMode: 'L1T5', rtx: { ssrc: 22222223 } }, + { ssrc: 22222224, scalabilityMode: 'L1T5', rtx: { ssrc: 22222225 } }, + { ssrc: 22222226, scalabilityMode: 'L1T5', rtx: { ssrc: 22222227 } }, + { ssrc: 22222228, scalabilityMode: 'L1T5', rtx: { ssrc: 22222229 } }, + ], + rtcp: { + cname: 'FOOBAR', + }, + }, + appData: { foo: 1, bar: '2' }, + }), + consumerDeviceCapabilities: utils.deepFreeze( + { + codecs: [ + { + mimeType: 'audio/opus', + kind: 'audio', + preferredPayloadType: 100, + clockRate: 48000, + channels: 2, + rtcpFeedback: [{ type: 'nack', parameter: '' }], + }, + { + mimeType: 'video/H264', + kind: 'video', + preferredPayloadType: 101, + clockRate: 90000, + parameters: { + 'level-asymmetry-allowed': 1, + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }, + rtcpFeedback: [ + { type: 'nack', parameter: '' }, + { type: 'nack', parameter: 'pli' }, + { type: 'ccm', parameter: 'fir' }, + { type: 'goog-remb', parameter: '' }, + ], + }, + { + mimeType: 'video/rtx', + kind: 'video', + preferredPayloadType: 102, + clockRate: 90000, + parameters: { + apt: 101, + }, + rtcpFeedback: [], + }, + ], + headerExtensions: [ + { + kind: 'audio', + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + preferredId: 1, + preferredEncrypt: false, + }, + { + kind: 'video', + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + preferredId: 1, + preferredEncrypt: false, + }, + { + kind: 'video', + uri: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', + preferredId: 2, + preferredEncrypt: false, + }, + { + kind: 'audio', + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + preferredId: 4, + preferredEncrypt: false, + }, + { + kind: 'video', + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + preferredId: 4, + preferredEncrypt: false, + }, + { + kind: 'audio', + uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + preferredId: 10, + preferredEncrypt: false, + }, + { + kind: 'video', + uri: 'urn:3gpp:video-orientation', + preferredId: 11, + preferredEncrypt: false, + }, + { + kind: 'video', + uri: 'urn:ietf:params:rtp-hdrext:toffset', + preferredId: 12, + preferredEncrypt: false, + }, + ], + } + ), +}; + +beforeEach(async () => { + ctx.worker = await mediasoup.createWorker(); + ctx.router = await ctx.worker.createRouter({ mediaCodecs: ctx.mediaCodecs }); + ctx.webRtcTransport1 = await ctx.router.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + }); + ctx.webRtcTransport2 = await ctx.router.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + }); + ctx.audioProducer = await ctx.webRtcTransport1.produce( + ctx.audioProducerOptions + ); + ctx.videoProducer = await ctx.webRtcTransport1.produce( + ctx.videoProducerOptions + ); +}); + +afterEach(async () => { + ctx.worker?.close(); + + if (ctx.worker?.subprocessClosed === false) { + await enhancedOnce(ctx.worker, 'subprocessclose'); + } +}); + +test('transport.consume() succeeds', async () => { + const onObserverNewConsumer1 = jest.fn(); + + ctx.webRtcTransport2!.observer.once('newconsumer', onObserverNewConsumer1); + + expect( + ctx.router!.canConsume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }) + ).toBe(true); + + const audioConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + appData: { baz: 'LOL' }, + }); + + expect(onObserverNewConsumer1).toHaveBeenCalledTimes(1); + expect(onObserverNewConsumer1).toHaveBeenCalledWith(audioConsumer); + expect(typeof audioConsumer.id).toBe('string'); + expect(audioConsumer.producerId).toBe(ctx.audioProducer!.id); + expect(audioConsumer.closed).toBe(false); + expect(audioConsumer.kind).toBe('audio'); + expect(typeof audioConsumer.rtpParameters).toBe('object'); + expect(audioConsumer.rtpParameters.mid).toBe('0'); + expect(audioConsumer.rtpParameters.codecs.length).toBe(1); + expect(audioConsumer.rtpParameters.codecs[0]).toEqual({ + mimeType: 'audio/opus', + payloadType: 100, + clockRate: 48000, + channels: 2, + parameters: { + useinbandfec: 1, + usedtx: 1, + foo: 222.222, + bar: '333', + }, + rtcpFeedback: [], + }); + expect(audioConsumer.type).toBe('simple'); + expect(audioConsumer.paused).toBe(false); + expect(audioConsumer.producerPaused).toBe(false); + expect(audioConsumer.priority).toBe(1); + expect(audioConsumer.score).toEqual({ + score: 10, + producerScore: 0, + producerScores: [0], + }); + expect(audioConsumer.preferredLayers).toBeUndefined(); + expect(audioConsumer.currentLayers).toBeUndefined(); + expect(audioConsumer.appData).toEqual({ baz: 'LOL' }); + + const dump1 = await ctx.router!.dump(); + + expect(dump1.mapProducerIdConsumerIds).toEqual( + expect.arrayContaining([ + { key: ctx.audioProducer!.id, values: [audioConsumer.id] }, + ]) + ); + + expect(dump1.mapConsumerIdProducerId).toEqual( + expect.arrayContaining([ + { key: audioConsumer.id, value: ctx.audioProducer!.id }, + ]) + ); + + await expect(ctx.webRtcTransport2!.dump()).resolves.toMatchObject({ + id: ctx.webRtcTransport2!.id, + producerIds: [], + consumerIds: [audioConsumer.id], + }); + + const onObserverNewConsumer2 = jest.fn(); + + ctx.webRtcTransport2!.observer.once('newconsumer', onObserverNewConsumer2); + + expect( + ctx.router!.canConsume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }) + ).toBe(true); + + // Pause videoProducer. + await ctx.videoProducer!.pause(); + + const videoConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + paused: true, + preferredLayers: { spatialLayer: 12, temporalLayer: 0 }, + appData: { baz: 'LOL' }, + }); + + expect(onObserverNewConsumer2).toHaveBeenCalledTimes(1); + expect(onObserverNewConsumer2).toHaveBeenCalledWith(videoConsumer); + expect(typeof videoConsumer.id).toBe('string'); + expect(videoConsumer.producerId).toBe(ctx.videoProducer!.id); + expect(videoConsumer.closed).toBe(false); + expect(videoConsumer.kind).toBe('video'); + expect(typeof videoConsumer.rtpParameters).toBe('object'); + expect(videoConsumer.rtpParameters.mid).toBe('1'); + expect(videoConsumer.rtpParameters.codecs.length).toBe(2); + expect(videoConsumer.rtpParameters.codecs[0]).toEqual({ + mimeType: 'video/H264', + payloadType: 103, + clockRate: 90000, + parameters: { + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }, + rtcpFeedback: [ + { type: 'nack', parameter: '' }, + { type: 'nack', parameter: 'pli' }, + { type: 'ccm', parameter: 'fir' }, + { type: 'goog-remb', parameter: '' }, + ], + }); + expect(videoConsumer.rtpParameters.codecs[1]).toEqual({ + mimeType: 'video/rtx', + payloadType: 104, + clockRate: 90000, + parameters: { apt: 103 }, + rtcpFeedback: [], + }); + expect(videoConsumer.type).toBe('simulcast'); + expect(videoConsumer.paused).toBe(true); + expect(videoConsumer.producerPaused).toBe(true); + expect(videoConsumer.priority).toBe(1); + expect(videoConsumer.score).toEqual({ + score: 10, + producerScore: 0, + producerScores: [0, 0, 0, 0], + }); + expect(videoConsumer.preferredLayers).toEqual({ + spatialLayer: 3, + temporalLayer: 0, + }); + expect(videoConsumer.currentLayers).toBeUndefined(); + expect(videoConsumer.appData).toEqual({ baz: 'LOL' }); + + const onObserverNewConsumer3 = jest.fn(); + + ctx.webRtcTransport2!.observer.once('newconsumer', onObserverNewConsumer3); + + expect( + ctx.router!.canConsume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }) + ).toBe(true); + + const videoPipeConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + pipe: true, + }); + + expect(onObserverNewConsumer3).toHaveBeenCalledTimes(1); + expect(onObserverNewConsumer3).toHaveBeenCalledWith(videoPipeConsumer); + expect(typeof videoPipeConsumer.id).toBe('string'); + expect(videoPipeConsumer.producerId).toBe(ctx.videoProducer!.id); + expect(videoPipeConsumer.closed).toBe(false); + expect(videoPipeConsumer.kind).toBe('video'); + expect(typeof videoPipeConsumer.rtpParameters).toBe('object'); + expect(videoPipeConsumer.rtpParameters.mid).toBeUndefined(); + expect(videoPipeConsumer.rtpParameters.codecs.length).toBe(2); + expect(videoPipeConsumer.rtpParameters.codecs[0]).toEqual({ + mimeType: 'video/H264', + payloadType: 103, + clockRate: 90000, + parameters: { + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }, + rtcpFeedback: [ + { type: 'nack', parameter: '' }, + { type: 'nack', parameter: 'pli' }, + { type: 'ccm', parameter: 'fir' }, + { type: 'goog-remb', parameter: '' }, + ], + }); + expect(videoPipeConsumer.rtpParameters.codecs[1]).toEqual({ + mimeType: 'video/rtx', + payloadType: 104, + clockRate: 90000, + parameters: { apt: 103 }, + rtcpFeedback: [], + }); + expect(videoPipeConsumer.type).toBe('pipe'); + expect(videoPipeConsumer.paused).toBe(false); + expect(videoPipeConsumer.producerPaused).toBe(true); + expect(videoPipeConsumer.priority).toBe(1); + expect(videoPipeConsumer.score).toEqual({ + score: 10, + producerScore: 10, + producerScores: [0, 0, 0, 0], + }); + expect(videoPipeConsumer.preferredLayers).toBeUndefined(); + expect(videoPipeConsumer.currentLayers).toBeUndefined(); + expect(videoPipeConsumer.appData).toEqual({}); + + const dump2 = await ctx.router!.dump(); + + expect(Array.isArray(dump2.mapProducerIdConsumerIds)).toBe(true); + + expect(dump2.mapProducerIdConsumerIds).toEqual( + expect.arrayContaining([ + { + key: ctx.audioProducer!.id, + values: [audioConsumer.id], + }, + { + key: ctx.videoProducer!.id, + values: expect.arrayContaining([ + videoConsumer.id, + videoPipeConsumer.id, + ]), + }, + ]) + ); + expect(dump2.mapConsumerIdProducerId).toEqual( + expect.arrayContaining([ + { key: audioConsumer.id, value: ctx.audioProducer!.id }, + { key: videoConsumer.id, value: ctx.videoProducer!.id }, + { key: videoPipeConsumer.id, value: ctx.videoProducer!.id }, + ]) + ); + + await expect(ctx.webRtcTransport2!.dump()).resolves.toMatchObject({ + id: ctx.webRtcTransport2!.id, + producerIds: [], + consumerIds: expect.arrayContaining([ + audioConsumer.id, + videoConsumer.id, + videoPipeConsumer.id, + ]), + }); +}, 2000); + +test('transport.consume() with enableRtx succeeds', async () => { + const audioConsumer2 = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + enableRtx: true, + }); + + expect(audioConsumer2.kind).toBe('audio'); + expect(audioConsumer2.rtpParameters.codecs.length).toBe(1); + expect(audioConsumer2.rtpParameters.codecs[0]).toEqual({ + mimeType: 'audio/opus', + payloadType: 100, + clockRate: 48000, + channels: 2, + parameters: { + useinbandfec: 1, + usedtx: 1, + foo: 222.222, + bar: '333', + }, + rtcpFeedback: [{ type: 'nack', parameter: '' }], + }); +}, 2000); + +test('transport.consume() can be created with user provided mid', async () => { + const audioConsumer1 = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + expect(audioConsumer1.rtpParameters.mid).toEqual( + expect.stringMatching(/^[0-9]+/) + ); + + const audioConsumer2 = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + mid: 'custom-mid', + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + expect(audioConsumer2.rtpParameters.mid).toBe('custom-mid'); + + const audioConsumer3 = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + expect(audioConsumer3.rtpParameters.mid).toEqual( + expect.stringMatching(/^[0-9]+/) + ); + expect(Number(audioConsumer1.rtpParameters.mid) + 1).toBe( + Number(audioConsumer3.rtpParameters.mid) + ); +}, 2000); + +test('transport.consume() with incompatible rtpCapabilities rejects with UnsupportedError', async () => { + let invalidDeviceCapabilities: mediasoup.types.RtpCapabilities; + + invalidDeviceCapabilities = { + codecs: [ + { + kind: 'audio', + mimeType: 'audio/ISAC', + preferredPayloadType: 100, + clockRate: 32000, + channels: 1, + }, + ], + headerExtensions: [], + }; + + expect( + ctx.router!.canConsume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: invalidDeviceCapabilities, + }) + ).toBe(false); + + await expect( + ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: invalidDeviceCapabilities, + }) + ).rejects.toThrow(UnsupportedError); + + invalidDeviceCapabilities = { + codecs: [], + headerExtensions: [], + }; + + expect( + ctx.router!.canConsume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: invalidDeviceCapabilities, + }) + ).toBe(false); + + await expect( + ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: invalidDeviceCapabilities, + }) + ).rejects.toThrow(UnsupportedError); +}, 2000); + +test('consumer.dump() succeeds', async () => { + const audioConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + const dump1 = await audioConsumer.dump(); + + expect(dump1.id).toBe(audioConsumer.id); + expect(dump1.producerId).toBe(audioConsumer.producerId); + expect(dump1.kind).toBe(audioConsumer.kind); + expect(typeof dump1.rtpParameters).toBe('object'); + expect(Array.isArray(dump1.rtpParameters.codecs)).toBe(true); + expect(dump1.rtpParameters.codecs.length).toBe(1); + expect(dump1.rtpParameters.codecs[0].mimeType).toBe('audio/opus'); + expect(dump1.rtpParameters.codecs[0].payloadType).toBe(100); + expect(dump1.rtpParameters.codecs[0].clockRate).toBe(48000); + expect(dump1.rtpParameters.codecs[0].channels).toBe(2); + expect(dump1.rtpParameters.codecs[0].parameters).toEqual({ + useinbandfec: 1, + usedtx: 1, + foo: 222.222, + bar: '333', + }); + expect(dump1.rtpParameters.codecs[0].rtcpFeedback).toEqual([]); + expect(Array.isArray(dump1.rtpParameters.headerExtensions)).toBe(true); + expect(dump1.rtpParameters.headerExtensions!.length).toBe(3); + expect(dump1.rtpParameters.headerExtensions).toEqual([ + { + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + id: 1, + encrypt: false, + parameters: {}, + }, + { + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + id: 4, + parameters: {}, + encrypt: false, + }, + { + uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + id: 10, + parameters: {}, + encrypt: false, + }, + ]); + expect(Array.isArray(dump1.rtpParameters.encodings)).toBe(true); + expect(dump1.rtpParameters.encodings!.length).toBe(1); + expect(dump1.rtpParameters.encodings).toEqual([ + expect.objectContaining({ + codecPayloadType: 100, + ssrc: audioConsumer.rtpParameters.encodings?.[0].ssrc, + }), + ]); + expect(dump1.type).toBe('simple'); + expect(Array.isArray(dump1.consumableRtpEncodings)).toBe(true); + expect(dump1.consumableRtpEncodings!.length).toBe(1); + expect(dump1.consumableRtpEncodings).toEqual([ + expect.objectContaining({ + ssrc: ctx.audioProducer!.consumableRtpParameters.encodings?.[0].ssrc, + }), + ]); + expect(dump1.supportedCodecPayloadTypes).toEqual([100]); + expect(dump1.paused).toBe(false); + expect(dump1.producerPaused).toBe(false); + expect(dump1.priority).toBe(1); + + const videoConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + paused: true, + }); + const dump2 = await videoConsumer.dump(); + + expect(dump2.id).toBe(videoConsumer.id); + expect(dump2.producerId).toBe(videoConsumer.producerId); + expect(dump2.kind).toBe(videoConsumer.kind); + expect(typeof dump2.rtpParameters).toBe('object'); + expect(Array.isArray(dump2.rtpParameters.codecs)).toBe(true); + expect(dump2.rtpParameters.codecs.length).toBe(2); + expect(dump2.rtpParameters.codecs[0].mimeType).toBe('video/H264'); + expect(dump2.rtpParameters.codecs[0].payloadType).toBe(103); + expect(dump2.rtpParameters.codecs[0].clockRate).toBe(90000); + expect(dump2.rtpParameters.codecs[0].channels).toBeUndefined(); + expect(dump2.rtpParameters.codecs[0].parameters).toEqual({ + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }); + expect(dump2.rtpParameters.codecs[0].rtcpFeedback).toEqual([ + { type: 'nack' }, + { type: 'nack', parameter: 'pli' }, + { type: 'ccm', parameter: 'fir' }, + { type: 'goog-remb' }, + ]); + expect(Array.isArray(dump2.rtpParameters.headerExtensions)).toBe(true); + expect(dump2.rtpParameters.headerExtensions!.length).toBe(4); + expect(dump2.rtpParameters.headerExtensions).toEqual([ + { + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + id: 1, + encrypt: false, + parameters: {}, + }, + { + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + id: 4, + parameters: {}, + encrypt: false, + }, + { + uri: 'urn:3gpp:video-orientation', + id: 11, + parameters: {}, + encrypt: false, + }, + { + uri: 'urn:ietf:params:rtp-hdrext:toffset', + id: 12, + parameters: {}, + encrypt: false, + }, + ]); + expect(Array.isArray(dump2.rtpParameters.encodings)).toBe(true); + expect(dump2.rtpParameters.encodings!.length).toBe(1); + expect(dump2.rtpParameters.encodings).toMatchObject([ + { + codecPayloadType: 103, + ssrc: videoConsumer.rtpParameters.encodings?.[0].ssrc, + rtx: { + ssrc: videoConsumer.rtpParameters.encodings?.[0].rtx?.ssrc, + }, + scalabilityMode: 'L4T5', + }, + ]); + expect(Array.isArray(dump2.consumableRtpEncodings)).toBe(true); + expect(dump2.consumableRtpEncodings!.length).toBe(4); + expect(dump2.consumableRtpEncodings![0]).toEqual( + expect.objectContaining({ + ssrc: ctx.videoProducer!.consumableRtpParameters.encodings?.[0].ssrc, + scalabilityMode: 'L1T5', + }) + ); + expect(dump2.consumableRtpEncodings![1]).toEqual( + expect.objectContaining({ + ssrc: ctx.videoProducer!.consumableRtpParameters.encodings?.[1].ssrc, + scalabilityMode: 'L1T5', + }) + ); + expect(dump2.consumableRtpEncodings![2]).toEqual( + expect.objectContaining({ + ssrc: ctx.videoProducer!.consumableRtpParameters.encodings?.[2].ssrc, + scalabilityMode: 'L1T5', + }) + ); + expect(dump2.consumableRtpEncodings![3]).toEqual( + expect.objectContaining({ + ssrc: ctx.videoProducer!.consumableRtpParameters.encodings?.[3].ssrc, + scalabilityMode: 'L1T5', + }) + ); + expect(dump2.supportedCodecPayloadTypes).toEqual([103]); + expect(dump2.paused).toBe(true); + expect(dump2.producerPaused).toBe(false); + expect(dump2.priority).toBe(1); +}, 2000); + +test('consumer.getStats() succeeds', async () => { + const audioConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + await expect(audioConsumer.getStats()).resolves.toEqual([ + expect.objectContaining({ + type: 'outbound-rtp', + kind: 'audio', + mimeType: 'audio/opus', + ssrc: audioConsumer.rtpParameters.encodings?.[0].ssrc, + }), + ]); + + const videoConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + await expect(videoConsumer.getStats()).resolves.toEqual([ + expect.objectContaining({ + type: 'outbound-rtp', + kind: 'video', + mimeType: 'video/H264', + ssrc: videoConsumer.rtpParameters.encodings?.[0].ssrc, + }), + ]); +}, 2000); + +test('consumer.pause() and resume() succeed', async () => { + const audioConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + const onObserverPause = jest.fn(); + const onObserverResume = jest.fn(); + + audioConsumer.observer.on('pause', onObserverPause); + audioConsumer.observer.on('resume', onObserverResume); + + await audioConsumer.pause(); + expect(audioConsumer.paused).toBe(true); + + await expect(audioConsumer.dump()).resolves.toMatchObject({ paused: true }); + + await audioConsumer.resume(); + expect(audioConsumer.paused).toBe(false); + + await expect(audioConsumer.dump()).resolves.toMatchObject({ paused: false }); + + // Even if we don't await for pause()/resume() completion, the observer must + // fire 'pause' and 'resume' events if state was the opposite. + void audioConsumer.pause(); + void audioConsumer.resume(); + void audioConsumer.pause(); + void audioConsumer.pause(); + void audioConsumer.pause(); + await audioConsumer.resume(); + + expect(onObserverPause).toHaveBeenCalledTimes(3); + expect(onObserverResume).toHaveBeenCalledTimes(3); +}, 2000); + +test('producer.pause() and resume() emit events', async () => { + const audioConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + const promises = []; + const events: string[] = []; + + audioConsumer.observer.once('resume', () => { + events.push('resume'); + }); + + audioConsumer.observer.once('pause', () => { + events.push('pause'); + }); + + promises.push(ctx.audioProducer!.pause()); + promises.push(ctx.audioProducer!.resume()); + + await Promise.all(promises); + + // Must also wait a bit for the corresponding events in the consumer. + await new Promise(resolve => setTimeout(resolve, 100)); + + expect(events).toEqual(['pause', 'resume']); + expect(audioConsumer.paused).toBe(false); +}, 2000); + +test('consumer.setPreferredLayers() succeed', async () => { + const audioConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + const videoConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + await audioConsumer.setPreferredLayers({ spatialLayer: 1, temporalLayer: 1 }); + + expect(audioConsumer.preferredLayers).toBeUndefined(); + + await videoConsumer.setPreferredLayers({ spatialLayer: 2, temporalLayer: 3 }); + + expect(videoConsumer.preferredLayers).toEqual({ + spatialLayer: 2, + temporalLayer: 3, + }); + + await videoConsumer.setPreferredLayers({ spatialLayer: 3 }); + + expect(videoConsumer.preferredLayers).toEqual({ + spatialLayer: 3, + temporalLayer: 4, + }); + + await videoConsumer.setPreferredLayers({ spatialLayer: 3, temporalLayer: 0 }); + + expect(videoConsumer.preferredLayers).toEqual({ + spatialLayer: 3, + temporalLayer: 0, + }); + + await videoConsumer.setPreferredLayers({ + spatialLayer: 66, + temporalLayer: 66, + }); + + expect(videoConsumer.preferredLayers).toEqual({ + spatialLayer: 3, + temporalLayer: 4, + }); +}, 2000); + +test('consumer.setPreferredLayers() with wrong arguments rejects with TypeError', async () => { + const videoConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + // @ts-expect-error --- Testing purposes. + await expect(videoConsumer.setPreferredLayers({})).rejects.toThrow(TypeError); + + await expect( + // @ts-expect-error --- Testing purposes. + videoConsumer.setPreferredLayers({ foo: '123' }) + ).rejects.toThrow(TypeError); + + // @ts-expect-error --- Testing purposes. + await expect(videoConsumer.setPreferredLayers('foo')).rejects.toThrow( + TypeError + ); + + // Missing spatialLayer. + await expect( + // @ts-expect-error --- Testing purposes. + videoConsumer.setPreferredLayers({ temporalLayer: 2 }) + ).rejects.toThrow(TypeError); +}, 2000); + +test('consumer.setPriority() succeed', async () => { + const videoConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + await videoConsumer.setPriority(2); + expect(videoConsumer.priority).toBe(2); +}, 2000); + +test('consumer.setPriority() with wrong arguments rejects with TypeError', async () => { + const videoConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + // @ts-expect-error --- Testing purposes. + await expect(videoConsumer.setPriority()).rejects.toThrow(TypeError); + + await expect(videoConsumer.setPriority(0)).rejects.toThrow(TypeError); + + // @ts-expect-error --- Testing purposes. + await expect(videoConsumer.setPriority('foo')).rejects.toThrow(TypeError); +}, 2000); + +test('consumer.unsetPriority() succeed', async () => { + const videoConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + await videoConsumer.unsetPriority(); + expect(videoConsumer.priority).toBe(1); +}, 2000); + +test('consumer.enableTraceEvent() succeed', async () => { + const audioConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + await audioConsumer.enableTraceEvent(['rtp', 'pli']); + const dump1 = await audioConsumer.dump(); + + expect(dump1.traceEventTypes).toEqual(expect.arrayContaining(['rtp', 'pli'])); + + await audioConsumer.enableTraceEvent(); + + const dump2 = await audioConsumer.dump(); + + expect(dump2.traceEventTypes).toEqual(expect.arrayContaining([])); + + // @ts-expect-error --- Testing purposes. + await audioConsumer.enableTraceEvent(['nack', 'FOO', 'fir']); + + const dump3 = await audioConsumer.dump(); + + expect(dump3.traceEventTypes).toEqual( + expect.arrayContaining(['nack', 'fir']) + ); + + await audioConsumer.enableTraceEvent(); + + const dump4 = await audioConsumer.dump(); + + expect(dump4.traceEventTypes).toEqual(expect.arrayContaining([])); +}, 2000); + +test('consumer.enableTraceEvent() with wrong arguments rejects with TypeError', async () => { + const audioConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + // @ts-expect-error --- Testing purposes. + await expect(audioConsumer.enableTraceEvent(123)).rejects.toThrow(TypeError); + + // @ts-expect-error --- Testing purposes. + await expect(audioConsumer.enableTraceEvent('rtp')).rejects.toThrow( + TypeError + ); + + await expect( + // @ts-expect-error --- Testing purposes. + audioConsumer.enableTraceEvent(['fir', 123.123]) + ).rejects.toThrow(TypeError); +}, 2000); + +test('Consumer emits "producerpause" and "producerresume"', async () => { + const audioConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + await Promise.all([ + enhancedOnce(audioConsumer, 'producerpause'), + + // Let's await for pause() to resolve to avoid aborted channel requests + // due to worker closure. + ctx.audioProducer!.pause(), + ]); + + expect(audioConsumer.paused).toBe(false); + expect(audioConsumer.producerPaused).toBe(true); + + await Promise.all([ + enhancedOnce(audioConsumer, 'producerresume'), + + // Let's await for resume() to resolve to avoid aborted channel requests + // due to worker closure. + ctx.audioProducer!.resume(), + ]); + + expect(audioConsumer.paused).toBe(false); + expect(audioConsumer.producerPaused).toBe(false); +}, 2000); + +test('Consumer emits "score"', async () => { + const audioConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + // API not exposed in the interface. + const channel = (audioConsumer as ConsumerImpl).channelForTesting; + const onScore = jest.fn(); + + audioConsumer.on('score', onScore); + + // Simulate a 'score' notification coming through the channel. + const builder = new flatbuffers.Builder(); + const consumerScore = new FbsConsumer.ConsumerScoreT(9, 10, [8]); + const consumerScoreNotification = new FbsConsumer.ScoreNotificationT( + consumerScore + ); + const notificationOffset = Notification.createNotification( + builder, + builder.createString(audioConsumer.id), + Event.CONSUMER_SCORE, + NotificationBody.Consumer_ScoreNotification, + consumerScoreNotification.pack(builder) + ); + + builder.finish(notificationOffset); + + const notification = Notification.getRootAsNotification( + new flatbuffers.ByteBuffer(builder.asUint8Array()) + ); + + channel.emit(audioConsumer.id, Event.CONSUMER_SCORE, notification); + channel.emit(audioConsumer.id, Event.CONSUMER_SCORE, notification); + channel.emit(audioConsumer.id, Event.CONSUMER_SCORE, notification); + + expect(onScore).toHaveBeenCalledTimes(3); + expect(audioConsumer.score).toEqual({ + score: 9, + producerScore: 10, + producerScores: [8], + }); +}, 2000); + +test('consumer.close() succeeds', async () => { + const audioConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + const videoConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + const onObserverClose = jest.fn(); + + audioConsumer.observer.once('close', onObserverClose); + audioConsumer.close(); + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(audioConsumer.closed).toBe(true); + + const routerDump = await ctx.router!.dump(); + + expect(routerDump.mapProducerIdConsumerIds).toEqual( + expect.arrayContaining([ + { key: ctx.audioProducer!.id, values: [] }, + { key: ctx.videoProducer!.id, values: [videoConsumer.id] }, + ]) + ); + expect(routerDump.mapConsumerIdProducerId).toEqual([ + { key: videoConsumer!.id, value: ctx.videoProducer!.id }, + ]); + + const transportDump = await ctx.webRtcTransport2!.dump(); + + expect(transportDump).toMatchObject({ + id: ctx.webRtcTransport2!.id, + producerIds: [], + consumerIds: [videoConsumer.id], + }); +}, 2000); + +test('Consumer methods reject if closed', async () => { + const audioConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + audioConsumer.close(); + + await expect(audioConsumer.dump()).rejects.toThrow(Error); + + await expect(audioConsumer.getStats()).rejects.toThrow(Error); + + await expect(audioConsumer.pause()).rejects.toThrow(Error); + + await expect(audioConsumer.resume()).rejects.toThrow(Error); + + // @ts-expect-error --- Testing purposes. + await expect(audioConsumer.setPreferredLayers({})).rejects.toThrow(Error); + + await expect(audioConsumer.setPriority(2)).rejects.toThrow(Error); + + await expect(audioConsumer.requestKeyFrame()).rejects.toThrow(Error); +}, 2000); + +test('Consumer emits "producerclose" if Producer is closed', async () => { + const audioConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.audioProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + const onObserverClose = jest.fn(); + + audioConsumer.observer.once('close', onObserverClose); + + const promise = enhancedOnce(audioConsumer, 'producerclose'); + + ctx.audioProducer!.close(); + await promise; + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(audioConsumer.closed).toBe(true); +}, 2000); + +test('Consumer emits "transportclose" if Transport is closed', async () => { + const videoConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + const onObserverClose = jest.fn(); + + videoConsumer.observer.once('close', onObserverClose); + + const promise = enhancedOnce(videoConsumer, 'transportclose'); + + ctx.webRtcTransport2!.close(); + await promise; + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(videoConsumer.closed).toBe(true); + + await expect(ctx.router!.dump()).resolves.toMatchObject({ + mapProducerIdConsumerIds: expect.arrayContaining([ + { key: ctx.audioProducer!.id, values: [] }, + { key: ctx.videoProducer!.id, values: [] }, + ]), + mapConsumerIdProducerId: [], + }); +}, 2000); diff --git a/node/src/test/test-DataConsumer.ts b/node/src/test/test-DataConsumer.ts new file mode 100644 index 0000000000..d8e800855b --- /dev/null +++ b/node/src/test/test-DataConsumer.ts @@ -0,0 +1,412 @@ +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { WorkerEvents, DataConsumerEvents } from '../types'; +import * as utils from '../utils'; + +type TestContext = { + dataProducerOptions: mediasoup.types.DataProducerOptions; + worker?: mediasoup.types.Worker; + router?: mediasoup.types.Router; + webRtcTransport1?: mediasoup.types.WebRtcTransport; + webRtcTransport2?: mediasoup.types.WebRtcTransport; + directTransport?: mediasoup.types.DirectTransport; + dataProducer?: mediasoup.types.DataProducer; +}; + +const ctx: TestContext = { + dataProducerOptions: utils.deepFreeze({ + sctpStreamParameters: { + streamId: 12345, + ordered: false, + maxPacketLifeTime: 5000, + }, + label: 'foo', + protocol: 'bar', + }), +}; + +beforeEach(async () => { + ctx.worker = await mediasoup.createWorker(); + ctx.router = await ctx.worker.createRouter(); + ctx.webRtcTransport1 = await ctx.router.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + enableSctp: true, + }); + ctx.webRtcTransport2 = await ctx.router.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + enableSctp: true, + }); + ctx.directTransport = await ctx.router.createDirectTransport(); + ctx.dataProducer = await ctx.webRtcTransport1.produceData( + ctx.dataProducerOptions + ); +}); + +afterEach(async () => { + ctx.worker?.close(); + + if (ctx.worker?.subprocessClosed === false) { + await enhancedOnce(ctx.worker, 'subprocessclose'); + } +}); + +test('transport.consumeData() succeeds', async () => { + const onObserverNewDataConsumer = jest.fn(); + + ctx.webRtcTransport2!.observer.once( + 'newdataconsumer', + onObserverNewDataConsumer + ); + + const dataConsumer1 = await ctx.webRtcTransport2!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + maxPacketLifeTime: 4000, + // Valid values are 0...65535 so others and duplicated ones will be + // discarded. + subchannels: [0, 1, 1, 1, 2, 65535, 65536, 65537, 100], + appData: { baz: 'LOL' }, + }); + + expect(onObserverNewDataConsumer).toHaveBeenCalledTimes(1); + expect(onObserverNewDataConsumer).toHaveBeenCalledWith(dataConsumer1); + expect(typeof dataConsumer1.id).toBe('string'); + expect(dataConsumer1.dataProducerId).toBe(ctx.dataProducer!.id); + expect(dataConsumer1.closed).toBe(false); + expect(dataConsumer1.type).toBe('sctp'); + expect(typeof dataConsumer1.sctpStreamParameters).toBe('object'); + expect(typeof dataConsumer1.sctpStreamParameters?.streamId).toBe('number'); + expect(dataConsumer1.sctpStreamParameters?.ordered).toBe(false); + expect(dataConsumer1.sctpStreamParameters?.maxPacketLifeTime).toBe(4000); + expect(dataConsumer1.sctpStreamParameters?.maxRetransmits).toBeUndefined(); + expect(dataConsumer1.label).toBe('foo'); + expect(dataConsumer1.protocol).toBe('bar'); + expect(dataConsumer1.paused).toBe(false); + expect(dataConsumer1.subchannels).toEqual( + expect.arrayContaining([0, 1, 2, 100, 65535]) + ); + expect(dataConsumer1.appData).toEqual({ baz: 'LOL' }); + + const dump = await ctx.router!.dump(); + + expect(dump.mapDataProducerIdDataConsumerIds).toEqual( + expect.arrayContaining([ + { key: ctx.dataProducer!.id, values: [dataConsumer1.id] }, + ]) + ); + + expect(dump.mapDataConsumerIdDataProducerId).toEqual( + expect.arrayContaining([ + { key: dataConsumer1.id, value: ctx.dataProducer!.id }, + ]) + ); + + await expect(ctx.webRtcTransport2!.dump()).resolves.toMatchObject({ + id: ctx.webRtcTransport2!.id, + dataProducerIds: [], + dataConsumerIds: [dataConsumer1.id], + }); +}, 2000); + +test('dataConsumer.dump() succeeds', async () => { + const dataConsumer = await ctx.webRtcTransport2!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + maxPacketLifeTime: 4000, + // Valid values are 0...65535 so others and duplicated ones will be + // discarded. + subchannels: [0, 1, 1, 1, 2, 65535, 65536, 65537, 100], + appData: { baz: 'LOL' }, + }); + + const dump = await dataConsumer.dump(); + + expect(dump.id).toBe(dataConsumer.id); + expect(dump.dataProducerId).toBe(dataConsumer.dataProducerId); + expect(dump.type).toBe('sctp'); + expect(typeof dump.sctpStreamParameters).toBe('object'); + expect(dump.sctpStreamParameters!.streamId).toBe( + dataConsumer.sctpStreamParameters?.streamId + ); + expect(dump.sctpStreamParameters!.ordered).toBe(false); + expect(dump.sctpStreamParameters!.maxPacketLifeTime).toBe(4000); + expect(dump.sctpStreamParameters!.maxRetransmits).toBeUndefined(); + expect(dump.label).toBe('foo'); + expect(dump.protocol).toBe('bar'); + expect(dump.paused).toBe(false); + expect(dump.dataProducerPaused).toBe(false); + expect(dump.subchannels).toEqual( + expect.arrayContaining([0, 1, 2, 100, 65535]) + ); +}, 2000); + +test('dataConsumer.getStats() succeeds', async () => { + const dataConsumer = await ctx.webRtcTransport2!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + }); + + await expect(dataConsumer.getStats()).resolves.toMatchObject([ + { + type: 'data-consumer', + label: dataConsumer.label, + protocol: dataConsumer.protocol, + messagesSent: 0, + bytesSent: 0, + }, + ]); +}, 2000); + +test('dataConsumer.setSubchannels() succeeds', async () => { + const dataConsumer = await ctx.webRtcTransport2!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + }); + + await dataConsumer.setSubchannels([999, 999, 998, 65536]); + + expect(dataConsumer.subchannels).toEqual( + expect.arrayContaining([0, 998, 999]) + ); +}, 2000); + +test('dataConsumer.addSubchannel() and .removeSubchannel() succeed', async () => { + const dataConsumer = await ctx.webRtcTransport2!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + }); + + await dataConsumer.setSubchannels([]); + expect(dataConsumer.subchannels).toEqual([]); + + await dataConsumer.addSubchannel(5); + expect(dataConsumer.subchannels).toEqual(expect.arrayContaining([5])); + + await dataConsumer.addSubchannel(10); + expect(dataConsumer.subchannels).toEqual(expect.arrayContaining([5, 10])); + + await dataConsumer.addSubchannel(5); + expect(dataConsumer.subchannels).toEqual(expect.arrayContaining([5, 10])); + + await dataConsumer.removeSubchannel(666); + expect(dataConsumer.subchannels).toEqual(expect.arrayContaining([5, 10])); + + await dataConsumer.removeSubchannel(5); + expect(dataConsumer.subchannels).toEqual(expect.arrayContaining([10])); + + await dataConsumer.setSubchannels([]); + expect(dataConsumer.subchannels).toEqual([]); +}, 2000); + +test('transport.consumeData() on a DirectTransport succeeds', async () => { + const onObserverNewDataConsumer = jest.fn(); + + ctx.directTransport!.observer.once( + 'newdataconsumer', + onObserverNewDataConsumer + ); + + const dataConsumer = await ctx.directTransport!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + paused: true, + appData: { hehe: 'HEHE' }, + }); + + expect(onObserverNewDataConsumer).toHaveBeenCalledTimes(1); + expect(onObserverNewDataConsumer).toHaveBeenCalledWith(dataConsumer); + expect(typeof dataConsumer.id).toBe('string'); + expect(dataConsumer.dataProducerId).toBe(ctx.dataProducer!.id); + expect(dataConsumer.closed).toBe(false); + expect(dataConsumer.type).toBe('direct'); + expect(dataConsumer.sctpStreamParameters).toBeUndefined(); + expect(dataConsumer.label).toBe('foo'); + expect(dataConsumer.protocol).toBe('bar'); + expect(dataConsumer.paused).toBe(true); + expect(dataConsumer.appData).toEqual({ hehe: 'HEHE' }); + + await expect(ctx.directTransport!.dump()).resolves.toMatchObject({ + id: ctx.directTransport!.id, + dataProducerIds: [], + dataConsumerIds: [dataConsumer.id], + }); +}, 2000); + +test('dataConsumer.dump() on a DirectTransport succeeds', async () => { + const dataConsumer = await ctx.directTransport!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + paused: true, + }); + + const dump = await dataConsumer.dump(); + + expect(dump.id).toBe(dataConsumer.id); + expect(dump.dataProducerId).toBe(dataConsumer.dataProducerId); + expect(dump.type).toBe('direct'); + expect(dump.sctpStreamParameters).toBeUndefined(); + expect(dump.label).toBe('foo'); + expect(dump.protocol).toBe('bar'); + expect(dump.paused).toBe(true); + expect(dump.subchannels).toEqual([]); +}, 2000); + +test('dataConsumer.getStats() on a DirectTransport succeeds', async () => { + const dataConsumer = await ctx.directTransport!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + }); + + await expect(dataConsumer.getStats()).resolves.toMatchObject([ + { + type: 'data-consumer', + label: dataConsumer.label, + protocol: dataConsumer.protocol, + messagesSent: 0, + bytesSent: 0, + }, + ]); +}, 2000); + +test('dataConsumer.pause() and resume() succeed', async () => { + const onObserverPause = jest.fn(); + const onObserverResume = jest.fn(); + + const dataConsumer = await ctx.webRtcTransport2!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + }); + + dataConsumer.observer.on('pause', onObserverPause); + dataConsumer.observer.on('resume', onObserverResume); + + await dataConsumer.pause(); + + expect(dataConsumer.paused).toBe(true); + + const dump1 = await dataConsumer.dump(); + + expect(dump1.paused).toBe(true); + + await dataConsumer.resume(); + + expect(dataConsumer.paused).toBe(false); + + const dump2 = await dataConsumer.dump(); + + expect(dump2.paused).toBe(false); + + // Even if we don't await for pause()/resume() completion, the observer must + // fire 'pause' and 'resume' events if state was the opposite. + void dataConsumer.pause(); + void dataConsumer.resume(); + void dataConsumer.pause(); + void dataConsumer.pause(); + void dataConsumer.pause(); + await dataConsumer.resume(); + + expect(onObserverPause).toHaveBeenCalledTimes(3); + expect(onObserverResume).toHaveBeenCalledTimes(3); +}, 2000); + +test('dataProducer.pause() and resume() emit events', async () => { + const dataConsumer = await ctx.webRtcTransport2!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + }); + const promises = []; + const events: string[] = []; + + dataConsumer.observer.once('resume', () => { + events.push('resume'); + }); + + dataConsumer.observer.once('pause', () => { + events.push('pause'); + }); + + promises.push(ctx.dataProducer!.pause()); + promises.push(ctx.dataProducer!.resume()); + + await Promise.all(promises); + + // Must also wait a bit for the corresponding events in the data consumer. + await new Promise(resolve => setTimeout(resolve, 100)); + + expect(events).toEqual(['pause', 'resume']); + expect(dataConsumer.paused).toBe(false); +}, 2000); + +test('dataConsumer.close() succeeds', async () => { + const onObserverClose = jest.fn(); + const dataConsumer = await ctx.webRtcTransport2!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + }); + + dataConsumer.observer.once('close', onObserverClose); + dataConsumer.close(); + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(dataConsumer.closed).toBe(true); + + const dump = await ctx.router!.dump(); + + expect(dump.mapDataProducerIdDataConsumerIds).toEqual( + expect.arrayContaining([{ key: ctx.dataProducer!.id, values: [] }]) + ); + + expect(dump.mapDataConsumerIdDataProducerId).toEqual([]); + + await expect(ctx.webRtcTransport2!.dump()).resolves.toMatchObject({ + id: ctx.webRtcTransport2!.id, + dataProducerIds: [], + dataConsumerIds: [], + }); +}, 2000); + +test('Consumer methods reject if closed', async () => { + const dataConsumer = await ctx.webRtcTransport2!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + }); + + dataConsumer.close(); + + await expect(dataConsumer.dump()).rejects.toThrow(Error); + + await expect(dataConsumer.getStats()).rejects.toThrow(Error); +}, 2000); + +test('DataConsumer emits "dataproducerclose" if DataProducer is closed', async () => { + const dataConsumer = await ctx.webRtcTransport2!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + }); + const onObserverClose = jest.fn(); + + dataConsumer.observer.once('close', onObserverClose); + + const promise = enhancedOnce( + dataConsumer, + 'dataproducerclose' + ); + + ctx.dataProducer!.close(); + await promise; + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(dataConsumer.closed).toBe(true); +}, 2000); + +test('DataConsumer emits "transportclose" if Transport is closed', async () => { + const dataConsumer = await ctx.webRtcTransport2!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + }); + const onObserverClose = jest.fn(); + + dataConsumer.observer.once('close', onObserverClose); + + const promise = enhancedOnce( + dataConsumer, + 'transportclose' + ); + + ctx.webRtcTransport2!.close(); + await promise; + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(dataConsumer.closed).toBe(true); + + await expect(ctx.router!.dump()).resolves.toMatchObject({ + mapDataProducerIdDataConsumerIds: {}, + mapDataConsumerIdDataProducerId: {}, + }); +}, 2000); diff --git a/node/src/test/test-DataProducer.ts b/node/src/test/test-DataProducer.ts new file mode 100644 index 0000000000..1dbd8691e7 --- /dev/null +++ b/node/src/test/test-DataProducer.ts @@ -0,0 +1,366 @@ +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { WorkerEvents, DataProducerEvents } from '../types'; +import * as utils from '../utils'; + +type TestContext = { + dataProducerOptions1: mediasoup.types.DataProducerOptions; + dataProducerOptions2: mediasoup.types.DataProducerOptions; + worker?: mediasoup.types.Worker; + router?: mediasoup.types.Router; + webRtcTransport1?: mediasoup.types.WebRtcTransport; + webRtcTransport2?: mediasoup.types.WebRtcTransport; +}; + +const ctx: TestContext = { + dataProducerOptions1: utils.deepFreeze({ + sctpStreamParameters: { + streamId: 666, + }, + label: 'foo', + protocol: 'bar', + appData: { foo: 1, bar: '2' }, + }), + dataProducerOptions2: utils.deepFreeze({ + sctpStreamParameters: { + streamId: 777, + maxRetransmits: 3, + }, + label: 'foo', + protocol: 'bar', + paused: true, + appData: { foo: 1, bar: '2' }, + }), +}; + +beforeEach(async () => { + ctx.worker = await mediasoup.createWorker(); + ctx.router = await ctx.worker.createRouter(); + ctx.webRtcTransport1 = await ctx.router.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + enableSctp: true, + }); + ctx.webRtcTransport2 = await ctx.router.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + enableSctp: true, + }); +}); + +afterEach(async () => { + ctx.worker?.close(); + + if (ctx.worker?.subprocessClosed === false) { + await enhancedOnce(ctx.worker, 'subprocessclose'); + } +}); + +test('webRtcTransport1.produceData() succeeds', async () => { + const onObserverNewDataProducer = jest.fn(); + + ctx.webRtcTransport1!.observer.once( + 'newdataproducer', + onObserverNewDataProducer + ); + + const dataProducer1 = await ctx.webRtcTransport1!.produceData( + ctx.dataProducerOptions1 + ); + + expect(onObserverNewDataProducer).toHaveBeenCalledTimes(1); + expect(onObserverNewDataProducer).toHaveBeenCalledWith(dataProducer1); + expect(typeof dataProducer1.id).toBe('string'); + expect(dataProducer1.closed).toBe(false); + expect(dataProducer1.type).toBe('sctp'); + expect(typeof dataProducer1.sctpStreamParameters).toBe('object'); + expect(dataProducer1.sctpStreamParameters?.streamId).toBe(666); + expect(dataProducer1.sctpStreamParameters?.ordered).toBe(true); + expect(dataProducer1.sctpStreamParameters?.maxPacketLifeTime).toBeUndefined(); + expect(dataProducer1.sctpStreamParameters?.maxRetransmits).toBeUndefined(); + expect(dataProducer1.label).toBe('foo'); + expect(dataProducer1.protocol).toBe('bar'); + expect(dataProducer1.paused).toBe(false); + expect(dataProducer1.appData).toEqual({ foo: 1, bar: '2' }); + + const dump = await ctx.router!.dump(); + + expect(dump.mapDataProducerIdDataConsumerIds).toEqual( + expect.arrayContaining([{ key: dataProducer1.id, values: [] }]) + ); + + expect(dump.mapDataConsumerIdDataProducerId.length).toBe(0); + + await expect(ctx.webRtcTransport1!.dump()).resolves.toMatchObject({ + id: ctx.webRtcTransport1!.id, + dataProducerIds: [dataProducer1.id], + dataConsumerIds: [], + }); +}, 2000); + +test('webRtcTransport2.produceData() succeeds', async () => { + const onObserverNewDataProducer = jest.fn(); + + ctx.webRtcTransport2!.observer.once( + 'newdataproducer', + onObserverNewDataProducer + ); + + const dataProducer2 = await ctx.webRtcTransport2!.produceData( + ctx.dataProducerOptions2 + ); + + expect(onObserverNewDataProducer).toHaveBeenCalledTimes(1); + expect(onObserverNewDataProducer).toHaveBeenCalledWith(dataProducer2); + expect(typeof dataProducer2.id).toBe('string'); + expect(dataProducer2.closed).toBe(false); + expect(dataProducer2.type).toBe('sctp'); + expect(typeof dataProducer2.sctpStreamParameters).toBe('object'); + expect(dataProducer2.sctpStreamParameters?.streamId).toBe(777); + expect(dataProducer2.sctpStreamParameters?.ordered).toBe(false); + expect(dataProducer2.sctpStreamParameters?.maxPacketLifeTime).toBeUndefined(); + expect(dataProducer2.sctpStreamParameters?.maxRetransmits).toBe(3); + expect(dataProducer2.label).toBe('foo'); + expect(dataProducer2.protocol).toBe('bar'); + expect(dataProducer2.paused).toBe(true); + expect(dataProducer2.appData).toEqual({ foo: 1, bar: '2' }); + + const dump = await ctx.router!.dump(); + + expect(dump.mapDataProducerIdDataConsumerIds).toEqual( + expect.arrayContaining([{ key: dataProducer2.id, values: [] }]) + ); + + expect(dump.mapDataConsumerIdDataProducerId.length).toBe(0); + + await expect(ctx.webRtcTransport2!.dump()).resolves.toMatchObject({ + id: ctx.webRtcTransport2!.id, + dataProducerIds: [dataProducer2.id], + dataConsumerIds: [], + }); +}, 2000); + +test('webRtcTransport1.produceData() with wrong arguments rejects with TypeError', async () => { + await expect(ctx.webRtcTransport1!.produceData({})).rejects.toThrow( + TypeError + ); + + // Missing or empty sctpStreamParameters.streamId. + await expect( + ctx.webRtcTransport1!.produceData({ + // @ts-expect-error --- Testing purposes. + sctpStreamParameters: { foo: 'foo' }, + }) + ).rejects.toThrow(TypeError); +}, 2000); + +test('transport.produceData() with already used streamId rejects with Error', async () => { + await ctx.webRtcTransport1!.produceData(ctx.dataProducerOptions1); + + await expect( + ctx.webRtcTransport1!.produceData({ + sctpStreamParameters: { + streamId: 666, + }, + }) + ).rejects.toThrow(Error); +}, 2000); + +test('transport.produceData() with ordered and maxPacketLifeTime rejects with TypeError', async () => { + await expect( + ctx.webRtcTransport1!.produceData({ + sctpStreamParameters: { + streamId: 999, + ordered: true, + maxPacketLifeTime: 4000, + }, + }) + ).rejects.toThrow(TypeError); +}, 2000); + +test('dataProducer.dump() succeeds', async () => { + const dataProducer1 = await ctx.webRtcTransport1!.produceData( + ctx.dataProducerOptions1 + ); + + const dump1 = await dataProducer1.dump(); + + expect(dump1.id).toBe(dataProducer1.id); + expect(dump1.type).toBe('sctp'); + expect(typeof dump1.sctpStreamParameters).toBe('object'); + expect(dump1.sctpStreamParameters!.streamId).toBe(666); + expect(dump1.sctpStreamParameters!.ordered).toBe(true); + expect(dump1.sctpStreamParameters!.maxPacketLifeTime).toBeUndefined(); + expect(dump1.sctpStreamParameters!.maxRetransmits).toBeUndefined(); + expect(dump1.label).toBe('foo'); + expect(dump1.protocol).toBe('bar'); + expect(dump1.paused).toBe(false); + + const dataProducer2 = await ctx.webRtcTransport2!.produceData( + ctx.dataProducerOptions2 + ); + + const dump2 = await dataProducer2.dump(); + + expect(dump2.id).toBe(dataProducer2.id); + expect(dump2.type).toBe('sctp'); + expect(typeof dump2.sctpStreamParameters).toBe('object'); + expect(dump2.sctpStreamParameters!.streamId).toBe(777); + expect(dump2.sctpStreamParameters!.ordered).toBe(false); + expect(dump2.sctpStreamParameters!.maxPacketLifeTime).toBeUndefined(); + expect(dump2.sctpStreamParameters!.maxRetransmits).toBe(3); + expect(dump2.label).toBe('foo'); + expect(dump2.protocol).toBe('bar'); + expect(dump2.paused).toBe(true); +}, 2000); + +test('dataProducer.getStats() succeeds', async () => { + const dataProducer1 = await ctx.webRtcTransport1!.produceData( + ctx.dataProducerOptions1 + ); + + await expect(dataProducer1.getStats()).resolves.toMatchObject([ + { + type: 'data-producer', + label: dataProducer1.label, + protocol: dataProducer1.protocol, + messagesReceived: 0, + bytesReceived: 0, + }, + ]); + + const dataProducer2 = await ctx.webRtcTransport2!.produceData( + ctx.dataProducerOptions2 + ); + + await expect(dataProducer2.getStats()).resolves.toMatchObject([ + { + type: 'data-producer', + label: dataProducer2.label, + protocol: dataProducer2.protocol, + messagesReceived: 0, + bytesReceived: 0, + }, + ]); +}, 2000); + +test('dataProducer.pause() and resume() succeed', async () => { + const dataProducer1 = await ctx.webRtcTransport1!.produceData( + ctx.dataProducerOptions1 + ); + + const onObserverPause = jest.fn(); + const onObserverResume = jest.fn(); + + dataProducer1.observer.on('pause', onObserverPause); + dataProducer1.observer.on('resume', onObserverResume); + + await dataProducer1.pause(); + + expect(dataProducer1.paused).toBe(true); + + const dump1 = await dataProducer1.dump(); + + expect(dump1.paused).toBe(true); + + await dataProducer1.resume(); + + expect(dataProducer1.paused).toBe(false); + + const dump2 = await dataProducer1.dump(); + + expect(dump2.paused).toBe(false); + + // Even if we don't await for pause()/resume() completion, the observer must + // fire 'pause' and 'resume' events if state was the opposite. + void dataProducer1.pause(); + void dataProducer1.resume(); + void dataProducer1.pause(); + void dataProducer1.pause(); + void dataProducer1.pause(); + await dataProducer1.resume(); + + expect(onObserverPause).toHaveBeenCalledTimes(3); + expect(onObserverResume).toHaveBeenCalledTimes(3); +}, 2000); + +test('producer.pause() and resume() emit events', async () => { + const dataProducer1 = await ctx.webRtcTransport1!.produceData( + ctx.dataProducerOptions1 + ); + + const promises = []; + const events: string[] = []; + + dataProducer1.observer.once('resume', () => { + events.push('resume'); + }); + + dataProducer1.observer.once('pause', () => { + events.push('pause'); + }); + + promises.push(dataProducer1.pause()); + promises.push(dataProducer1.resume()); + + await Promise.all(promises); + + expect(events).toEqual(['pause', 'resume']); + expect(dataProducer1.paused).toBe(false); +}, 2000); + +test('dataProducer.close() succeeds', async () => { + const dataProducer1 = await ctx.webRtcTransport1!.produceData( + ctx.dataProducerOptions1 + ); + + const onObserverClose = jest.fn(); + + dataProducer1.observer.once('close', onObserverClose); + dataProducer1.close(); + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(dataProducer1.closed).toBe(true); + + await expect(ctx.router!.dump()).resolves.toMatchObject({ + mapDataProducerIdDataConsumerIds: {}, + mapDataConsumerIdDataProducerId: {}, + }); + + await expect(ctx.webRtcTransport1!.dump()).resolves.toMatchObject({ + id: ctx.webRtcTransport1!.id, + dataProducerIds: [], + dataConsumerIds: [], + }); +}, 2000); + +test('DataProducer methods reject if closed', async () => { + const dataProducer1 = await ctx.webRtcTransport1!.produceData( + ctx.dataProducerOptions1 + ); + + dataProducer1.close(); + + await expect(dataProducer1.dump()).rejects.toThrow(Error); + + await expect(dataProducer1.getStats()).rejects.toThrow(Error); +}, 2000); + +test('DataProducer emits "transportclose" if Transport is closed', async () => { + const dataProducer2 = await ctx.webRtcTransport2!.produceData( + ctx.dataProducerOptions2 + ); + + const onObserverClose = jest.fn(); + + dataProducer2.observer.once('close', onObserverClose); + + const promise = enhancedOnce( + dataProducer2, + 'transportclose' + ); + + ctx.webRtcTransport2!.close(); + await promise; + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(dataProducer2.closed).toBe(true); +}, 2000); diff --git a/node/src/test/test-DirectTransport.ts b/node/src/test/test-DirectTransport.ts new file mode 100644 index 0000000000..a552e78b39 --- /dev/null +++ b/node/src/test/test-DirectTransport.ts @@ -0,0 +1,429 @@ +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { DirectTransportEvents } from '../DirectTransportTypes'; +import type { WorkerEvents } from '../types'; + +type TestContext = { + worker?: mediasoup.types.Worker; + router?: mediasoup.types.Router; +}; + +const ctx: TestContext = {}; + +beforeEach(async () => { + ctx.worker = await mediasoup.createWorker(); + ctx.router = await ctx.worker.createRouter(); +}); + +afterEach(async () => { + ctx.worker?.close(); + + if (ctx.worker?.subprocessClosed === false) { + await enhancedOnce(ctx.worker, 'subprocessclose'); + } +}); + +test('router.createDirectTransport() succeeds', async () => { + const onObserverNewTransport = jest.fn(); + + ctx.router!.observer.once('newtransport', onObserverNewTransport); + + const directTransport = await ctx.router!.createDirectTransport({ + maxMessageSize: 1024, + appData: { foo: 'bar' }, + }); + + await expect(ctx.router!.dump()).resolves.toMatchObject({ + transportIds: [directTransport.id], + }); + + expect(onObserverNewTransport).toHaveBeenCalledTimes(1); + expect(onObserverNewTransport).toHaveBeenCalledWith(directTransport); + expect(typeof directTransport.id).toBe('string'); + expect(directTransport.type).toBe('direct'); + expect(directTransport.closed).toBe(false); + expect(directTransport.appData).toEqual({ foo: 'bar' }); + + const dump = await directTransport.dump(); + + expect(dump.id).toBe(directTransport.id); + expect(dump.producerIds).toEqual([]); + expect(dump.consumerIds).toEqual([]); + expect(dump.dataProducerIds).toEqual([]); + expect(dump.dataConsumerIds).toEqual([]); + expect(dump.recvRtpHeaderExtensions).toBeDefined(); + expect(typeof dump.rtpListener).toBe('object'); + + directTransport.close(); + expect(directTransport.closed).toBe(true); +}, 2000); + +test('router.createDirectTransport() with wrong arguments rejects with TypeError', async () => { + await expect( + // @ts-expect-error --- Testing purposes. + ctx.router!.createDirectTransport({ maxMessageSize: 'foo' }) + ).rejects.toThrow(TypeError); + + await expect( + ctx.router!.createDirectTransport({ maxMessageSize: -2000 }) + ).rejects.toThrow(TypeError); +}, 2000); + +test('directTransport.getStats() succeeds', async () => { + const directTransport = await ctx.router!.createDirectTransport(); + + const stats = await directTransport.getStats(); + + expect(Array.isArray(stats)).toBe(true); + expect(stats.length).toBe(1); + expect(stats[0].type).toBe('direct-transport'); + expect(stats[0].transportId).toBe(directTransport.id); + expect(typeof stats[0].timestamp).toBe('number'); + expect(stats[0].bytesReceived).toBe(0); + expect(stats[0].recvBitrate).toBe(0); + expect(stats[0].bytesSent).toBe(0); + expect(stats[0].sendBitrate).toBe(0); + expect(stats[0].rtpBytesReceived).toBe(0); + expect(stats[0].rtpRecvBitrate).toBe(0); + expect(stats[0].rtpBytesSent).toBe(0); + expect(stats[0].rtpSendBitrate).toBe(0); + expect(stats[0].rtxBytesReceived).toBe(0); + expect(stats[0].rtxRecvBitrate).toBe(0); + expect(stats[0].rtxBytesSent).toBe(0); + expect(stats[0].rtxSendBitrate).toBe(0); + expect(stats[0].probationBytesSent).toBe(0); + expect(stats[0].probationSendBitrate).toBe(0); +}, 2000); + +test('directTransport.connect() succeeds', async () => { + const directTransport = await ctx.router!.createDirectTransport(); + + await expect(directTransport.connect()).resolves.toBeUndefined(); +}, 2000); + +test('dataProducer.send() succeeds', async () => { + const directTransport = await ctx.router!.createDirectTransport(); + const dataProducer = await directTransport.produceData({ + label: 'foo', + protocol: 'bar', + appData: { foo: 'bar' }, + }); + const dataConsumer = await directTransport.consumeData({ + dataProducerId: dataProducer.id, + }); + const numMessages = 200; + const pauseSendingAtMessage = 10; + const resumeSendingAtMessage = 20; + const pauseReceivingAtMessage = 40; + const resumeReceivingAtMessage = 60; + const expectedReceivedNumMessages = + numMessages - + (resumeSendingAtMessage - pauseSendingAtMessage) - + (resumeReceivingAtMessage - pauseReceivingAtMessage); + + let sentMessageBytes = 0; + let effectivelySentMessageBytes = 0; + let recvMessageBytes = 0; + let numSentMessages = 0; + let numReceivedMessages = 0; + + async function sendNextMessage(): Promise { + const id = ++numSentMessages; + let message: Buffer | string; + + if (id === pauseSendingAtMessage) { + await dataProducer.pause(); + } else if (id === resumeSendingAtMessage) { + await dataProducer.resume(); + } else if (id === pauseReceivingAtMessage) { + await dataConsumer.pause(); + } else if (id === resumeReceivingAtMessage) { + await dataConsumer.resume(); + } + + // Send string (WebRTC DataChannel string). + if (id < numMessages / 2) { + message = String(id); + } + // Send string (WebRTC DataChannel binary). + else { + message = Buffer.from(String(id)); + } + + dataProducer.send(message); + + const messageSize = Buffer.from(message).byteLength; + + sentMessageBytes += messageSize; + + if (!dataProducer.paused && !dataConsumer.paused) { + effectivelySentMessageBytes += messageSize; + } + + if (id < numMessages) { + void sendNextMessage(); + } + } + + await new Promise((resolve, reject) => { + dataProducer.on('listenererror', (eventName, error) => { + reject( + new Error( + `dataProducer 'listenererror' [eventName:${eventName}]: ${error.toString()}` + ) + ); + }); + + dataConsumer.on('listenererror', (eventName, error) => { + reject( + new Error( + `dataConsumer 'listenererror' [eventName:${eventName}]: ${error.toString()}` + ) + ); + }); + + dataConsumer.on('message', (message, ppid) => { + ++numReceivedMessages; + + // message is always a Buffer. + recvMessageBytes += message.byteLength; + + const id = Number(message.toString('utf8')); + + if (id === numMessages) { + resolve(); + } + // PPID of WebRTC DataChannel string. + else if (id < numMessages / 2 && ppid !== 51) { + reject( + new Error( + `ppid in message with id ${id} should be 51 but it is ${ppid}` + ) + ); + } + // PPID of WebRTC DataChannel binary. + else if (id > numMessages / 2 && ppid !== 53) { + reject( + new Error( + `ppid in message with id ${id} should be 53 but it is ${ppid}` + ) + ); + } + }); + + void sendNextMessage(); + }); + + expect(numSentMessages).toBe(numMessages); + expect(numReceivedMessages).toBe(expectedReceivedNumMessages); + expect(recvMessageBytes).toBe(effectivelySentMessageBytes); + + await expect(dataProducer.getStats()).resolves.toMatchObject([ + { + type: 'data-producer', + label: dataProducer.label, + protocol: dataProducer.protocol, + messagesReceived: numMessages, + bytesReceived: sentMessageBytes, + }, + ]); + + await expect(dataConsumer.getStats()).resolves.toMatchObject([ + { + type: 'data-consumer', + label: dataConsumer.label, + protocol: dataConsumer.protocol, + messagesSent: expectedReceivedNumMessages, + bytesSent: recvMessageBytes, + }, + ]); +}, 5000); + +test('dataProducer.send() with subchannels succeeds', async () => { + const directTransport = await ctx.router!.createDirectTransport(); + const dataProducer = await directTransport.produceData(); + const dataConsumer1 = await directTransport.consumeData({ + dataProducerId: dataProducer.id, + subchannels: [1, 11, 666], + }); + const dataConsumer2 = await directTransport.consumeData({ + dataProducerId: dataProducer.id, + subchannels: [2, 22, 666], + }); + const expectedReceivedNumMessages1 = 7; + const expectedReceivedNumMessages2 = 5; + const receivedMessages1: string[] = []; + const receivedMessages2: string[] = []; + + await new Promise(resolve => { + // Must be received by dataConsumer1 and dataConsumer2. + dataProducer.send( + 'both', + /* ppid */ undefined, + /* subchannels */ undefined, + /* requiredSubchannel */ undefined + ); + + // Must be received by dataConsumer1 and dataConsumer2. + dataProducer.send( + 'both', + /* ppid */ undefined, + /* subchannels */ [1, 2], + /* requiredSubchannel */ undefined + ); + + // Must be received by dataConsumer1 and dataConsumer2. + dataProducer.send( + 'both', + /* ppid */ undefined, + /* subchannels */ [11, 22, 33], + /* requiredSubchannel */ 666 + ); + + // Must not be received by neither dataConsumer1 nor dataConsumer2. + dataProducer.send( + 'none', + /* ppid */ undefined, + /* subchannels */ [3], + /* requiredSubchannel */ 666 + ); + + // Must not be received by neither dataConsumer1 nor dataConsumer2. + dataProducer.send( + 'none', + /* ppid */ undefined, + /* subchannels */ [666], + /* requiredSubchannel */ 3 + ); + + // Must be received by dataConsumer1. + dataProducer.send( + 'dc1', + /* ppid */ undefined, + /* subchannels */ [1], + /* requiredSubchannel */ undefined + ); + + // Must be received by dataConsumer1. + dataProducer.send( + 'dc1', + /* ppid */ undefined, + /* subchannels */ [11], + /* requiredSubchannel */ 1 + ); + + // Must be received by dataConsumer1. + dataProducer.send( + 'dc1', + /* ppid */ undefined, + /* subchannels */ [666], + /* requiredSubchannel */ 11 + ); + + // Must be received by dataConsumer2. + dataProducer.send( + 'dc2', + /* ppid */ undefined, + /* subchannels */ [666], + /* requiredSubchannel */ 2 + ); + + // Make dataConsumer2 also subscribe to subchannel 1. + // NOTE: No need to await for this call. + void dataConsumer2.setSubchannels([...dataConsumer2.subchannels, 1]); + + // Must be received by dataConsumer1 and dataConsumer2. + dataProducer.send( + 'both', + /* ppid */ undefined, + /* subchannels */ [1], + /* requiredSubchannel */ 666 + ); + + dataConsumer1.on('message', message => { + receivedMessages1.push(message.toString('utf8')); + + if ( + receivedMessages1.length === expectedReceivedNumMessages1 && + receivedMessages2.length === expectedReceivedNumMessages2 + ) { + resolve(); + } + }); + + dataConsumer2.on('message', message => { + receivedMessages2.push(message.toString('utf8')); + + if ( + receivedMessages1.length === expectedReceivedNumMessages1 && + receivedMessages2.length === expectedReceivedNumMessages2 + ) { + resolve(); + } + }); + }); + + expect(receivedMessages1.length).toBe(expectedReceivedNumMessages1); + expect(receivedMessages2.length).toBe(expectedReceivedNumMessages2); + + for (const message of receivedMessages1) { + expect(['both', 'dc1'].includes(message)).toBe(true); + expect(['dc2'].includes(message)).toBe(false); + } + + for (const message of receivedMessages2) { + expect(['both', 'dc2'].includes(message)).toBe(true); + expect(['dc1'].includes(message)).toBe(false); + } +}, 5000); + +test('DirectTransport methods reject if closed', async () => { + const directTransport = await ctx.router!.createDirectTransport(); + const onObserverClose = jest.fn(); + + directTransport.observer.once('close', onObserverClose); + directTransport.close(); + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(directTransport.closed).toBe(true); + + await expect(directTransport.dump()).rejects.toThrow(Error); + + await expect(directTransport.getStats()).rejects.toThrow(Error); +}, 2000); + +test('DirectTransport emits "routerclose" if Router is closed', async () => { + const directTransport = await ctx.router!.createDirectTransport(); + const onObserverClose = jest.fn(); + + directTransport.observer.once('close', onObserverClose); + + const promise = enhancedOnce( + directTransport, + 'routerclose' + ); + + ctx.router!.close(); + await promise; + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(directTransport.closed).toBe(true); +}, 2000); + +test('DirectTransport emits "routerclose" if Worker is closed', async () => { + const directTransport = await ctx.router!.createDirectTransport(); + const onObserverClose = jest.fn(); + + directTransport.observer.once('close', onObserverClose); + + const promise = enhancedOnce( + directTransport, + 'routerclose' + ); + + ctx.worker!.close(); + await promise; + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(directTransport.closed).toBe(true); +}, 2000); diff --git a/node/src/test/test-PipeTransport.ts b/node/src/test/test-PipeTransport.ts new file mode 100644 index 0000000000..2bffccc5a8 --- /dev/null +++ b/node/src/test/test-PipeTransport.ts @@ -0,0 +1,1130 @@ +import { pickPort } from 'pick-port'; +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { + WorkerEvents, + ConsumerEvents, + ProducerObserverEvents, + DataConsumerEvents, +} from '../types'; +import * as utils from '../utils'; + +type TestContext = { + mediaCodecs: mediasoup.types.RtpCodecCapability[]; + audioProducerOptions: mediasoup.types.ProducerOptions; + videoProducerOptions: mediasoup.types.ProducerOptions; + dataProducerOptions: mediasoup.types.DataProducerOptions; + consumerDeviceCapabilities: mediasoup.types.RtpCapabilities; + worker1?: mediasoup.types.Worker; + worker2?: mediasoup.types.Worker; + router1?: mediasoup.types.Router; + router2?: mediasoup.types.Router; + webRtcTransport1?: mediasoup.types.WebRtcTransport; + webRtcTransport2?: mediasoup.types.WebRtcTransport; + audioProducer?: mediasoup.types.Producer; + videoProducer?: mediasoup.types.Producer; + videoConsumer?: mediasoup.types.Consumer; + dataProducer?: mediasoup.types.DataProducer; + dataConsumer?: mediasoup.types.DataConsumer; +}; + +const ctx: TestContext = { + mediaCodecs: utils.deepFreeze([ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + }, + { + kind: 'video', + mimeType: 'video/VP8', + clockRate: 90000, + }, + ]), + audioProducerOptions: utils.deepFreeze({ + kind: 'audio', + rtpParameters: { + mid: 'AUDIO', + codecs: [ + { + mimeType: 'audio/opus', + payloadType: 111, + clockRate: 48000, + channels: 2, + parameters: { + useinbandfec: 1, + foo: 'bar1', + }, + }, + ], + headerExtensions: [ + { + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + id: 10, + }, + ], + encodings: [{ ssrc: 11111111 }], + rtcp: { + cname: 'FOOBAR', + }, + }, + appData: { foo: 'bar1' }, + }), + videoProducerOptions: utils.deepFreeze({ + kind: 'video', + rtpParameters: { + mid: 'VIDEO', + codecs: [ + { + mimeType: 'video/VP8', + payloadType: 112, + clockRate: 90000, + rtcpFeedback: [ + { type: 'nack' }, + { type: 'nack', parameter: 'pli' }, + { type: 'goog-remb' }, + { type: 'lalala' }, + ], + }, + ], + headerExtensions: [ + { + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + id: 10, + }, + { + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + id: 11, + }, + { + uri: 'urn:3gpp:video-orientation', + id: 13, + }, + ], + encodings: [{ ssrc: 22222222 }, { ssrc: 22222223 }, { ssrc: 22222224 }], + rtcp: { + cname: 'FOOBAR', + }, + }, + appData: { foo: 'bar2' }, + }), + dataProducerOptions: utils.deepFreeze({ + sctpStreamParameters: { + streamId: 666, + ordered: false, + maxPacketLifeTime: 5000, + }, + label: 'foo', + protocol: 'bar', + }), + consumerDeviceCapabilities: utils.deepFreeze( + { + codecs: [ + { + kind: 'audio', + mimeType: 'audio/opus', + preferredPayloadType: 100, + clockRate: 48000, + channels: 2, + }, + { + kind: 'video', + mimeType: 'video/VP8', + preferredPayloadType: 101, + clockRate: 90000, + rtcpFeedback: [ + { type: 'nack' }, + { type: 'ccm', parameter: 'fir' }, + { type: 'transport-cc' }, + ], + }, + { + kind: 'video', + mimeType: 'video/rtx', + preferredPayloadType: 102, + clockRate: 90000, + parameters: { + apt: 101, + }, + rtcpFeedback: [], + }, + ], + headerExtensions: [ + { + kind: 'video', + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + preferredId: 4, + preferredEncrypt: false, + direction: 'sendrecv', + }, + { + kind: 'video', + uri: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', + preferredId: 5, + preferredEncrypt: false, + }, + { + kind: 'audio', + uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + preferredId: 10, + preferredEncrypt: false, + }, + ], + } + ), +}; + +beforeEach(async () => { + ctx.worker1 = await mediasoup.createWorker(); + ctx.worker2 = await mediasoup.createWorker(); + ctx.router1 = await ctx.worker1.createRouter({ + mediaCodecs: ctx.mediaCodecs, + }); + ctx.router2 = await ctx.worker2.createRouter({ + mediaCodecs: ctx.mediaCodecs, + }); + ctx.webRtcTransport1 = await ctx.router1.createWebRtcTransport({ + listenInfos: [{ protocol: 'udp', ip: '127.0.0.1' }], + enableSctp: true, + }); + ctx.webRtcTransport2 = await ctx.router2.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + enableSctp: true, + }); + ctx.audioProducer = await ctx.webRtcTransport1.produce( + ctx.audioProducerOptions + ); + ctx.videoProducer = await ctx.webRtcTransport1.produce( + ctx.videoProducerOptions + ); + ctx.dataProducer = await ctx.webRtcTransport1.produceData( + ctx.dataProducerOptions + ); +}); + +afterEach(async () => { + ctx.worker1?.close(); + ctx.worker2?.close(); + + if (ctx.worker1?.subprocessClosed === false) { + await enhancedOnce(ctx.worker1, 'subprocessclose'); + } + + if (ctx.worker2?.subprocessClosed === false) { + await enhancedOnce(ctx.worker2, 'subprocessclose'); + } +}); + +test('router.pipeToRouter() succeeds with audio', async () => { + const { pipeConsumer, pipeProducer } = (await ctx.router1!.pipeToRouter({ + producerId: ctx.audioProducer!.id, + router: ctx.router2!, + })) as { + pipeConsumer: mediasoup.types.Consumer; + pipeProducer: mediasoup.types.Producer; + }; + + const dump1 = await ctx.router1!.dump(); + + // There should be two Transports in router1: + // - WebRtcTransport for audioProducer and videoProducer. + // - PipeTransport between router1 and router2. + expect(dump1.transportIds.length).toBe(2); + + const dump2 = await ctx.router2!.dump(); + + // There should be two Transports in router2: + // - WebRtcTransport for audioConsumer and videoConsumer. + // - PipeTransport between router2 and router1. + expect(dump2.transportIds.length).toBe(2); + + expect(typeof pipeConsumer.id).toBe('string'); + expect(pipeConsumer.closed).toBe(false); + expect(pipeConsumer.kind).toBe('audio'); + expect(typeof pipeConsumer.rtpParameters).toBe('object'); + expect(pipeConsumer.rtpParameters.mid).toBeUndefined(); + expect(pipeConsumer.rtpParameters.codecs).toEqual([ + { + mimeType: 'audio/opus', + clockRate: 48000, + payloadType: 100, + channels: 2, + parameters: { + useinbandfec: 1, + foo: 'bar1', + }, + rtcpFeedback: [], + }, + ]); + + expect(pipeConsumer.rtpParameters.headerExtensions).toEqual([ + { + uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + id: 10, + encrypt: false, + parameters: {}, + }, + { + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', + id: 13, + encrypt: false, + parameters: {}, + }, + { + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay', + id: 14, + encrypt: false, + parameters: {}, + }, + ]); + expect(pipeConsumer.type).toBe('pipe'); + expect(pipeConsumer.paused).toBe(false); + expect(pipeConsumer.producerPaused).toBe(false); + expect(pipeConsumer.score).toEqual({ + score: 10, + producerScore: 10, + producerScores: [], + }); + expect(pipeConsumer.appData).toEqual({}); + + expect(pipeProducer.id).toBe(ctx.audioProducer!.id); + expect(pipeProducer.closed).toBe(false); + expect(pipeProducer.kind).toBe('audio'); + expect(typeof pipeProducer.rtpParameters).toBe('object'); + expect(pipeProducer.rtpParameters.mid).toBeUndefined(); + expect(pipeProducer.rtpParameters.codecs).toEqual([ + { + mimeType: 'audio/opus', + payloadType: 100, + clockRate: 48000, + channels: 2, + parameters: { + useinbandfec: 1, + foo: 'bar1', + }, + rtcpFeedback: [], + }, + ]); + expect(pipeProducer.rtpParameters.headerExtensions).toEqual([ + { + uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + id: 10, + encrypt: false, + parameters: {}, + }, + { + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', + id: 13, + encrypt: false, + parameters: {}, + }, + { + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay', + id: 14, + encrypt: false, + parameters: {}, + }, + ]); + expect(pipeProducer.paused).toBe(false); +}, 2000); + +test('router.pipeToRouter() succeeds with video', async () => { + await ctx.videoProducer!.pause(); + + const { pipeConsumer, pipeProducer } = (await ctx.router1!.pipeToRouter({ + producerId: ctx.videoProducer!.id, + router: ctx.router2!, + })) as { + pipeConsumer: mediasoup.types.Consumer; + pipeProducer: mediasoup.types.Producer; + }; + + const dump1 = await ctx.router1!.dump(); + + // No new PipeTransport should has been created. The existing one is used. + expect(dump1.transportIds.length).toBe(2); + + const dump2 = await ctx.router2!.dump(); + + // No new PipeTransport should has been created. The existing one is used. + expect(dump2.transportIds.length).toBe(2); + + expect(typeof pipeConsumer.id).toBe('string'); + expect(pipeConsumer.closed).toBe(false); + expect(pipeConsumer.kind).toBe('video'); + expect(typeof pipeConsumer.rtpParameters).toBe('object'); + expect(pipeConsumer.rtpParameters.mid).toBeUndefined(); + expect(pipeConsumer.rtpParameters.codecs).toEqual([ + { + mimeType: 'video/VP8', + payloadType: 101, + clockRate: 90000, + parameters: {}, + rtcpFeedback: [ + { type: 'nack', parameter: 'pli' }, + { type: 'ccm', parameter: 'fir' }, + ], + }, + ]); + expect(pipeConsumer.rtpParameters.headerExtensions).toEqual([ + // NOTE: Remove this once framemarking draft becomes RFC. + { + uri: 'http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07', + id: 6, + encrypt: false, + parameters: {}, + }, + { + uri: 'urn:ietf:params:rtp-hdrext:framemarking', + id: 7, + encrypt: false, + parameters: {}, + }, + { + uri: 'urn:3gpp:video-orientation', + id: 11, + encrypt: false, + parameters: {}, + }, + { + uri: 'urn:ietf:params:rtp-hdrext:toffset', + id: 12, + encrypt: false, + parameters: {}, + }, + { + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', + id: 13, + encrypt: false, + parameters: {}, + }, + { + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay', + id: 14, + encrypt: false, + parameters: {}, + }, + ]); + + expect(pipeConsumer.type).toBe('pipe'); + expect(pipeConsumer.paused).toBe(false); + expect(pipeConsumer.producerPaused).toBe(true); + expect(pipeConsumer.score).toEqual({ + score: 10, + producerScore: 10, + producerScores: [], + }); + expect(pipeConsumer.appData).toEqual({}); + + expect(pipeProducer.id).toBe(ctx.videoProducer!.id); + expect(pipeProducer.closed).toBe(false); + expect(pipeProducer.kind).toBe('video'); + expect(typeof pipeProducer.rtpParameters).toBe('object'); + expect(pipeProducer.rtpParameters.mid).toBeUndefined(); + expect(pipeProducer.rtpParameters.codecs).toEqual([ + { + mimeType: 'video/VP8', + payloadType: 101, + clockRate: 90000, + parameters: {}, + rtcpFeedback: [ + { type: 'nack', parameter: 'pli' }, + { type: 'ccm', parameter: 'fir' }, + ], + }, + ]); + expect(pipeProducer.rtpParameters.headerExtensions).toEqual([ + // NOTE: Remove this once framemarking draft becomes RFC. + { + uri: 'http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07', + id: 6, + encrypt: false, + parameters: {}, + }, + { + uri: 'urn:ietf:params:rtp-hdrext:framemarking', + id: 7, + encrypt: false, + parameters: {}, + }, + { + uri: 'urn:3gpp:video-orientation', + id: 11, + encrypt: false, + parameters: {}, + }, + { + uri: 'urn:ietf:params:rtp-hdrext:toffset', + id: 12, + encrypt: false, + parameters: {}, + }, + { + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', + id: 13, + encrypt: false, + parameters: {}, + }, + { + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay', + id: 14, + encrypt: false, + parameters: {}, + }, + ]); + expect(pipeProducer.paused).toBe(true); +}, 2000); + +test('router.createPipeTransport() with wrong arguments rejects with TypeError', async () => { + // @ts-expect-error --- Testing purposes. + await expect(ctx.router1!.createPipeTransport({})).rejects.toThrow(TypeError); + + await expect( + ctx.router1!.createPipeTransport({ + listenInfo: { + protocol: 'udp', + ip: '127.0.0.1', + portRange: { min: 4000, max: 3000 }, + }, + }) + ).rejects.toThrow(TypeError); + + await expect( + ctx.router1!.createPipeTransport({ listenIp: '123' }) + ).rejects.toThrow(TypeError); + + await expect( + // @ts-expect-error --- Testing purposes. + ctx.router1!.createPipeTransport({ listenIp: ['127.0.0.1'] }) + ).rejects.toThrow(TypeError); + + await expect( + ctx.router1!.createPipeTransport({ + listenInfo: { protocol: 'tcp', ip: '127.0.0.1' }, + }) + ).rejects.toThrow(TypeError); + + await expect( + ctx.router1!.createPipeTransport({ + listenInfo: { protocol: 'udp', ip: '127.0.0.1' }, + // @ts-expect-error --- Testing purposes. + appData: 'NOT-AN-OBJECT', + }) + ).rejects.toThrow(TypeError); +}, 2000); + +test('router.createPipeTransport() with enableRtx succeeds', async () => { + const pipeTransport = await ctx.router1!.createPipeTransport({ + listenInfo: { + protocol: 'udp', + ip: '127.0.0.1', + portRange: { min: 2000, max: 3000 }, + }, + enableRtx: true, + }); + + expect(pipeTransport.type).toBe('pipe'); + + const pipeConsumer = await pipeTransport.consume({ + producerId: ctx.videoProducer!.id, + }); + + expect(typeof pipeConsumer.id).toBe('string'); + expect(pipeConsumer.closed).toBe(false); + expect(pipeConsumer.kind).toBe('video'); + expect(typeof pipeConsumer.rtpParameters).toBe('object'); + expect(pipeConsumer.rtpParameters.mid).toBeUndefined(); + expect(pipeConsumer.rtpParameters.codecs).toEqual([ + { + mimeType: 'video/VP8', + payloadType: 101, + clockRate: 90000, + parameters: {}, + rtcpFeedback: [ + { type: 'nack', parameter: '' }, + { type: 'nack', parameter: 'pli' }, + { type: 'ccm', parameter: 'fir' }, + ], + }, + { + mimeType: 'video/rtx', + payloadType: 102, + clockRate: 90000, + parameters: { apt: 101 }, + rtcpFeedback: [], + }, + ]); + expect(pipeConsumer.rtpParameters.headerExtensions).toEqual([ + // NOTE: Remove this once framemarking draft becomes RFC. + { + uri: 'http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07', + id: 6, + encrypt: false, + parameters: {}, + }, + { + uri: 'urn:ietf:params:rtp-hdrext:framemarking', + id: 7, + encrypt: false, + parameters: {}, + }, + { + uri: 'urn:3gpp:video-orientation', + id: 11, + encrypt: false, + parameters: {}, + }, + { + uri: 'urn:ietf:params:rtp-hdrext:toffset', + id: 12, + encrypt: false, + parameters: {}, + }, + { + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', + id: 13, + encrypt: false, + parameters: {}, + }, + { + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/playout-delay', + id: 14, + encrypt: false, + parameters: {}, + }, + ]); + + expect(pipeConsumer.type).toBe('pipe'); + expect(pipeConsumer.paused).toBe(false); + expect(pipeConsumer.producerPaused).toBe(false); + expect(pipeConsumer.score).toEqual({ + score: 10, + producerScore: 10, + producerScores: [], + }); + expect(pipeConsumer.appData).toEqual({}); +}, 2000); + +test('pipeTransport.connect() with valid SRTP parameters succeeds', async () => { + const pipeTransport = await ctx.router1!.createPipeTransport({ + listenIp: '127.0.0.1', + enableSrtp: true, + }); + + expect(typeof pipeTransport.srtpParameters).toBe('object'); + // The master length of AEAD_AES_256_GCM. + expect(pipeTransport.srtpParameters?.keyBase64.length).toBe(60); + + // Valid srtpParameters. + await expect( + pipeTransport.connect({ + ip: '127.0.0.2', + port: 9999, + srtpParameters: { + cryptoSuite: 'AEAD_AES_256_GCM', + keyBase64: + 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=', + }, + }) + ).resolves.toBeUndefined(); +}, 2000); + +test('pipeTransport.connect() with srtpParameters fails if enableSrtp is unset', async () => { + const pipeTransport = await ctx.router1!.createPipeTransport({ + listenInfo: { + protocol: 'udp', + ip: '127.0.0.1', + portRange: { min: 2000, max: 3000 }, + }, + enableRtx: true, + }); + + expect(pipeTransport.srtpParameters).toBeUndefined(); + + // No SRTP enabled so passing srtpParameters must fail. + await expect( + pipeTransport.connect({ + ip: '127.0.0.2', + port: 9999, + srtpParameters: { + cryptoSuite: 'AEAD_AES_256_GCM', + keyBase64: + 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=', + }, + }) + ).rejects.toThrow(TypeError); + + // No SRTP enabled so passing srtpParameters (even if invalid) must fail. + await expect( + pipeTransport.connect({ + ip: '127.0.0.2', + port: 9999, + // @ts-expect-error --- Testing purposes. + srtpParameters: 'invalid', + }) + ).rejects.toThrow(TypeError); +}); + +test('pipeTransport.connect() with invalid srtpParameters fails', async () => { + const pipeTransport = await ctx.router1!.createPipeTransport({ + listenIp: '127.0.0.1', + enableSrtp: true, + }); + + expect(typeof pipeTransport.id).toBe('string'); + expect(typeof pipeTransport.srtpParameters).toBe('object'); + // The master length of AEAD_AES_256_GCM. + expect(pipeTransport.srtpParameters?.keyBase64.length).toBe(60); + + // Missing srtpParameters. + await expect( + pipeTransport.connect({ + ip: '127.0.0.2', + port: 9999, + }) + ).rejects.toThrow(TypeError); + + // Invalid srtpParameters. + await expect( + pipeTransport.connect({ + ip: '127.0.0.2', + port: 9999, + // @ts-expect-error --- Testing purposes. + srtpParameters: 1, + }) + ).rejects.toThrow(TypeError); + + // Missing srtpParameters.cryptoSuite. + await expect( + pipeTransport.connect({ + ip: '127.0.0.2', + port: 9999, + // @ts-expect-error --- Testing purposes. + srtpParameters: { + keyBase64: + 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=', + }, + }) + ).rejects.toThrow(TypeError); + + // Missing srtpParameters.keyBase64. + await expect( + pipeTransport.connect({ + ip: '127.0.0.2', + port: 9999, + // @ts-expect-error --- Testing purposes. + srtpParameters: { + cryptoSuite: 'AEAD_AES_256_GCM', + }, + }) + ).rejects.toThrow(TypeError); + + // Invalid srtpParameters.cryptoSuite. + await expect( + pipeTransport.connect({ + ip: '127.0.0.2', + port: 9999, + srtpParameters: { + // @ts-expect-error --- Testing purposes. + cryptoSuite: 'FOO', + keyBase64: + 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=', + }, + }) + ).rejects.toThrow(TypeError); + + // Invalid srtpParameters.cryptoSuite. + await expect( + pipeTransport.connect({ + ip: '127.0.0.2', + port: 9999, + srtpParameters: { + // @ts-expect-error --- Testing purposes. + cryptoSuite: 123, + keyBase64: + 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=', + }, + }) + ).rejects.toThrow(TypeError); + + // Invalid srtpParameters.keyBase64. + await expect( + pipeTransport.connect({ + ip: '127.0.0.2', + port: 9999, + srtpParameters: { + cryptoSuite: 'AEAD_AES_256_GCM', + // @ts-expect-error --- Testing purposes. + keyBase64: [], + }, + }) + ).rejects.toThrow(TypeError); +}, 2000); + +test('router.createPipeTransport() with fixed port succeeds', async () => { + const port = await pickPort({ + type: 'udp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + const pipeTransport = await ctx.router1!.createPipeTransport({ + listenInfo: { protocol: 'udp', ip: '127.0.0.1', port }, + }); + + expect(pipeTransport.tuple.localPort).toEqual(port); +}, 2000); + +test('transport.consume() for a pipe Producer succeeds', async () => { + await ctx.router1!.pipeToRouter({ + producerId: ctx.videoProducer!.id, + router: ctx.router2!, + }); + + const videoConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + expect(typeof videoConsumer.id).toBe('string'); + expect(videoConsumer.closed).toBe(false); + expect(videoConsumer.kind).toBe('video'); + expect(typeof videoConsumer.rtpParameters).toBe('object'); + expect(videoConsumer.rtpParameters.mid).toBe('0'); + expect(videoConsumer.rtpParameters.codecs).toEqual([ + { + mimeType: 'video/VP8', + payloadType: 101, + clockRate: 90000, + parameters: {}, + rtcpFeedback: [ + { type: 'nack', parameter: '' }, + { type: 'ccm', parameter: 'fir' }, + { type: 'transport-cc', parameter: '' }, + ], + }, + { + mimeType: 'video/rtx', + payloadType: 102, + clockRate: 90000, + parameters: { + apt: 101, + }, + rtcpFeedback: [], + }, + ]); + expect(videoConsumer.rtpParameters.headerExtensions).toEqual([ + { + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + id: 4, + encrypt: false, + parameters: {}, + }, + { + uri: 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', + id: 5, + encrypt: false, + parameters: {}, + }, + ]); + expect(videoConsumer.rtpParameters.encodings?.length).toBe(1); + expect(typeof videoConsumer.rtpParameters.encodings?.[0].ssrc).toBe('number'); + expect(typeof videoConsumer.rtpParameters.encodings?.[0].rtx).toBe('object'); + expect(typeof videoConsumer.rtpParameters.encodings?.[0].rtx?.ssrc).toBe( + 'number' + ); + expect(videoConsumer.type).toBe('simulcast'); + expect(videoConsumer.paused).toBe(false); + expect(videoConsumer.producerPaused).toBe(false); + expect(videoConsumer.score).toEqual({ + score: 10, + producerScore: 0, + producerScores: [0, 0, 0], + }); + expect(videoConsumer.appData).toEqual({}); +}, 2000); + +test('producer.pause() and producer.resume() are transmitted to pipe Consumer', async () => { + await ctx.videoProducer!.pause(); + + // We need to obtain the pipeProducer to await for its 'puase' and 'resume' + // events, otherwise we may get errors like this: + // InvalidStateError: Channel closed, pending request aborted [method:PRODUCER_PAUSE, id:8] + // See related fixed issue: + // https://github.com/versatica/mediasoup/issues/1374 + const { pipeProducer: pipeVideoProducer } = await ctx.router1!.pipeToRouter({ + producerId: ctx.videoProducer!.id, + router: ctx.router2!, + }); + + const videoConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + expect(ctx.videoProducer!.paused).toBe(true); + expect(videoConsumer.producerPaused).toBe(true); + expect(videoConsumer.paused).toBe(false); + + // NOTE: Let's use a Promise since otherwise there may be race conditions + // between events and await lines below. + const promise1 = enhancedOnce( + videoConsumer, + 'producerresume' + ); + const promise2 = enhancedOnce( + pipeVideoProducer!.observer, + 'resume' + ); + + await ctx.videoProducer!.resume(); + await Promise.all([promise1, promise2]); + + expect(videoConsumer.producerPaused).toBe(false); + expect(videoConsumer.paused).toBe(false); + expect(pipeVideoProducer!.paused).toBe(false); + + const promise3 = enhancedOnce(videoConsumer, 'producerpause'); + const promise4 = enhancedOnce( + pipeVideoProducer!.observer, + 'pause' + ); + + await ctx.videoProducer!.pause(); + await Promise.all([promise3, promise4]); + + expect(videoConsumer.producerPaused).toBe(true); + expect(videoConsumer.paused).toBe(false); + expect(pipeVideoProducer!.paused).toBe(true); +}, 2000); + +test('producer.close() is transmitted to pipe Consumer', async () => { + await ctx.router1!.pipeToRouter({ + producerId: ctx.videoProducer!.id, + router: ctx.router2!, + }); + + const videoConsumer = await ctx.webRtcTransport2!.consume({ + producerId: ctx.videoProducer!.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + ctx.videoProducer!.close(); + + expect(ctx.videoProducer!.closed).toBe(true); + + if (!videoConsumer.closed) { + await enhancedOnce(videoConsumer, 'producerclose'); + } + + expect(videoConsumer.closed).toBe(true); +}, 2000); + +test('router.pipeToRouter() fails if both Routers belong to the same Worker', async () => { + const router1bis = await ctx.worker1!.createRouter({ + mediaCodecs: ctx.mediaCodecs, + }); + + await expect( + ctx.router1!.pipeToRouter({ + producerId: ctx.videoProducer!.id, + router: router1bis, + }) + ).rejects.toThrow(Error); +}, 2000); + +test('router.pipeToRouter() succeeds with data', async () => { + const { pipeDataConsumer, pipeDataProducer } = + (await ctx.router1!.pipeToRouter({ + dataProducerId: ctx.dataProducer!.id, + router: ctx.router2!, + })) as { + pipeDataConsumer: mediasoup.types.DataConsumer; + pipeDataProducer: mediasoup.types.DataProducer; + }; + + const dump1 = await ctx.router1!.dump(); + + // There should be two Transports in router1: + // - WebRtcTransport for audioProducer, videoProducer and dataProducer. + // - PipeTransport between router1 and router2. + expect(dump1.transportIds.length).toBe(2); + + const dump2 = await ctx.router2!.dump(); + + // There should be two Transports in router2: + // - WebRtcTransport for audioConsumer, videoConsumer and dataConsumer. + // - PipeTransport between router2 and router1. + expect(dump2.transportIds.length).toBe(2); + + expect(typeof pipeDataConsumer.id).toBe('string'); + expect(pipeDataConsumer.closed).toBe(false); + expect(pipeDataConsumer.type).toBe('sctp'); + expect(typeof pipeDataConsumer.sctpStreamParameters).toBe('object'); + expect(typeof pipeDataConsumer.sctpStreamParameters?.streamId).toBe('number'); + expect(pipeDataConsumer.sctpStreamParameters?.ordered).toBe(false); + expect(pipeDataConsumer.sctpStreamParameters?.maxPacketLifeTime).toBe(5000); + expect(pipeDataConsumer.sctpStreamParameters?.maxRetransmits).toBeUndefined(); + expect(pipeDataConsumer.label).toBe('foo'); + expect(pipeDataConsumer.protocol).toBe('bar'); + + expect(pipeDataProducer.id).toBe(ctx.dataProducer!.id); + expect(pipeDataProducer.closed).toBe(false); + expect(pipeDataProducer.type).toBe('sctp'); + expect(typeof pipeDataProducer.sctpStreamParameters).toBe('object'); + expect(typeof pipeDataProducer.sctpStreamParameters?.streamId).toBe('number'); + expect(pipeDataProducer.sctpStreamParameters?.ordered).toBe(false); + expect(pipeDataProducer.sctpStreamParameters?.maxPacketLifeTime).toBe(5000); + expect(pipeDataProducer.sctpStreamParameters?.maxRetransmits).toBeUndefined(); + expect(pipeDataProducer.label).toBe('foo'); + expect(pipeDataProducer.protocol).toBe('bar'); +}, 2000); + +test('transport.dataConsume() for a pipe DataProducer succeeds', async () => { + await ctx.router1!.pipeToRouter({ + dataProducerId: ctx.dataProducer!.id, + router: ctx.router2!, + }); + + const dataConsumer = await ctx.webRtcTransport2!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + }); + + expect(typeof dataConsumer.id).toBe('string'); + expect(dataConsumer.closed).toBe(false); + expect(dataConsumer.type).toBe('sctp'); + expect(typeof dataConsumer.sctpStreamParameters).toBe('object'); + expect(typeof dataConsumer.sctpStreamParameters?.streamId).toBe('number'); + expect(dataConsumer.sctpStreamParameters?.ordered).toBe(false); + expect(dataConsumer.sctpStreamParameters?.maxPacketLifeTime).toBe(5000); + expect(dataConsumer.sctpStreamParameters?.maxRetransmits).toBeUndefined(); + expect(dataConsumer.label).toBe('foo'); + expect(dataConsumer.protocol).toBe('bar'); +}, 2000); + +test('dataProducer.close() is transmitted to pipe DataConsumer', async () => { + await ctx.router1!.pipeToRouter({ + dataProducerId: ctx.dataProducer!.id, + router: ctx.router2!, + }); + + const dataConsumer = await ctx.webRtcTransport2!.consumeData({ + dataProducerId: ctx.dataProducer!.id, + }); + + ctx.dataProducer!.close(); + + expect(ctx.dataProducer!.closed).toBe(true); + + if (!dataConsumer.closed) { + await enhancedOnce(dataConsumer, 'dataproducerclose'); + } + + expect(dataConsumer.closed).toBe(true); +}, 2000); + +test('router.pipeToRouter() called twice generates a single PipeTransport pair', async () => { + const routerA = await ctx.worker1!.createRouter({ + mediaCodecs: ctx.mediaCodecs, + }); + const routerB = await ctx.worker2!.createRouter({ + mediaCodecs: ctx.mediaCodecs, + }); + const transportA1 = await routerA.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + }); + const transportA2 = await routerA.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + }); + const audioProducerA1 = await transportA1.produce(ctx.audioProducerOptions); + const audioProducerA2 = await transportA2.produce(ctx.audioProducerOptions); + + await Promise.all([ + routerA.pipeToRouter({ + producerId: audioProducerA1.id, + router: routerB, + }), + routerA.pipeToRouter({ + producerId: audioProducerA2.id, + router: routerB, + }), + ]); + + const dump1 = await routerA.dump(); + + // There should be 3 Transports in routerA: + // - WebRtcTransport for audioProducerA1 and audioProducerA2. + // - PipeTransport between routerA and routerB. + expect(dump1.transportIds.length).toBe(3); + + const dump2 = await routerB.dump(); + + // There should be 1 Transport in routerB: + // - PipeTransport between routerA and routerB. + expect(dump2.transportIds.length).toBe(1); +}, 2000); + +test('router.pipeToRouter() called in two Routers passing one to each other as argument generates a single PipeTransport pair', async () => { + const routerA = await ctx.worker1!.createRouter({ + mediaCodecs: ctx.mediaCodecs, + }); + const routerB = await ctx.worker2!.createRouter({ + mediaCodecs: ctx.mediaCodecs, + }); + const transportA = await routerA.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + }); + const transportB = await routerB.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + }); + const audioProducerA = await transportA.produce(ctx.audioProducerOptions); + const audioProducerB = await transportB.produce(ctx.audioProducerOptions); + const pipeTransportsA = new Map(); + const pipeTransportsB = new Map(); + + routerA.observer.on('newtransport', transport => { + if (transport.constructor.name !== 'PipeTransportImpl') { + return; + } + + pipeTransportsA.set(transport.id, transport); + transport.observer.on('close', () => pipeTransportsA.delete(transport.id)); + }); + + routerB.observer.on('newtransport', transport => { + if (transport.constructor.name !== 'PipeTransportImpl') { + return; + } + + pipeTransportsB.set(transport.id, transport); + transport.observer.on('close', () => pipeTransportsB.delete(transport.id)); + }); + + await Promise.all([ + routerA.pipeToRouter({ + producerId: audioProducerA.id, + router: routerB, + }), + routerB.pipeToRouter({ + producerId: audioProducerB.id, + router: routerA, + }), + ]); + + // There should be a single PipeTransport in each Router and they must be + // connected. + + expect(pipeTransportsA.size).toBe(1); + expect(pipeTransportsB.size).toBe(1); + + const pipeTransportA = Array.from(pipeTransportsA.values())[0]; + const pipeTransportB = Array.from(pipeTransportsB.values())[0]; + + expect(pipeTransportA.tuple.localPort).toBe(pipeTransportB.tuple.remotePort); + expect(pipeTransportB.tuple.localPort).toBe(pipeTransportA.tuple.remotePort); + + routerA.close(); + + expect(pipeTransportsA.size).toBe(0); + expect(pipeTransportsB.size).toBe(0); +}, 2000); diff --git a/node/src/test/test-PlainTransport.ts b/node/src/test/test-PlainTransport.ts new file mode 100644 index 0000000000..9fd58564a6 --- /dev/null +++ b/node/src/test/test-PlainTransport.ts @@ -0,0 +1,572 @@ +import * as os from 'node:os'; +import { pickPort } from 'pick-port'; +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { WorkerEvents, PlainTransportEvents } from '../types'; +import * as utils from '../utils'; + +const IS_WINDOWS = os.platform() === 'win32'; + +type TestContext = { + mediaCodecs: mediasoup.types.RtpCodecCapability[]; + worker?: mediasoup.types.Worker; + router?: mediasoup.types.Router; +}; + +const ctx: TestContext = { + mediaCodecs: utils.deepFreeze([ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + parameters: { + useinbandfec: 1, + foo: 'bar', + }, + }, + { + kind: 'video', + mimeType: 'video/VP8', + clockRate: 90000, + }, + { + kind: 'video', + mimeType: 'video/H264', + clockRate: 90000, + parameters: { + 'level-asymmetry-allowed': 1, + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + foo: 'bar', + }, + rtcpFeedback: [], // Will be ignored. + }, + ]), +}; + +beforeEach(async () => { + ctx.worker = await mediasoup.createWorker(); + ctx.router = await ctx.worker.createRouter({ mediaCodecs: ctx.mediaCodecs }); +}); + +afterEach(async () => { + ctx.worker?.close(); + + if (ctx.worker?.subprocessClosed === false) { + await enhancedOnce(ctx.worker, 'subprocessclose'); + } +}); + +test('router.createPlainTransport() succeeds', async () => { + const plainTransport = await ctx.router!.createPlainTransport({ + listenInfo: { + protocol: 'udp', + ip: '127.0.0.1', + portRange: { min: 2000, max: 3000 }, + }, + }); + + await expect(ctx.router!.dump()).resolves.toMatchObject({ + transportIds: [plainTransport.id], + }); + + const onObserverNewTransport = jest.fn(); + + ctx.router!.observer.once('newtransport', onObserverNewTransport); + + // Create a separate transport here. + const plainTransport2 = await ctx.router!.createPlainTransport({ + listenInfo: { + protocol: 'udp', + ip: '127.0.0.1', + announcedAddress: '9.9.9.1', + portRange: { min: 2000, max: 3000 }, + }, + enableSctp: true, + appData: { foo: 'bar' }, + }); + + expect(onObserverNewTransport).toHaveBeenCalledTimes(1); + expect(onObserverNewTransport).toHaveBeenCalledWith(plainTransport2); + expect(typeof plainTransport2.id).toBe('string'); + expect(plainTransport2.closed).toBe(false); + expect(plainTransport2.type).toBe('plain'); + expect(plainTransport2.appData).toEqual({ foo: 'bar' }); + expect(typeof plainTransport2.tuple).toBe('object'); + // @deprecated Use tuple.localAddress instead. + expect(plainTransport2.tuple.localIp).toBe('9.9.9.1'); + expect(plainTransport2.tuple.localAddress).toBe('9.9.9.1'); + expect(typeof plainTransport2.tuple.localPort).toBe('number'); + expect(plainTransport2.tuple.protocol).toBe('udp'); + expect(plainTransport2.rtcpTuple).toBeUndefined(); + expect(plainTransport2.sctpParameters).toMatchObject({ + port: 5000, + OS: 1024, + MIS: 1024, + maxMessageSize: 262144, + }); + expect(plainTransport2.sctpState).toBe('new'); + expect(plainTransport2.srtpParameters).toBeUndefined(); + + const dump1 = await plainTransport2.dump(); + + expect(dump1.id).toBe(plainTransport2.id); + expect(dump1.producerIds).toEqual([]); + expect(dump1.consumerIds).toEqual([]); + expect(dump1.tuple).toEqual(plainTransport2.tuple); + expect(dump1.rtcpTuple).toEqual(plainTransport2.rtcpTuple); + expect(dump1.sctpParameters).toEqual(plainTransport2.sctpParameters); + expect(dump1.sctpState).toBe('new'); + expect(dump1.recvRtpHeaderExtensions).toBeDefined(); + expect(typeof dump1.rtpListener).toBe('object'); + + plainTransport2.close(); + expect(plainTransport2.closed).toBe(true); + + const anotherTransport = await ctx.router!.createPlainTransport({ + listenIp: '127.0.0.1', + }); + + expect(typeof anotherTransport).toBe('object'); + + const rtpPort = await pickPort({ + type: 'udp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + const rtcpPort = await pickPort({ + type: 'udp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + const transport2 = await ctx.router!.createPlainTransport({ + listenInfo: { protocol: 'udp', ip: '127.0.0.1', port: rtpPort }, + rtcpListenInfo: { protocol: 'udp', ip: '127.0.0.1', port: rtcpPort }, + }); + + expect(typeof transport2.id).toBe('string'); + expect(transport2.closed).toBe(false); + expect(transport2.appData).toEqual({}); + expect(typeof transport2.tuple).toBe('object'); + // @deprecated Use tuple.localAddress instead. + expect(transport2.tuple.localIp).toBe('127.0.0.1'); + expect(transport2.tuple.localAddress).toBe('127.0.0.1'); + expect(transport2.tuple.localPort).toBe(rtpPort); + expect(transport2.tuple.protocol).toBe('udp'); + expect(typeof transport2.rtcpTuple).toBe('object'); + // @deprecated Use tuple.localAddress instead. + expect(transport2.rtcpTuple?.localIp).toBe('127.0.0.1'); + expect(transport2.rtcpTuple?.localAddress).toBe('127.0.0.1'); + expect(transport2.rtcpTuple?.localPort).toBe(rtcpPort); + expect(transport2.rtcpTuple?.protocol).toBe('udp'); + expect(transport2.sctpParameters).toBeUndefined(); + expect(transport2.sctpState).toBeUndefined(); + + const dump2 = await transport2.dump(); + + expect(dump2.id).toBe(transport2.id); + expect(dump2.tuple).toEqual(transport2.tuple); + expect(dump2.rtcpTuple).toEqual(transport2.rtcpTuple); + expect(dump2.sctpState).toBeUndefined(); +}, 2000); + +test('router.createPlainTransport() with wrong arguments rejects with TypeError', async () => { + // @ts-expect-error --- Testing purposes. + await expect(ctx.router!.createPlainTransport({})).rejects.toThrow(TypeError); + + await expect( + ctx.router!.createPlainTransport({ + listenInfo: { + protocol: 'udp', + ip: '127.0.0.1', + portRange: { min: 4000, max: 3000 }, + }, + }) + ).rejects.toThrow(TypeError); + + await expect( + ctx.router!.createPlainTransport({ listenIp: '123' }) + ).rejects.toThrow(TypeError); + + await expect( + // @ts-expect-error --- Testing purposes. + ctx.router!.createPlainTransport({ listenIp: ['127.0.0.1'] }) + ).rejects.toThrow(TypeError); + + await expect( + ctx.router!.createPlainTransport({ + listenInfo: { protocol: 'tcp', ip: '127.0.0.1' }, + }) + ).rejects.toThrow(TypeError); + + await expect( + ctx.router!.createPlainTransport({ + listenInfo: { protocol: 'udp', ip: '127.0.0.1' }, + // @ts-expect-error --- Testing purposes. + appData: 'NOT-AN-OBJECT', + }) + ).rejects.toThrow(TypeError); +}, 2000); + +test('router.createPlainTransport() with enableSrtp succeeds', async () => { + // Use default cryptoSuite: 'AES_CM_128_HMAC_SHA1_80'. + const plainTransport = await ctx.router!.createPlainTransport({ + listenIp: '127.0.0.1', + enableSrtp: true, + }); + + expect(typeof plainTransport.id).toBe('string'); + expect(typeof plainTransport.srtpParameters).toBe('object'); + expect(plainTransport.srtpParameters?.cryptoSuite).toBe( + 'AES_CM_128_HMAC_SHA1_80' + ); + expect(plainTransport.srtpParameters?.keyBase64.length).toBe(40); + + // Missing srtpParameters. + await expect( + plainTransport.connect({ + ip: '127.0.0.2', + port: 9999, + }) + ).rejects.toThrow(TypeError); + + // Invalid srtpParameters. + await expect( + plainTransport.connect({ + ip: '127.0.0.2', + port: 9999, + // @ts-expect-error --- Testing purposes. + srtpParameters: 1, + }) + ).rejects.toThrow(TypeError); + + // Missing srtpParameters.cryptoSuite. + await expect( + plainTransport.connect({ + ip: '127.0.0.2', + port: 9999, + // @ts-expect-error --- Testing purposes. + srtpParameters: { + keyBase64: 'ZnQ3eWJraDg0d3ZoYzM5cXN1Y2pnaHU5NWxrZTVv', + }, + }) + ).rejects.toThrow(TypeError); + + // Missing srtpParameters.keyBase64. + await expect( + plainTransport.connect({ + ip: '127.0.0.2', + port: 9999, + // @ts-expect-error --- Testing purposes. + srtpParameters: { + cryptoSuite: 'AES_CM_128_HMAC_SHA1_80', + }, + }) + ).rejects.toThrow(TypeError); + + // Invalid srtpParameters.cryptoSuite. + await expect( + plainTransport.connect({ + ip: '127.0.0.2', + port: 9999, + srtpParameters: { + // @ts-expect-error --- Testing purposes. + cryptoSuite: 'FOO', + keyBase64: 'ZnQ3eWJraDg0d3ZoYzM5cXN1Y2pnaHU5NWxrZTVv', + }, + }) + ).rejects.toThrow(TypeError); + + // Invalid srtpParameters.cryptoSuite. + await expect( + plainTransport.connect({ + ip: '127.0.0.2', + port: 9999, + srtpParameters: { + // @ts-expect-error --- Testing purposes. + cryptoSuite: 123, + keyBase64: 'ZnQ3eWJraDg0d3ZoYzM5cXN1Y2pnaHU5NWxrZTVv', + }, + }) + ).rejects.toThrow(TypeError); + + // Invalid srtpParameters.keyBase64. + await expect( + plainTransport.connect({ + ip: '127.0.0.2', + port: 9999, + srtpParameters: { + cryptoSuite: 'AES_CM_128_HMAC_SHA1_80', + // @ts-expect-error --- Testing purposes. + keyBase64: [], + }, + }) + ).rejects.toThrow(TypeError); + + // Valid srtpParameters. And let's update the crypto suite. + await expect( + plainTransport.connect({ + ip: '127.0.0.2', + port: 9999, + srtpParameters: { + cryptoSuite: 'AEAD_AES_256_GCM', + keyBase64: + 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=', + }, + }) + ).resolves.toBeUndefined(); + + expect(plainTransport.srtpParameters?.cryptoSuite).toBe('AEAD_AES_256_GCM'); + expect(plainTransport.srtpParameters?.keyBase64.length).toBe(60); +}, 2000); + +test('router.createPlainTransport() with non bindable IP rejects with Error', async () => { + await expect( + ctx.router!.createPlainTransport({ listenIp: '8.8.8.8' }) + ).rejects.toThrow(Error); +}, 2000); + +if (!IS_WINDOWS) { + test('two transports binding to the same IP:port with udpReusePort flag succeed', async () => { + const multicastIp = '224.0.0.1'; + const port = await pickPort({ + type: 'udp', + ip: multicastIp, + reserveTimeout: 0, + }); + + await expect( + ctx.router!.createPlainTransport({ + listenInfo: { + protocol: 'udp', + ip: multicastIp, + port: port, + // NOTE: ipv6Only flag will be ignored since ip is IPv4. + flags: { udpReusePort: true, ipv6Only: true }, + }, + }) + ).resolves.toBeDefined(); + + await expect( + ctx.router!.createPlainTransport({ + listenInfo: { + protocol: 'udp', + ip: multicastIp, + port: port, + flags: { udpReusePort: true }, + }, + }) + ).resolves.toBeDefined(); + }, 2000); + + test('two transports binding to the same IP:port without udpReusePort flag fail', async () => { + const multicastIp = '224.0.0.1'; + const port = await pickPort({ + type: 'udp', + ip: multicastIp, + reserveTimeout: 0, + }); + + await expect( + ctx.router!.createPlainTransport({ + listenInfo: { + protocol: 'udp', + ip: multicastIp, + port: port, + flags: { udpReusePort: false }, + }, + }) + ).resolves.toBeDefined(); + + await expect( + ctx.router!.createPlainTransport({ + listenInfo: { + protocol: 'udp', + ip: multicastIp, + port: port, + flags: { udpReusePort: false }, + }, + }) + ).rejects.toThrow(); + }, 2000); +} + +test('plainTransport.getStats() succeeds', async () => { + const plainTransport = await ctx.router!.createPlainTransport({ + listenIp: '127.0.0.1', + }); + + const stats = await plainTransport.getStats(); + + expect(Array.isArray(stats)).toBe(true); + expect(stats.length).toBe(1); + expect(stats[0].type).toBe('plain-rtp-transport'); + expect(stats[0].transportId).toBe(plainTransport.id); + expect(typeof stats[0].timestamp).toBe('number'); + expect(stats[0].bytesReceived).toBe(0); + expect(stats[0].recvBitrate).toBe(0); + expect(stats[0].bytesSent).toBe(0); + expect(stats[0].sendBitrate).toBe(0); + expect(stats[0].rtpBytesReceived).toBe(0); + expect(stats[0].rtpRecvBitrate).toBe(0); + expect(stats[0].rtpBytesSent).toBe(0); + expect(stats[0].rtpSendBitrate).toBe(0); + expect(stats[0].rtxBytesReceived).toBe(0); + expect(stats[0].rtxRecvBitrate).toBe(0); + expect(stats[0].rtxBytesSent).toBe(0); + expect(stats[0].rtxSendBitrate).toBe(0); + expect(stats[0].probationBytesSent).toBe(0); + expect(stats[0].probationSendBitrate).toBe(0); + expect(typeof stats[0].tuple).toBe('object'); + // @deprecated Use tuple.localAddress instead. + expect(stats[0].tuple.localIp).toBe('127.0.0.1'); + expect(stats[0].tuple.localAddress).toBe('127.0.0.1'); + expect(typeof stats[0].tuple.localPort).toBe('number'); + expect(stats[0].tuple.protocol).toBe('udp'); + expect(stats[0].rtcpTuple).toBeUndefined(); +}, 2000); + +test('plainTransport.connect() succeeds', async () => { + const plainTransport = await ctx.router!.createPlainTransport({ + listenIp: '127.0.0.1', + rtcpMux: false, + }); + + await expect( + plainTransport.connect({ ip: '1.2.3.4', port: 1234, rtcpPort: 1235 }) + ).resolves.toBeUndefined(); + + // Must fail if connected. + await expect( + plainTransport.connect({ ip: '1.2.3.4', port: 1234, rtcpPort: 1235 }) + ).rejects.toThrow(Error); + + expect(plainTransport.tuple.remoteIp).toBe('1.2.3.4'); + expect(plainTransport.tuple.remotePort).toBe(1234); + expect(plainTransport.tuple.protocol).toBe('udp'); + expect(plainTransport.rtcpTuple?.remoteIp).toBe('1.2.3.4'); + expect(plainTransport.rtcpTuple?.remotePort).toBe(1235); + expect(plainTransport.rtcpTuple?.protocol).toBe('udp'); +}, 2000); + +test('plainTransport.connect() with wrong arguments rejects with TypeError', async () => { + const plainTransport = await ctx.router!.createPlainTransport({ + listenIp: '127.0.0.1', + rtcpMux: false, + }); + + // No SRTP enabled so passing srtpParameters must fail. + await expect( + plainTransport.connect({ + ip: '127.0.0.2', + port: 9998, + rtcpPort: 9999, + srtpParameters: { + cryptoSuite: 'AES_CM_128_HMAC_SHA1_80', + keyBase64: 'ZnQ3eWJraDg0d3ZoYzM5cXN1Y2pnaHU5NWxrZTVv', + }, + }) + ).rejects.toThrow(TypeError); + + await expect(plainTransport.connect({})).rejects.toThrow(TypeError); + + await expect(plainTransport.connect({ ip: '::::1234' })).rejects.toThrow( + TypeError + ); + + // Must fail because transport has rtcpMux: false so rtcpPort must be given + // in connect(). + await expect( + plainTransport.connect({ + ip: '127.0.0.1', + port: 1234, + // @ts-expect-error --- Testing purposes. + __rtcpPort: 1235, + }) + ).rejects.toThrow(TypeError); + + await expect( + plainTransport.connect({ + ip: '127.0.0.1', + // @ts-expect-error --- Testing purposes. + __port: 'chicken', + rtcpPort: 1235, + }) + ).rejects.toThrow(TypeError); +}, 2000); + +test('PlainTransport methods reject if closed', async () => { + const plainTransport = await ctx.router!.createPlainTransport({ + listenIp: '127.0.0.1', + }); + + const onObserverClose = jest.fn(); + + plainTransport.observer.once('close', onObserverClose); + plainTransport.close(); + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(plainTransport.closed).toBe(true); + + await expect(plainTransport.dump()).rejects.toThrow(Error); + + await expect(plainTransport.getStats()).rejects.toThrow(Error); + + await expect(plainTransport.connect({})).rejects.toThrow(Error); +}, 2000); + +test('router.createPlainTransport() with fixed port succeeds', async () => { + const port = await pickPort({ + type: 'udp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + const plainTransport = await ctx.router!.createPlainTransport({ + listenInfo: { protocol: 'udp', ip: '127.0.0.1', port }, + }); + + expect(plainTransport.tuple.localPort).toEqual(port); +}, 2000); + +test('PlainTransport emits "routerclose" if Router is closed', async () => { + const plainTransport = await ctx.router!.createPlainTransport({ + listenIp: '127.0.0.1', + }); + + const onObserverClose = jest.fn(); + + plainTransport.observer.once('close', onObserverClose); + + const promise = enhancedOnce( + plainTransport, + 'routerclose' + ); + + ctx.router!.close(); + await promise; + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(plainTransport.closed).toBe(true); +}, 2000); + +test('PlainTransport emits "routerclose" if Worker is closed', async () => { + const plainTransport = await ctx.router!.createPlainTransport({ + listenIp: '127.0.0.1', + }); + + const onObserverClose = jest.fn(); + + plainTransport.observer.once('close', onObserverClose); + + const promise = enhancedOnce( + plainTransport, + 'routerclose' + ); + + ctx.worker!.close(); + await promise; + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(plainTransport.closed).toBe(true); +}, 2000); diff --git a/node/src/test/test-Producer.ts b/node/src/test/test-Producer.ts new file mode 100644 index 0000000000..38ad33607f --- /dev/null +++ b/node/src/test/test-Producer.ts @@ -0,0 +1,831 @@ +import * as flatbuffers from 'flatbuffers'; +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { WorkerEvents, ProducerEvents } from '../types'; +import type { ProducerImpl } from '../Producer'; +import { UnsupportedError } from '../errors'; +import * as utils from '../utils'; +import { + Notification, + Body as NotificationBody, + Event, +} from '../fbs/notification'; +import * as FbsProducer from '../fbs/producer'; + +type TestContext = { + mediaCodecs: mediasoup.types.RtpCodecCapability[]; + audioProducerOptions: mediasoup.types.ProducerOptions; + videoProducerOptions: mediasoup.types.ProducerOptions; + worker?: mediasoup.types.Worker; + router?: mediasoup.types.Router; + webRtcTransport1?: mediasoup.types.WebRtcTransport; + webRtcTransport2?: mediasoup.types.WebRtcTransport; +}; + +const ctx: TestContext = { + mediaCodecs: utils.deepFreeze([ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + parameters: { + foo: '111', + }, + }, + { + kind: 'video', + mimeType: 'video/VP8', + clockRate: 90000, + }, + { + kind: 'video', + mimeType: 'video/H264', + clockRate: 90000, + parameters: { + 'level-asymmetry-allowed': 1, + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + foo: 'bar', + }, + rtcpFeedback: [], // Will be ignored. + }, + ]), + audioProducerOptions: utils.deepFreeze({ + kind: 'audio', + rtpParameters: { + mid: 'AUDIO', + codecs: [ + { + mimeType: 'audio/opus', + payloadType: 0, + clockRate: 48000, + channels: 2, + parameters: { + useinbandfec: 1, + usedtx: 1, + foo: 222.222, + bar: '333', + }, + }, + ], + headerExtensions: [ + { + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + id: 10, + }, + { + uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + id: 12, + }, + ], + // Missing encodings on purpose. + rtcp: { + cname: 'audio-1', + }, + }, + appData: { foo: 1, bar: '2' }, + }), + videoProducerOptions: utils.deepFreeze({ + kind: 'video', + rtpParameters: { + mid: 'VIDEO', + codecs: [ + { + mimeType: 'video/h264', + payloadType: 112, + clockRate: 90000, + parameters: { + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }, + rtcpFeedback: [ + { type: 'nack' }, + { type: 'nack', parameter: 'pli' }, + { type: 'goog-remb' }, + ], + }, + { + mimeType: 'video/rtx', + payloadType: 113, + clockRate: 90000, + parameters: { apt: 112 }, + }, + ], + headerExtensions: [ + { + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + id: 10, + }, + { + uri: 'urn:3gpp:video-orientation', + id: 13, + }, + ], + encodings: [ + { ssrc: 22222222, rtx: { ssrc: 22222223 }, scalabilityMode: 'L1T3' }, + { ssrc: 22222224, rtx: { ssrc: 22222225 } }, + { ssrc: 22222226, rtx: { ssrc: 22222227 } }, + { ssrc: 22222228, rtx: { ssrc: 22222229 } }, + ], + rtcp: { + cname: 'video-1', + }, + }, + appData: { foo: 1, bar: '2' }, + }), +}; + +beforeEach(async () => { + ctx.worker = await mediasoup.createWorker(); + ctx.router = await ctx.worker.createRouter({ mediaCodecs: ctx.mediaCodecs }); + ctx.webRtcTransport1 = await ctx.router.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + }); + ctx.webRtcTransport2 = await ctx.router.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + }); +}); + +afterEach(async () => { + ctx.worker?.close(); + + if (ctx.worker?.subprocessClosed === false) { + await enhancedOnce(ctx.worker, 'subprocessclose'); + } +}); + +test('webRtcTransport1.produce() succeeds', async () => { + const onObserverNewProducer = jest.fn(); + + ctx.webRtcTransport1!.observer.once('newproducer', onObserverNewProducer); + + const audioProducer = await ctx.webRtcTransport1!.produce( + ctx.audioProducerOptions + ); + + expect(onObserverNewProducer).toHaveBeenCalledTimes(1); + expect(onObserverNewProducer).toHaveBeenCalledWith(audioProducer); + expect(typeof audioProducer.id).toBe('string'); + expect(audioProducer.closed).toBe(false); + expect(audioProducer.kind).toBe('audio'); + expect(typeof audioProducer.rtpParameters).toBe('object'); + expect(audioProducer.type).toBe('simple'); + // Private API. + expect(typeof audioProducer.consumableRtpParameters).toBe('object'); + expect(audioProducer.paused).toBe(false); + expect(audioProducer.score).toEqual([]); + expect(audioProducer.appData).toEqual({ foo: 1, bar: '2' }); + + await expect(ctx.router!.dump()).resolves.toMatchObject({ + mapProducerIdConsumerIds: [{ key: audioProducer.id, values: [] }], + mapConsumerIdProducerId: [], + }); + + await expect(ctx.webRtcTransport1!.dump()).resolves.toMatchObject({ + id: ctx.webRtcTransport1!.id, + producerIds: [audioProducer.id], + consumerIds: [], + }); +}, 2000); + +test('webRtcTransport2.produce() succeeds', async () => { + const onObserverNewProducer = jest.fn(); + + ctx.webRtcTransport2!.observer.once('newproducer', onObserverNewProducer); + + const videoProducer = await ctx.webRtcTransport2!.produce( + ctx.videoProducerOptions + ); + + expect(onObserverNewProducer).toHaveBeenCalledTimes(1); + expect(onObserverNewProducer).toHaveBeenCalledWith(videoProducer); + expect(typeof videoProducer.id).toBe('string'); + expect(videoProducer.closed).toBe(false); + expect(videoProducer.kind).toBe('video'); + expect(typeof videoProducer.rtpParameters).toBe('object'); + expect(videoProducer.type).toBe('simulcast'); + // Private API. + expect(typeof videoProducer.consumableRtpParameters).toBe('object'); + expect(videoProducer.paused).toBe(false); + expect(videoProducer.score).toEqual([]); + expect(videoProducer.appData).toEqual({ foo: 1, bar: '2' }); + + const dump = await ctx.router!.dump(); + + expect(dump.mapProducerIdConsumerIds).toEqual( + expect.arrayContaining([{ key: videoProducer.id, values: [] }]) + ); + + expect(dump.mapConsumerIdProducerId.length).toBe(0); + + await expect(ctx.webRtcTransport2!.dump()).resolves.toMatchObject({ + id: ctx.webRtcTransport2!.id, + producerIds: [videoProducer.id], + consumerIds: [], + }); +}, 2000); + +test('webRtcTransport1.produce() without header extensions and rtcp succeeds', async () => { + const onObserverNewProducer = jest.fn(); + + ctx.webRtcTransport1!.observer.once('newproducer', onObserverNewProducer); + + const audioProducer = await ctx.webRtcTransport1!.produce({ + kind: 'audio', + rtpParameters: { + mid: 'AUDIO2', + codecs: [ + { + mimeType: 'audio/opus', + payloadType: 0, + clockRate: 48000, + channels: 2, + parameters: { + useinbandfec: 1, + usedtx: 1, + foo: 222.222, + bar: '333', + }, + }, + ], + }, + appData: { foo: 1, bar: '2' }, + }); + + expect(onObserverNewProducer).toHaveBeenCalledTimes(1); + expect(onObserverNewProducer).toHaveBeenCalledWith(audioProducer!); + expect(typeof audioProducer!.id).toBe('string'); + expect(audioProducer!.closed).toBe(false); + expect(audioProducer!.kind).toBe('audio'); + expect(typeof audioProducer!.rtpParameters).toBe('object'); + expect(audioProducer!.type).toBe('simple'); + // Private API. + expect(typeof audioProducer!.consumableRtpParameters).toBe('object'); + expect(audioProducer!.paused).toBe(false); + expect(audioProducer!.score).toEqual([]); + expect(audioProducer!.appData).toEqual({ foo: 1, bar: '2' }); + + audioProducer.close(); +}, 2000); + +test('webRtcTransport1.produce() with wrong arguments rejects with TypeError', async () => { + await expect( + ctx.webRtcTransport1!.produce({ + // @ts-expect-error --- Testing purposes. + kind: 'chicken', + // @ts-expect-error --- Testing purposes. + rtpParameters: {}, + }) + ).rejects.toThrow(TypeError); + + await expect( + ctx.webRtcTransport1!.produce({ + kind: 'audio', + // @ts-expect-error --- Testing purposes. + rtpParameters: {}, + }) + ).rejects.toThrow(TypeError); + + // Invalid ssrc. + await expect( + ctx.webRtcTransport1!.produce({ + kind: 'audio', + rtpParameters: { + codecs: [], + headerExtensions: [], + // @ts-expect-error --- Testing purposes. + encodings: [{ ssrc: '1111' }], + rtcp: { cname: 'qwerty' }, + }, + }) + ).rejects.toThrow(TypeError); + + // Missing or empty rtpParameters.encodings. + await expect( + ctx.webRtcTransport1!.produce({ + kind: 'video', + rtpParameters: { + codecs: [ + { + mimeType: 'video/h264', + payloadType: 112, + clockRate: 90000, + parameters: { + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }, + }, + { + mimeType: 'video/rtx', + payloadType: 113, + clockRate: 90000, + parameters: { apt: 112 }, + }, + ], + headerExtensions: [], + encodings: [], + rtcp: { cname: 'qwerty' }, + }, + }) + ).rejects.toThrow(TypeError); + + // Wrong apt in RTX codec. + await expect( + ctx.webRtcTransport1!.produce({ + kind: 'audio', + rtpParameters: { + codecs: [ + { + mimeType: 'video/h264', + payloadType: 112, + clockRate: 90000, + parameters: { + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }, + }, + { + mimeType: 'video/rtx', + payloadType: 113, + clockRate: 90000, + parameters: { apt: 111 }, + }, + ], + headerExtensions: [], + encodings: [{ ssrc: 6666, rtx: { ssrc: 6667 } }], + rtcp: { + cname: 'video-1', + }, + }, + }) + ).rejects.toThrow(TypeError); +}, 2000); + +test('webRtcTransport1.produce() with unsupported codecs rejects with UnsupportedError', async () => { + await expect( + ctx.webRtcTransport1!.produce({ + kind: 'audio', + rtpParameters: { + codecs: [ + { + mimeType: 'audio/ISAC', + payloadType: 108, + clockRate: 32000, + }, + ], + headerExtensions: [], + encodings: [{ ssrc: 1111 }], + rtcp: { cname: 'audio' }, + }, + }) + ).rejects.toThrow(UnsupportedError); + + // Invalid H264 profile-level-id. + await expect( + ctx.webRtcTransport1!.produce({ + kind: 'video', + rtpParameters: { + codecs: [ + { + mimeType: 'video/h264', + payloadType: 112, + clockRate: 90000, + parameters: { + 'packetization-mode': 1, + 'profile-level-id': 'CHICKEN', + }, + }, + { + mimeType: 'video/rtx', + payloadType: 113, + clockRate: 90000, + parameters: { apt: 112 }, + }, + ], + headerExtensions: [], + encodings: [{ ssrc: 6666, rtx: { ssrc: 6667 } }], + }, + }) + ).rejects.toThrow(UnsupportedError); +}, 2000); + +test('transport.produce() with already used MID or SSRC rejects with Error', async () => { + const audioProducerOptions: mediasoup.types.ProducerOptions = { + kind: 'audio', + rtpParameters: { + mid: 'AUDIO', + codecs: [ + { + mimeType: 'audio/opus', + payloadType: 0, + clockRate: 48000, + channels: 2, + }, + ], + encodings: [{ ssrc: 33333333 }], + }, + }; + + const videoProducerOptions: mediasoup.types.ProducerOptions = { + kind: 'video', + rtpParameters: { + mid: 'VIDEO2', + codecs: [ + { + mimeType: 'video/h264', + payloadType: 112, + clockRate: 90000, + parameters: { + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }, + }, + ], + headerExtensions: [ + { + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + id: 10, + }, + ], + encodings: [{ ssrc: 22222222 }], + rtcp: { + cname: 'video-1', + }, + }, + }; + + await ctx.webRtcTransport1!.produce(audioProducerOptions); + + await expect( + ctx.webRtcTransport1!.produce(audioProducerOptions) + ).rejects.toThrow(Error); + + await ctx.webRtcTransport2!.produce(videoProducerOptions); + + await expect( + ctx.webRtcTransport2!.produce(videoProducerOptions) + ).rejects.toThrow(Error); +}, 2000); + +test('transport.produce() with no MID and with single encoding without RID or SSRC rejects with Error', async () => { + await expect( + ctx.webRtcTransport1!.produce({ + kind: 'audio', + rtpParameters: { + codecs: [ + { + mimeType: 'audio/opus', + payloadType: 111, + clockRate: 48000, + channels: 2, + }, + ], + encodings: [{}], + }, + }) + ).rejects.toThrow(Error); +}, 2000); + +test('producer.dump() succeeds', async () => { + const audioProducer = await ctx.webRtcTransport1!.produce( + ctx.audioProducerOptions + ); + + const dump1 = await audioProducer.dump(); + + expect(dump1.id).toBe(audioProducer.id); + expect(dump1.kind).toBe(audioProducer.kind); + expect(typeof dump1.rtpParameters).toBe('object'); + expect(Array.isArray(dump1.rtpParameters.codecs)).toBe(true); + expect(dump1.rtpParameters.codecs.length).toBe(1); + expect(dump1.rtpParameters.codecs[0].mimeType).toBe('audio/opus'); + expect(dump1.rtpParameters.codecs[0].payloadType).toBe(0); + expect(dump1.rtpParameters.codecs[0].clockRate).toBe(48000); + expect(dump1.rtpParameters.codecs[0].channels).toBe(2); + expect(dump1.rtpParameters.codecs[0].parameters).toEqual({ + useinbandfec: 1, + usedtx: 1, + foo: 222.222, + bar: '333', + }); + expect(dump1.rtpParameters.codecs[0].rtcpFeedback).toEqual([]); + expect(Array.isArray(dump1.rtpParameters.headerExtensions)).toBe(true); + expect(dump1.rtpParameters.headerExtensions!.length).toBe(2); + expect(dump1.rtpParameters.headerExtensions).toEqual([ + { + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + id: 10, + parameters: {}, + encrypt: false, + }, + { + uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + id: 12, + parameters: {}, + encrypt: false, + }, + ]); + expect(Array.isArray(dump1.rtpParameters.encodings)).toBe(true); + expect(dump1.rtpParameters.encodings!.length).toBe(1); + expect(dump1.rtpParameters.encodings![0]).toEqual( + expect.objectContaining({ + codecPayloadType: 0, + }) + ); + expect(dump1.type).toBe('simple'); + + const videoProducer = await ctx.webRtcTransport2!.produce( + ctx.videoProducerOptions + ); + + const dump2 = await videoProducer.dump(); + + expect(dump2.id).toBe(videoProducer.id); + expect(dump2.kind).toBe(videoProducer.kind); + expect(typeof dump2.rtpParameters).toBe('object'); + expect(Array.isArray(dump2.rtpParameters.codecs)).toBe(true); + expect(dump2.rtpParameters.codecs.length).toBe(2); + expect(dump2.rtpParameters.codecs[0].mimeType).toBe('video/H264'); + expect(dump2.rtpParameters.codecs[0].payloadType).toBe(112); + expect(dump2.rtpParameters.codecs[0].clockRate).toBe(90000); + expect(dump2.rtpParameters.codecs[0].channels).toBeUndefined(); + expect(dump2.rtpParameters.codecs[0].parameters).toEqual({ + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }); + expect(dump2.rtpParameters.codecs[0].rtcpFeedback).toEqual([ + { type: 'nack' }, + { type: 'nack', parameter: 'pli' }, + { type: 'goog-remb' }, + ]); + expect(dump2.rtpParameters.codecs[1].mimeType).toBe('video/rtx'); + expect(dump2.rtpParameters.codecs[1].payloadType).toBe(113); + expect(dump2.rtpParameters.codecs[1].clockRate).toBe(90000); + expect(dump2.rtpParameters.codecs[1].channels).toBeUndefined(); + expect(dump2.rtpParameters.codecs[1].parameters).toEqual({ apt: 112 }); + expect(dump2.rtpParameters.codecs[1].rtcpFeedback).toEqual([]); + expect(Array.isArray(dump2.rtpParameters.headerExtensions)).toBe(true); + expect(dump2.rtpParameters.headerExtensions!.length).toBe(2); + expect(dump2.rtpParameters.headerExtensions).toEqual([ + { + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + id: 10, + parameters: {}, + encrypt: false, + }, + { + uri: 'urn:3gpp:video-orientation', + id: 13, + parameters: {}, + encrypt: false, + }, + ]); + expect(Array.isArray(dump2.rtpParameters.encodings)).toBe(true); + expect(dump2.rtpParameters.encodings!.length).toBe(4); + expect(dump2.rtpParameters.encodings).toMatchObject([ + { + codecPayloadType: 112, + ssrc: 22222222, + rtx: { ssrc: 22222223 }, + scalabilityMode: 'L1T3', + }, + { codecPayloadType: 112, ssrc: 22222224, rtx: { ssrc: 22222225 } }, + { codecPayloadType: 112, ssrc: 22222226, rtx: { ssrc: 22222227 } }, + { codecPayloadType: 112, ssrc: 22222228, rtx: { ssrc: 22222229 } }, + ]); + expect(dump2.type).toBe('simulcast'); +}, 2000); + +test('producer.getStats() succeeds', async () => { + const audioProducer = await ctx.webRtcTransport1!.produce( + ctx.audioProducerOptions + ); + + const videoProducer = await ctx.webRtcTransport2!.produce( + ctx.videoProducerOptions + ); + + await expect(audioProducer.getStats()).resolves.toEqual([]); + + await expect(videoProducer.getStats()).resolves.toEqual([]); +}, 2000); + +test('producer.pause() and resume() succeed', async () => { + const audioProducer = await ctx.webRtcTransport1!.produce( + ctx.audioProducerOptions + ); + + const onObserverPause = jest.fn(); + const onObserverResume = jest.fn(); + + audioProducer.observer.on('pause', onObserverPause); + audioProducer.observer.on('resume', onObserverResume); + + await audioProducer.pause(); + expect(audioProducer.paused).toBe(true); + + await expect(audioProducer.dump()).resolves.toMatchObject({ paused: true }); + + await audioProducer.resume(); + expect(audioProducer.paused).toBe(false); + + await expect(audioProducer.dump()).resolves.toMatchObject({ paused: false }); + + // Even if we don't await for pause()/resume() completion, the observer must + // fire 'pause' and 'resume' events if state was the opposite. + void audioProducer.pause(); + void audioProducer.resume(); + void audioProducer.pause(); + void audioProducer.pause(); + void audioProducer.pause(); + await audioProducer.resume(); + + expect(onObserverPause).toHaveBeenCalledTimes(3); + expect(onObserverResume).toHaveBeenCalledTimes(3); +}, 2000); + +test('producer.pause() and resume() emit events', async () => { + const audioProducer = await ctx.webRtcTransport1!.produce( + ctx.audioProducerOptions + ); + + const promises = []; + const events: string[] = []; + + audioProducer.observer.once('resume', () => { + events.push('resume'); + }); + + audioProducer.observer.once('pause', () => { + events.push('pause'); + }); + + promises.push(audioProducer.pause()); + promises.push(audioProducer.resume()); + + await Promise.all(promises); + + expect(events).toEqual(['pause', 'resume']); + expect(audioProducer.paused).toBe(false); +}, 2000); + +test('producer.enableTraceEvent() succeed', async () => { + const audioProducer = await ctx.webRtcTransport1!.produce( + ctx.audioProducerOptions + ); + + await audioProducer.enableTraceEvent(['rtp', 'pli']); + + const dump1 = await audioProducer.dump(); + + expect(dump1.traceEventTypes).toEqual(expect.arrayContaining(['rtp', 'pli'])); + + await audioProducer.enableTraceEvent(); + + const dump2 = await audioProducer.dump(); + + expect(dump2.traceEventTypes).toEqual(expect.arrayContaining([])); + + // @ts-expect-error --- Testing purposes. + await audioProducer.enableTraceEvent(['nack', 'FOO', 'fir']); + + const dump3 = await audioProducer.dump(); + + expect(dump3.traceEventTypes).toEqual( + expect.arrayContaining(['nack', 'fir']) + ); + + await audioProducer.enableTraceEvent(); + + const dump4 = await audioProducer.dump(); + + expect(dump4.traceEventTypes).toEqual(expect.arrayContaining([])); +}, 2000); + +test('producer.enableTraceEvent() with wrong arguments rejects with TypeError', async () => { + const audioProducer = await ctx.webRtcTransport1!.produce( + ctx.audioProducerOptions + ); + + // @ts-expect-error --- Testing purposes. + await expect(audioProducer.enableTraceEvent(123)).rejects.toThrow(TypeError); + + // @ts-expect-error --- Testing purposes. + await expect(audioProducer.enableTraceEvent('rtp')).rejects.toThrow( + TypeError + ); + + await expect( + // @ts-expect-error --- Testing purposes. + audioProducer.enableTraceEvent(['fir', 123.123]) + ).rejects.toThrow(TypeError); +}, 2000); + +test('Producer emits "score"', async () => { + const videoProducer = await ctx.webRtcTransport2!.produce( + ctx.videoProducerOptions + ); + + // API not exposed in the interface. + const channel = (videoProducer as ProducerImpl).channelForTesting; + const onScore = jest.fn(); + + videoProducer.on('score', onScore); + + // Simulate a 'score' notification coming through the channel. + const builder = new flatbuffers.Builder(); + const producerScoreNotification = new FbsProducer.ScoreNotificationT([ + new FbsProducer.ScoreT( + /* encodingIdx */ 0, + /* ssrc */ 11, + /* rid */ undefined, + /* score */ 10 + ), + new FbsProducer.ScoreT( + /* encodingIdx */ 1, + /* ssrc */ 22, + /* rid */ undefined, + /* score */ 9 + ), + ]); + const notificationOffset = Notification.createNotification( + builder, + builder.createString(videoProducer.id), + Event.PRODUCER_SCORE, + NotificationBody.Producer_ScoreNotification, + producerScoreNotification.pack(builder) + ); + + builder.finish(notificationOffset); + + const notification = Notification.getRootAsNotification( + new flatbuffers.ByteBuffer(builder.asUint8Array()) + ); + + channel.emit(videoProducer.id, Event.PRODUCER_SCORE, notification); + channel.emit(videoProducer.id, Event.PRODUCER_SCORE, notification); + channel.emit(videoProducer.id, Event.PRODUCER_SCORE, notification); + + expect(onScore).toHaveBeenCalledTimes(3); + expect(videoProducer.score).toEqual([ + { ssrc: 11, rid: undefined, score: 10, encodingIdx: 0 }, + { ssrc: 22, rid: undefined, score: 9, encodingIdx: 1 }, + ]); +}, 2000); + +test('producer.close() succeeds', async () => { + const audioProducer = await ctx.webRtcTransport1!.produce( + ctx.audioProducerOptions + ); + + const onObserverClose = jest.fn(); + + audioProducer.observer.once('close', onObserverClose); + audioProducer.close(); + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(audioProducer.closed).toBe(true); + + await expect(ctx.router!.dump()).resolves.toMatchObject({ + mapProducerIdConsumerIds: [], + mapConsumerIdProducerId: [], + }); + + await expect(ctx.webRtcTransport1!.dump()).resolves.toMatchObject({ + id: ctx.webRtcTransport1!.id, + producerIds: [], + consumerIds: [], + }); +}, 2000); + +test('Producer methods reject if closed', async () => { + const audioProducer = await ctx.webRtcTransport1!.produce( + ctx.audioProducerOptions + ); + + audioProducer.close(); + + await expect(audioProducer.dump()).rejects.toThrow(Error); + await expect(audioProducer.getStats()).rejects.toThrow(Error); + await expect(audioProducer.pause()).rejects.toThrow(Error); + await expect(audioProducer.resume()).rejects.toThrow(Error); +}, 2000); + +test('Producer emits "transportclose" if Transport is closed', async () => { + const videoProducer = await ctx.webRtcTransport2!.produce( + ctx.videoProducerOptions + ); + + const onObserverClose = jest.fn(); + + videoProducer.observer.once('close', onObserverClose); + + const promise = enhancedOnce(videoProducer, 'transportclose'); + + ctx.webRtcTransport2!.close(); + await promise; + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(videoProducer.closed).toBe(true); +}, 2000); diff --git a/node/src/test/test-Router.ts b/node/src/test/test-Router.ts new file mode 100644 index 0000000000..0affb75ef6 --- /dev/null +++ b/node/src/test/test-Router.ts @@ -0,0 +1,159 @@ +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { WorkerImpl } from '../Worker'; +import type { WorkerEvents, RouterEvents } from '../types'; +import { InvalidStateError } from '../errors'; +import * as utils from '../utils'; + +type TestContext = { + mediaCodecs: mediasoup.types.RtpCodecCapability[]; + worker?: mediasoup.types.Worker; +}; + +const ctx: TestContext = { + mediaCodecs: utils.deepFreeze([ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + parameters: { + useinbandfec: 1, + foo: 'bar', + }, + }, + { + kind: 'video', + mimeType: 'video/VP8', + clockRate: 90000, + }, + { + kind: 'video', + mimeType: 'video/H264', + clockRate: 90000, + parameters: { + 'level-asymmetry-allowed': 1, + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }, + rtcpFeedback: [], // Will be ignored. + }, + ]), +}; + +beforeEach(async () => { + ctx.worker = await mediasoup.createWorker(); +}); + +afterEach(async () => { + ctx.worker?.close(); + + if (ctx.worker?.subprocessClosed === false) { + await enhancedOnce(ctx.worker, 'subprocessclose'); + } +}); + +test('worker.createRouter() succeeds', async () => { + const onObserverNewRouter = jest.fn(); + + ctx.worker!.observer.once('newrouter', onObserverNewRouter); + + const router = await ctx.worker!.createRouter<{ foo: number; bar?: string }>({ + mediaCodecs: ctx.mediaCodecs, + appData: { foo: 123 }, + }); + + expect(onObserverNewRouter).toHaveBeenCalledTimes(1); + expect(onObserverNewRouter).toHaveBeenCalledWith(router); + expect(typeof router.id).toBe('string'); + expect(router.closed).toBe(false); + expect(typeof router.rtpCapabilities).toBe('object'); + expect(Array.isArray(router.rtpCapabilities.codecs)).toBe(true); + expect(Array.isArray(router.rtpCapabilities.headerExtensions)).toBe(true); + expect(router.appData).toEqual({ foo: 123 }); + + expect(() => (router.appData = { foo: 222, bar: 'BBB' })).not.toThrow(); + + await expect(ctx.worker!.dump()).resolves.toMatchObject({ + pid: ctx.worker!.pid, + webRtcServerIds: [], + routerIds: [router.id], + channelMessageHandlers: { + channelRequestHandlers: [router.id], + channelNotificationHandlers: [], + }, + }); + + await expect(router.dump()).resolves.toMatchObject({ + id: router.id, + transportIds: [], + rtpObserverIds: [], + mapProducerIdConsumerIds: {}, + mapConsumerIdProducerId: {}, + mapProducerIdObserverIds: {}, + mapDataProducerIdDataConsumerIds: {}, + mapDataConsumerIdDataProducerId: {}, + }); + + // API not exposed in the interface. + expect((ctx.worker! as WorkerImpl).routersForTesting.size).toBe(1); + + ctx.worker!.close(); + + expect(router.closed).toBe(true); + + // API not exposed in the interface. + expect((ctx.worker! as WorkerImpl).routersForTesting.size).toBe(0); +}, 2000); + +test('worker.createRouter() with wrong arguments rejects with TypeError', async () => { + // @ts-expect-error --- Testing purposes. + await expect(ctx.worker!.createRouter({ mediaCodecs: {} })).rejects.toThrow( + TypeError + ); + + await expect( + // @ts-expect-error --- Testing purposes. + ctx.worker!.createRouter({ appData: 'NOT-AN-OBJECT' }) + ).rejects.toThrow(TypeError); +}, 2000); + +test('worker.createRouter() rejects with InvalidStateError if Worker is closed', async () => { + ctx.worker!.close(); + + await expect( + ctx.worker!.createRouter({ mediaCodecs: ctx.mediaCodecs }) + ).rejects.toThrow(InvalidStateError); +}, 2000); + +test('router.close() succeeds', async () => { + const router = await ctx.worker!.createRouter({ + mediaCodecs: ctx.mediaCodecs, + }); + + const onObserverClose = jest.fn(); + + router.observer.once('close', onObserverClose); + router.close(); + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(router.closed).toBe(true); +}, 2000); + +test('Router emits "workerclose" if Worker is closed', async () => { + const router = await ctx.worker!.createRouter({ + mediaCodecs: ctx.mediaCodecs, + }); + + const onObserverClose = jest.fn(); + + router.observer.once('close', onObserverClose); + + const promise = enhancedOnce(router, 'workerclose'); + + ctx.worker!.close(); + await promise; + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(router.closed).toBe(true); +}, 2000); diff --git a/node/src/test/test-WebRtcServer.ts b/node/src/test/test-WebRtcServer.ts new file mode 100644 index 0000000000..669e8ab8ef --- /dev/null +++ b/node/src/test/test-WebRtcServer.ts @@ -0,0 +1,620 @@ +import { pickPort } from 'pick-port'; +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { WorkerImpl } from '../Worker'; +import type { WorkerEvents, WebRtcServerEvents } from '../types'; +import type { WebRtcServerImpl } from '../WebRtcServer'; +import type { RouterImpl } from '../Router'; +import { InvalidStateError } from '../errors'; + +type TestContext = { + worker?: mediasoup.types.Worker; +}; + +const ctx: TestContext = {}; + +beforeEach(async () => { + ctx.worker = await mediasoup.createWorker(); +}); + +afterEach(async () => { + ctx.worker?.close(); + + if (ctx.worker?.subprocessClosed === false) { + await enhancedOnce(ctx.worker, 'subprocessclose'); + } +}); + +test('worker.createWebRtcServer() succeeds', async () => { + const onObserverNewWebRtcServer = jest.fn(); + + ctx.worker!.observer.once('newwebrtcserver', onObserverNewWebRtcServer); + + const port1 = await pickPort({ + type: 'udp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + const port2 = await pickPort({ + type: 'tcp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + + const webRtcServer = await ctx.worker!.createWebRtcServer<{ foo?: number }>({ + listenInfos: [ + { + protocol: 'udp', + ip: '127.0.0.1', + port: port1, + }, + { + protocol: 'tcp', + ip: '127.0.0.1', + announcedAddress: 'foo.bar.org', + port: port2, + }, + ], + appData: { foo: 123 }, + }); + + expect(onObserverNewWebRtcServer).toHaveBeenCalledTimes(1); + expect(onObserverNewWebRtcServer).toHaveBeenCalledWith(webRtcServer); + expect(typeof webRtcServer.id).toBe('string'); + expect(webRtcServer.closed).toBe(false); + expect(webRtcServer.appData).toEqual({ foo: 123 }); + + await expect(ctx.worker!.dump()).resolves.toMatchObject({ + pid: ctx.worker!.pid, + webRtcServerIds: [webRtcServer.id], + routerIds: [], + channelMessageHandlers: { + channelRequestHandlers: [webRtcServer.id], + channelNotificationHandlers: [], + }, + }); + + await expect(webRtcServer.dump()).resolves.toMatchObject({ + id: webRtcServer.id, + udpSockets: [{ ip: '127.0.0.1', port: port1 }], + tcpServers: [{ ip: '127.0.0.1', port: port2 }], + webRtcTransportIds: [], + localIceUsernameFragments: [], + tupleHashes: [], + }); + + // API not exposed in the interface. + expect((ctx.worker! as WorkerImpl).webRtcServersForTesting.size).toBe(1); + + ctx.worker!.close(); + + expect(webRtcServer.closed).toBe(true); + + // API not exposed in the interface. + expect((ctx.worker! as WorkerImpl).webRtcServersForTesting.size).toBe(0); +}, 2000); + +test('worker.createWebRtcServer() with portRange succeeds', async () => { + const onObserverNewWebRtcServer = jest.fn(); + + ctx.worker!.observer.once('newwebrtcserver', onObserverNewWebRtcServer); + + const port1 = await pickPort({ + type: 'udp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + const port2 = await pickPort({ + type: 'udp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + + const webRtcServer = await ctx.worker!.createWebRtcServer({ + listenInfos: [ + { + protocol: 'udp', + ip: '127.0.0.1', + portRange: { min: port1, max: port1 }, + }, + { + protocol: 'tcp', + ip: '127.0.0.1', + announcedAddress: '1.2.3.4', + portRange: { min: port2, max: port2 }, + }, + ], + appData: { foo: 123 }, + }); + + expect(onObserverNewWebRtcServer).toHaveBeenCalledTimes(1); + expect(onObserverNewWebRtcServer).toHaveBeenCalledWith(webRtcServer); + expect(typeof webRtcServer.id).toBe('string'); + expect(webRtcServer.closed).toBe(false); + expect(webRtcServer.appData).toEqual({ foo: 123 }); + + await expect(ctx.worker!.dump()).resolves.toMatchObject({ + pid: ctx.worker!.pid, + webRtcServerIds: [webRtcServer.id], + routerIds: [], + channelMessageHandlers: { + channelRequestHandlers: [webRtcServer.id], + channelNotificationHandlers: [], + }, + }); + + await expect(webRtcServer.dump()).resolves.toMatchObject({ + id: webRtcServer.id, + udpSockets: [{ ip: '127.0.0.1', port: port1 }], + tcpServers: [{ ip: '127.0.0.1', port: port2 }], + webRtcTransportIds: [], + localIceUsernameFragments: [], + tupleHashes: [], + }); + + // API not exposed in the interface. + expect((ctx.worker! as WorkerImpl).webRtcServersForTesting.size).toBe(1); + + ctx.worker!.close(); + + expect(webRtcServer.closed).toBe(true); + + // API not exposed in the interface. + expect((ctx.worker! as WorkerImpl).webRtcServersForTesting.size).toBe(0); +}, 2000); + +test('worker.createWebRtcServer() without specifying port/portRange succeeds', async () => { + const onObserverNewWebRtcServer = jest.fn(); + + ctx.worker!.observer.once('newwebrtcserver', onObserverNewWebRtcServer); + + const webRtcServer = await ctx.worker!.createWebRtcServer({ + listenInfos: [ + { + protocol: 'udp', + ip: '127.0.0.1', + }, + { + protocol: 'tcp', + ip: '127.0.0.1', + announcedAddress: '1.2.3.4', + }, + ], + appData: { foo: 123 }, + }); + + expect(onObserverNewWebRtcServer).toHaveBeenCalledTimes(1); + expect(onObserverNewWebRtcServer).toHaveBeenCalledWith(webRtcServer); + expect(typeof webRtcServer.id).toBe('string'); + expect(webRtcServer.closed).toBe(false); + expect(webRtcServer.appData).toEqual({ foo: 123 }); + + await expect(ctx.worker!.dump()).resolves.toMatchObject({ + pid: ctx.worker!.pid, + webRtcServerIds: [webRtcServer.id], + routerIds: [], + channelMessageHandlers: { + channelRequestHandlers: [webRtcServer.id], + channelNotificationHandlers: [], + }, + }); + + await expect(webRtcServer.dump()).resolves.toMatchObject({ + id: webRtcServer.id, + udpSockets: [{ ip: '127.0.0.1', port: expect.any(Number) }], + tcpServers: [{ ip: '127.0.0.1', port: expect.any(Number) }], + webRtcTransportIds: [], + localIceUsernameFragments: [], + tupleHashes: [], + }); + + // API not exposed in the interface. + expect((ctx.worker! as WorkerImpl).webRtcServersForTesting.size).toBe(1); + + ctx.worker!.close(); + + expect(webRtcServer.closed).toBe(true); + + // API not exposed in the interface. + expect((ctx.worker! as WorkerImpl).webRtcServersForTesting.size).toBe(0); +}, 2000); + +test('worker.createWebRtcServer() with wrong arguments rejects with TypeError', async () => { + // @ts-expect-error --- Testing purposes. + await expect(ctx.worker!.createWebRtcServer({})).rejects.toThrow(TypeError); + + await expect( + // @ts-expect-error --- Testing purposes. + ctx.worker!.createWebRtcServer({ listenInfos: 'NOT-AN-ARRAY' }) + ).rejects.toThrow(TypeError); + + await expect( + // @ts-expect-error --- Testing purposes. + ctx.worker!.createWebRtcServer({ listenInfos: ['NOT-AN-OBJECT'] }) + ).rejects.toThrow(Error); + + // Empty listenInfos so should fail. + await expect( + ctx.worker!.createWebRtcServer({ listenInfos: [] }) + ).rejects.toThrow(TypeError); +}, 2000); + +test('worker.createWebRtcServer() with unavailable listenInfos rejects with Error', async () => { + const worker2 = await mediasoup.createWorker(); + const port1 = await pickPort({ + type: 'udp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + const port2 = await pickPort({ + type: 'udp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + + // Using an unavailable listen IP. + await expect( + ctx.worker!.createWebRtcServer({ + listenInfos: [ + { + protocol: 'udp', + ip: '127.0.0.1', + port: port1, + }, + { + protocol: 'udp', + ip: '1.2.3.4', + port: port2, + }, + ], + }) + ).rejects.toThrow(Error); + + // Using the same UDP port in two listenInfos. + await expect( + ctx.worker!.createWebRtcServer({ + listenInfos: [ + { + protocol: 'udp', + ip: '127.0.0.1', + port: port1, + }, + { + protocol: 'udp', + ip: '127.0.0.1', + announcedAddress: '1.2.3.4', + port: port1, + }, + ], + }) + ).rejects.toThrow(Error); + + await ctx.worker!.createWebRtcServer({ + listenInfos: [ + { + protocol: 'udp', + ip: '127.0.0.1', + port: port1, + }, + ], + }); + + // Using the same UDP port in a second Worker. + await expect( + worker2.createWebRtcServer({ + listenInfos: [ + { + protocol: 'udp', + ip: '127.0.0.1', + port: port1, + }, + ], + }) + ).rejects.toThrow(Error); + + worker2.close(); +}, 2000); + +test('worker.createWebRtcServer() rejects with InvalidStateError if Worker is closed', async () => { + ctx.worker!.close(); + + const port = await pickPort({ + type: 'udp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + + await expect( + ctx.worker!.createWebRtcServer({ + listenInfos: [{ protocol: 'udp', ip: '127.0.0.1', port }], + }) + ).rejects.toThrow(InvalidStateError); +}, 2000); + +test('webRtcServer.close() succeeds', async () => { + const port = await pickPort({ + type: 'udp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + const webRtcServer = await ctx.worker!.createWebRtcServer({ + listenInfos: [{ protocol: 'udp', ip: '127.0.0.1', port }], + }); + const onObserverClose = jest.fn(); + + webRtcServer.observer.once('close', onObserverClose); + webRtcServer.close(); + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(webRtcServer.closed).toBe(true); +}, 2000); + +test('WebRtcServer emits "workerclose" if Worker is closed', async () => { + const port = await pickPort({ + type: 'udp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + const webRtcServer = await ctx.worker!.createWebRtcServer({ + listenInfos: [{ protocol: 'tcp', ip: '127.0.0.1', port }], + }); + const onObserverClose = jest.fn(); + + webRtcServer.observer.once('close', onObserverClose); + + const promise = enhancedOnce(webRtcServer, 'workerclose'); + + ctx.worker!.close(); + await promise; + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(webRtcServer.closed).toBe(true); +}, 2000); + +test('router.createWebRtcTransport() with webRtcServer succeeds and transport is closed', async () => { + const port1 = await pickPort({ + type: 'udp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + const port2 = await pickPort({ + type: 'tcp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + const webRtcServer = await ctx.worker!.createWebRtcServer({ + listenInfos: [ + { protocol: 'udp', ip: '127.0.0.1', port: port1 }, + { protocol: 'tcp', ip: '127.0.0.1', port: port2 }, + ], + }); + + const onObserverWebRtcTransportHandled = jest.fn(); + const onObserverWebRtcTransportUnhandled = jest.fn(); + + webRtcServer.observer.once( + 'webrtctransporthandled', + onObserverWebRtcTransportHandled + ); + webRtcServer.observer.once( + 'webrtctransportunhandled', + onObserverWebRtcTransportUnhandled + ); + + const router = await ctx.worker!.createRouter(); + + const onObserverNewTransport = jest.fn(); + + router.observer.once('newtransport', onObserverNewTransport); + + const transport = await router.createWebRtcTransport({ + webRtcServer, + // Let's disable UDP so resulting ICE candidates should only contain TCP. + enableUdp: false, + appData: { foo: 'bar' }, + }); + + await expect(router.dump()).resolves.toMatchObject({ + transportIds: [transport.id], + }); + + expect(onObserverWebRtcTransportHandled).toHaveBeenCalledTimes(1); + expect(onObserverWebRtcTransportHandled).toHaveBeenCalledWith(transport); + expect(onObserverNewTransport).toHaveBeenCalledTimes(1); + expect(onObserverNewTransport).toHaveBeenCalledWith(transport); + expect(typeof transport.id).toBe('string'); + expect(transport.closed).toBe(false); + expect(transport.appData).toEqual({ foo: 'bar' }); + + const iceCandidates = transport.iceCandidates; + + expect(iceCandidates.length).toBe(1); + expect(iceCandidates[0].ip).toBe('127.0.0.1'); + expect(iceCandidates[0].port).toBe(port2); + expect(iceCandidates[0].protocol).toBe('tcp'); + expect(iceCandidates[0].type).toBe('host'); + expect(iceCandidates[0].tcpType).toBe('passive'); + + expect(transport.iceState).toBe('new'); + expect(transport.iceSelectedTuple).toBeUndefined(); + + // API not exposed in the interface. + expect( + (webRtcServer as WebRtcServerImpl).webRtcTransportsForTesting.size + ).toBe(1); + // API not exposed in the interface. + expect((router as RouterImpl).transportsForTesting.size).toBe(1); + + await expect(webRtcServer.dump()).resolves.toMatchObject({ + id: webRtcServer.id, + udpSockets: [{ ip: '127.0.0.1', port: port1 }], + tcpServers: [{ ip: '127.0.0.1', port: port2 }], + webRtcTransportIds: [transport.id], + localIceUsernameFragments: [ + { /* localIceUsernameFragment: xxx, */ webRtcTransportId: transport.id }, + ], + tupleHashes: [], + }); + + transport.close(); + + expect(transport.closed).toBe(true); + expect(onObserverWebRtcTransportUnhandled).toHaveBeenCalledTimes(1); + expect(onObserverWebRtcTransportUnhandled).toHaveBeenCalledWith(transport); + // API not exposed in the interface. + expect( + (webRtcServer as WebRtcServerImpl).webRtcTransportsForTesting.size + ).toBe(0); + // API not exposed in the interface. + expect((router as RouterImpl).transportsForTesting.size).toBe(0); + + await expect(webRtcServer.dump()).resolves.toMatchObject({ + id: webRtcServer.id, + udpSockets: [{ ip: '127.0.0.1', port: port1 }], + tcpServers: [{ ip: '127.0.0.1', port: port2 }], + webRtcTransportIds: [], + localIceUsernameFragments: [], + tupleHashes: [], + }); +}, 2000); + +test('router.createWebRtcTransport() with webRtcServer succeeds and webRtcServer is closed', async () => { + const port1 = await pickPort({ + type: 'udp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + const port2 = await pickPort({ + type: 'tcp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + const webRtcServer = await ctx.worker!.createWebRtcServer({ + listenInfos: [ + { protocol: 'udp', ip: '127.0.0.1', port: port1 }, + { protocol: 'tcp', ip: '127.0.0.1', port: port2 }, + ], + }); + + const onObserverWebRtcTransportHandled = jest.fn(); + const onObserverWebRtcTransportUnhandled = jest.fn(); + + webRtcServer.observer.once( + 'webrtctransporthandled', + onObserverWebRtcTransportHandled + ); + webRtcServer.observer.once( + 'webrtctransportunhandled', + onObserverWebRtcTransportUnhandled + ); + + const router = await ctx.worker!.createRouter(); + const transport = await router.createWebRtcTransport({ + webRtcServer, + appData: { foo: 'bar' }, + }); + + expect(onObserverWebRtcTransportHandled).toHaveBeenCalledTimes(1); + expect(onObserverWebRtcTransportHandled).toHaveBeenCalledWith(transport); + + await expect(router.dump()).resolves.toMatchObject({ + transportIds: [transport.id], + }); + + expect(typeof transport.id).toBe('string'); + expect(transport.closed).toBe(false); + expect(transport.appData).toEqual({ foo: 'bar' }); + + const iceCandidates = transport.iceCandidates; + + expect(iceCandidates.length).toBe(2); + expect(iceCandidates[0].ip).toBe('127.0.0.1'); + expect(iceCandidates[0].port).toBe(port1); + expect(iceCandidates[0].protocol).toBe('udp'); + expect(iceCandidates[0].type).toBe('host'); + expect(iceCandidates[0].tcpType).toBeUndefined(); + expect(iceCandidates[1].ip).toBe('127.0.0.1'); + expect(iceCandidates[1].port).toBe(port2); + expect(iceCandidates[1].protocol).toBe('tcp'); + expect(iceCandidates[1].type).toBe('host'); + expect(iceCandidates[1].tcpType).toBe('passive'); + + expect(transport.iceState).toBe('new'); + expect(transport.iceSelectedTuple).toBeUndefined(); + + // API not exposed in the interface. + expect( + (webRtcServer as WebRtcServerImpl).webRtcTransportsForTesting.size + ).toBe(1); + // API not exposed in the interface. + expect((router as RouterImpl).transportsForTesting.size).toBe(1); + + await expect(webRtcServer.dump()).resolves.toMatchObject({ + id: webRtcServer.id, + udpSockets: [{ ip: '127.0.0.1', port: port1 }], + tcpServers: [{ ip: '127.0.0.1', port: port2 }], + webRtcTransportIds: [transport.id], + localIceUsernameFragments: [ + { /* localIceUsernameFragment: xxx, */ webRtcTransportId: transport.id }, + ], + tupleHashes: [], + }); + + // Let's restart ICE in the transport so it should add a new entry in + // localIceUsernameFragments in the WebRtcServer. + await transport.restartIce(); + + await expect(webRtcServer.dump()).resolves.toMatchObject({ + id: webRtcServer.id, + udpSockets: [{ ip: '127.0.0.1', port: port1 }], + tcpServers: [{ ip: '127.0.0.1', port: port2 }], + webRtcTransportIds: [transport.id], + localIceUsernameFragments: [ + { /* localIceUsernameFragment: xxx, */ webRtcTransportId: transport.id }, + { /* localIceUsernameFragment: yyy, */ webRtcTransportId: transport.id }, + ], + tupleHashes: [], + }); + + const onObserverClose = jest.fn(); + + webRtcServer.observer.once('close', onObserverClose); + + const onListenServerClose = jest.fn(); + + transport.once('listenserverclose', onListenServerClose); + + webRtcServer.close(); + + expect(webRtcServer.closed).toBe(true); + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(onListenServerClose).toHaveBeenCalledTimes(1); + expect(onObserverWebRtcTransportUnhandled).toHaveBeenCalledTimes(1); + expect(onObserverWebRtcTransportUnhandled).toHaveBeenCalledWith(transport); + expect(transport.closed).toBe(true); + expect(transport.iceState).toBe('closed'); + expect(transport.iceSelectedTuple).toBe(undefined); + expect(transport.dtlsState).toBe('closed'); + expect(transport.sctpState).toBe(undefined); + // API not exposed in the interface. + expect( + (webRtcServer as WebRtcServerImpl).webRtcTransportsForTesting.size + ).toBe(0); + // API not exposed in the interface. + expect((router as RouterImpl).transportsForTesting.size).toBe(0); + + await expect(ctx.worker!.dump()).resolves.toMatchObject({ + pid: ctx.worker!.pid, + webRtcServerIds: [], + routerIds: [router.id], + channelMessageHandlers: { + channelRequestHandlers: [router.id], + channelNotificationHandlers: [], + }, + }); + + await expect(router.dump()).resolves.toMatchObject({ + id: router.id, + transportIds: [], + }); +}, 2000); diff --git a/node/src/test/test-WebRtcTransport.ts b/node/src/test/test-WebRtcTransport.ts new file mode 100644 index 0000000000..a241bcf2f7 --- /dev/null +++ b/node/src/test/test-WebRtcTransport.ts @@ -0,0 +1,890 @@ +import { pickPort } from 'pick-port'; +import * as flatbuffers from 'flatbuffers'; +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { WorkerEvents, WebRtcTransportEvents } from '../types'; +import type { WebRtcTransportImpl } from '../WebRtcTransport'; +import type { TransportTuple } from '../TransportTypes'; +import { serializeProtocol } from '../Transport'; +import * as utils from '../utils'; +import { + Notification, + Body as NotificationBody, + Event, +} from '../fbs/notification'; +import * as FbsTransport from '../fbs/transport'; +import * as FbsWebRtcTransport from '../fbs/web-rtc-transport'; + +type TestContext = { + mediaCodecs: mediasoup.types.RtpCodecCapability[]; + worker?: mediasoup.types.Worker; + router?: mediasoup.types.Router; +}; + +const ctx: TestContext = { + mediaCodecs: utils.deepFreeze([ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + parameters: { + useinbandfec: 1, + foo: 'bar', + }, + }, + { + kind: 'video', + mimeType: 'video/VP8', + clockRate: 90000, + }, + { + kind: 'video', + mimeType: 'video/H264', + clockRate: 90000, + parameters: { + 'level-asymmetry-allowed': 1, + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + foo: 'bar', + }, + }, + ]), +}; + +beforeEach(async () => { + ctx.worker = await mediasoup.createWorker(); + ctx.router = await ctx.worker.createRouter({ mediaCodecs: ctx.mediaCodecs }); +}); + +afterEach(async () => { + ctx.worker?.close(); + + if (ctx.worker?.subprocessClosed === false) { + await enhancedOnce(ctx.worker, 'subprocessclose'); + } +}); + +test('router.createWebRtcTransport() succeeds', async () => { + const onObserverNewTransport = jest.fn(); + + ctx.router!.observer.once('newtransport', onObserverNewTransport); + + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { + protocol: 'udp', + ip: '127.0.0.1', + announcedAddress: '9.9.9.1', + portRange: { min: 2000, max: 3000 }, + }, + { + protocol: 'tcp', + ip: '127.0.0.1', + announcedAddress: '9.9.9.1', + portRange: { min: 2000, max: 3000 }, + }, + { + protocol: 'udp', + ip: '0.0.0.0', + announcedAddress: 'foo1.bar.org', + portRange: { min: 2000, max: 3000 }, + }, + { + protocol: 'tcp', + ip: '0.0.0.0', + announcedAddress: 'foo2.bar.org', + portRange: { min: 2000, max: 3000 }, + }, + { + protocol: 'udp', + ip: '127.0.0.1', + announcedAddress: undefined, + portRange: { min: 2000, max: 3000 }, + }, + { + protocol: 'tcp', + ip: '127.0.0.1', + announcedAddress: undefined, + portRange: { min: 2000, max: 3000 }, + }, + ], + enableTcp: true, + preferUdp: true, + enableSctp: true, + numSctpStreams: { OS: 2048, MIS: 2048 }, + maxSctpMessageSize: 1000000, + appData: { foo: 'bar' }, + }); + + await expect(ctx.router!.dump()).resolves.toMatchObject({ + transportIds: [webRtcTransport.id], + }); + + expect(onObserverNewTransport).toHaveBeenCalledTimes(1); + expect(onObserverNewTransport).toHaveBeenCalledWith(webRtcTransport); + expect(typeof webRtcTransport.id).toBe('string'); + expect(webRtcTransport.closed).toBe(false); + expect(webRtcTransport.type).toBe('webrtc'); + expect(webRtcTransport.appData).toEqual({ foo: 'bar' }); + expect(webRtcTransport.iceRole).toBe('controlled'); + expect(typeof webRtcTransport.iceParameters).toBe('object'); + expect(webRtcTransport.iceParameters.iceLite).toBe(true); + expect(typeof webRtcTransport.iceParameters.usernameFragment).toBe('string'); + expect(typeof webRtcTransport.iceParameters.password).toBe('string'); + expect(webRtcTransport.sctpParameters).toMatchObject({ + port: 5000, + OS: 2048, + MIS: 2048, + maxMessageSize: 1000000, + }); + expect(Array.isArray(webRtcTransport.iceCandidates)).toBe(true); + expect(webRtcTransport.iceCandidates.length).toBe(6); + + const iceCandidates = webRtcTransport.iceCandidates; + + expect(iceCandidates[0].ip).toBe('9.9.9.1'); + expect(iceCandidates[0].protocol).toBe('udp'); + expect(iceCandidates[0].type).toBe('host'); + expect(iceCandidates[0].tcpType).toBeUndefined(); + expect(iceCandidates[1].ip).toBe('9.9.9.1'); + expect(iceCandidates[1].protocol).toBe('tcp'); + expect(iceCandidates[1].type).toBe('host'); + expect(iceCandidates[1].tcpType).toBe('passive'); + expect(iceCandidates[2].ip).toBe('foo1.bar.org'); + expect(iceCandidates[2].protocol).toBe('udp'); + expect(iceCandidates[2].type).toBe('host'); + expect(iceCandidates[2].tcpType).toBeUndefined(); + expect(iceCandidates[3].ip).toBe('foo2.bar.org'); + expect(iceCandidates[3].protocol).toBe('tcp'); + expect(iceCandidates[3].type).toBe('host'); + expect(iceCandidates[3].tcpType).toBe('passive'); + expect(iceCandidates[4].ip).toBe('127.0.0.1'); + expect(iceCandidates[4].protocol).toBe('udp'); + expect(iceCandidates[4].type).toBe('host'); + expect(iceCandidates[4].tcpType).toBeUndefined(); + expect(iceCandidates[5].ip).toBe('127.0.0.1'); + expect(iceCandidates[5].protocol).toBe('tcp'); + expect(iceCandidates[5].type).toBe('host'); + expect(iceCandidates[5].tcpType).toBe('passive'); + expect(iceCandidates[0].priority).toBeGreaterThan(iceCandidates[1].priority); + expect(iceCandidates[1].priority).toBeGreaterThan(iceCandidates[2].priority); + expect(iceCandidates[2].priority).toBeGreaterThan(iceCandidates[3].priority); + expect(iceCandidates[3].priority).toBeGreaterThan(iceCandidates[4].priority); + expect(iceCandidates[4].priority).toBeGreaterThan(iceCandidates[5].priority); + + expect(webRtcTransport.iceState).toBe('new'); + expect(webRtcTransport.iceSelectedTuple).toBeUndefined(); + expect(typeof webRtcTransport.dtlsParameters).toBe('object'); + expect(Array.isArray(webRtcTransport.dtlsParameters.fingerprints)).toBe(true); + expect(webRtcTransport.dtlsParameters.role).toBe('auto'); + expect(webRtcTransport.dtlsState).toBe('new'); + expect(webRtcTransport.dtlsRemoteCert).toBeUndefined(); + expect(webRtcTransport.sctpState).toBe('new'); + + const dump = await webRtcTransport.dump(); + + expect(dump.id).toBe(webRtcTransport.id); + expect(dump.producerIds).toEqual([]); + expect(dump.consumerIds).toEqual([]); + expect(dump.iceRole).toBe(webRtcTransport.iceRole); + expect(dump.iceParameters).toEqual(webRtcTransport.iceParameters); + expect(dump.iceCandidates).toEqual(webRtcTransport.iceCandidates); + expect(dump.iceState).toBe(webRtcTransport.iceState); + expect(dump.iceSelectedTuple).toEqual(webRtcTransport.iceSelectedTuple); + expect(dump.dtlsParameters).toEqual(webRtcTransport.dtlsParameters); + expect(dump.dtlsState).toBe(webRtcTransport.dtlsState); + expect(dump.sctpParameters).toEqual(webRtcTransport.sctpParameters); + expect(dump.sctpState).toBe(webRtcTransport.sctpState); + expect(dump.recvRtpHeaderExtensions).toBeDefined(); + expect(typeof dump.rtpListener).toBe('object'); + + webRtcTransport.close(); + + expect(webRtcTransport.closed).toBe(true); +}, 2000); + +test('router.createWebRtcTransport() with deprecated listenIps succeeds', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenIps: [{ ip: '127.0.0.1', announcedIp: undefined }], + enableUdp: true, + enableTcp: true, + preferUdp: false, + initialAvailableOutgoingBitrate: 1000000, + }); + + expect(Array.isArray(webRtcTransport.iceCandidates)).toBe(true); + expect(webRtcTransport.iceCandidates.length).toBe(2); + + const iceCandidates = webRtcTransport.iceCandidates; + + expect(iceCandidates[0].ip).toBe('127.0.0.1'); + expect(iceCandidates[0].protocol).toBe('udp'); + expect(iceCandidates[0].type).toBe('host'); + expect(iceCandidates[0].tcpType).toBeUndefined(); + expect(iceCandidates[1].ip).toBe('127.0.0.1'); + expect(iceCandidates[1].protocol).toBe('tcp'); + expect(iceCandidates[1].type).toBe('host'); + expect(iceCandidates[1].tcpType).toBe('passive'); + expect(iceCandidates[0].priority).toBeGreaterThan(iceCandidates[1].priority); +}, 2000); + +test('router.createWebRtcTransport() with fixed port succeeds', async () => { + const port = await pickPort({ + type: 'tcp', + ip: '127.0.0.1', + reserveTimeout: 0, + }); + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + // NOTE: udpReusePort flag will be ignored since protocol is TCP. + { protocol: 'tcp', ip: '127.0.0.1', port, flags: { udpReusePort: true } }, + ], + }); + + expect(webRtcTransport.iceCandidates[0].port).toEqual(port); +}, 2000); + +test('router.createWebRtcTransport() with portRange succeeds', async () => { + const portRange = { min: 11111, max: 11112 }; + + const webRtcTransport1 = await ctx.router!.createWebRtcTransport({ + listenInfos: [{ protocol: 'udp', ip: '127.0.0.1', portRange }], + }); + + const iceCandidate1 = webRtcTransport1.iceCandidates[0]; + + expect(iceCandidate1.ip).toBe('127.0.0.1'); + expect( + iceCandidate1.port >= portRange.min && iceCandidate1.port <= portRange.max + ).toBe(true); + expect(iceCandidate1.protocol).toBe('udp'); + + const webRtcTransport2 = await ctx.router!.createWebRtcTransport({ + listenInfos: [{ protocol: 'udp', ip: '127.0.0.1', portRange }], + }); + + const iceCandidate2 = webRtcTransport2.iceCandidates[0]; + + expect(iceCandidate2.ip).toBe('127.0.0.1'); + expect( + iceCandidate1.port >= portRange.min && iceCandidate1.port <= portRange.max + ).toBe(true); + expect(iceCandidate2.protocol).toBe('udp'); + + // No more available ports so it must fail. + await expect( + ctx.router!.createWebRtcTransport({ + listenInfos: [{ protocol: 'udp', ip: '127.0.0.1', portRange }], + }) + ).rejects.toThrow(Error); +}, 2000); + +test('router.createWebRtcTransport() with wrong arguments rejects with TypeError', async () => { + // @ts-expect-error --- Testing purposes. + await expect(ctx.router!.createWebRtcTransport({})).rejects.toThrow( + TypeError + ); + + await expect( + ctx.router!.createWebRtcTransport({ + listenInfos: [ + { + protocol: 'udp', + ip: '127.0.0.1', + portRange: { min: 4000, max: 3000 }, + }, + ], + }) + ).rejects.toThrow(TypeError); + + await expect( + // @ts-expect-error --- Testing purposes. + ctx.router!.createWebRtcTransport({ listenIps: [123] }) + ).rejects.toThrow(TypeError); + + await expect( + // @ts-expect-error --- Testing purposes. + ctx.router!.createWebRtcTransport({ listenInfos: '127.0.0.1' }) + ).rejects.toThrow(TypeError); + + await expect( + // @ts-expect-error --- Testing purposes. + ctx.router!.createWebRtcTransport({ listenIps: '127.0.0.1' }) + ).rejects.toThrow(TypeError); + + await expect( + ctx.router!.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + // @ts-expect-error --- Testing purposes. + appData: 'NOT-AN-OBJECT', + }) + ).rejects.toThrow(TypeError); + + await expect( + ctx.router!.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + enableSctp: true, + // @ts-expect-error --- Testing purposes. + numSctpStreams: 'foo', + }) + ).rejects.toThrow(TypeError); +}, 2000); + +test('router.createWebRtcTransport() with non bindable IP rejects with Error', async () => { + await expect( + ctx.router!.createWebRtcTransport({ + listenInfos: [ + { protocol: 'udp', ip: '8.8.8.8', portRange: { min: 2000, max: 3000 } }, + ], + }) + ).rejects.toThrow(Error); +}, 2000); + +test('webRtcTransport.getStats() succeeds', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { + protocol: 'udp', + ip: '127.0.0.1', + announcedAddress: '9.9.9.1', + portRange: { min: 2000, max: 3000 }, + }, + ], + }); + + const stats = await webRtcTransport.getStats(); + + expect(Array.isArray(stats)).toBe(true); + expect(stats.length).toBe(1); + expect(stats[0].type).toBe('webrtc-transport'); + expect(stats[0].transportId).toBe(webRtcTransport.id); + expect(typeof stats[0].timestamp).toBe('number'); + expect(stats[0].iceRole).toBe('controlled'); + expect(stats[0].iceState).toBe('new'); + expect(stats[0].dtlsState).toBe('new'); + expect(stats[0].sctpState).toBeUndefined(); + expect(stats[0].bytesReceived).toBe(0); + expect(stats[0].recvBitrate).toBe(0); + expect(stats[0].bytesSent).toBe(0); + expect(stats[0].sendBitrate).toBe(0); + expect(stats[0].rtpBytesReceived).toBe(0); + expect(stats[0].rtpRecvBitrate).toBe(0); + expect(stats[0].rtpBytesSent).toBe(0); + expect(stats[0].rtpSendBitrate).toBe(0); + expect(stats[0].rtxBytesReceived).toBe(0); + expect(stats[0].rtxRecvBitrate).toBe(0); + expect(stats[0].rtxBytesSent).toBe(0); + expect(stats[0].rtxSendBitrate).toBe(0); + expect(stats[0].probationBytesSent).toBe(0); + expect(stats[0].probationSendBitrate).toBe(0); + expect(stats[0].iceSelectedTuple).toBeUndefined(); + expect(stats[0].maxIncomingBitrate).toBeUndefined(); +}, 2000); + +test('webRtcTransport.connect() succeeds', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { + protocol: 'udp', + ip: '127.0.0.1', + announcedAddress: '9.9.9.1', + portRange: { min: 2000, max: 3000 }, + }, + ], + }); + + const dtlsRemoteParameters: mediasoup.types.DtlsParameters = { + fingerprints: [ + { + algorithm: 'sha-256', + value: + '82:5A:68:3D:36:C3:0A:DE:AF:E7:32:43:D2:88:83:57:AC:2D:65:E5:80:C4:B6:FB:AF:1A:A0:21:9F:6D:0C:AD', + }, + ], + role: 'client', + }; + + await expect( + webRtcTransport.connect({ + dtlsParameters: dtlsRemoteParameters, + }) + ).resolves.toBeUndefined(); + + // Must fail if connected. + await expect( + webRtcTransport.connect({ + dtlsParameters: dtlsRemoteParameters, + }) + ).rejects.toThrow(Error); + + expect(webRtcTransport.dtlsParameters.role).toBe('server'); +}, 2000); + +test('webRtcTransport.connect() with wrong arguments rejects with TypeError', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { + protocol: 'udp', + ip: '127.0.0.1', + announcedAddress: '9.9.9.1', + portRange: { min: 2000, max: 3000 }, + }, + ], + }); + + let dtlsRemoteParameters: mediasoup.types.DtlsParameters; + + // @ts-expect-error --- Testing purposes. + await expect(webRtcTransport.connect({})).rejects.toThrow(TypeError); + + dtlsRemoteParameters = { + fingerprints: [ + { + // @ts-expect-error --- Testing purposes.. + algorithm: 'sha-256000', + value: + '82:5A:68:3D:36:C3:0A:DE:AF:E7:32:43:D2:88:83:57:AC:2D:65:E5:80:C4:B6:FB:AF:1A:A0:21:9F:6D:0C:AD', + }, + ], + role: 'client', + }; + + await expect( + webRtcTransport.connect({ dtlsParameters: dtlsRemoteParameters }) + ).rejects.toThrow(TypeError); + + dtlsRemoteParameters = { + fingerprints: [ + { + algorithm: 'sha-256', + value: + '82:5A:68:3D:36:C3:0A:DE:AF:E7:32:43:D2:88:83:57:AC:2D:65:E5:80:C4:B6:FB:AF:1A:A0:21:9F:6D:0C:AD', + }, + ], + // @ts-expect-error --- Testing purposes. + role: 'chicken', + }; + + await expect( + webRtcTransport.connect({ dtlsParameters: dtlsRemoteParameters }) + ).rejects.toThrow(TypeError); + + dtlsRemoteParameters = { + fingerprints: [], + role: 'client', + }; + + await expect( + webRtcTransport.connect({ dtlsParameters: dtlsRemoteParameters }) + ).rejects.toThrow(TypeError); + + await expect( + webRtcTransport.connect({ dtlsParameters: dtlsRemoteParameters }) + ).rejects.toThrow(TypeError); + + expect(webRtcTransport.dtlsParameters.role).toBe('auto'); +}, 2000); + +test('webRtcTransport.setMaxIncomingBitrate() succeeds', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { + protocol: 'udp', + ip: '127.0.0.1', + announcedAddress: '9.9.9.1', + portRange: { min: 2000, max: 3000 }, + }, + ], + }); + + await expect( + webRtcTransport.setMaxIncomingBitrate(1000000) + ).resolves.toBeUndefined(); + + // Remove limit. + await expect( + webRtcTransport.setMaxIncomingBitrate(0) + ).resolves.toBeUndefined(); +}, 2000); + +test('webRtcTransport.setMaxOutgoingBitrate() succeeds', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { protocol: 'udp', ip: '127.0.0.1', portRange: { min: 2000, max: 3000 } }, + ], + }); + + await expect( + webRtcTransport.setMaxOutgoingBitrate(2000000) + ).resolves.toBeUndefined(); + + // Remove limit. + await expect( + webRtcTransport.setMaxOutgoingBitrate(0) + ).resolves.toBeUndefined(); +}, 2000); + +test('webRtcTransport.setMinOutgoingBitrate() succeeds', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { protocol: 'udp', ip: '127.0.0.1', portRange: { min: 2000, max: 3000 } }, + ], + }); + + await expect( + webRtcTransport.setMinOutgoingBitrate(100000) + ).resolves.toBeUndefined(); + + // Remove limit. + await expect( + webRtcTransport.setMinOutgoingBitrate(0) + ).resolves.toBeUndefined(); +}, 2000); + +test('webRtcTransport.setMaxOutgoingBitrate() fails if value is lower than current min limit', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { protocol: 'udp', ip: '127.0.0.1', portRange: { min: 2000, max: 3000 } }, + ], + }); + + await expect( + webRtcTransport.setMinOutgoingBitrate(3000000) + ).resolves.toBeUndefined(); + + await expect(webRtcTransport.setMaxOutgoingBitrate(2000000)).rejects.toThrow( + Error + ); + + // Remove limit. + await expect( + webRtcTransport.setMinOutgoingBitrate(0) + ).resolves.toBeUndefined(); +}, 2000); + +test('webRtcTransport.setMinOutgoingBitrate() fails if value is higher than current max limit', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { protocol: 'udp', ip: '127.0.0.1', portRange: { min: 2000, max: 3000 } }, + ], + }); + + await expect( + webRtcTransport.setMaxOutgoingBitrate(2000000) + ).resolves.toBeUndefined(); + + await expect(webRtcTransport.setMinOutgoingBitrate(3000000)).rejects.toThrow( + Error + ); + + // Remove limit. + await expect( + webRtcTransport.setMaxOutgoingBitrate(0) + ).resolves.toBeUndefined(); +}, 2000); + +test('webRtcTransport.restartIce() succeeds', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { protocol: 'udp', ip: '127.0.0.1', portRange: { min: 2000, max: 3000 } }, + ], + }); + + const previousIceUsernameFragment = + webRtcTransport.iceParameters.usernameFragment; + const previousIcePassword = webRtcTransport.iceParameters.password; + + await expect(webRtcTransport.restartIce()).resolves.toMatchObject({ + usernameFragment: expect.any(String), + password: expect.any(String), + iceLite: true, + }); + + expect(typeof webRtcTransport.iceParameters.usernameFragment).toBe('string'); + expect(typeof webRtcTransport.iceParameters.password).toBe('string'); + expect(webRtcTransport.iceParameters.usernameFragment).not.toBe( + previousIceUsernameFragment + ); + expect(webRtcTransport.iceParameters.password).not.toBe(previousIcePassword); +}, 2000); + +test('transport.enableTraceEvent() succeed', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { protocol: 'udp', ip: '127.0.0.1', portRange: { min: 2000, max: 3000 } }, + ], + }); + + // @ts-expect-error --- Testing purposes. + await webRtcTransport.enableTraceEvent(['foo', 'probation']); + await expect(webRtcTransport.dump()).resolves.toMatchObject({ + traceEventTypes: ['probation'], + }); + + await webRtcTransport.enableTraceEvent(); + await expect(webRtcTransport.dump()).resolves.toMatchObject({ + traceEventTypes: [], + }); + + // @ts-expect-error --- Testing purposes. + await webRtcTransport.enableTraceEvent(['probation', 'FOO', 'bwe', 'BAR']); + await expect(webRtcTransport.dump()).resolves.toMatchObject({ + traceEventTypes: ['probation', 'bwe'], + }); + + await webRtcTransport.enableTraceEvent(); + await expect(webRtcTransport.dump()).resolves.toMatchObject({ + traceEventTypes: [], + }); +}, 2000); + +test('transport.enableTraceEvent() with wrong arguments rejects with TypeError', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { protocol: 'udp', ip: '127.0.0.1', portRange: { min: 2000, max: 3000 } }, + ], + }); + + // @ts-expect-error --- Testing purposes. + await expect(webRtcTransport.enableTraceEvent(123)).rejects.toThrow( + TypeError + ); + + // @ts-expect-error --- Testing purposes. + await expect(webRtcTransport.enableTraceEvent('probation')).rejects.toThrow( + TypeError + ); + + await expect( + // @ts-expect-error --- Testing purposes. + webRtcTransport.enableTraceEvent(['probation', 123.123]) + ).rejects.toThrow(TypeError); +}, 2000); + +test('WebRtcTransport events succeed', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { protocol: 'udp', ip: '127.0.0.1', portRange: { min: 2000, max: 3000 } }, + ], + }); + + // API not exposed in the interface. + const channel = (webRtcTransport as WebRtcTransportImpl).channelForTesting; + const onIceStateChange = jest.fn(); + + webRtcTransport.on('icestatechange', onIceStateChange); + + // Simulate a 'iceselectedtuplechange' notification coming through the + // channel. + const builder = new flatbuffers.Builder(); + const iceStateChangeNotification = + new FbsWebRtcTransport.IceStateChangeNotificationT( + FbsWebRtcTransport.IceState.COMPLETED + ); + + let notificationOffset = Notification.createNotification( + builder, + builder.createString(webRtcTransport.id), + Event.WEBRTCTRANSPORT_ICE_STATE_CHANGE, + NotificationBody.WebRtcTransport_IceStateChangeNotification, + iceStateChangeNotification.pack(builder) + ); + + builder.finish(notificationOffset); + + let notification = Notification.getRootAsNotification( + new flatbuffers.ByteBuffer(builder.asUint8Array()) + ); + + channel.emit( + webRtcTransport.id, + Event.WEBRTCTRANSPORT_ICE_STATE_CHANGE, + notification + ); + + expect(onIceStateChange).toHaveBeenCalledTimes(1); + expect(onIceStateChange).toHaveBeenCalledWith('completed'); + expect(webRtcTransport.iceState).toBe('completed'); + + builder.clear(); + + const onIceSelectedTuple = jest.fn(); + const iceSelectedTuple: TransportTuple = { + // @deprecated Use localAddress. + localIp: '1.1.1.1', + localAddress: '1.1.1.1', + localPort: 1111, + remoteIp: '2.2.2.2', + remotePort: 2222, + protocol: 'udp', + }; + + webRtcTransport.on('iceselectedtuplechange', onIceSelectedTuple); + + // Simulate a 'icestatechange' notification coming through the channel. + const iceSelectedTupleChangeNotification = + new FbsWebRtcTransport.IceSelectedTupleChangeNotificationT( + new FbsTransport.TupleT( + iceSelectedTuple.localAddress, + iceSelectedTuple.localPort, + iceSelectedTuple.remoteIp, + iceSelectedTuple.remotePort, + serializeProtocol(iceSelectedTuple.protocol) + ) + ); + + notificationOffset = Notification.createNotification( + builder, + builder.createString(webRtcTransport.id), + Event.WEBRTCTRANSPORT_ICE_SELECTED_TUPLE_CHANGE, + NotificationBody.WebRtcTransport_IceSelectedTupleChangeNotification, + iceSelectedTupleChangeNotification.pack(builder) + ); + + builder.finish(notificationOffset); + + notification = Notification.getRootAsNotification( + new flatbuffers.ByteBuffer(builder.asUint8Array()) + ); + + channel.emit( + webRtcTransport.id, + Event.WEBRTCTRANSPORT_ICE_SELECTED_TUPLE_CHANGE, + notification + ); + + expect(onIceSelectedTuple).toHaveBeenCalledTimes(1); + expect(onIceSelectedTuple).toHaveBeenCalledWith(iceSelectedTuple); + expect(webRtcTransport.iceSelectedTuple).toEqual(iceSelectedTuple); + + builder.clear(); + + const onDtlsStateChange = jest.fn(); + + webRtcTransport.on('dtlsstatechange', onDtlsStateChange); + + // Simulate a 'dtlsstatechange' notification coming through the channel. + const dtlsStateChangeNotification = + new FbsWebRtcTransport.DtlsStateChangeNotificationT( + FbsWebRtcTransport.DtlsState.CONNECTING + ); + + notificationOffset = Notification.createNotification( + builder, + builder.createString(webRtcTransport.id), + Event.WEBRTCTRANSPORT_DTLS_STATE_CHANGE, + NotificationBody.WebRtcTransport_DtlsStateChangeNotification, + dtlsStateChangeNotification.pack(builder) + ); + + builder.finish(notificationOffset); + + notification = Notification.getRootAsNotification( + new flatbuffers.ByteBuffer(builder.asUint8Array()) + ); + + channel.emit( + webRtcTransport.id, + Event.WEBRTCTRANSPORT_DTLS_STATE_CHANGE, + notification + ); + + expect(onDtlsStateChange).toHaveBeenCalledTimes(1); + expect(onDtlsStateChange).toHaveBeenCalledWith('connecting'); + expect(webRtcTransport.dtlsState).toBe('connecting'); +}, 2000); + +test('WebRtcTransport methods reject if closed', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { protocol: 'udp', ip: '127.0.0.1', portRange: { min: 2000, max: 3000 } }, + ], + }); + + const onObserverClose = jest.fn(); + + webRtcTransport.observer.once('close', onObserverClose); + webRtcTransport.close(); + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(webRtcTransport.closed).toBe(true); + expect(webRtcTransport.iceState).toBe('closed'); + expect(webRtcTransport.iceSelectedTuple).toBeUndefined(); + expect(webRtcTransport.dtlsState).toBe('closed'); + expect(webRtcTransport.sctpState).toBeUndefined(); + + await expect(webRtcTransport.dump()).rejects.toThrow(Error); + + await expect(webRtcTransport.getStats()).rejects.toThrow(Error); + + // @ts-expect-error --- Testing purposes. + await expect(webRtcTransport.connect({})).rejects.toThrow(Error); + + await expect(webRtcTransport.setMaxIncomingBitrate(200000)).rejects.toThrow( + Error + ); + + await expect(webRtcTransport.setMaxOutgoingBitrate(200000)).rejects.toThrow( + Error + ); + + await expect(webRtcTransport.setMinOutgoingBitrate(100000)).rejects.toThrow( + Error + ); + + await expect(webRtcTransport.restartIce()).rejects.toThrow(Error); +}, 2000); + +test('WebRtcTransport emits "routerclose" if Router is closed', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenIps: ['127.0.0.1'], + enableSctp: true, + }); + + const onObserverClose = jest.fn(); + + webRtcTransport.observer.once('close', onObserverClose); + + const promise = enhancedOnce( + webRtcTransport, + 'routerclose' + ); + + ctx.router!.close(); + await promise; + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(webRtcTransport.closed).toBe(true); + expect(webRtcTransport.iceState).toBe('closed'); + expect(webRtcTransport.iceSelectedTuple).toBeUndefined(); + expect(webRtcTransport.dtlsState).toBe('closed'); + expect(webRtcTransport.sctpState).toBe('closed'); +}, 2000); + +test('WebRtcTransport emits "routerclose" if Worker is closed', async () => { + const webRtcTransport = await ctx.router!.createWebRtcTransport({ + listenInfos: [ + { protocol: 'udp', ip: '127.0.0.1', portRange: { min: 2000, max: 3000 } }, + ], + }); + + const onObserverClose = jest.fn(); + + webRtcTransport.observer.once('close', onObserverClose); + + const promise = enhancedOnce( + webRtcTransport, + 'routerclose' + ); + + ctx.worker!.close(); + await promise; + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(webRtcTransport.closed).toBe(true); + expect(webRtcTransport.iceState).toBe('closed'); + expect(webRtcTransport.iceSelectedTuple).toBeUndefined(); + expect(webRtcTransport.dtlsState).toBe('closed'); + expect(webRtcTransport.sctpState).toBeUndefined(); +}, 2000); diff --git a/node/src/test/test-Worker.ts b/node/src/test/test-Worker.ts new file mode 100644 index 0000000000..ecf6210059 --- /dev/null +++ b/node/src/test/test-Worker.ts @@ -0,0 +1,330 @@ +import * as os from 'node:os'; +import * as process from 'node:process'; +import * as path from 'node:path'; +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { WorkerEvents } from '../types'; +import { InvalidStateError } from '../errors'; + +test('Worker.workerBin matches mediasoup-worker absolute path', () => { + const workerBin = process.env.MEDIASOUP_WORKER_BIN + ? process.env.MEDIASOUP_WORKER_BIN + : process.env.MEDIASOUP_BUILDTYPE === 'Debug' + ? path.join( + __dirname, + '..', + '..', + '..', + 'worker', + 'out', + 'Debug', + 'mediasoup-worker' + ) + : path.join( + __dirname, + '..', + '..', + '..', + 'worker', + 'out', + 'Release', + 'mediasoup-worker' + ); + + expect(mediasoup.workerBin).toBe(workerBin); +}); + +test('createWorker() succeeds', async () => { + const onObserverNewWorker = jest.fn(); + + mediasoup.observer.once('newworker', onObserverNewWorker); + + const worker1 = await mediasoup.createWorker(); + + expect(onObserverNewWorker).toHaveBeenCalledTimes(1); + expect(onObserverNewWorker).toHaveBeenCalledWith(worker1); + expect(worker1.constructor.name).toBe('WorkerImpl'); + expect(typeof worker1.pid).toBe('number'); + expect(worker1.closed).toBe(false); + expect(worker1.died).toBe(false); + + worker1.close(); + + await enhancedOnce(worker1, 'subprocessclose'); + + expect(worker1.closed).toBe(true); + expect(worker1.died).toBe(false); + + const worker2 = await mediasoup.createWorker<{ foo: number; bar?: string }>({ + logLevel: 'debug', + logTags: ['info'], + rtcMinPort: 0, + rtcMaxPort: 9999, + dtlsCertificateFile: path.join(__dirname, 'data', 'dtls-cert.pem'), + dtlsPrivateKeyFile: path.join(__dirname, 'data', 'dtls-key.pem'), + libwebrtcFieldTrials: 'WebRTC-Bwe-AlrLimitedBackoff/Disabled/', + disableLiburing: true, + appData: { foo: 456 }, + }); + + expect(worker2.constructor.name).toBe('WorkerImpl'); + expect(typeof worker2.pid).toBe('number'); + expect(worker2.closed).toBe(false); + expect(worker2.died).toBe(false); + expect(worker2.appData).toEqual({ foo: 456 }); + + worker2.close(); + + await enhancedOnce(worker2, 'subprocessclose'); + + expect(worker2.closed).toBe(true); + expect(worker2.died).toBe(false); +}, 2000); + +test('createWorker() with wrong settings rejects with TypeError', async () => { + // @ts-expect-error --- Testing purposes. + await expect(mediasoup.createWorker({ logLevel: 'chicken' })).rejects.toThrow( + TypeError + ); + + await expect( + mediasoup.createWorker({ rtcMinPort: 1000, rtcMaxPort: 999 }) + ).rejects.toThrow(TypeError); + + // Port is from 0 to 65535. + await expect( + mediasoup.createWorker({ rtcMinPort: 1000, rtcMaxPort: 65536 }) + ).rejects.toThrow(TypeError); + + await expect( + mediasoup.createWorker({ dtlsCertificateFile: '/notfound/cert.pem' }) + ).rejects.toThrow(TypeError); + + await expect( + mediasoup.createWorker({ dtlsPrivateKeyFile: '/notfound/priv.pem' }) + ).rejects.toThrow(TypeError); + + await expect( + // @ts-expect-error --- Testing purposes. + mediasoup.createWorker({ appData: 'NOT-AN-OBJECT' }) + ).rejects.toThrow(TypeError); +}, 2000); + +test('worker.updateSettings() succeeds', async () => { + const worker = await mediasoup.createWorker(); + + await expect( + worker.updateSettings({ logLevel: 'debug', logTags: ['ice'] }) + ).resolves.toBeUndefined(); + + worker.close(); + + await enhancedOnce(worker, 'subprocessclose'); +}, 2000); + +test('worker.updateSettings() with wrong settings rejects with TypeError', async () => { + const worker = await mediasoup.createWorker(); + + // @ts-expect-error --- Testing purposes. + await expect(worker.updateSettings({ logLevel: 'chicken' })).rejects.toThrow( + TypeError + ); + + worker.close(); + + await enhancedOnce(worker, 'subprocessclose'); +}, 2000); + +test('worker.updateSettings() rejects with InvalidStateError if closed', async () => { + const worker = await mediasoup.createWorker(); + + worker.close(); + + await enhancedOnce(worker, 'subprocessclose'); + + await expect(worker.updateSettings({ logLevel: 'error' })).rejects.toThrow( + InvalidStateError + ); +}, 2000); + +test('worker.dump() succeeds', async () => { + const worker = await mediasoup.createWorker(); + + await expect(worker.dump()).resolves.toMatchObject({ + pid: worker.pid, + webRtcServerIds: [], + routerIds: [], + channelMessageHandlers: { + channelRequestHandlers: [], + channelNotificationHandlers: [], + }, + }); + + worker.close(); +}, 2000); + +test('worker.dump() rejects with InvalidStateError if closed', async () => { + const worker = await mediasoup.createWorker(); + + worker.close(); + + await enhancedOnce(worker, 'subprocessclose'); + + await expect(worker.dump()).rejects.toThrow(InvalidStateError); +}, 2000); + +test('worker.getResourceUsage() succeeds', async () => { + const worker = await mediasoup.createWorker(); + + await expect(worker.getResourceUsage()).resolves.toMatchObject({}); + + worker.close(); + + await enhancedOnce(worker, 'subprocessclose'); +}, 2000); + +test('worker.close() succeeds', async () => { + const worker = await mediasoup.createWorker({ logLevel: 'warn' }); + const onObserverClose = jest.fn(); + + worker.observer.once('close', onObserverClose); + worker.close(); + + await enhancedOnce(worker, 'subprocessclose'); + + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(worker.closed).toBe(true); + expect(worker.died).toBe(false); +}, 2000); + +test('Worker emits "died" if worker process died unexpectedly', async () => { + let onDied: ReturnType; + let onObserverClose: ReturnType; + + const worker1 = await mediasoup.createWorker({ logLevel: 'warn' }); + + onDied = jest.fn(); + onObserverClose = jest.fn(); + + worker1.observer.once('close', onObserverClose); + + await new Promise((resolve, reject) => { + worker1.on('died', () => { + onDied(); + + if (onObserverClose.mock.calls.length > 0) { + reject( + new Error('observer "close" event emitted before worker "died" event') + ); + } else if (worker1.closed) { + resolve(); + } else { + reject(new Error('worker.closed is false')); + } + }); + + process.kill(worker1.pid, 'SIGINT'); + }); + + if (!worker1.subprocessClosed) { + await enhancedOnce(worker1, 'subprocessclose'); + } + + expect(onDied).toHaveBeenCalledTimes(1); + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(worker1.closed).toBe(true); + expect(worker1.died).toBe(true); + + const worker2 = await mediasoup.createWorker({ logLevel: 'warn' }); + + onDied = jest.fn(); + onObserverClose = jest.fn(); + + worker2.observer.once('close', onObserverClose); + + await new Promise((resolve, reject) => { + worker2.on('died', () => { + onDied(); + + if (onObserverClose.mock.calls.length > 0) { + reject( + new Error('observer "close" event emitted before worker "died" event') + ); + } else if (worker2.closed) { + resolve(); + } else { + reject(new Error('worker.closed is false')); + } + }); + + process.kill(worker2.pid, 'SIGTERM'); + }); + + if (!worker2.subprocessClosed) { + await enhancedOnce(worker2, 'subprocessclose'); + } + + expect(onDied).toHaveBeenCalledTimes(1); + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(worker2.closed).toBe(true); + expect(worker2.died).toBe(true); + + const worker3 = await mediasoup.createWorker({ logLevel: 'warn' }); + + onDied = jest.fn(); + onObserverClose = jest.fn(); + + worker3.observer.once('close', onObserverClose); + + await new Promise((resolve, reject) => { + worker3.on('died', () => { + onDied(); + + if (onObserverClose.mock.calls.length > 0) { + reject( + new Error('observer "close" event emitted before worker "died" event') + ); + } else if (worker3.closed) { + resolve(); + } else { + reject(new Error('worker.closed is false')); + } + }); + + process.kill(worker3.pid, 'SIGKILL'); + }); + + if (!worker3.subprocessClosed) { + await enhancedOnce(worker3, 'subprocessclose'); + } + + expect(onDied).toHaveBeenCalledTimes(1); + expect(onObserverClose).toHaveBeenCalledTimes(1); + expect(worker3.closed).toBe(true); + expect(worker3.died).toBe(true); +}, 5000); + +// Windows doesn't have some signals such as SIGPIPE, SIGALRM, SIGUSR1, SIGUSR2 +// so we just skip this test in Windows. +if (os.platform() !== 'win32') { + test('worker process ignores PIPE, HUP, ALRM, USR1 and USR2 signals', async () => { + const worker = await mediasoup.createWorker({ logLevel: 'warn' }); + + await new Promise((resolve, reject) => { + worker.on('died', reject); + + process.kill(worker.pid, 'SIGPIPE'); + process.kill(worker.pid, 'SIGHUP'); + process.kill(worker.pid, 'SIGALRM'); + process.kill(worker.pid, 'SIGUSR1'); + process.kill(worker.pid, 'SIGUSR2'); + + setTimeout(() => { + expect(worker.closed).toBe(false); + + worker.close(); + worker.on('subprocessclose', resolve); + }, 2000); + }); + }, 3000); +} diff --git a/node/src/test/test-mediasoup.ts b/node/src/test/test-mediasoup.ts new file mode 100644 index 0000000000..98dad17115 --- /dev/null +++ b/node/src/test/test-mediasoup.ts @@ -0,0 +1,115 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { enhancedOnce } from '../enhancedEvents'; +import * as mediasoup from '../'; +import type { WorkerEvents } from '../types'; + +const PKG = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', '..', '..', 'package.json'), { + encoding: 'utf-8', + }) +); + +const { version, getSupportedRtpCapabilities, parseScalabilityMode } = + mediasoup; + +test('mediasoup.version matches version field in package.json', () => { + expect(version).toBe(PKG.version); +}); + +test('setLoggerEventListeners() works', async () => { + const onDebug = jest.fn(); + + mediasoup.setLogEventListeners({ + ondebug: onDebug, + onwarn: undefined, + onerror: undefined, + }); + + const worker = await mediasoup.createWorker(); + + worker.close(); + + expect(onDebug).toHaveBeenCalled(); + + if (worker.subprocessClosed === false) { + await enhancedOnce(worker, 'subprocessclose'); + } +}, 2000); + +test('mediasoup.getSupportedRtpCapabilities() returns the mediasoup RTP capabilities', () => { + const rtpCapabilities = getSupportedRtpCapabilities(); + + expect(typeof rtpCapabilities).toBe('object'); + + // Mangle retrieved codecs to check that, if called again, + // getSupportedRtpCapabilities() returns a cloned object. + // @ts-expect-error --- Testing purposes. + rtpCapabilities.codecs = 'bar'; + + const rtpCapabilities2 = getSupportedRtpCapabilities(); + + expect(rtpCapabilities2).not.toEqual(rtpCapabilities); +}); + +test('parseScalabilityMode() works', () => { + expect(parseScalabilityMode('L1T3')).toEqual({ + spatialLayers: 1, + temporalLayers: 3, + ksvc: false, + }); + + expect(parseScalabilityMode('L3T2_KEY')).toEqual({ + spatialLayers: 3, + temporalLayers: 2, + ksvc: true, + }); + + expect(parseScalabilityMode('S2T3')).toEqual({ + spatialLayers: 2, + temporalLayers: 3, + ksvc: false, + }); + + expect(parseScalabilityMode('foo')).toEqual({ + spatialLayers: 1, + temporalLayers: 1, + ksvc: false, + }); + + expect(parseScalabilityMode(undefined)).toEqual({ + spatialLayers: 1, + temporalLayers: 1, + ksvc: false, + }); + + expect(parseScalabilityMode('S0T3')).toEqual({ + spatialLayers: 1, + temporalLayers: 1, + ksvc: false, + }); + + expect(parseScalabilityMode('S1T0')).toEqual({ + spatialLayers: 1, + temporalLayers: 1, + ksvc: false, + }); + + expect(parseScalabilityMode('L20T3')).toEqual({ + spatialLayers: 20, + temporalLayers: 3, + ksvc: false, + }); + + expect(parseScalabilityMode('S200T3')).toEqual({ + spatialLayers: 1, + temporalLayers: 1, + ksvc: false, + }); + + expect(parseScalabilityMode('L4T7_KEY_SHIFT')).toEqual({ + spatialLayers: 4, + temporalLayers: 7, + ksvc: true, + }); +}); diff --git a/node/src/test/test-multiopus.ts b/node/src/test/test-multiopus.ts new file mode 100644 index 0000000000..e1925fe7a1 --- /dev/null +++ b/node/src/test/test-multiopus.ts @@ -0,0 +1,249 @@ +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { WorkerEvents } from '../types'; +import { UnsupportedError } from '../errors'; +import * as utils from '../utils'; + +type TestContext = { + mediaCodecs: mediasoup.types.RtpCodecCapability[]; + audioProducerOptions: mediasoup.types.ProducerOptions; + consumerDeviceCapabilities: mediasoup.types.RtpCapabilities; + worker?: mediasoup.types.Worker; + router?: mediasoup.types.Router; + webRtcTransport?: mediasoup.types.WebRtcTransport; +}; + +const ctx: TestContext = { + mediaCodecs: utils.deepFreeze([ + { + kind: 'audio', + mimeType: 'audio/multiopus', + preferredPayloadType: 100, + clockRate: 48000, + channels: 6, + parameters: { + useinbandfec: 1, + channel_mapping: '0,4,1,2,3,5', + num_streams: 4, + coupled_streams: 2, + }, + }, + ]), + audioProducerOptions: utils.deepFreeze({ + kind: 'audio', + rtpParameters: { + mid: 'AUDIO', + codecs: [ + { + mimeType: 'audio/multiopus', + payloadType: 0, + clockRate: 48000, + channels: 6, + parameters: { + useinbandfec: 1, + channel_mapping: '0,4,1,2,3,5', + num_streams: 4, + coupled_streams: 2, + }, + }, + ], + headerExtensions: [ + { + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + id: 10, + }, + { + uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + id: 12, + }, + ], + }, + }), + consumerDeviceCapabilities: utils.deepFreeze( + { + codecs: [ + { + mimeType: 'audio/multiopus', + kind: 'audio', + preferredPayloadType: 100, + clockRate: 48000, + channels: 6, + parameters: { + channel_mapping: '0,4,1,2,3,5', + num_streams: 4, + coupled_streams: 2, + }, + }, + ], + headerExtensions: [ + { + kind: 'audio', + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + preferredId: 1, + preferredEncrypt: false, + }, + { + kind: 'audio', + uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', + preferredId: 4, + preferredEncrypt: false, + }, + { + kind: 'audio', + uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + preferredId: 10, + preferredEncrypt: false, + }, + ], + } + ), +}; + +beforeEach(async () => { + ctx.worker = await mediasoup.createWorker(); + ctx.router = await ctx.worker.createRouter({ mediaCodecs: ctx.mediaCodecs }); + ctx.webRtcTransport = await ctx.router.createWebRtcTransport({ + listenInfos: [{ protocol: 'udp', ip: '127.0.0.1' }], + }); +}); + +afterEach(async () => { + ctx.worker?.close(); + + if (ctx.worker?.subprocessClosed === false) { + await enhancedOnce(ctx.worker, 'subprocessclose'); + } +}); + +test('produce() and consume() succeed', async () => { + const audioProducer = await ctx.webRtcTransport!.produce( + ctx.audioProducerOptions + ); + + expect(audioProducer.rtpParameters.codecs).toEqual([ + { + mimeType: 'audio/multiopus', + payloadType: 0, + clockRate: 48000, + channels: 6, + parameters: { + useinbandfec: 1, + channel_mapping: '0,4,1,2,3,5', + num_streams: 4, + coupled_streams: 2, + }, + rtcpFeedback: [], + }, + ]); + + expect( + ctx.router!.canConsume({ + producerId: audioProducer.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }) + ).toBe(true); + + const audioConsumer = await ctx.webRtcTransport!.consume({ + producerId: audioProducer.id, + rtpCapabilities: ctx.consumerDeviceCapabilities, + }); + + expect(audioConsumer.rtpParameters.codecs).toEqual([ + { + mimeType: 'audio/multiopus', + payloadType: 100, + clockRate: 48000, + channels: 6, + parameters: { + useinbandfec: 1, + channel_mapping: '0,4,1,2,3,5', + num_streams: 4, + coupled_streams: 2, + }, + rtcpFeedback: [], + }, + ]); +}, 2000); + +test('fails to produce wrong parameters', async () => { + await expect( + ctx.webRtcTransport!.produce({ + kind: 'audio', + rtpParameters: { + mid: 'AUDIO', + codecs: [ + { + mimeType: 'audio/multiopus', + payloadType: 0, + clockRate: 48000, + channels: 6, + parameters: { + channel_mapping: '0,4,1,2,3,5', + num_streams: 2, + coupled_streams: 2, + }, + }, + ], + }, + }) + ).rejects.toThrow(UnsupportedError); + + await expect( + ctx.webRtcTransport!.produce({ + kind: 'audio', + rtpParameters: { + mid: 'AUDIO', + codecs: [ + { + mimeType: 'audio/multiopus', + payloadType: 0, + clockRate: 48000, + channels: 6, + parameters: { + channel_mapping: '0,4,1,2,3,5', + num_streams: 4, + coupled_streams: 1, + }, + }, + ], + }, + }) + ).rejects.toThrow(UnsupportedError); +}, 2000); + +test('fails to consume wrong channels', async () => { + const audioProducer = await ctx.webRtcTransport!.produce( + ctx.audioProducerOptions + ); + + const localConsumerDeviceCapabilities: mediasoup.types.RtpCapabilities = { + codecs: [ + { + mimeType: 'audio/multiopus', + kind: 'audio', + preferredPayloadType: 100, + clockRate: 48000, + channels: 8, + parameters: { + channel_mapping: '0,4,1,2,3,5', + num_streams: 4, + coupled_streams: 2, + }, + }, + ], + }; + + expect( + !ctx.router!.canConsume({ + producerId: audioProducer.id, + rtpCapabilities: localConsumerDeviceCapabilities, + }) + ).toBe(true); + + await expect( + ctx.webRtcTransport!.consume({ + producerId: audioProducer.id, + rtpCapabilities: localConsumerDeviceCapabilities, + }) + ).rejects.toThrow(Error); +}, 2000); diff --git a/node/src/test/test-node-sctp.ts b/node/src/test/test-node-sctp.ts new file mode 100644 index 0000000000..87a79cbcc6 --- /dev/null +++ b/node/src/test/test-node-sctp.ts @@ -0,0 +1,220 @@ +import * as dgram from 'node:dgram'; +// @ts-expect-error -- sctp library doesn't have TS types. +import * as sctp from 'sctp'; +import * as mediasoup from '../'; +import { enhancedOnce } from '../enhancedEvents'; +import type { WorkerEvents } from '../types'; + +type TestContext = { + worker?: mediasoup.types.Worker; + router?: mediasoup.types.Router; + plainTransport?: mediasoup.types.PlainTransport; + dataProducer?: mediasoup.types.DataProducer; + dataConsumer?: mediasoup.types.DataConsumer; + udpSocket?: dgram.Socket; + sctpSocket?: any; + sctpSendStreamId?: number; + sctpSendStream?: any; +}; + +const ctx: TestContext = {}; + +beforeEach(async () => { + // Set node-sctp default PMTU to 1200. + sctp.defaults({ PMTU: 1200 }); + + ctx.worker = await mediasoup.createWorker({ disableLiburing: true }); + ctx.router = await ctx.worker.createRouter(); + ctx.plainTransport = await ctx.router.createPlainTransport({ + // https://github.com/nodejs/node/issues/14900. + listenIp: '127.0.0.1', + // So we don't need to call plainTransport.connect(). + comedia: true, + enableSctp: true, + numSctpStreams: { OS: 256, MIS: 256 }, + }); + + // Node UDP socket for SCTP. + ctx.udpSocket = dgram.createSocket({ type: 'udp4' }); + + await new Promise(resolve => { + ctx.udpSocket!.bind(0, '127.0.0.1', resolve); + }); + + const remoteUdpIp = ctx.plainTransport.tuple.localAddress; + const remoteUdpPort = ctx.plainTransport.tuple.localPort; + const { OS, MIS } = ctx.plainTransport.sctpParameters!; + + await new Promise((resolve, reject) => { + ctx.udpSocket?.on('error', error => { + reject(error); + }); + + ctx.udpSocket?.connect(remoteUdpPort, remoteUdpIp, () => { + ctx.sctpSocket = sctp.connect({ + localPort: 5000, // Required for SCTP over UDP in mediasoup. + port: 5000, // Required for SCTP over UDP in mediasoup. + OS: OS, + MIS: MIS, + udpTransport: ctx.udpSocket, + }); + + resolve(); + }); + }); + + // Wait for the SCTP association to be open. + await Promise.race([ + enhancedOnce(ctx.sctpSocket, 'connect'), + new Promise((resolve, reject) => + setTimeout(() => reject(new Error('SCTP connection timeout')), 3000) + ), + ]); + + // Create an explicit SCTP outgoing stream with id 123 (id 0 is already used + // by the implicit SCTP outgoing stream built-in the SCTP socket). + ctx.sctpSendStreamId = 123; + ctx.sctpSendStream = ctx.sctpSocket.createStream(ctx.sctpSendStreamId); + + // Create a DataProducer with the corresponding SCTP stream id. + ctx.dataProducer = await ctx.plainTransport.produceData({ + sctpStreamParameters: { + streamId: ctx.sctpSendStreamId, + ordered: true, + }, + label: 'node-sctp', + protocol: 'foo & bar 😀😀😀', + }); + + // Create a DataConsumer to receive messages from the DataProducer over the + // same plainTransport. + ctx.dataConsumer = await ctx.plainTransport.consumeData({ + dataProducerId: ctx.dataProducer.id, + }); +}); + +afterEach(async () => { + ctx.udpSocket?.close(); + ctx.sctpSocket?.end(); + ctx.worker?.close(); + + if (ctx.worker?.subprocessClosed === false) { + await enhancedOnce(ctx.worker, 'subprocessclose'); + } + + // NOTE: For some reason we have to wait a bit for the SCTP stuff to release + // internal things, otherwise Jest reports open handles. We don't care much + // honestly. + await new Promise(resolve => setTimeout(resolve, 1000)); +}); + +test('ordered DataProducer delivers all SCTP messages to the DataConsumer', async () => { + const onStream = jest.fn(); + const numMessages = 200; + let sentMessageBytes = 0; + let recvMessageBytes = 0; + let numSentMessages = 0; + let numReceivedMessages = 0; + + // It must be zero because it's the first DataConsumer on the plainTransport. + expect(ctx.dataConsumer!.sctpStreamParameters?.streamId).toBe(0); + + await new Promise((resolve, reject) => { + sendNextMessage(); + + function sendNextMessage(): void { + const id = ++numSentMessages; + const data = Buffer.from(String(id)); + + // Set ppid of type WebRTC DataChannel string. + if (id < numMessages / 2) { + // @ts-expect-errors --- sctp library needs `ppid` field.` + data.ppid = sctp.PPID.WEBRTC_STRING; + } + // Set ppid of type WebRTC DataChannel binary. + else { + // @ts-expect-errors --- sctp library needs `ppid` field. + data.ppid = sctp.PPID.WEBRTC_BINARY; + } + + ctx.sctpSendStream!.write(data); + sentMessageBytes += data.byteLength; + + if (id < numMessages) { + sendNextMessage(); + } + } + + ctx.sctpSocket!.on('stream', onStream); + + // Handle the generated SCTP incoming stream and SCTP messages receives on it. + // @ts-expect-error --- Custom event of sctp library. + ctx.sctpSocket.on('stream', (stream, streamId) => { + // It must be zero because it's the first SCTP incoming stream (so first + // DataConsumer). + if (streamId !== 0) { + reject(new Error(`streamId should be 0 but it is ${streamId}`)); + + return; + } + + stream.on('data', (data: Buffer) => { + ++numReceivedMessages; + recvMessageBytes += data.byteLength; + + const id = Number(data.toString('utf8')); + // @ts-expect-errors --- sctp library uses `ppid` field. + const ppid = data.ppid; + + if (id !== numReceivedMessages) { + reject( + new Error( + `id ${id} in message should match numReceivedMessages ${numReceivedMessages}` + ) + ); + } else if (id === numMessages) { + resolve(); + } else if (id < numMessages / 2 && ppid !== sctp.PPID.WEBRTC_STRING) { + reject( + new Error( + `ppid in message with id ${id} should be ${sctp.PPID.WEBRTC_STRING} but it is ${ppid}` + ) + ); + } else if (id > numMessages / 2 && ppid !== sctp.PPID.WEBRTC_BINARY) { + reject( + new Error( + `ppid in message with id ${id} should be ${sctp.PPID.WEBRTC_BINARY} but it is ${ppid}` + ) + ); + + return; + } + }); + }); + }); + + expect(onStream).toHaveBeenCalledTimes(1); + expect(numSentMessages).toBe(numMessages); + expect(numReceivedMessages).toBe(numMessages); + expect(recvMessageBytes).toBe(sentMessageBytes); + + await expect(ctx.dataProducer!.getStats()).resolves.toMatchObject([ + { + type: 'data-producer', + label: ctx.dataProducer!.label, + protocol: ctx.dataProducer!.protocol, + messagesReceived: numMessages, + bytesReceived: sentMessageBytes, + }, + ]); + + await expect(ctx.dataConsumer!.getStats()).resolves.toMatchObject([ + { + type: 'data-consumer', + label: ctx.dataConsumer!.label, + protocol: ctx.dataConsumer!.protocol, + messagesSent: numMessages, + bytesSent: recvMessageBytes, + }, + ]); +}, 10000); diff --git a/node/src/test/test-ortc.ts b/node/src/test/test-ortc.ts new file mode 100644 index 0000000000..e07ad9d091 --- /dev/null +++ b/node/src/test/test-ortc.ts @@ -0,0 +1,554 @@ +import * as mediasoup from '../'; +import * as ortc from '../ortc'; +import { UnsupportedError } from '../errors'; + +test('generateRouterRtpCapabilities() succeeds', () => { + const mediaCodecs: mediasoup.types.RtpCodecCapability[] = [ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + parameters: { + useinbandfec: 1, + foo: 'bar', + }, + }, + { + kind: 'video', + mimeType: 'video/VP8', + preferredPayloadType: 125, // Let's force it. + clockRate: 90000, + }, + { + kind: 'video', + mimeType: 'video/H264', + clockRate: 90000, + parameters: { + 'level-asymmetry-allowed': 1, + 'profile-level-id': '42e01f', + foo: 'bar', + }, + rtcpFeedback: [], // Will be ignored. + }, + ]; + + const rtpCapabilities = ortc.generateRouterRtpCapabilities(mediaCodecs); + + expect(rtpCapabilities.codecs?.length).toBe(5); + + // opus. + expect(rtpCapabilities.codecs?.[0]).toEqual({ + kind: 'audio', + mimeType: 'audio/opus', + preferredPayloadType: 100, // 100 is the first available dynamic PT. + clockRate: 48000, + channels: 2, + parameters: { + useinbandfec: 1, + foo: 'bar', + }, + rtcpFeedback: [ + { type: 'nack', parameter: '' }, + { type: 'transport-cc', parameter: '' }, + ], + }); + + // VP8. + expect(rtpCapabilities.codecs?.[1]).toEqual({ + kind: 'video', + mimeType: 'video/VP8', + preferredPayloadType: 125, + clockRate: 90000, + parameters: {}, + rtcpFeedback: [ + { type: 'nack', parameter: '' }, + { type: 'nack', parameter: 'pli' }, + { type: 'ccm', parameter: 'fir' }, + { type: 'goog-remb', parameter: '' }, + { type: 'transport-cc', parameter: '' }, + ], + }); + + // VP8 RTX. + expect(rtpCapabilities.codecs?.[2]).toEqual({ + kind: 'video', + mimeType: 'video/rtx', + preferredPayloadType: 101, // 101 is the second available dynamic PT. + clockRate: 90000, + parameters: { + apt: 125, + }, + rtcpFeedback: [], + }); + + // H264. + expect(rtpCapabilities.codecs?.[3]).toEqual({ + kind: 'video', + mimeType: 'video/H264', + preferredPayloadType: 102, // 102 is the third available dynamic PT. + clockRate: 90000, + parameters: { + // Since packetization-mode param was not included in the H264 codec + // and it's default value is 0, it's not added by ortc file. + // 'packetization-mode' : 0, + 'level-asymmetry-allowed': 1, + 'profile-level-id': '42e01f', + foo: 'bar', + }, + rtcpFeedback: [ + { type: 'nack', parameter: '' }, + { type: 'nack', parameter: 'pli' }, + { type: 'ccm', parameter: 'fir' }, + { type: 'goog-remb', parameter: '' }, + { type: 'transport-cc', parameter: '' }, + ], + }); + + // H264 RTX. + expect(rtpCapabilities.codecs?.[4]).toEqual({ + kind: 'video', + mimeType: 'video/rtx', + preferredPayloadType: 103, + clockRate: 90000, + parameters: { + apt: 102, + }, + rtcpFeedback: [], + }); +}); + +test('generateRouterRtpCapabilities() with unsupported codecs throws UnsupportedError', () => { + let mediaCodecs: mediasoup.types.RtpCodecCapability[]; + + mediaCodecs = [ + { + kind: 'audio', + mimeType: 'audio/chicken', + clockRate: 8000, + channels: 4, + }, + ]; + + expect(() => ortc.generateRouterRtpCapabilities(mediaCodecs)).toThrow( + UnsupportedError + ); + + mediaCodecs = [ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 1, + }, + ]; + + expect(() => ortc.generateRouterRtpCapabilities(mediaCodecs)).toThrow( + UnsupportedError + ); +}); + +test('generateRouterRtpCapabilities() with too many codecs throws', () => { + const mediaCodecs: mediasoup.types.RtpCodecCapability[] = []; + + for (let i = 0; i < 100; ++i) { + mediaCodecs.push({ + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + }); + } + + expect(() => ortc.generateRouterRtpCapabilities(mediaCodecs)).toThrow( + 'cannot allocate' + ); +}); + +test('getProducerRtpParametersMapping(), getConsumableRtpParameters(), getConsumerRtpParameters() and getPipeConsumerRtpParameters() succeed', () => { + const mediaCodecs: mediasoup.types.RtpCodecCapability[] = [ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + }, + { + kind: 'video', + mimeType: 'video/H264', + clockRate: 90000, + parameters: { + 'level-asymmetry-allowed': 1, + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + bar: 'lalala', + }, + }, + ]; + + const routerRtpCapabilities = ortc.generateRouterRtpCapabilities(mediaCodecs); + + const rtpParameters: mediasoup.types.RtpParameters = { + codecs: [ + { + mimeType: 'video/H264', + payloadType: 111, + clockRate: 90000, + parameters: { + foo: 1234, + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }, + rtcpFeedback: [ + { type: 'nack', parameter: '' }, + { type: 'nack', parameter: 'pli' }, + { type: 'goog-remb', parameter: '' }, + ], + }, + { + mimeType: 'video/rtx', + payloadType: 112, + clockRate: 90000, + parameters: { + apt: 111, + }, + rtcpFeedback: [], + }, + ], + headerExtensions: [ + { + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + id: 1, + }, + { + uri: 'urn:3gpp:video-orientation', + id: 2, + }, + ], + encodings: [ + { + ssrc: 11111111, + rtx: { ssrc: 11111112 }, + maxBitrate: 111111, + scalabilityMode: 'L1T3', + }, + { + ssrc: 21111111, + rtx: { ssrc: 21111112 }, + maxBitrate: 222222, + scalabilityMode: 'L1T3', + }, + { + rid: 'high', + maxBitrate: 333333, + scalabilityMode: 'L1T3', + }, + ], + rtcp: { + cname: 'qwerty1234', + }, + }; + + const rtpMapping = ortc.getProducerRtpParametersMapping( + rtpParameters, + routerRtpCapabilities + ); + + expect(rtpMapping.codecs).toEqual([ + { payloadType: 111, mappedPayloadType: 101 }, + { payloadType: 112, mappedPayloadType: 102 }, + ]); + + expect(rtpMapping.encodings[0].ssrc).toBe(11111111); + expect(rtpMapping.encodings[0].rid).toBeUndefined(); + expect(typeof rtpMapping.encodings[0].mappedSsrc).toBe('number'); + expect(rtpMapping.encodings[1].ssrc).toBe(21111111); + expect(rtpMapping.encodings[1].rid).toBeUndefined(); + expect(typeof rtpMapping.encodings[1].mappedSsrc).toBe('number'); + expect(rtpMapping.encodings[2].ssrc).toBeUndefined(); + expect(rtpMapping.encodings[2].rid).toBe('high'); + expect(typeof rtpMapping.encodings[2].mappedSsrc).toBe('number'); + + const consumableRtpParameters = ortc.getConsumableRtpParameters( + 'video', + rtpParameters, + routerRtpCapabilities, + rtpMapping + ); + + expect(consumableRtpParameters.codecs[0].mimeType).toBe('video/H264'); + expect(consumableRtpParameters.codecs[0].payloadType).toBe(101); + expect(consumableRtpParameters.codecs[0].clockRate).toBe(90000); + expect(consumableRtpParameters.codecs[0].parameters).toEqual({ + foo: 1234, + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }); + + expect(consumableRtpParameters.codecs[1].mimeType).toBe('video/rtx'); + expect(consumableRtpParameters.codecs[1].payloadType).toBe(102); + expect(consumableRtpParameters.codecs[1].clockRate).toBe(90000); + expect(consumableRtpParameters.codecs[1].parameters).toEqual({ apt: 101 }); + + expect(consumableRtpParameters.encodings?.[0]).toEqual({ + ssrc: rtpMapping.encodings[0].mappedSsrc, + maxBitrate: 111111, + scalabilityMode: 'L1T3', + }); + expect(consumableRtpParameters.encodings?.[1]).toEqual({ + ssrc: rtpMapping.encodings[1].mappedSsrc, + maxBitrate: 222222, + scalabilityMode: 'L1T3', + }); + expect(consumableRtpParameters.encodings?.[2]).toEqual({ + ssrc: rtpMapping.encodings[2].mappedSsrc, + maxBitrate: 333333, + scalabilityMode: 'L1T3', + }); + + expect(consumableRtpParameters.rtcp).toEqual({ + cname: rtpParameters.rtcp?.cname, + reducedSize: true, + }); + + const remoteRtpCapabilities: mediasoup.types.RtpCapabilities = { + codecs: [ + { + kind: 'audio', + mimeType: 'audio/opus', + preferredPayloadType: 100, + clockRate: 48000, + channels: 2, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: 'video', + mimeType: 'video/H264', + preferredPayloadType: 101, + clockRate: 90000, + parameters: { + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + baz: 'LOLOLO', + }, + rtcpFeedback: [ + { type: 'nack', parameter: '' }, + { type: 'nack', parameter: 'pli' }, + { type: 'foo', parameter: 'FOO' }, + ], + }, + { + kind: 'video', + mimeType: 'video/rtx', + preferredPayloadType: 102, + clockRate: 90000, + parameters: { + apt: 101, + }, + rtcpFeedback: [], + }, + ], + headerExtensions: [ + { + kind: 'audio', + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + preferredId: 1, + preferredEncrypt: false, + direction: 'sendrecv', + }, + { + kind: 'video', + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + preferredId: 1, + preferredEncrypt: false, + direction: 'sendrecv', + }, + { + kind: 'video', + uri: 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', + preferredId: 2, + preferredEncrypt: false, + direction: 'sendrecv', + }, + { + kind: 'audio', + uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + preferredId: 8, + preferredEncrypt: false, + direction: 'sendrecv', + }, + { + kind: 'video', + uri: 'urn:3gpp:video-orientation', + preferredId: 11, + preferredEncrypt: false, + direction: 'sendrecv', + }, + { + kind: 'video', + uri: 'urn:ietf:params:rtp-hdrext:toffset', + preferredId: 12, + preferredEncrypt: false, + direction: 'sendrecv', + }, + ], + }; + + const consumerRtpParameters = ortc.getConsumerRtpParameters({ + consumableRtpParameters, + remoteRtpCapabilities, + pipe: false, + enableRtx: true, + }); + + expect(consumerRtpParameters.codecs.length).toEqual(2); + expect(consumerRtpParameters.codecs[0]).toEqual({ + mimeType: 'video/H264', + payloadType: 101, + clockRate: 90000, + parameters: { + foo: 1234, + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }, + rtcpFeedback: [ + { type: 'nack', parameter: '' }, + { type: 'nack', parameter: 'pli' }, + { type: 'foo', parameter: 'FOO' }, + ], + }); + expect(consumerRtpParameters.codecs[1]).toEqual({ + mimeType: 'video/rtx', + payloadType: 102, + clockRate: 90000, + parameters: { + apt: 101, + }, + rtcpFeedback: [], + }); + + expect(consumerRtpParameters.encodings?.length).toBe(1); + expect(typeof consumerRtpParameters.encodings?.[0].ssrc).toBe('number'); + expect(typeof consumerRtpParameters.encodings?.[0].rtx).toBe('object'); + expect(typeof consumerRtpParameters.encodings?.[0].rtx?.ssrc).toBe('number'); + expect(consumerRtpParameters.encodings?.[0].scalabilityMode).toBe('L3T3'); + expect(consumerRtpParameters.encodings?.[0].maxBitrate).toBe(333333); + + expect(consumerRtpParameters.headerExtensions).toEqual([ + { + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + id: 1, + encrypt: false, + parameters: {}, + }, + { + uri: 'urn:3gpp:video-orientation', + id: 11, + encrypt: false, + parameters: {}, + }, + { + uri: 'urn:ietf:params:rtp-hdrext:toffset', + id: 12, + encrypt: false, + parameters: {}, + }, + ]); + + expect(consumerRtpParameters.rtcp).toEqual({ + cname: rtpParameters.rtcp?.cname, + reducedSize: true, + }); + + const pipeConsumerRtpParameters = ortc.getPipeConsumerRtpParameters({ + consumableRtpParameters, + enableRtx: false, + }); + + expect(pipeConsumerRtpParameters.codecs.length).toEqual(1); + expect(pipeConsumerRtpParameters.codecs[0]).toEqual({ + mimeType: 'video/H264', + payloadType: 101, + clockRate: 90000, + parameters: { + foo: 1234, + 'packetization-mode': 1, + 'profile-level-id': '4d0032', + }, + rtcpFeedback: [ + { type: 'nack', parameter: 'pli' }, + { type: 'ccm', parameter: 'fir' }, + ], + }); + + expect(pipeConsumerRtpParameters.encodings?.length).toBe(3); + expect(typeof pipeConsumerRtpParameters.encodings?.[0].ssrc).toBe('number'); + expect(pipeConsumerRtpParameters.encodings?.[0].rtx).toBeUndefined(); + expect(typeof pipeConsumerRtpParameters.encodings?.[0].maxBitrate).toBe( + 'number' + ); + expect(pipeConsumerRtpParameters.encodings?.[0].scalabilityMode).toBe('L1T3'); + expect(typeof pipeConsumerRtpParameters.encodings?.[1].ssrc).toBe('number'); + expect(pipeConsumerRtpParameters.encodings?.[1].rtx).toBeUndefined(); + expect(typeof pipeConsumerRtpParameters.encodings?.[1].maxBitrate).toBe( + 'number' + ); + expect(pipeConsumerRtpParameters.encodings?.[1].scalabilityMode).toBe('L1T3'); + expect(typeof pipeConsumerRtpParameters.encodings?.[2].ssrc).toBe('number'); + expect(pipeConsumerRtpParameters.encodings?.[2].rtx).toBeUndefined(); + expect(typeof pipeConsumerRtpParameters.encodings?.[2].maxBitrate).toBe( + 'number' + ); + expect(pipeConsumerRtpParameters.encodings?.[2].scalabilityMode).toBe('L1T3'); + + expect(pipeConsumerRtpParameters.rtcp).toEqual({ + cname: rtpParameters.rtcp?.cname, + reducedSize: true, + }); +}); + +test('getProducerRtpParametersMapping() with incompatible params throws UnsupportedError', () => { + const mediaCodecs: mediasoup.types.RtpCodecCapability[] = [ + { + kind: 'audio', + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + }, + { + kind: 'video', + mimeType: 'video/H264', + clockRate: 90000, + parameters: { + 'packetization-mode': 1, + 'profile-level-id': '640032', + }, + }, + ]; + + const routerRtpCapabilities = ortc.generateRouterRtpCapabilities(mediaCodecs); + + const rtpParameters = { + codecs: [ + { + mimeType: 'video/VP8', + payloadType: 120, + clockRate: 90000, + rtcpFeedback: [ + { type: 'nack', parameter: '' }, + { type: 'nack', parameter: 'fir' }, + ], + }, + ], + headerExtensions: [], + encodings: [{ ssrc: 11111111 }], + rtcp: { + cname: 'qwerty1234', + }, + }; + + expect(() => + ortc.getProducerRtpParametersMapping(rtpParameters, routerRtpCapabilities) + ).toThrow(UnsupportedError); +}); diff --git a/node/src/tests/test-ActiveSpeakerObserver.ts b/node/src/tests/test-ActiveSpeakerObserver.ts deleted file mode 100644 index 248b8bf0d5..0000000000 --- a/node/src/tests/test-ActiveSpeakerObserver.ts +++ /dev/null @@ -1,123 +0,0 @@ -import * as mediasoup from '../'; - -const { createWorker } = mediasoup; - -let worker: mediasoup.types.Worker; -let router: mediasoup.types.Router; -let activeSpeakerObserver: mediasoup.types.ActiveSpeakerObserver; - -const mediaCodecs: mediasoup.types.RtpCodecCapability[] = -[ - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 2, - parameters : - { - useinbandfec : 1, - foo : 'bar' - } - } -]; - -beforeAll(async () => -{ - worker = await createWorker(); - router = await worker.createRouter({ mediaCodecs }); -}); - -afterAll(() => worker.close()); - -test('router.createActiveSpeakerObserver() succeeds', async () => -{ - const onObserverNewRtpObserver = jest.fn(); - - router.observer.once('newrtpobserver', onObserverNewRtpObserver); - - activeSpeakerObserver = await router.createActiveSpeakerObserver(); - - expect(onObserverNewRtpObserver).toHaveBeenCalledTimes(1); - expect(onObserverNewRtpObserver).toHaveBeenCalledWith(activeSpeakerObserver); - expect(typeof activeSpeakerObserver.id).toBe('string'); - expect(activeSpeakerObserver.closed).toBe(false); - expect(activeSpeakerObserver.paused).toBe(false); - expect(activeSpeakerObserver.appData).toEqual({}); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - rtpObserverIds : [ activeSpeakerObserver.id ] - }); -}, 2000); - -test('router.createActiveSpeakerObserver() with wrong arguments rejects with TypeError', async () => -{ - // @ts-ignore - await expect(router.createActiveSpeakerObserver({ interval: false })) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(router.createActiveSpeakerObserver({ appData: 'NOT-AN-OBJECT' })) - .rejects - .toThrow(TypeError); -}, 2000); - -test('activeSpeakerObserver.pause() and resume() succeed', async () => -{ - await activeSpeakerObserver.pause(); - - expect(activeSpeakerObserver.paused).toBe(true); - - await activeSpeakerObserver.resume(); - - expect(activeSpeakerObserver.paused).toBe(false); -}, 2000); - -test('activeSpeakerObserver.close() succeeds', async () => -{ - // We need different a AudioLevelObserver instance here. - const activeSpeakerObserver2 = - await router.createAudioLevelObserver({ maxEntries: 8 }); - - let dump = await router.dump(); - - expect(dump.rtpObserverIds.length).toBe(2); - - activeSpeakerObserver2.close(); - - expect(activeSpeakerObserver2.closed).toBe(true); - - dump = await router.dump(); - - expect(dump.rtpObserverIds.length).toBe(1); - -}, 2000); - -test('ActiveSpeakerObserver emits "routerclose" if Router is closed', async () => -{ - // We need different Router and AudioLevelObserver instances here. - const router2 = await worker.createRouter({ mediaCodecs }); - const activeSpeakerObserver2 = await router2.createAudioLevelObserver(); - - await new Promise((resolve) => - { - activeSpeakerObserver2.on('routerclose', resolve); - router2.close(); - }); - - expect(activeSpeakerObserver2.closed).toBe(true); -}, 2000); - -test('ActiveSpeakerObserver emits "routerclose" if Worker is closed', async () => -{ - await new Promise((resolve) => - { - activeSpeakerObserver.on('routerclose', resolve); - worker.close(); - }); - - expect(activeSpeakerObserver.closed).toBe(true); -}, 2000); diff --git a/node/src/tests/test-AudioLevelObserver.ts b/node/src/tests/test-AudioLevelObserver.ts deleted file mode 100644 index cc8155e3a5..0000000000 --- a/node/src/tests/test-AudioLevelObserver.ts +++ /dev/null @@ -1,135 +0,0 @@ -import * as mediasoup from '../'; -const { createWorker } = mediasoup; - -let worker: mediasoup.types.Worker; -let router: mediasoup.types.Router; -let audioLevelObserver: mediasoup.types.AudioLevelObserver; - -const mediaCodecs: mediasoup.types.RtpCodecCapability[] = -[ - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 2, - parameters : - { - useinbandfec : 1, - foo : 'bar' - } - } -]; - -beforeAll(async () => -{ - worker = await createWorker(); - router = await worker.createRouter({ mediaCodecs }); -}); - -afterAll(() => worker.close()); - -test('router.createAudioLevelObserver() succeeds', async () => -{ - const onObserverNewRtpObserver = jest.fn(); - - router.observer.once('newrtpobserver', onObserverNewRtpObserver); - - audioLevelObserver = await router.createAudioLevelObserver(); - - expect(onObserverNewRtpObserver).toHaveBeenCalledTimes(1); - expect(onObserverNewRtpObserver).toHaveBeenCalledWith(audioLevelObserver); - expect(typeof audioLevelObserver.id).toBe('string'); - expect(audioLevelObserver.closed).toBe(false); - expect(audioLevelObserver.paused).toBe(false); - expect(audioLevelObserver.appData).toEqual({}); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - rtpObserverIds : [ audioLevelObserver.id ] - }); -}, 2000); - -test('router.createAudioLevelObserver() with wrong arguments rejects with TypeError', async () => -{ - await expect(router.createAudioLevelObserver({ maxEntries: 0 })) - .rejects - .toThrow(TypeError); - - await expect(router.createAudioLevelObserver({ maxEntries: -10 })) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(router.createAudioLevelObserver({ threshold: 'foo' })) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(router.createAudioLevelObserver({ interval: false })) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(router.createAudioLevelObserver({ appData: 'NOT-AN-OBJECT' })) - .rejects - .toThrow(TypeError); -}, 2000); - -test('audioLevelObserver.pause() and resume() succeed', async () => -{ - await audioLevelObserver.pause(); - - expect(audioLevelObserver.paused).toBe(true); - - await audioLevelObserver.resume(); - - expect(audioLevelObserver.paused).toBe(false); -}, 2000); - -test('audioLevelObserver.close() succeeds', async () => -{ - // We need different a AudioLevelObserver instance here. - const audioLevelObserver2 = - await router.createAudioLevelObserver({ maxEntries: 8 }); - - let dump = await router.dump(); - - expect(dump.rtpObserverIds.length).toBe(2); - - audioLevelObserver2.close(); - - expect(audioLevelObserver2.closed).toBe(true); - - dump = await router.dump(); - - expect(dump.rtpObserverIds.length).toBe(1); - -}, 2000); - -test('AudioLevelObserver emits "routerclose" if Router is closed', async () => -{ - // We need different Router and AudioLevelObserver instances here. - const router2 = await worker.createRouter({ mediaCodecs }); - const audioLevelObserver2 = await router2.createAudioLevelObserver(); - - await new Promise((resolve) => - { - audioLevelObserver2.on('routerclose', resolve); - router2.close(); - }); - - expect(audioLevelObserver2.closed).toBe(true); -}, 2000); - -test('AudioLevelObserver emits "routerclose" if Worker is closed', async () => -{ - await new Promise((resolve) => - { - audioLevelObserver.on('routerclose', resolve); - worker.close(); - }); - - expect(audioLevelObserver.closed).toBe(true); -}, 2000); diff --git a/node/src/tests/test-Consumer.ts b/node/src/tests/test-Consumer.ts deleted file mode 100644 index bdee353a10..0000000000 --- a/node/src/tests/test-Consumer.ts +++ /dev/null @@ -1,1036 +0,0 @@ -import * as mediasoup from '../'; -import { UnsupportedError } from '../errors'; - -const { createWorker } = mediasoup; - -let worker: mediasoup.types.Worker; -let router: mediasoup.types.Router; -let transport1: mediasoup.types.WebRtcTransport; -let transport2: mediasoup.types.WebRtcTransport; -let audioProducer: mediasoup.types.Producer; -let videoProducer: mediasoup.types.Producer; -let audioConsumer: mediasoup.types.Consumer; -let videoConsumer: mediasoup.types.Consumer; -let videoPipeConsumer: mediasoup.types.Consumer; - -const mediaCodecs: mediasoup.types.RtpCodecCapability[] = -[ - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 2, - parameters : - { - foo : 'bar' - } - }, - { - kind : 'video', - mimeType : 'video/VP8', - clockRate : 90000 - }, - { - kind : 'video', - mimeType : 'video/H264', - clockRate : 90000, - parameters : - { - 'level-asymmetry-allowed' : 1, - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032', - foo : 'bar' - } - } -]; - -const audioProducerParameters: mediasoup.types.ProducerOptions = -{ - kind : 'audio', - rtpParameters : - { - mid : 'AUDIO', - codecs : - [ - { - mimeType : 'audio/opus', - payloadType : 111, - clockRate : 48000, - channels : 2, - parameters : - { - useinbandfec : 1, - usedtx : 1, - foo : 222.222, - bar : '333' - } - } - ], - headerExtensions : - [ - { - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - id : 10 - }, - { - uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', - id : 12 - } - ], - encodings : [ { ssrc: 11111111 } ], - rtcp : - { - cname : 'FOOBAR' - } - }, - appData : { foo: 1, bar: '2' } -}; - -const videoProducerParameters: mediasoup.types.ProducerOptions = -{ - kind : 'video', - rtpParameters : - { - mid : 'VIDEO', - codecs : - [ - { - mimeType : 'video/h264', - payloadType : 112, - clockRate : 90000, - parameters : - { - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - }, - rtcpFeedback : - [ - { type: 'nack', parameter: '' }, - { type: 'nack', parameter: 'pli' }, - { type: 'goog-remb', parameter: '' } - ] - }, - { - mimeType : 'video/rtx', - payloadType : 113, - clockRate : 90000, - parameters : { apt: 112 } - } - ], - headerExtensions : - [ - { - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - id : 10 - }, - { - uri : 'urn:3gpp:video-orientation', - id : 13 - } - ], - encodings : - [ - { ssrc: 22222222, rtx: { ssrc: 22222223 } }, - { ssrc: 22222224, rtx: { ssrc: 22222225 } }, - { ssrc: 22222226, rtx: { ssrc: 22222227 } }, - { ssrc: 22222228, rtx: { ssrc: 22222229 } } - ], - rtcp : - { - cname : 'FOOBAR' - } - }, - appData : { foo: 1, bar: '2' } -}; - -const consumerDeviceCapabilities: mediasoup.types.RtpCapabilities = -{ - codecs : - [ - { - mimeType : 'audio/opus', - kind : 'audio', - preferredPayloadType : 100, - clockRate : 48000, - channels : 2 - }, - { - mimeType : 'video/H264', - kind : 'video', - preferredPayloadType : 101, - clockRate : 90000, - parameters : - { - 'level-asymmetry-allowed' : 1, - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - }, - rtcpFeedback : - [ - { type: 'nack', parameter: '' }, - { type: 'nack', parameter: 'pli' }, - { type: 'ccm', parameter: 'fir' }, - { type: 'goog-remb', parameter: '' } - ] - }, - { - mimeType : 'video/rtx', - kind : 'video', - preferredPayloadType : 102, - clockRate : 90000, - parameters : - { - apt : 101 - }, - rtcpFeedback : [] - } - ], - headerExtensions : - [ - { - kind : 'audio', - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - preferredId : 1, - preferredEncrypt : false - }, - { - kind : 'video', - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - preferredId : 1, - preferredEncrypt : false - }, - { - kind : 'video', - uri : 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', - preferredId : 2, - preferredEncrypt : false - }, - { - kind : 'audio', - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', // eslint-disable-line max-len - preferredId : 4, - preferredEncrypt : false - }, - { - kind : 'video', - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', // eslint-disable-line max-len - preferredId : 4, - preferredEncrypt : false - }, - { - kind : 'audio', - uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', - preferredId : 10, - preferredEncrypt : false - }, - { - kind : 'video', - uri : 'urn:3gpp:video-orientation', - preferredId : 11, - preferredEncrypt : false - }, - { - kind : 'video', - uri : 'urn:ietf:params:rtp-hdrext:toffset', - preferredId : 12, - preferredEncrypt : false - } - ] -}; - -beforeAll(async () => -{ - worker = await createWorker(); - router = await worker.createRouter({ mediaCodecs }); - transport1 = await router.createWebRtcTransport( - { - listenIps : [ '127.0.0.1' ] - }); - transport2 = await router.createWebRtcTransport( - { - listenIps : [ '127.0.0.1' ] - }); - audioProducer = await transport1.produce(audioProducerParameters); - videoProducer = await transport1.produce(videoProducerParameters); - - // Pause the videoProducer. - await videoProducer.pause(); -}); - -afterAll(() => worker.close()); - -test('transport.consume() succeeds', async () => -{ - const onObserverNewConsumer1 = jest.fn(); - - transport2.observer.once('newconsumer', onObserverNewConsumer1); - - expect(router.canConsume( - { - producerId : audioProducer.id, - rtpCapabilities : consumerDeviceCapabilities - })) - .toBe(true); - - audioConsumer = await transport2.consume( - { - producerId : audioProducer.id, - rtpCapabilities : consumerDeviceCapabilities, - appData : { baz: 'LOL' } - }); - - expect(onObserverNewConsumer1).toHaveBeenCalledTimes(1); - expect(onObserverNewConsumer1).toHaveBeenCalledWith(audioConsumer); - expect(typeof audioConsumer.id).toBe('string'); - expect(audioConsumer.producerId).toBe(audioProducer.id); - expect(audioConsumer.closed).toBe(false); - expect(audioConsumer.kind).toBe('audio'); - expect(typeof audioConsumer.rtpParameters).toBe('object'); - expect(audioConsumer.rtpParameters.mid).toBe('0'); - expect(audioConsumer.rtpParameters.codecs.length).toBe(1); - expect(audioConsumer.rtpParameters.codecs[0]).toEqual( - { - mimeType : 'audio/opus', - payloadType : 100, - clockRate : 48000, - channels : 2, - parameters : - { - useinbandfec : 1, - usedtx : 1, - foo : 222.222, - bar : '333' - }, - rtcpFeedback : [] - }); - expect(audioConsumer.type).toBe('simple'); - expect(audioConsumer.paused).toBe(false); - expect(audioConsumer.producerPaused).toBe(false); - expect(audioConsumer.priority).toBe(1); - expect(audioConsumer.score).toEqual( - { score: 10, producerScore: 0, producerScores: [ 0 ] }); - expect(audioConsumer.preferredLayers).toBeUndefined(); - expect(audioConsumer.currentLayers).toBeUndefined(); - expect(audioConsumer.appData).toEqual({ baz: 'LOL' }); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - mapProducerIdConsumerIds : { [audioProducer.id]: [ audioConsumer.id ] }, - mapConsumerIdProducerId : { [audioConsumer.id]: audioProducer.id } - }); - - await expect(transport2.dump()) - .resolves - .toMatchObject( - { - id : transport2.id, - producerIds : [], - consumerIds : [ audioConsumer.id ] - }); - - const onObserverNewConsumer2 = jest.fn(); - - transport2.observer.once('newconsumer', onObserverNewConsumer2); - - expect(router.canConsume( - { - producerId : videoProducer.id, - rtpCapabilities : consumerDeviceCapabilities - })) - .toBe(true); - - videoConsumer = await transport2.consume( - { - producerId : videoProducer.id, - rtpCapabilities : consumerDeviceCapabilities, - paused : true, - preferredLayers : { spatialLayer: 12 }, - appData : { baz: 'LOL' } - }); - - expect(onObserverNewConsumer2).toHaveBeenCalledTimes(1); - expect(onObserverNewConsumer2).toHaveBeenCalledWith(videoConsumer); - expect(typeof videoConsumer.id).toBe('string'); - expect(videoConsumer.producerId).toBe(videoProducer.id); - expect(videoConsumer.closed).toBe(false); - expect(videoConsumer.kind).toBe('video'); - expect(typeof videoConsumer.rtpParameters).toBe('object'); - expect(videoConsumer.rtpParameters.mid).toBe('1'); - expect(videoConsumer.rtpParameters.codecs.length).toBe(2); - expect(videoConsumer.rtpParameters.codecs[0]).toEqual( - { - mimeType : 'video/H264', - payloadType : 103, - clockRate : 90000, - parameters : - { - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - }, - rtcpFeedback : - [ - { type: 'nack', parameter: '' }, - { type: 'nack', parameter: 'pli' }, - { type: 'ccm', parameter: 'fir' }, - { type: 'goog-remb', parameter: '' } - ] - }); - expect(videoConsumer.rtpParameters.codecs[1]).toEqual( - { - mimeType : 'video/rtx', - payloadType : 104, - clockRate : 90000, - parameters : { apt: 103 }, - rtcpFeedback : [] - }); - expect(videoConsumer.type).toBe('simulcast'); - expect(videoConsumer.paused).toBe(true); - expect(videoConsumer.producerPaused).toBe(true); - expect(videoConsumer.priority).toBe(1); - expect(videoConsumer.score).toEqual( - { score: 10, producerScore: 0, producerScores: [ 0, 0, 0, 0 ] }); - expect(videoConsumer.preferredLayers).toEqual({ spatialLayer: 3, temporalLayer: 0 }); - expect(videoConsumer.currentLayers).toBeUndefined(); - expect(videoConsumer.appData).toEqual({ baz: 'LOL' }); - - const onObserverNewConsumer3 = jest.fn(); - - transport2.observer.once('newconsumer', onObserverNewConsumer3); - - expect(router.canConsume( - { - producerId : videoProducer.id, - rtpCapabilities : consumerDeviceCapabilities - })) - .toBe(true); - - videoPipeConsumer = await transport2.consume( - { - producerId : videoProducer.id, - rtpCapabilities : consumerDeviceCapabilities, - pipe : true - }); - - expect(onObserverNewConsumer3).toHaveBeenCalledTimes(1); - expect(onObserverNewConsumer3).toHaveBeenCalledWith(videoPipeConsumer); - expect(typeof videoPipeConsumer.id).toBe('string'); - expect(videoPipeConsumer.producerId).toBe(videoProducer.id); - expect(videoPipeConsumer.closed).toBe(false); - expect(videoPipeConsumer.kind).toBe('video'); - expect(typeof videoPipeConsumer.rtpParameters).toBe('object'); - expect(videoPipeConsumer.rtpParameters.mid).toBeUndefined(); - expect(videoPipeConsumer.rtpParameters.codecs.length).toBe(2); - expect(videoPipeConsumer.rtpParameters.codecs[0]).toEqual( - { - mimeType : 'video/H264', - payloadType : 103, - clockRate : 90000, - parameters : - { - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - }, - rtcpFeedback : - [ - { type: 'nack', parameter: '' }, - { type: 'nack', parameter: 'pli' }, - { type: 'ccm', parameter: 'fir' }, - { type: 'goog-remb', parameter: '' } - ] - }); - expect(videoPipeConsumer.rtpParameters.codecs[1]).toEqual( - { - mimeType : 'video/rtx', - payloadType : 104, - clockRate : 90000, - parameters : { apt: 103 }, - rtcpFeedback : [] - }); - expect(videoPipeConsumer.type).toBe('pipe'); - expect(videoPipeConsumer.paused).toBe(false); - expect(videoPipeConsumer.producerPaused).toBe(true); - expect(videoPipeConsumer.priority).toBe(1); - expect(videoPipeConsumer.score).toEqual( - { score: 10, producerScore: 10, producerScores: [ 0, 0, 0, 0 ] }); - expect(videoPipeConsumer.preferredLayers).toBeUndefined(); - expect(videoPipeConsumer.currentLayers).toBeUndefined(); - expect(videoPipeConsumer.appData).toBeUndefined; - - const dump = await router.dump(); - - for (const key of Object.keys(dump.mapProducerIdConsumerIds)) - { - dump.mapProducerIdConsumerIds[key] = dump.mapProducerIdConsumerIds[key].sort(); - } - - expect(dump).toMatchObject( - { - mapProducerIdConsumerIds : - { - [audioProducer.id] : [ audioConsumer.id ], - [videoProducer.id] : [ videoConsumer.id, videoPipeConsumer.id ].sort() - }, - mapConsumerIdProducerId : - { - [audioConsumer.id] : audioProducer.id, - [videoConsumer.id] : videoProducer.id, - [videoPipeConsumer.id] : videoProducer.id - } - }); - - await expect(transport2.dump()) - .resolves - .toMatchObject( - { - id : transport2.id, - producerIds : [], - consumerIds : expect.arrayContaining( - [ - audioConsumer.id, - videoConsumer.id, - videoPipeConsumer.id - ]) - }); -}, 2000); - -test('transport.consume() can be created with user provided mid', async () => -{ - const audioConsumer1 = await transport2.consume( - { - producerId : audioProducer.id, - rtpCapabilities : consumerDeviceCapabilities - }); - - expect(audioConsumer1.rtpParameters.mid).toEqual( - expect.stringMatching(/^[0-9]+/)); - - const audioConsumer2 = await transport2.consume( - { - producerId : audioProducer.id, - mid : 'custom-mid', - rtpCapabilities : consumerDeviceCapabilities - }); - - expect(audioConsumer2.rtpParameters.mid).toBe('custom-mid'); - - const audioConsumer3 = await transport2.consume( - { - producerId : audioProducer.id, - rtpCapabilities : consumerDeviceCapabilities - }); - - expect(audioConsumer3.rtpParameters.mid).toEqual( - expect.stringMatching(/^[0-9]+/)); - expect(Number(audioConsumer1.rtpParameters.mid) + 1).toBe( - Number(audioConsumer3.rtpParameters.mid)); - - audioConsumer3.close(); - audioConsumer2.close(); - audioConsumer1.close(); -}, 2000); - -test('transport.consume() with incompatible rtpCapabilities rejects with UnsupportedError', async () => -{ - let invalidDeviceCapabilities: mediasoup.types.RtpCapabilities; - - invalidDeviceCapabilities = - { - codecs : - [ - { - kind : 'audio', - mimeType : 'audio/ISAC', - preferredPayloadType : 100, - clockRate : 32000, - channels : 1 - } - ], - headerExtensions : [] - }; - - expect(router.canConsume( - { producerId: audioProducer.id, rtpCapabilities: invalidDeviceCapabilities })) - .toBe(false); - - await expect(transport2.consume( - { - producerId : audioProducer.id, - rtpCapabilities : invalidDeviceCapabilities - })) - .rejects - .toThrow(UnsupportedError); - - invalidDeviceCapabilities = - { - codecs : [], - headerExtensions : [] - }; - - expect(router.canConsume( - { producerId: audioProducer.id, rtpCapabilities: invalidDeviceCapabilities })) - .toBe(false); - - await expect(transport2.consume( - { - producerId : audioProducer.id, - rtpCapabilities : invalidDeviceCapabilities - })) - .rejects - .toThrow(UnsupportedError); -}, 2000); - -test('consumer.dump() succeeds', async () => -{ - let data; - - data = await audioConsumer.dump(); - - expect(data.id).toBe(audioConsumer.id); - expect(data.producerId).toBe(audioConsumer.producerId); - expect(data.kind).toBe(audioConsumer.kind); - expect(typeof data.rtpParameters).toBe('object'); - expect(Array.isArray(data.rtpParameters.codecs)).toBe(true); - expect(data.rtpParameters.codecs.length).toBe(1); - expect(data.rtpParameters.codecs[0].mimeType).toBe('audio/opus'); - expect(data.rtpParameters.codecs[0].payloadType).toBe(100); - expect(data.rtpParameters.codecs[0].clockRate).toBe(48000); - expect(data.rtpParameters.codecs[0].channels).toBe(2); - expect(data.rtpParameters.codecs[0].parameters) - .toEqual( - { - useinbandfec : 1, - usedtx : 1, - foo : 222.222, - bar : '333' - }); - expect(data.rtpParameters.codecs[0].rtcpFeedback).toEqual([]); - expect(Array.isArray(data.rtpParameters.headerExtensions)).toBe(true); - expect(data.rtpParameters.headerExtensions.length).toBe(3); - expect(data.rtpParameters.headerExtensions).toEqual( - [ - { - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - id : 1, - encrypt : false, - parameters : {} - }, - { - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', - id : 4, - parameters : {}, - encrypt : false - }, - { - uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', - id : 10, - parameters : {}, - encrypt : false - } - ]); - expect(Array.isArray(data.rtpParameters.encodings)).toBe(true); - expect(data.rtpParameters.encodings.length).toBe(1); - expect(data.rtpParameters.encodings).toEqual( - [ - { - codecPayloadType : 100, - ssrc : audioConsumer.rtpParameters.encodings?.[0].ssrc - } - ]); - expect(data.type).toBe('simple'); - expect(Array.isArray(data.consumableRtpEncodings)).toBe(true); - expect(data.consumableRtpEncodings.length).toBe(1); - expect(data.consumableRtpEncodings).toEqual( - [ - { ssrc: audioProducer.consumableRtpParameters.encodings?.[0].ssrc } - ]); - expect(data.supportedCodecPayloadTypes).toEqual([ 100 ]); - expect(data.paused).toBe(false); - expect(data.producerPaused).toBe(false); - expect(data.priority).toBe(1); - - data = await videoConsumer.dump(); - - expect(data.id).toBe(videoConsumer.id); - expect(data.producerId).toBe(videoConsumer.producerId); - expect(data.kind).toBe(videoConsumer.kind); - expect(typeof data.rtpParameters).toBe('object'); - expect(Array.isArray(data.rtpParameters.codecs)).toBe(true); - expect(data.rtpParameters.codecs.length).toBe(2); - expect(data.rtpParameters.codecs[0].mimeType).toBe('video/H264'); - expect(data.rtpParameters.codecs[0].payloadType).toBe(103); - expect(data.rtpParameters.codecs[0].clockRate).toBe(90000); - expect(data.rtpParameters.codecs[0].channels).toBeUndefined(); - expect(data.rtpParameters.codecs[0].parameters) - .toEqual( - { - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - }); - expect(data.rtpParameters.codecs[0].rtcpFeedback).toEqual( - [ - { type: 'nack' }, - { type: 'nack', parameter: 'pli' }, - { type: 'ccm', parameter: 'fir' }, - { type: 'goog-remb' } - ]); - expect(Array.isArray(data.rtpParameters.headerExtensions)).toBe(true); - expect(data.rtpParameters.headerExtensions.length).toBe(4); - expect(data.rtpParameters.headerExtensions).toEqual( - [ - { - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - id : 1, - encrypt : false, - parameters : {} - }, - { - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', - id : 4, - parameters : {}, - encrypt : false - }, - { - uri : 'urn:3gpp:video-orientation', - id : 11, - parameters : {}, - encrypt : false - }, - { - uri : 'urn:ietf:params:rtp-hdrext:toffset', - id : 12, - parameters : {}, - encrypt : false - } - ]); - expect(Array.isArray(data.rtpParameters.encodings)).toBe(true); - expect(data.rtpParameters.encodings.length).toBe(1); - expect(data.rtpParameters.encodings).toMatchObject( - [ - { - codecPayloadType : 103, - ssrc : videoConsumer.rtpParameters.encodings?.[0].ssrc, - rtx : - { - ssrc : videoConsumer.rtpParameters.encodings?.[0].rtx?.ssrc - }, - scalabilityMode : 'S4T1' - } - ]); - expect(Array.isArray(data.consumableRtpEncodings)).toBe(true); - expect(data.consumableRtpEncodings.length).toBe(4); - expect(data.consumableRtpEncodings).toEqual( - [ - { ssrc: videoProducer.consumableRtpParameters.encodings?.[0].ssrc }, - { ssrc: videoProducer.consumableRtpParameters.encodings?.[1].ssrc }, - { ssrc: videoProducer.consumableRtpParameters.encodings?.[2].ssrc }, - { ssrc: videoProducer.consumableRtpParameters.encodings?.[3].ssrc } - ]); - expect(data.supportedCodecPayloadTypes).toEqual([ 103 ]); - expect(data.paused).toBe(true); - expect(data.producerPaused).toBe(true); - expect(data.priority).toBe(1); -}, 2000); - -test('consumer.getStats() succeeds', async () => -{ - await expect(audioConsumer.getStats()) - .resolves - .toEqual( - [ - expect.objectContaining( - { - type : 'outbound-rtp', - kind : 'audio', - mimeType : 'audio/opus', - ssrc : audioConsumer.rtpParameters.encodings?.[0].ssrc - }) - ]); - - await expect(videoConsumer.getStats()) - .resolves - .toEqual( - [ - expect.objectContaining( - { - type : 'outbound-rtp', - kind : 'video', - mimeType : 'video/H264', - ssrc : videoConsumer.rtpParameters.encodings?.[0].ssrc - }) - ]); -}, 2000); - -test('consumer.pause() and resume() succeed', async () => -{ - await audioConsumer.pause(); - expect(audioConsumer.paused).toBe(true); - - await expect(audioConsumer.dump()) - .resolves - .toMatchObject({ paused: true }); - - await audioConsumer.resume(); - expect(audioConsumer.paused).toBe(false); - - await expect(audioConsumer.dump()) - .resolves - .toMatchObject({ paused: false }); -}, 2000); - -test('consumer.setPreferredLayers() succeed', async () => -{ - await audioConsumer.setPreferredLayers({ spatialLayer: 1, temporalLayer: 1 }); - expect(audioConsumer.preferredLayers).toBeUndefined(); - - await videoConsumer.setPreferredLayers({ spatialLayer: 2, temporalLayer: 3 }); - expect(videoConsumer.preferredLayers).toEqual({ spatialLayer: 2, temporalLayer: 0 }); -}, 2000); - -test('consumer.setPreferredLayers() with wrong arguments rejects with TypeError', async () => -{ - // @ts-ignore - await expect(videoConsumer.setPreferredLayers({})) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(videoConsumer.setPreferredLayers({ foo: '123' })) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(videoConsumer.setPreferredLayers('foo')) - .rejects - .toThrow(TypeError); - - // Missing spatialLayer. - // @ts-ignore - await expect(videoConsumer.setPreferredLayers({ temporalLayer: 2 })) - .rejects - .toThrow(TypeError); -}, 2000); - -test('consumer.setPriority() succeed', async () => -{ - await videoConsumer.setPriority(2); - expect(videoConsumer.priority).toBe(2); -}, 2000); - -test('consumer.setPriority() with wrong arguments rejects with TypeError', async () => -{ - // @ts-ignore - await expect(videoConsumer.setPriority()) - .rejects - .toThrow(TypeError); - - await expect(videoConsumer.setPriority(0)) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(videoConsumer.setPriority('foo')) - .rejects - .toThrow(TypeError); -}, 2000); - -test('consumer.unsetPriority() succeed', async () => -{ - await videoConsumer.unsetPriority(); - expect(videoConsumer.priority).toBe(1); -}, 2000); - -test('consumer.enableTraceEvent() succeed', async () => -{ - await audioConsumer.enableTraceEvent([ 'rtp', 'pli' ]); - await expect(audioConsumer.dump()) - .resolves - .toMatchObject({ traceEventTypes: 'rtp,pli' }); - - await audioConsumer.enableTraceEvent([]); - await expect(audioConsumer.dump()) - .resolves - .toMatchObject({ traceEventTypes: '' }); - - // @ts-ignore - await audioConsumer.enableTraceEvent([ 'nack', 'FOO', 'fir' ]); - await expect(audioConsumer.dump()) - .resolves - .toMatchObject({ traceEventTypes: 'nack,fir' }); - - await audioConsumer.enableTraceEvent(); - await expect(audioConsumer.dump()) - .resolves - .toMatchObject({ traceEventTypes: '' }); -}, 2000); - -test('consumer.enableTraceEvent() with wrong arguments rejects with TypeError', async () => -{ - // @ts-ignore - await expect(audioConsumer.enableTraceEvent(123)) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(audioConsumer.enableTraceEvent('rtp')) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(audioConsumer.enableTraceEvent([ 'fir', 123.123 ])) - .rejects - .toThrow(TypeError); -}, 2000); - -test('Consumer emits "producerpause" and "producerresume"', async () => -{ - await new Promise((resolve) => - { - audioConsumer.on('producerpause', resolve); - audioProducer.pause(); - }); - - expect(audioConsumer.paused).toBe(false); - expect(audioConsumer.producerPaused).toBe(true); - - await new Promise((resolve) => - { - audioConsumer.on('producerresume', resolve); - audioProducer.resume(); - }); - - expect(audioConsumer.paused).toBe(false); - expect(audioConsumer.producerPaused).toBe(false); -}, 2000); - -test('Consumer emits "score"', async () => -{ - // Private API. - const channel = audioConsumer.channelForTesting; - const onScore = jest.fn(); - - audioConsumer.on('score', onScore); - - channel.emit(audioConsumer.id, 'score', { producer: 10, consumer: 9 }); - channel.emit(audioConsumer.id, 'score', { producer: 9, consumer: 9 }); - channel.emit(audioConsumer.id, 'score', { producer: 8, consumer: 8 }); - - expect(onScore).toHaveBeenCalledTimes(3); - expect(audioConsumer.score).toEqual({ producer: 8, consumer: 8 }); -}, 2000); - -test('consumer.close() succeeds', async () => -{ - const onObserverClose = jest.fn(); - - audioConsumer.observer.once('close', onObserverClose); - audioConsumer.close(); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(audioConsumer.closed).toBe(true); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - mapProducerIdConsumerIds : { [audioProducer.id]: [] }, - mapConsumerIdProducerId : {} - }); - - const dump = await transport2.dump(); - - dump.consumerIds = dump.consumerIds.sort(); - - expect(dump) - .toMatchObject( - { - id : transport2.id, - producerIds : [], - consumerIds : [ videoConsumer.id, videoPipeConsumer.id ].sort() - }); -}, 2000); - -test('Consumer methods reject if closed', async () => -{ - await expect(audioConsumer.dump()) - .rejects - .toThrow(Error); - - await expect(audioConsumer.getStats()) - .rejects - .toThrow(Error); - - await expect(audioConsumer.pause()) - .rejects - .toThrow(Error); - - await expect(audioConsumer.resume()) - .rejects - .toThrow(Error); - - // @ts-ignore - await expect(audioConsumer.setPreferredLayers({})) - .rejects - .toThrow(Error); - - await expect(audioConsumer.setPriority(2)) - .rejects - .toThrow(Error); - - await expect(audioConsumer.requestKeyFrame()) - .rejects - .toThrow(Error); -}, 2000); - -test('Consumer emits "producerclose" if Producer is closed', async () => -{ - audioConsumer = await transport2.consume( - { - producerId : audioProducer.id, - rtpCapabilities : consumerDeviceCapabilities - }); - - const onObserverClose = jest.fn(); - - audioConsumer.observer.once('close', onObserverClose); - - await new Promise((resolve) => - { - audioConsumer.on('producerclose', resolve); - audioProducer.close(); - }); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(audioConsumer.closed).toBe(true); -}, 2000); - -test('Consumer emits "transportclose" if Transport is closed', async () => -{ - videoConsumer = await transport2.consume( - { - producerId : videoProducer.id, - rtpCapabilities : consumerDeviceCapabilities - }); - - const onObserverClose = jest.fn(); - - videoConsumer.observer.once('close', onObserverClose); - - await new Promise((resolve) => - { - videoConsumer.on('transportclose', resolve); - transport2.close(); - }); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(videoConsumer.closed).toBe(true); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - mapProducerIdConsumerIds : {}, - mapConsumerIdProducerId : {} - }); -}, 2000); diff --git a/node/src/tests/test-DataConsumer.ts b/node/src/tests/test-DataConsumer.ts deleted file mode 100644 index 08241fcf3e..0000000000 --- a/node/src/tests/test-DataConsumer.ts +++ /dev/null @@ -1,274 +0,0 @@ -import * as mediasoup from '../'; - -const { createWorker } = mediasoup; - -let worker: mediasoup.types.Worker; -let router: mediasoup.types.Router; -let transport1: mediasoup.types.WebRtcTransport; -let transport2: mediasoup.types.PlainTransport; -let transport3: mediasoup.types.DirectTransport; -let dataProducer: mediasoup.types.DataProducer; -let dataConsumer1: mediasoup.types.DataConsumer; -let dataConsumer2: mediasoup.types.DataConsumer; - -const dataProducerParameters: mediasoup.types.DataProducerOptions = -{ - sctpStreamParameters : - { - streamId : 12345, - ordered : false, - maxPacketLifeTime : 5000 - }, - label : 'foo', - protocol : 'bar' -}; - -beforeAll(async () => -{ - worker = await createWorker(); - router = await worker.createRouter(); - transport1 = await router.createWebRtcTransport( - { - listenIps : [ '127.0.0.1' ], - enableSctp : true - }); - transport2 = await router.createPlainTransport( - { - listenIp : '127.0.0.1', - enableSctp : true - }); - transport3 = await router.createDirectTransport(); - dataProducer = await transport1.produceData(dataProducerParameters); -}); - -afterAll(() => worker.close()); - -test('transport.consumeData() succeeds', async () => -{ - const onObserverNewDataConsumer = jest.fn(); - - transport2.observer.once('newdataconsumer', onObserverNewDataConsumer); - - dataConsumer1 = await transport2.consumeData( - { - dataProducerId : dataProducer.id, - maxPacketLifeTime : 4000, - appData : { baz: 'LOL' } - }); - - expect(onObserverNewDataConsumer).toHaveBeenCalledTimes(1); - expect(onObserverNewDataConsumer).toHaveBeenCalledWith(dataConsumer1); - expect(typeof dataConsumer1.id).toBe('string'); - expect(dataConsumer1.dataProducerId).toBe(dataProducer.id); - expect(dataConsumer1.closed).toBe(false); - expect(dataConsumer1.type).toBe('sctp'); - expect(typeof dataConsumer1.sctpStreamParameters).toBe('object'); - expect(typeof dataConsumer1.sctpStreamParameters?.streamId).toBe('number'); - expect(dataConsumer1.sctpStreamParameters?.ordered).toBe(false); - expect(dataConsumer1.sctpStreamParameters?.maxPacketLifeTime).toBe(4000); - expect(dataConsumer1.sctpStreamParameters?.maxRetransmits).toBeUndefined(); - expect(dataConsumer1.label).toBe('foo'); - expect(dataConsumer1.protocol).toBe('bar'); - expect(dataConsumer1.appData).toEqual({ baz: 'LOL' }); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - mapDataProducerIdDataConsumerIds : { [dataProducer.id]: [ dataConsumer1.id ] }, - mapDataConsumerIdDataProducerId : { [dataConsumer1.id]: dataProducer.id } - }); - - await expect(transport2.dump()) - .resolves - .toMatchObject( - { - id : transport2.id, - dataProducerIds : [], - dataConsumerIds : [ dataConsumer1.id ] - }); -}, 2000); - -test('dataConsumer.dump() succeeds', async () => -{ - const data = await dataConsumer1.dump(); - - expect(data.id).toBe(dataConsumer1.id); - expect(data.dataProducerId).toBe(dataConsumer1.dataProducerId); - expect(data.type).toBe('sctp'); - expect(typeof data.sctpStreamParameters).toBe('object'); - expect(data.sctpStreamParameters.streamId) - .toBe(dataConsumer1.sctpStreamParameters?.streamId); - expect(data.sctpStreamParameters.ordered).toBe(false); - expect(data.sctpStreamParameters.maxPacketLifeTime).toBe(4000); - expect(data.sctpStreamParameters.maxRetransmits).toBeUndefined(); - expect(data.label).toBe('foo'); - expect(data.protocol).toBe('bar'); -}, 2000); - -test('dataConsumer.getStats() succeeds', async () => -{ - await expect(dataConsumer1.getStats()) - .resolves - .toMatchObject( - [ - { - type : 'data-consumer', - label : dataConsumer1.label, - protocol : dataConsumer1.protocol, - messagesSent : 0, - bytesSent : 0 - } - ]); -}, 2000); - -test('transport.consumeData() on a DirectTransport succeeds', async () => -{ - const onObserverNewDataConsumer = jest.fn(); - - transport3.observer.once('newdataconsumer', onObserverNewDataConsumer); - - dataConsumer2 = await transport3.consumeData( - { - dataProducerId : dataProducer.id, - appData : { hehe: 'HEHE' } - }); - - expect(onObserverNewDataConsumer).toHaveBeenCalledTimes(1); - expect(onObserverNewDataConsumer).toHaveBeenCalledWith(dataConsumer2); - expect(typeof dataConsumer2.id).toBe('string'); - expect(dataConsumer2.dataProducerId).toBe(dataProducer.id); - expect(dataConsumer2.closed).toBe(false); - expect(dataConsumer2.type).toBe('direct'); - expect(dataConsumer2.sctpStreamParameters).toBeUndefined(); - expect(dataConsumer2.label).toBe('foo'); - expect(dataConsumer2.protocol).toBe('bar'); - expect(dataConsumer2.appData).toEqual({ hehe: 'HEHE' }); - - await expect(transport3.dump()) - .resolves - .toMatchObject( - { - id : transport3.id, - dataProducerIds : [], - dataConsumerIds : [ dataConsumer2.id ] - }); -}, 2000); - -test('dataConsumer.dump() on a DirectTransport succeeds', async () => -{ - const data = await dataConsumer2.dump(); - - expect(data.id).toBe(dataConsumer2.id); - expect(data.dataProducerId).toBe(dataConsumer2.dataProducerId); - expect(data.type).toBe('direct'); - expect(data.sctpStreamParameters).toBeUndefined(); - expect(data.label).toBe('foo'); - expect(data.protocol).toBe('bar'); -}, 2000); - -test('dataConsumer.getStats() on a DirectTransport succeeds', async () => -{ - await expect(dataConsumer2.getStats()) - .resolves - .toMatchObject( - [ - { - type : 'data-consumer', - label : dataConsumer2.label, - protocol : dataConsumer2.protocol, - messagesSent : 0, - bytesSent : 0 - } - ]); -}, 2000); - -test('dataConsumer.close() succeeds', async () => -{ - const onObserverClose = jest.fn(); - - dataConsumer1.observer.once('close', onObserverClose); - dataConsumer1.close(); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(dataConsumer1.closed).toBe(true); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - mapDataProducerIdDataConsumerIds : { [dataProducer.id]: [ dataConsumer2.id ] }, - mapDataConsumerIdDataProducerId : { [dataConsumer2.id]: dataProducer.id } - }); - - await expect(transport2.dump()) - .resolves - .toMatchObject( - { - id : transport2.id, - dataProducerIds : [], - dataConsumerIds : [] - }); -}, 2000); - -test('Consumer methods reject if closed', async () => -{ - await expect(dataConsumer1.dump()) - .rejects - .toThrow(Error); - - await expect(dataConsumer1.getStats()) - .rejects - .toThrow(Error); -}, 2000); - -test('DataConsumer emits "dataproducerclose" if DataProducer is closed', async () => -{ - dataConsumer1 = await transport2.consumeData( - { - dataProducerId : dataProducer.id - }); - - const onObserverClose = jest.fn(); - - dataConsumer1.observer.once('close', onObserverClose); - - await new Promise((resolve) => - { - dataConsumer1.on('dataproducerclose', resolve); - dataProducer.close(); - }); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(dataConsumer1.closed).toBe(true); -}, 2000); - -test('DataConsumer emits "transportclose" if Transport is closed', async () => -{ - dataProducer = await transport1.produceData(dataProducerParameters); - dataConsumer1 = await transport2.consumeData( - { - dataProducerId : dataProducer.id - }); - - const onObserverClose = jest.fn(); - - dataConsumer1.observer.once('close', onObserverClose); - - await new Promise((resolve) => - { - dataConsumer1.on('transportclose', resolve); - transport2.close(); - }); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(dataConsumer1.closed).toBe(true); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - mapDataProducerIdDataConsumerIds : {}, - mapDataConsumerIdDataProducerId : {} - }); -}, 2000); diff --git a/node/src/tests/test-DataProducer.ts b/node/src/tests/test-DataProducer.ts deleted file mode 100644 index 711fde3d79..0000000000 --- a/node/src/tests/test-DataProducer.ts +++ /dev/null @@ -1,284 +0,0 @@ -import * as mediasoup from '../'; - -const { createWorker } = mediasoup; - -let worker: mediasoup.types.Worker; -let router: mediasoup.types.Router; -let transport1: mediasoup.types.WebRtcTransport; -let transport2: mediasoup.types.PlainTransport; -let dataProducer1: mediasoup.types.DataProducer; -let dataProducer2: mediasoup.types.DataProducer; - -beforeAll(async () => -{ - worker = await createWorker(); - router = await worker.createRouter(); - transport1 = await router.createWebRtcTransport( - { - listenIps : [ '127.0.0.1' ], - enableSctp : true - }); - transport2 = await router.createPlainTransport( - { - listenIp : '127.0.0.1', - enableSctp : true - }); -}); - -afterAll(() => worker.close()); - -test('transport1.produceData() succeeds', async () => -{ - const onObserverNewDataProducer = jest.fn(); - - transport1.observer.once('newdataproducer', onObserverNewDataProducer); - - dataProducer1 = await transport1.produceData( - { - sctpStreamParameters : - { - streamId : 666 - }, - label : 'foo', - protocol : 'bar', - appData : { foo: 1, bar: '2' } - }); - - expect(onObserverNewDataProducer).toHaveBeenCalledTimes(1); - expect(onObserverNewDataProducer).toHaveBeenCalledWith(dataProducer1); - expect(typeof dataProducer1.id).toBe('string'); - expect(dataProducer1.closed).toBe(false); - expect(dataProducer1.type).toBe('sctp'); - expect(typeof dataProducer1.sctpStreamParameters).toBe('object'); - expect(dataProducer1.sctpStreamParameters?.streamId).toBe(666); - expect(dataProducer1.sctpStreamParameters?.ordered).toBe(true); - expect(dataProducer1.sctpStreamParameters?.maxPacketLifeTime).toBeUndefined(); - expect(dataProducer1.sctpStreamParameters?.maxRetransmits).toBeUndefined(); - expect(dataProducer1.label).toBe('foo'); - expect(dataProducer1.protocol).toBe('bar'); - expect(dataProducer1.appData).toEqual({ foo: 1, bar: '2' }); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - mapDataProducerIdDataConsumerIds : { [dataProducer1.id]: [] }, - mapDataConsumerIdDataProducerId : {} - }); - - await expect(transport1.dump()) - .resolves - .toMatchObject( - { - id : transport1.id, - dataProducerIds : [ dataProducer1.id ], - dataConsumerIds : [] - }); -}, 2000); - -test('transport2.produceData() succeeds', async () => -{ - const onObserverNewDataProducer = jest.fn(); - - transport2.observer.once('newdataproducer', onObserverNewDataProducer); - - dataProducer2 = await transport2.produceData( - { - sctpStreamParameters : - { - streamId : 777, - maxRetransmits : 3 - }, - label : 'foo', - protocol : 'bar', - appData : { foo: 1, bar: '2' } - }); - - expect(onObserverNewDataProducer).toHaveBeenCalledTimes(1); - expect(onObserverNewDataProducer).toHaveBeenCalledWith(dataProducer2); - expect(typeof dataProducer2.id).toBe('string'); - expect(dataProducer2.closed).toBe(false); - expect(dataProducer2.type).toBe('sctp'); - expect(typeof dataProducer2.sctpStreamParameters).toBe('object'); - expect(dataProducer2.sctpStreamParameters?.streamId).toBe(777); - expect(dataProducer2.sctpStreamParameters?.ordered).toBe(false); - expect(dataProducer2.sctpStreamParameters?.maxPacketLifeTime).toBeUndefined(); - expect(dataProducer2.sctpStreamParameters?.maxRetransmits).toBe(3); - expect(dataProducer2.label).toBe('foo'); - expect(dataProducer2.protocol).toBe('bar'); - expect(dataProducer2.appData).toEqual({ foo: 1, bar: '2' }); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - mapDataProducerIdDataConsumerIds : { [dataProducer2.id]: [] }, - mapDataConsumerIdDataProducerId : {} - }); - - await expect(transport2.dump()) - .resolves - .toMatchObject( - { - id : transport2.id, - dataProducerIds : [ dataProducer2.id ], - dataConsumerIds : [] - }); -}, 2000); - -test('transport1.produceData() with wrong arguments rejects with TypeError', async () => -{ - await expect(transport1.produceData({})) - .rejects - .toThrow(TypeError); - - // Missing or empty sctpStreamParameters.streamId. - await expect(transport1.produceData( - { - // @ts-ignore - sctpStreamParameters : { foo: 'foo' } - })) - .rejects - .toThrow(TypeError); -}, 2000); - -test('transport.produceData() with already used streamId rejects with Error', async () => -{ - await expect(transport1.produceData( - { - sctpStreamParameters : - { - streamId : 666 - } - })) - .rejects - .toThrow(Error); -}, 2000); - -test('transport.produceData() with ordered and maxPacketLifeTime rejects with TypeError', async () => -{ - await expect(transport1.produceData( - { - sctpStreamParameters : - { - streamId : 999, - ordered : true, - maxPacketLifeTime : 4000 - } - })) - .rejects - .toThrow(TypeError); -}, 2000); - -test('dataProducer.dump() succeeds', async () => -{ - let data; - - data = await dataProducer1.dump(); - - expect(data.id).toBe(dataProducer1.id); - expect(data.type).toBe('sctp'); - expect(typeof data.sctpStreamParameters).toBe('object'); - expect(data.sctpStreamParameters.streamId).toBe(666); - expect(data.sctpStreamParameters.ordered).toBe(true); - expect(data.sctpStreamParameters.maxPacketLifeTime).toBeUndefined(); - expect(data.sctpStreamParameters.maxRetransmits).toBeUndefined(); - expect(data.label).toBe('foo'); - expect(data.protocol).toBe('bar'); - - data = await dataProducer2.dump(); - - expect(data.id).toBe(dataProducer2.id); - expect(data.type).toBe('sctp'); - expect(typeof data.sctpStreamParameters).toBe('object'); - expect(data.sctpStreamParameters.streamId).toBe(777); - expect(data.sctpStreamParameters.ordered).toBe(false); - expect(data.sctpStreamParameters.maxPacketLifeTime).toBeUndefined(); - expect(data.sctpStreamParameters.maxRetransmits).toBe(3); - expect(data.label).toBe('foo'); - expect(data.protocol).toBe('bar'); -}, 2000); - -test('dataProducer.getStats() succeeds', async () => -{ - await expect(dataProducer1.getStats()) - .resolves - .toMatchObject( - [ - { - type : 'data-producer', - label : dataProducer1.label, - protocol : dataProducer1.protocol, - messagesReceived : 0, - bytesReceived : 0 - } - ]); - - await expect(dataProducer2.getStats()) - .resolves - .toMatchObject( - [ - { - type : 'data-producer', - label : dataProducer2.label, - protocol : dataProducer2.protocol, - messagesReceived : 0, - bytesReceived : 0 - } - ]); -}, 2000); - -test('dataProducer.close() succeeds', async () => -{ - const onObserverClose = jest.fn(); - - dataProducer1.observer.once('close', onObserverClose); - dataProducer1.close(); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(dataProducer1.closed).toBe(true); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - mapDataProducerIdDataConsumerIds : {}, - mapDataConsumerIdDataProducerId : {} - }); - - await expect(transport1.dump()) - .resolves - .toMatchObject( - { - id : transport1.id, - dataProducerIds : [], - dataConsumerIds : [] - }); -}, 2000); - -test('DataProducer methods reject if closed', async () => -{ - await expect(dataProducer1.dump()) - .rejects - .toThrow(Error); - - await expect(dataProducer1.getStats()) - .rejects - .toThrow(Error); -}, 2000); - -test('DataProducer emits "transportclose" if Transport is closed', async () => -{ - const onObserverClose = jest.fn(); - - dataProducer2.observer.once('close', onObserverClose); - - await new Promise((resolve) => - { - dataProducer2.on('transportclose', resolve); - transport2.close(); - }); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(dataProducer2.closed).toBe(true); -}, 2000); diff --git a/node/src/tests/test-DirectTransport.ts b/node/src/tests/test-DirectTransport.ts deleted file mode 100644 index a00e88840d..0000000000 --- a/node/src/tests/test-DirectTransport.ts +++ /dev/null @@ -1,257 +0,0 @@ -import * as mediasoup from '../'; - -const { createWorker } = mediasoup; - -let worker: mediasoup.types.Worker; -let router: mediasoup.types.Router; -let transport: mediasoup.types.DirectTransport; - -beforeAll(async () => -{ - worker = await createWorker(); - router = await worker.createRouter(); -}); - -afterAll(() => worker.close()); - -beforeEach(async () => -{ - transport = await router.createDirectTransport(); -}); - -afterEach(() => transport.close()); - -test('router.createDirectTransport() succeeds', async () => -{ - await expect(router.dump()) - .resolves - .toMatchObject({ transportIds: [ transport.id ] }); - - const onObserverNewTransport = jest.fn(); - - router.observer.once('newtransport', onObserverNewTransport); - - // Create a separate transport here. - const transport1 = await router.createDirectTransport( - { - maxMessageSize : 1024, - appData : { foo: 'bar' } - }); - - expect(onObserverNewTransport).toHaveBeenCalledTimes(1); - expect(onObserverNewTransport).toHaveBeenCalledWith(transport1); - expect(typeof transport1.id).toBe('string'); - expect(transport1.closed).toBe(false); - expect(transport1.appData).toEqual({ foo: 'bar' }); - - const data1 = await transport1.dump(); - - expect(data1.id).toBe(transport1.id); - expect(data1.direct).toBe(true); - expect(data1.producerIds).toEqual([]); - expect(data1.consumerIds).toEqual([]); - expect(data1.dataProducerIds).toEqual([]); - expect(data1.dataConsumerIds).toEqual([]); - expect(typeof data1.recvRtpHeaderExtensions).toBe('object'); - expect(typeof data1.rtpListener).toBe('object'); - - transport1.close(); - expect(transport1.closed).toBe(true); -}, 2000); - -test('router.createDirectTransport() with wrong arguments rejects with TypeError', async () => -{ - // @ts-ignore - await expect(router.createDirectTransport({ maxMessageSize: 'foo' })) - .rejects - .toThrow(TypeError); - - await expect(router.createDirectTransport({ maxMessageSize: -2000 })) - .rejects - .toThrow(TypeError); -}, 2000); - -test('directTransport.getStats() succeeds', async () => -{ - const data = await transport.getStats(); - - expect(Array.isArray(data)).toBe(true); - expect(data.length).toBe(1); - expect(data[0].type).toBe('direct-transport'); - expect(data[0].transportId).toBe(transport.id); - expect(typeof data[0].timestamp).toBe('number'); - expect(data[0].bytesReceived).toBe(0); - expect(data[0].recvBitrate).toBe(0); - expect(data[0].bytesSent).toBe(0); - expect(data[0].sendBitrate).toBe(0); - expect(data[0].rtpBytesReceived).toBe(0); - expect(data[0].rtpRecvBitrate).toBe(0); - expect(data[0].rtpBytesSent).toBe(0); - expect(data[0].rtpSendBitrate).toBe(0); - expect(data[0].rtxBytesReceived).toBe(0); - expect(data[0].rtxRecvBitrate).toBe(0); - expect(data[0].rtxBytesSent).toBe(0); - expect(data[0].rtxSendBitrate).toBe(0); - expect(data[0].probationBytesSent).toBe(0); - expect(data[0].probationSendBitrate).toBe(0); -}, 2000); - -test('directTransport.connect() succeeds', async () => -{ - await expect(transport.connect()) - .resolves - .toBeUndefined(); -}, 2000); - -test('dataProducer.send() succeeds', async () => -{ - const transport2 = await router.createDirectTransport(); - const dataProducer = await transport2.produceData( - { - label : 'foo', - protocol : 'bar', - appData : { foo: 'bar' } - }); - const dataConsumer = await transport2.consumeData( - { - dataProducerId : dataProducer.id - }); - const numMessages = 200; - let sentMessageBytes = 0; - let recvMessageBytes = 0; - let lastSentMessageId = 0; - let lastRecvMessageId = 0; - - await new Promise((resolve) => - { - // Send messages over the sctpSendStream created above. - const interval = setInterval(() => - { - const id = ++lastSentMessageId; - let ppid; - let message; - - // Send string (WebRTC DataChannel string). - if (id < numMessages / 2) - { - message = String(id); - } - // Send string (WebRTC DataChannel binary). - else - { - message = Buffer.from(String(id)); - } - - dataProducer.send(message, ppid); - sentMessageBytes += Buffer.from(message).byteLength; - - if (id === numMessages) - clearInterval(interval); - }, 0); - - dataConsumer.on('message', (message, ppid) => - { - // message is always a Buffer. - recvMessageBytes += message.byteLength; - - const id = Number(message.toString('utf8')); - - if (id === numMessages) - { - clearInterval(interval); - resolve(); - } - - if (id < numMessages / 2) - expect(ppid).toBe(51); // PPID of WebRTC DataChannel string. - else - expect(ppid).toBe(53); // PPID of WebRTC DataChannel binary. - - expect(id).toBe(++lastRecvMessageId); - }); - }); - - expect(lastSentMessageId).toBe(numMessages); - expect(lastRecvMessageId).toBe(numMessages); - expect(recvMessageBytes).toBe(sentMessageBytes); - - await expect(dataProducer.getStats()) - .resolves - .toMatchObject( - [ - { - type : 'data-producer', - label : dataProducer.label, - protocol : dataProducer.protocol, - messagesReceived : numMessages, - bytesReceived : sentMessageBytes - } - ]); - - await expect(dataConsumer.getStats()) - .resolves - .toMatchObject( - [ - { - type : 'data-consumer', - label : dataConsumer.label, - protocol : dataConsumer.protocol, - messagesSent : numMessages, - bytesSent : recvMessageBytes - } - ]); -}, 5000); - -test('DirectTransport methods reject if closed', async () => -{ - const onObserverClose = jest.fn(); - - transport.observer.once('close', onObserverClose); - transport.close(); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(transport.closed).toBe(true); - - await expect(transport.dump()) - .rejects - .toThrow(Error); - - await expect(transport.getStats()) - .rejects - .toThrow(Error); -}, 2000); - -test('DirectTransport emits "routerclose" if Router is closed', async () => -{ - // We need different Router and DirectTransport instances here. - const router2 = await worker.createRouter(); - const transport2 = await router2.createDirectTransport(); - const onObserverClose = jest.fn(); - - transport2.observer.once('close', onObserverClose); - - await new Promise((resolve) => - { - transport2.on('routerclose', resolve); - router2.close(); - }); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(transport2.closed).toBe(true); -}, 2000); - -test('DirectTransport emits "routerclose" if Worker is closed', async () => -{ - const onObserverClose = jest.fn(); - - transport.observer.once('close', onObserverClose); - - await new Promise((resolve) => - { - transport.on('routerclose', resolve); - worker.close(); - }); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(transport.closed).toBe(true); -}, 2000); diff --git a/node/src/tests/test-PipeTransport.ts b/node/src/tests/test-PipeTransport.ts deleted file mode 100644 index 6ececbbe5e..0000000000 --- a/node/src/tests/test-PipeTransport.ts +++ /dev/null @@ -1,1036 +0,0 @@ -// @ts-ignore -import * as pickPort from 'pick-port'; -import * as mediasoup from '../'; - -const { createWorker } = mediasoup; - -let worker1: mediasoup.types.Worker; -let worker2: mediasoup.types.Worker; -let router1: mediasoup.types.Router; -let router2: mediasoup.types.Router; -let transport1: mediasoup.types.WebRtcTransport; -let audioProducer: mediasoup.types.Producer; -let videoProducer: mediasoup.types.Producer; -let transport2: mediasoup.types.WebRtcTransport; -let videoConsumer: mediasoup.types.Consumer; -let dataProducer: mediasoup.types.DataProducer; -let dataConsumer: mediasoup.types.DataConsumer; - -const mediaCodecs: mediasoup.types.RtpCodecCapability[] = -[ - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 2 - }, - { - kind : 'video', - mimeType : 'video/VP8', - clockRate : 90000 - } -]; - -const audioProducerParameters: mediasoup.types.ProducerOptions = -{ - kind : 'audio', - rtpParameters : - { - mid : 'AUDIO', - codecs : - [ - { - mimeType : 'audio/opus', - payloadType : 111, - clockRate : 48000, - channels : 2, - parameters : - { - useinbandfec : 1, - foo : 'bar1' - } - } - ], - headerExtensions : - [ - { - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - id : 10 - } - ], - encodings : [ { ssrc: 11111111 } ], - rtcp : - { - cname : 'FOOBAR' - } - }, - appData : { foo: 'bar1' } -}; - -const videoProducerParameters: mediasoup.types.ProducerOptions = -{ - kind : 'video', - rtpParameters : - { - mid : 'VIDEO', - codecs : - [ - { - mimeType : 'video/VP8', - payloadType : 112, - clockRate : 90000, - rtcpFeedback : - [ - { type: 'nack' }, - { type: 'nack', parameter: 'pli' }, - { type: 'goog-remb' }, - { type: 'lalala' } - ] - } - ], - headerExtensions : - [ - { - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - id : 10 - }, - { - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', - id : 11 - }, - { - uri : 'urn:3gpp:video-orientation', - id : 13 - } - ], - encodings : - [ - { ssrc: 22222222 }, - { ssrc: 22222223 }, - { ssrc: 22222224 } - ], - rtcp : - { - cname : 'FOOBAR' - } - }, - appData : { foo: 'bar2' } -}; - -const dataProducerParameters: mediasoup.types.DataProducerOptions = -{ - sctpStreamParameters : - { - streamId : 666, - ordered : false, - maxPacketLifeTime : 5000 - }, - label : 'foo', - protocol : 'bar' -}; - -const consumerDeviceCapabilities: mediasoup.types.RtpCapabilities = -{ - codecs : - [ - { - kind : 'audio', - mimeType : 'audio/opus', - preferredPayloadType : 100, - clockRate : 48000, - channels : 2 - }, - { - kind : 'video', - mimeType : 'video/VP8', - preferredPayloadType : 101, - clockRate : 90000, - rtcpFeedback : - [ - { type: 'nack' }, - { type: 'ccm', parameter: 'fir' }, - { type: 'transport-cc' } - ] - }, - { - kind : 'video', - mimeType : 'video/rtx', - preferredPayloadType : 102, - clockRate : 90000, - parameters : - { - apt : 101 - }, - rtcpFeedback : [] - } - ], - headerExtensions : - [ - { - kind : 'video', - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', - preferredId : 4, - preferredEncrypt : false, - direction : 'sendrecv' - }, - { - kind : 'video', - uri : 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', - preferredId : 5, - preferredEncrypt : false - }, - { - kind : 'audio', - uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', - preferredId : 10, - preferredEncrypt : false - } - ] -}; - -beforeAll(async () => -{ - worker1 = await createWorker(); - worker2 = await createWorker(); - router1 = await worker1.createRouter({ mediaCodecs }); - router2 = await worker2.createRouter({ mediaCodecs }); - transport1 = await router1.createWebRtcTransport( - { - listenIps : [ '127.0.0.1' ], - enableSctp : true - }); - transport2 = await router2.createWebRtcTransport( - { - listenIps : [ '127.0.0.1' ], - enableSctp : true - }); - audioProducer = await transport1.produce(audioProducerParameters); - videoProducer = await transport1.produce(videoProducerParameters); - dataProducer = await transport1.produceData(dataProducerParameters); - - // Pause the videoProducer. - await videoProducer.pause(); -}); - -afterAll(() => -{ - worker1.close(); - worker2.close(); -}); - -test('router.pipeToRouter() succeeds with audio', async () => -{ - let dump; - - const { pipeConsumer, pipeProducer } = await router1.pipeToRouter( - { - producerId : audioProducer.id, - router : router2 - }) as { - pipeConsumer: mediasoup.types.Consumer; - pipeProducer: mediasoup.types.Producer; - }; - - dump = await router1.dump(); - - // There should be two Transports in router1: - // - WebRtcTransport for audioProducer and videoProducer. - // - PipeTransport between router1 and router2. - expect(dump.transportIds.length).toBe(2); - - dump = await router2.dump(); - - // There should be two Transports in router2: - // - WebRtcTransport for audioConsumer and videoConsumer. - // - PipeTransport between router2 and router1. - expect(dump.transportIds.length).toBe(2); - - expect(typeof pipeConsumer.id).toBe('string'); - expect(pipeConsumer.closed).toBe(false); - expect(pipeConsumer.kind).toBe('audio'); - expect(typeof pipeConsumer.rtpParameters).toBe('object'); - expect(pipeConsumer.rtpParameters.mid).toBeUndefined(); - expect(pipeConsumer.rtpParameters.codecs).toEqual( - [ - { - mimeType : 'audio/opus', - clockRate : 48000, - payloadType : 100, - channels : 2, - parameters : - { - useinbandfec : 1, - foo : 'bar1' - }, - rtcpFeedback : [] - } - ]); - - expect(pipeConsumer.rtpParameters.headerExtensions).toEqual( - [ - { - uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', - id : 10, - encrypt : false, - parameters : {} - }, - { - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', - id : 13, - encrypt : false, - parameters : {} - } - ]); - expect(pipeConsumer.type).toBe('pipe'); - expect(pipeConsumer.paused).toBe(false); - expect(pipeConsumer.producerPaused).toBe(false); - expect(pipeConsumer.score).toEqual( - { score: 10, producerScore: 10, producerScores: [] }); - expect(pipeConsumer.appData).toEqual({}); - - expect(pipeProducer.id).toBe(audioProducer.id); - expect(pipeProducer.closed).toBe(false); - expect(pipeProducer.kind).toBe('audio'); - expect(typeof pipeProducer.rtpParameters).toBe('object'); - expect(pipeProducer.rtpParameters.mid).toBeUndefined(); - expect(pipeProducer.rtpParameters.codecs).toEqual( - [ - { - mimeType : 'audio/opus', - payloadType : 100, - clockRate : 48000, - channels : 2, - parameters : - { - useinbandfec : 1, - foo : 'bar1' - }, - rtcpFeedback : [] - } - ]); - expect(pipeProducer.rtpParameters.headerExtensions).toEqual( - [ - { - uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', - id : 10, - encrypt : false, - parameters : {} - }, - { - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', - id : 13, - encrypt : false, - parameters : {} - } - ]); - expect(pipeProducer.paused).toBe(false); -}, 2000); - -test('router.pipeToRouter() succeeds with video', async () => -{ - let dump; - - const { pipeConsumer, pipeProducer } = await router1.pipeToRouter( - { - producerId : videoProducer.id, - router : router2 - }) as { - pipeConsumer: mediasoup.types.Consumer; - pipeProducer: mediasoup.types.Producer; - }; - - dump = await router1.dump(); - - // No new PipeTransport should has been created. The existing one is used. - expect(dump.transportIds.length).toBe(2); - - dump = await router2.dump(); - - // No new PipeTransport should has been created. The existing one is used. - expect(dump.transportIds.length).toBe(2); - - expect(typeof pipeConsumer.id).toBe('string'); - expect(pipeConsumer.closed).toBe(false); - expect(pipeConsumer.kind).toBe('video'); - expect(typeof pipeConsumer.rtpParameters).toBe('object'); - expect(pipeConsumer.rtpParameters.mid).toBeUndefined(); - expect(pipeConsumer.rtpParameters.codecs).toEqual( - [ - { - mimeType : 'video/VP8', - payloadType : 101, - clockRate : 90000, - parameters : {}, - rtcpFeedback : - [ - { type: 'nack', parameter: 'pli' }, - { type: 'ccm', parameter: 'fir' } - ] - } - ]); - expect(pipeConsumer.rtpParameters.headerExtensions).toEqual( - [ - // NOTE: Remove this once framemarking draft becomes RFC. - { - uri : 'http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07', - id : 6, - encrypt : false, - parameters : {} - }, - { - uri : 'urn:ietf:params:rtp-hdrext:framemarking', - id : 7, - encrypt : false, - parameters : {} - }, - { - uri : 'urn:3gpp:video-orientation', - id : 11, - encrypt : false, - parameters : {} - }, - { - uri : 'urn:ietf:params:rtp-hdrext:toffset', - id : 12, - encrypt : false, - parameters : {} - }, - { - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', - id : 13, - encrypt : false, - parameters : {} - } - ]); - - expect(pipeConsumer.type).toBe('pipe'); - expect(pipeConsumer.paused).toBe(false); - expect(pipeConsumer.producerPaused).toBe(true); - expect(pipeConsumer.score).toEqual( - { score: 10, producerScore: 10, producerScores: [] }); - expect(pipeConsumer.appData).toEqual({}); - - expect(pipeProducer.id).toBe(videoProducer.id); - expect(pipeProducer.closed).toBe(false); - expect(pipeProducer.kind).toBe('video'); - expect(typeof pipeProducer.rtpParameters).toBe('object'); - expect(pipeProducer.rtpParameters.mid).toBeUndefined(); - expect(pipeProducer.rtpParameters.codecs).toEqual( - [ - { - mimeType : 'video/VP8', - payloadType : 101, - clockRate : 90000, - parameters : {}, - rtcpFeedback : - [ - { type: 'nack', parameter: 'pli' }, - { type: 'ccm', parameter: 'fir' } - ] - } - ]); - expect(pipeProducer.rtpParameters.headerExtensions).toEqual( - [ - // NOTE: Remove this once framemarking draft becomes RFC. - { - uri : 'http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07', - id : 6, - encrypt : false, - parameters : {} - }, - { - uri : 'urn:ietf:params:rtp-hdrext:framemarking', - id : 7, - encrypt : false, - parameters : {} - }, - { - uri : 'urn:3gpp:video-orientation', - id : 11, - encrypt : false, - parameters : {} - }, - { - uri : 'urn:ietf:params:rtp-hdrext:toffset', - id : 12, - encrypt : false, - parameters : {} - }, - { - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', - id : 13, - encrypt : false, - parameters : {} - } - ]); - expect(pipeProducer.paused).toBe(true); -}, 2000); - -test('router.pipeToRouter() fails if both Routers belong to the same Worker', async () => -{ - const router1bis = await worker1.createRouter({ mediaCodecs }); - - await expect(router1.pipeToRouter( - { - producerId : videoProducer.id, - router : router1bis - })) - .rejects - .toThrow(Error); - - router1bis.close(); -}, 2000); - -test('router.createPipeTransport() with enableRtx succeeds', async () => -{ - const pipeTransport = await router1.createPipeTransport( - { - listenIp : '127.0.0.1', - enableRtx : true - }); - - const pipeConsumer = - await pipeTransport.consume({ producerId: videoProducer.id }); - - expect(typeof pipeConsumer.id).toBe('string'); - expect(pipeConsumer.closed).toBe(false); - expect(pipeConsumer.kind).toBe('video'); - expect(typeof pipeConsumer.rtpParameters).toBe('object'); - expect(pipeConsumer.rtpParameters.mid).toBeUndefined(); - expect(pipeConsumer.rtpParameters.codecs).toEqual( - [ - { - mimeType : 'video/VP8', - payloadType : 101, - clockRate : 90000, - parameters : {}, - rtcpFeedback : - [ - { type: 'nack', parameter: '' }, - { type: 'nack', parameter: 'pli' }, - { type: 'ccm', parameter: 'fir' } - ] - }, - { - mimeType : 'video/rtx', - payloadType : 102, - clockRate : 90000, - parameters : { apt: 101 }, - rtcpFeedback : [] - } - ]); - expect(pipeConsumer.rtpParameters.headerExtensions).toEqual( - [ - // NOTE: Remove this once framemarking draft becomes RFC. - { - uri : 'http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07', - id : 6, - encrypt : false, - parameters : {} - }, - { - uri : 'urn:ietf:params:rtp-hdrext:framemarking', - id : 7, - encrypt : false, - parameters : {} - }, - { - uri : 'urn:3gpp:video-orientation', - id : 11, - encrypt : false, - parameters : {} - }, - { - uri : 'urn:ietf:params:rtp-hdrext:toffset', - id : 12, - encrypt : false, - parameters : {} - }, - { - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', - id : 13, - encrypt : false, - parameters : {} - } - ]); - - expect(pipeConsumer.type).toBe('pipe'); - expect(pipeConsumer.paused).toBe(false); - expect(pipeConsumer.producerPaused).toBe(true); - expect(pipeConsumer.score).toEqual( - { score: 10, producerScore: 10, producerScores: [] }); - expect(pipeConsumer.appData).toEqual({}); - - pipeTransport.close(); -}, 2000); - -test('router.createPipeTransport() with invalid srtpParameters must fail', async () => -{ - const pipeTransport = await router1.createPipeTransport( - { - listenIp : '127.0.0.1', - enableRtx : true - }); - - expect(pipeTransport.srtpParameters).toBeUndefined(); - - // No SRTP enabled so passing srtpParameters must fail. - await expect(pipeTransport.connect( - { - ip : '127.0.0.2', - port : 9999, - srtpParameters : - { - cryptoSuite : 'AEAD_AES_256_GCM', - keyBase64 : 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=' - } - })) - .rejects - .toThrow(TypeError); - - // No SRTP enabled so passing srtpParameters (even if invalid) must fail. - await expect(pipeTransport.connect( - { - ip : '127.0.0.2', - port : 9999, - // @ts-ignore - srtpParameters : 'invalid' - })) - .rejects - .toThrow(TypeError); - - pipeTransport.close(); -}); - -test('router.createPipeTransport() with enableSrtp succeeds', async () => -{ - const pipeTransport = await router1.createPipeTransport( - { - listenIp : '127.0.0.1', - enableSrtp : true - }); - - expect(typeof pipeTransport.id).toBe('string'); - expect(typeof pipeTransport.srtpParameters).toBe('object'); - // The master length of AEAD_AES_256_GCM. - expect(pipeTransport.srtpParameters?.keyBase64.length).toBe(60); - - // Missing srtpParameters. - await expect(pipeTransport.connect( - { - ip : '127.0.0.2', - port : 9999 - })) - .rejects - .toThrow(TypeError); - - // Invalid srtpParameters. - await expect(pipeTransport.connect( - { - ip : '127.0.0.2', - port : 9999, - // @ts-ignore - srtpParameters : 1 - })) - .rejects - .toThrow(TypeError); - - // Missing srtpParameters.cryptoSuite. - await expect(pipeTransport.connect( - { - ip : '127.0.0.2', - port : 9999, - // @ts-ignore - srtpParameters : - { - keyBase64 : 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=' - } - })) - .rejects - .toThrow(TypeError); - - // Missing srtpParameters.keyBase64. - await expect(pipeTransport.connect( - { - ip : '127.0.0.2', - port : 9999, - // @ts-ignore - srtpParameters : - { - cryptoSuite : 'AEAD_AES_256_GCM' - } - })) - .rejects - .toThrow(TypeError); - - // Invalid srtpParameters.cryptoSuite. - await expect(pipeTransport.connect( - { - ip : '127.0.0.2', - port : 9999, - srtpParameters : - { - // @ts-ignore - cryptoSuite : 'FOO', - keyBase64 : 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=' - } - })) - .rejects - .toThrow(TypeError); - - // Invalid srtpParameters.cryptoSuite. - await expect(pipeTransport.connect( - { - ip : '127.0.0.2', - port : 9999, - srtpParameters : - { - // @ts-ignore - cryptoSuite : 123, - keyBase64 : 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=' - } - })) - .rejects - .toThrow(TypeError); - - // Invalid srtpParameters.keyBase64. - await expect(pipeTransport.connect( - { - ip : '127.0.0.2', - port : 9999, - srtpParameters : - { - cryptoSuite : 'AEAD_AES_256_GCM', - // @ts-ignore - keyBase64 : [] - } - })) - .rejects - .toThrow(TypeError); - - // Valid srtpParameters. - await expect(pipeTransport.connect( - { - ip : '127.0.0.2', - port : 9999, - srtpParameters : - { - cryptoSuite : 'AEAD_AES_256_GCM', - keyBase64 : 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=' - } - })) - .resolves - .toBeUndefined(); - - pipeTransport.close(); -}, 2000); - -test('router.createPipeTransport() with fixed port succeeds', async () => -{ - const port = await pickPort({ ip: '127.0.0.1', reserveTimeout: 0 }); - const pipeTransport = await router1.createPipeTransport( - { - listenIp : '127.0.0.1', - port - }); - - expect(pipeTransport.tuple.localPort).toEqual(port); - - pipeTransport.close(); -}, 2000); - -test('transport.consume() for a pipe Producer succeeds', async () => -{ - videoConsumer = await transport2.consume( - { - producerId : videoProducer.id, - rtpCapabilities : consumerDeviceCapabilities - }); - - expect(typeof videoConsumer.id).toBe('string'); - expect(videoConsumer.closed).toBe(false); - expect(videoConsumer.kind).toBe('video'); - expect(typeof videoConsumer.rtpParameters).toBe('object'); - expect(videoConsumer.rtpParameters.mid).toBe('0'); - expect(videoConsumer.rtpParameters.codecs).toEqual( - [ - { - mimeType : 'video/VP8', - payloadType : 101, - clockRate : 90000, - parameters : {}, - rtcpFeedback : - [ - { type: 'nack', parameter: '' }, - { type: 'ccm', parameter: 'fir' }, - { type: 'transport-cc', parameter: '' } - ] - }, - { - mimeType : 'video/rtx', - payloadType : 102, - clockRate : 90000, - parameters : - { - apt : 101 - }, - rtcpFeedback : [] - } - ]); - expect(videoConsumer.rtpParameters.headerExtensions).toEqual( - [ - { - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', - id : 4, - encrypt : false, - parameters : {} - }, - { - uri : 'http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01', - id : 5, - encrypt : false, - parameters : {} - } - ]); - expect(videoConsumer.rtpParameters.encodings?.length).toBe(1); - expect(typeof videoConsumer.rtpParameters.encodings?.[0].ssrc).toBe('number'); - expect(typeof videoConsumer.rtpParameters.encodings?.[0].rtx).toBe('object'); - expect(typeof videoConsumer.rtpParameters.encodings?.[0].rtx?.ssrc).toBe('number'); - expect(videoConsumer.type).toBe('simulcast'); - expect(videoConsumer.paused).toBe(false); - expect(videoConsumer.producerPaused).toBe(true); - expect(videoConsumer.score).toEqual( - { score: 10, producerScore: 0, producerScores: [ 0, 0, 0 ] }); - expect(videoConsumer.appData).toEqual({}); -}, 2000); - -test('producer.pause() and producer.resume() are transmitted to pipe Consumer', async () => -{ - // NOTE: Let's use a Promise since otherwise there may be race conditions - // between events and await lines below. - let promise; - - expect(videoProducer.paused).toBe(true); - expect(videoConsumer.producerPaused).toBe(true); - expect(videoConsumer.paused).toBe(false); - - promise = new Promise((resolve) => videoConsumer.once('producerresume', resolve)); - - await videoProducer.resume(); - await promise; - - expect(videoConsumer.producerPaused).toBe(false); - expect(videoConsumer.paused).toBe(false); - - promise = new Promise((resolve) => videoConsumer.once('producerpause', resolve)); - - await videoProducer.pause(); - await promise; - - expect(videoConsumer.producerPaused).toBe(true); - expect(videoConsumer.paused).toBe(false); -}, 2000); - -test('producer.close() is transmitted to pipe Consumer', async () => -{ - await videoProducer.close(); - - expect(videoProducer.closed).toBe(true); - - if (!videoConsumer.closed) - await new Promise((resolve) => videoConsumer.once('producerclose', resolve)); - - expect(videoConsumer.closed).toBe(true); -}, 2000); - -test('router.pipeToRouter() succeeds with data', async () => -{ - let dump; - - const { pipeDataConsumer, pipeDataProducer } = await router1.pipeToRouter( - { - dataProducerId : dataProducer.id, - router : router2 - }) as { - pipeDataConsumer: mediasoup.types.DataConsumer; - pipeDataProducer: mediasoup.types.DataProducer; - }; - - dump = await router1.dump(); - - // There should be two Transports in router1: - // - WebRtcTransport for audioProducer, videoProducer and dataProducer. - // - PipeTransport between router1 and router2. - expect(dump.transportIds.length).toBe(2); - - dump = await router2.dump(); - - // There should be two Transports in router2: - // - WebRtcTransport for audioConsumer, videoConsumer and dataConsumer. - // - PipeTransport between router2 and router1. - expect(dump.transportIds.length).toBe(2); - - expect(typeof pipeDataConsumer.id).toBe('string'); - expect(pipeDataConsumer.closed).toBe(false); - expect(pipeDataConsumer.type).toBe('sctp'); - expect(typeof pipeDataConsumer.sctpStreamParameters).toBe('object'); - expect(typeof pipeDataConsumer.sctpStreamParameters?.streamId).toBe('number'); - expect(pipeDataConsumer.sctpStreamParameters?.ordered).toBe(false); - expect(pipeDataConsumer.sctpStreamParameters?.maxPacketLifeTime).toBe(5000); - expect(pipeDataConsumer.sctpStreamParameters?.maxRetransmits).toBeUndefined(); - expect(pipeDataConsumer.label).toBe('foo'); - expect(pipeDataConsumer.protocol).toBe('bar'); - - expect(pipeDataProducer.id).toBe(dataProducer.id); - expect(pipeDataProducer.closed).toBe(false); - expect(pipeDataProducer.type).toBe('sctp'); - expect(typeof pipeDataProducer.sctpStreamParameters).toBe('object'); - expect(typeof pipeDataProducer.sctpStreamParameters?.streamId).toBe('number'); - expect(pipeDataProducer.sctpStreamParameters?.ordered).toBe(false); - expect(pipeDataProducer.sctpStreamParameters?.maxPacketLifeTime).toBe(5000); - expect(pipeDataProducer.sctpStreamParameters?.maxRetransmits).toBeUndefined(); - expect(pipeDataProducer.label).toBe('foo'); - expect(pipeDataProducer.protocol).toBe('bar'); -}, 2000); - -test('transport.dataConsume() for a pipe DataProducer succeeds', async () => -{ - dataConsumer = await transport2.consumeData( - { - dataProducerId : dataProducer.id - }); - - expect(typeof dataConsumer.id).toBe('string'); - expect(dataConsumer.closed).toBe(false); - expect(dataConsumer.type).toBe('sctp'); - expect(typeof dataConsumer.sctpStreamParameters).toBe('object'); - expect(typeof dataConsumer.sctpStreamParameters?.streamId).toBe('number'); - expect(dataConsumer.sctpStreamParameters?.ordered).toBe(false); - expect(dataConsumer.sctpStreamParameters?.maxPacketLifeTime).toBe(5000); - expect(dataConsumer.sctpStreamParameters?.maxRetransmits).toBeUndefined(); - expect(dataConsumer.label).toBe('foo'); - expect(dataConsumer.protocol).toBe('bar'); -}, 2000); - -test('dataProducer.close() is transmitted to pipe DataConsumer', async () => -{ - await dataProducer.close(); - - expect(dataProducer.closed).toBe(true); - - if (!dataConsumer.closed) - await new Promise((resolve) => dataConsumer.once('dataproducerclose', resolve)); - - expect(dataConsumer.closed).toBe(true); -}, 2000); - -test('router.pipeToRouter() called twice generates a single PipeTransport pair', async () => -{ - const routerA = await worker1.createRouter({ mediaCodecs }); - const routerB = await worker2.createRouter({ mediaCodecs }); - const transportA1 = await routerA.createWebRtcTransport({ listenIps: [ '127.0.0.1' ] }); - const transportA2 = await routerA.createWebRtcTransport({ listenIps: [ '127.0.0.1' ] }); - const audioProducerA1 = await transportA1.produce(audioProducerParameters); - const audioProducerA2 = await transportA2.produce(audioProducerParameters); - let dump; - - await Promise.all( - [ - routerA.pipeToRouter( - { - producerId : audioProducerA1.id, - router : routerB - }), - routerA.pipeToRouter( - { - producerId : audioProducerA2.id, - router : routerB - }) - ]); - - dump = await routerA.dump(); - - // There should be 3 Transports in routerA: - // - WebRtcTransport for audioProducerA1 and audioProducerA2. - // - PipeTransport between routerA and routerB. - expect(dump.transportIds.length).toBe(3); - - dump = await routerB.dump(); - - // There should be 1 Transport in routerB: - // - PipeTransport between routerA and routerB. - expect(dump.transportIds.length).toBe(1); - - routerA.close(); - routerB.close(); -}, 2000); - -test('router.pipeToRouter() called in two Routers passing one to each other as argument generates a single a single PipeTransport pair', async () => -{ - // We must close other Routers so previously generated PipeTransports are - // closed and removed from the map. - router1.close(); - router2.close(); - - const routerA = await worker1.createRouter({ mediaCodecs }); - const routerB = await worker2.createRouter({ mediaCodecs }); - const transportA = await routerA.createWebRtcTransport({ listenIps: [ '127.0.0.1' ] }); - const transportB = await routerB.createWebRtcTransport({ listenIps: [ '127.0.0.1' ] }); - const audioProducerA = await transportA.produce(audioProducerParameters); - const audioProducerB = await transportB.produce(audioProducerParameters); - const pipeTransportsA = new Map(); - const pipeTransportsB = new Map(); - - routerA.observer.on('newtransport', (transport) => - { - if (transport.constructor.name !== 'PipeTransport') - return; - - pipeTransportsA.set(transport.id, transport); - - transport.observer.on('close', () => pipeTransportsA.delete(transport.id)); - }); - - routerB.observer.on('newtransport', (transport) => - { - if (transport.constructor.name !== 'PipeTransport') - return; - - pipeTransportsB.set(transport.id, transport); - - transport.observer.on('close', () => pipeTransportsB.delete(transport.id)); - }); - - await Promise.all( - [ - routerA.pipeToRouter( - { - producerId : audioProducerA.id, - router : routerB - }), - routerB.pipeToRouter( - { - producerId : audioProducerB.id, - router : routerA - }) - ]); - - // There should be a single PipeTransport in each Router and they must be - // connected. - - expect(pipeTransportsA.size).toBe(1); - expect(pipeTransportsB.size).toBe(1); - - const pipeTransportA = Array.from(pipeTransportsA.values())[0]; - const pipeTransportB = Array.from(pipeTransportsB.values())[0]; - - expect(pipeTransportA.tuple.localPort).toBe(pipeTransportB.tuple.remotePort); - expect(pipeTransportB.tuple.localPort).toBe(pipeTransportA.tuple.remotePort); - - routerA.close(); - - expect(pipeTransportsA.size).toBe(0); - expect(pipeTransportsB.size).toBe(0); - - routerB.close(); -}, 2000); diff --git a/node/src/tests/test-PlainTransport.ts b/node/src/tests/test-PlainTransport.ts deleted file mode 100644 index bfa2553d9e..0000000000 --- a/node/src/tests/test-PlainTransport.ts +++ /dev/null @@ -1,467 +0,0 @@ -// @ts-ignore -import * as pickPort from 'pick-port'; -import * as mediasoup from '../'; - -const { createWorker } = mediasoup; - -let worker: mediasoup.types.Worker; -let router: mediasoup.types.Router; -let transport: mediasoup.types.PlainTransport; - -const mediaCodecs: mediasoup.types.RtpCodecCapability[] = -[ - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 2, - parameters : - { - useinbandfec : 1, - foo : 'bar' - } - }, - { - kind : 'video', - mimeType : 'video/VP8', - clockRate : 90000 - }, - { - kind : 'video', - mimeType : 'video/H264', - clockRate : 90000, - parameters : - { - 'level-asymmetry-allowed' : 1, - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032', - foo : 'bar' - }, - rtcpFeedback : [] // Will be ignored. - } -]; - -beforeAll(async () => -{ - worker = await createWorker(); - router = await worker.createRouter({ mediaCodecs }); -}); - -afterAll(() => worker.close()); - -beforeEach(async () => -{ - transport = await router.createPlainTransport( - { - listenIp : { ip: '127.0.0.1', announcedIp: '4.4.4.4' }, - rtcpMux : false - }); -}); - -afterEach(() => transport.close()); - -test('router.createPlainTransport() succeeds', async () => -{ - await expect(router.dump()) - .resolves - .toMatchObject({ transportIds: [ transport.id ] }); - - const onObserverNewTransport = jest.fn(); - - router.observer.once('newtransport', onObserverNewTransport); - - // Create a separate transport here. - const transport1 = await router.createPlainTransport( - { - listenIp : { ip: '127.0.0.1', announcedIp: '9.9.9.1' }, - rtcpMux : true, - enableSctp : true, - appData : { foo: 'bar' } - }); - - expect(onObserverNewTransport).toHaveBeenCalledTimes(1); - expect(onObserverNewTransport).toHaveBeenCalledWith(transport1); - expect(typeof transport1.id).toBe('string'); - expect(transport1.closed).toBe(false); - expect(transport1.appData).toEqual({ foo: 'bar' }); - expect(typeof transport1.tuple).toBe('object'); - expect(transport1.tuple.localIp).toBe('9.9.9.1'); - expect(typeof transport1.tuple.localPort).toBe('number'); - expect(transport1.tuple.protocol).toBe('udp'); - expect(transport1.rtcpTuple).toBeUndefined(); - expect(transport1.sctpParameters).toMatchObject( - { - port : 5000, - OS : 1024, - MIS : 1024, - maxMessageSize : 262144 - }); - expect(transport1.sctpState).toBe('new'); - expect(transport1.srtpParameters).toBeUndefined(); - - const data1 = await transport1.dump(); - - expect(data1.id).toBe(transport1.id); - expect(data1.direct).toBe(false); - expect(data1.producerIds).toEqual([]); - expect(data1.consumerIds).toEqual([]); - expect(data1.tuple).toEqual(transport1.tuple); - expect(data1.rtcpTuple).toEqual(transport1.rtcpTuple); - expect(data1.sctpParameters).toEqual(transport1.sctpParameters); - expect(data1.sctpState).toBe('new'); - expect(typeof data1.recvRtpHeaderExtensions).toBe('object'); - expect(typeof data1.rtpListener).toBe('object'); - - transport1.close(); - expect(transport1.closed).toBe(true); - - const anotherTransport = await router.createPlainTransport({ listenIp: '127.0.0.1' }); - - expect(typeof anotherTransport).toBe('object'); - - const transport2 = await router.createPlainTransport( - { - listenIp : '127.0.0.1', - rtcpMux : false - }); - - expect(typeof transport2.id).toBe('string'); - expect(transport2.closed).toBe(false); - expect(transport2.appData).toEqual({}); - expect(typeof transport2.tuple).toBe('object'); - expect(transport2.tuple.localIp).toBe('127.0.0.1'); - expect(typeof transport2.tuple.localPort).toBe('number'); - expect(transport2.tuple.protocol).toBe('udp'); - expect(typeof transport2.rtcpTuple).toBe('object'); - expect(transport2.rtcpTuple?.localIp).toBe('127.0.0.1'); - expect(typeof transport2.rtcpTuple?.localPort).toBe('number'); - expect(transport2.rtcpTuple?.protocol).toBe('udp'); - expect(transport2.sctpParameters).toBeUndefined(); - expect(transport2.sctpState).toBeUndefined(); - - const data2 = await transport2.dump(); - - expect(data2.id).toBe(transport2.id); - expect(data2.direct).toBe(false); - expect(data2.tuple).toEqual(transport2.tuple); - expect(data2.rtcpTuple).toEqual(transport2.rtcpTuple); - expect(data2.sctpState).toBeUndefined(); -}, 2000); - -test('router.createPlainTransport() with wrong arguments rejects with TypeError', async () => -{ - // @ts-ignore - await expect(router.createPlainTransport({})) - .rejects - .toThrow(TypeError); - - await expect(router.createPlainTransport({ listenIp: '123' })) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(router.createPlainTransport({ listenIp: [ '127.0.0.1' ] })) - .rejects - .toThrow(TypeError); - - await expect(router.createPlainTransport( - { - listenIp : '127.0.0.1', - // @ts-ignore - appData : 'NOT-AN-OBJECT' - })) - .rejects - .toThrow(TypeError); -}, 2000); - -test('router.createPlainTransport() with enableSrtp succeeds', async () => -{ - // Use default cryptoSuite: 'AES_CM_128_HMAC_SHA1_80'. - const transport1 = await router.createPlainTransport( - { - listenIp : '127.0.0.1', - enableSrtp : true - }); - - expect(typeof transport1.id).toBe('string'); - expect(typeof transport1.srtpParameters).toBe('object'); - expect(transport1.srtpParameters?.cryptoSuite).toBe('AES_CM_128_HMAC_SHA1_80'); - expect(transport1.srtpParameters?.keyBase64.length).toBe(40); - - // Missing srtpParameters. - await expect(transport1.connect( - { - ip : '127.0.0.2', - port : 9999 - })) - .rejects - .toThrow(TypeError); - - // Invalid srtpParameters. - await expect(transport1.connect( - { - ip : '127.0.0.2', - port : 9999, - // @ts-ignore - srtpParameters : 1 - })) - .rejects - .toThrow(TypeError); - - // Missing srtpParameters.cryptoSuite. - await expect(transport1.connect( - { - ip : '127.0.0.2', - port : 9999, - // @ts-ignore - srtpParameters : - { - keyBase64 : 'ZnQ3eWJraDg0d3ZoYzM5cXN1Y2pnaHU5NWxrZTVv' - } - })) - .rejects - .toThrow(TypeError); - - // Missing srtpParameters.keyBase64. - await expect(transport1.connect( - { - ip : '127.0.0.2', - port : 9999, - // @ts-ignore - srtpParameters : - { - cryptoSuite : 'AES_CM_128_HMAC_SHA1_80' - } - })) - .rejects - .toThrow(TypeError); - - // Invalid srtpParameters.cryptoSuite. - await expect(transport1.connect( - { - ip : '127.0.0.2', - port : 9999, - srtpParameters : - { - // @ts-ignore - cryptoSuite : 'FOO', - keyBase64 : 'ZnQ3eWJraDg0d3ZoYzM5cXN1Y2pnaHU5NWxrZTVv' - } - })) - .rejects - .toThrow(TypeError); - - // Invalid srtpParameters.cryptoSuite. - await expect(transport1.connect( - { - ip : '127.0.0.2', - port : 9999, - srtpParameters : - { - // @ts-ignore - cryptoSuite : 123, - keyBase64 : 'ZnQ3eWJraDg0d3ZoYzM5cXN1Y2pnaHU5NWxrZTVv' - } - })) - .rejects - .toThrow(TypeError); - - // Invalid srtpParameters.keyBase64. - await expect(transport1.connect( - { - ip : '127.0.0.2', - port : 9999, - srtpParameters : - { - cryptoSuite : 'AES_CM_128_HMAC_SHA1_80', - // @ts-ignore - keyBase64 : [] - } - })) - .rejects - .toThrow(TypeError); - - // Valid srtpParameters. And let's update the crypto suite. - await expect(transport1.connect( - { - ip : '127.0.0.2', - port : 9999, - srtpParameters : - { - cryptoSuite : 'AEAD_AES_256_GCM', - keyBase64 : 'YTdjcDBvY2JoMGY5YXNlNDc0eDJsdGgwaWRvNnJsamRrdG16aWVpZHphdHo=' - } - })) - .resolves - .toBeUndefined(); - - expect(transport1.srtpParameters?.cryptoSuite).toBe('AEAD_AES_256_GCM'); - expect(transport1.srtpParameters?.keyBase64.length).toBe(60); - - transport1.close(); -}, 2000); - -test('router.createPlainTransport() with non bindable IP rejects with Error', async () => -{ - await expect(router.createPlainTransport({ listenIp: '8.8.8.8' })) - .rejects - .toThrow(Error); -}, 2000); - -test('plainTransport.getStats() succeeds', async () => -{ - const data = await transport.getStats(); - - expect(Array.isArray(data)).toBe(true); - expect(data.length).toBe(1); - expect(data[0].type).toBe('plain-rtp-transport'); - expect(data[0].transportId).toBe(transport.id); - expect(typeof data[0].timestamp).toBe('number'); - expect(data[0].bytesReceived).toBe(0); - expect(data[0].recvBitrate).toBe(0); - expect(data[0].bytesSent).toBe(0); - expect(data[0].sendBitrate).toBe(0); - expect(data[0].rtpBytesReceived).toBe(0); - expect(data[0].rtpRecvBitrate).toBe(0); - expect(data[0].rtpBytesSent).toBe(0); - expect(data[0].rtpSendBitrate).toBe(0); - expect(data[0].rtxBytesReceived).toBe(0); - expect(data[0].rtxRecvBitrate).toBe(0); - expect(data[0].rtxBytesSent).toBe(0); - expect(data[0].rtxSendBitrate).toBe(0); - expect(data[0].probationBytesSent).toBe(0); - expect(data[0].probationSendBitrate).toBe(0); - expect(typeof data[0].tuple).toBe('object'); - expect(data[0].tuple.localIp).toBe('4.4.4.4'); - expect(typeof data[0].tuple.localPort).toBe('number'); - expect(data[0].tuple.protocol).toBe('udp'); - expect(data[0].rtcpTuple).toBeUndefined(); -}, 2000); - -test('plainTransport.connect() succeeds', async () => -{ - await expect(transport.connect({ ip: '1.2.3.4', port: 1234, rtcpPort: 1235 })) - .resolves - .toBeUndefined(); - - // Must fail if connected. - await expect(transport.connect({ ip: '1.2.3.4', port: 1234, rtcpPort: 1235 })) - .rejects - .toThrow(Error); - - expect(transport.tuple.remoteIp).toBe('1.2.3.4'); - expect(transport.tuple.remotePort).toBe(1234); - expect(transport.tuple.protocol).toBe('udp'); - expect(transport.rtcpTuple?.remoteIp).toBe('1.2.3.4'); - expect(transport.rtcpTuple?.remotePort).toBe(1235); - expect(transport.rtcpTuple?.protocol).toBe('udp'); -}, 2000); - -test('plainTransport.connect() with wrong arguments rejects with TypeError', async () => -{ - // No SRTP enabled so passing srtpParameters must fail. - await expect(transport.connect( - { - ip : '127.0.0.2', - port : 9998, - rtcpPort : 9999, - srtpParameters : - { - cryptoSuite : 'AES_CM_128_HMAC_SHA1_80', - keyBase64 : 'ZnQ3eWJraDg0d3ZoYzM5cXN1Y2pnaHU5NWxrZTVv' - } - })) - .rejects - .toThrow(TypeError); - - await expect(transport.connect({})) - .rejects - .toThrow(TypeError); - - await expect(transport.connect({ ip: '::::1234' })) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(transport.connect({ ip: '127.0.0.1', port: 1234, __rtcpPort: 1235 })) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(transport.connect({ ip: '127.0.0.1', __port: 'chicken', rtcpPort: 1235 })) - .rejects - .toThrow(TypeError); -}, 2000); - -test('PlainTransport methods reject if closed', async () => -{ - const onObserverClose = jest.fn(); - - transport.observer.once('close', onObserverClose); - transport.close(); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(transport.closed).toBe(true); - - await expect(transport.dump()) - .rejects - .toThrow(Error); - - await expect(transport.getStats()) - .rejects - .toThrow(Error); - - await expect(transport.connect({})) - .rejects - .toThrow(Error); -}, 2000); - -test('router.createPlainTransport() with fixed port succeeds', async () => -{ - const port = await pickPort({ ip: '127.0.0.1', reserveTimeout: 0 }); - const plainTransport = await router.createPlainTransport( - { - listenIp : '127.0.0.1', - port - }); - - expect(plainTransport.tuple.localPort).toEqual(port); - - plainTransport.close(); -}, 2000); - -test('PlainTransport emits "routerclose" if Router is closed', async () => -{ - // We need different Router and PlainTransport instances here. - const router2 = await worker.createRouter({ mediaCodecs }); - const transport2 = - await router2.createPlainTransport({ listenIp: '127.0.0.1' }); - const onObserverClose = jest.fn(); - - transport2.observer.once('close', onObserverClose); - - await new Promise((resolve) => - { - transport2.on('routerclose', resolve); - router2.close(); - }); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(transport2.closed).toBe(true); -}, 2000); - -test('PlainTransport emits "routerclose" if Worker is closed', async () => -{ - const onObserverClose = jest.fn(); - - transport.observer.once('close', onObserverClose); - - await new Promise((resolve) => - { - transport.on('routerclose', resolve); - worker.close(); - }); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(transport.closed).toBe(true); -}, 2000); diff --git a/node/src/tests/test-Producer.ts b/node/src/tests/test-Producer.ts deleted file mode 100644 index 3c86e6680c..0000000000 --- a/node/src/tests/test-Producer.ts +++ /dev/null @@ -1,758 +0,0 @@ -import * as mediasoup from '../'; -import { UnsupportedError } from '../errors'; - -const { createWorker } = mediasoup; - -let worker: mediasoup.types.Worker; -let router: mediasoup.types.Router; -let transport1: mediasoup.types.WebRtcTransport; -let transport2: mediasoup.types.PlainTransport; -let audioProducer: mediasoup.types.Producer; -let videoProducer: mediasoup.types.Producer; - -const mediaCodecs: mediasoup.types.RtpCodecCapability[] = -[ - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 2, - parameters : - { - foo : '111' - } - }, - { - kind : 'video', - mimeType : 'video/VP8', - clockRate : 90000 - }, - { - kind : 'video', - mimeType : 'video/H264', - clockRate : 90000, - parameters : - { - 'level-asymmetry-allowed' : 1, - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032', - foo : 'bar' - }, - rtcpFeedback : [] // Will be ignored. - } -]; - -beforeAll(async () => -{ - worker = await createWorker(); - router = await worker.createRouter({ mediaCodecs }); - transport1 = await router.createWebRtcTransport( - { - listenIps : [ '127.0.0.1' ] - }); - transport2 = await router.createPlainTransport( - { - listenIp : '127.0.0.1' - }); -}); - -afterAll(() => worker.close()); - -test('transport1.produce() succeeds', async () => -{ - const onObserverNewProducer = jest.fn(); - - transport1.observer.once('newproducer', onObserverNewProducer); - - audioProducer = await transport1.produce( - { - kind : 'audio', - rtpParameters : - { - mid : 'AUDIO', - codecs : - [ - { - mimeType : 'audio/opus', - payloadType : 0, - clockRate : 48000, - channels : 2, - parameters : - { - useinbandfec : 1, - usedtx : 1, - foo : 222.222, - bar : '333' - } - } - ], - headerExtensions : - [ - { - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - id : 10 - }, - { - uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', - id : 12 - } - ], - // Missing encodings on purpose. - rtcp : - { - cname : 'audio-1' - } - }, - appData : { foo: 1, bar: '2' } - }); - - expect(onObserverNewProducer).toHaveBeenCalledTimes(1); - expect(onObserverNewProducer).toHaveBeenCalledWith(audioProducer); - expect(typeof audioProducer.id).toBe('string'); - expect(audioProducer.closed).toBe(false); - expect(audioProducer.kind).toBe('audio'); - expect(typeof audioProducer.rtpParameters).toBe('object'); - expect(audioProducer.type).toBe('simple'); - // Private API. - expect(typeof audioProducer.consumableRtpParameters).toBe('object'); - expect(audioProducer.paused).toBe(false); - expect(audioProducer.score).toEqual([]); - expect(audioProducer.appData).toEqual({ foo: 1, bar: '2' }); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - mapProducerIdConsumerIds : { [audioProducer.id]: [] }, - mapConsumerIdProducerId : {} - }); - - await expect(transport1.dump()) - .resolves - .toMatchObject( - { - id : transport1.id, - producerIds : [ audioProducer.id ], - consumerIds : [] - }); -}, 2000); - -test('transport2.produce() succeeds', async () => -{ - const onObserverNewProducer = jest.fn(); - - transport2.observer.once('newproducer', onObserverNewProducer); - - videoProducer = await transport2.produce( - { - kind : 'video', - rtpParameters : - { - mid : 'VIDEO', - codecs : - [ - { - mimeType : 'video/h264', - payloadType : 112, - clockRate : 90000, - parameters : - { - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - }, - rtcpFeedback : - [ - { type: 'nack' }, - { type: 'nack', parameter: 'pli' }, - { type: 'goog-remb' } - ] - }, - { - mimeType : 'video/rtx', - payloadType : 113, - clockRate : 90000, - parameters : { apt: 112 } - } - ], - headerExtensions : - [ - { - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - id : 10 - }, - { - uri : 'urn:3gpp:video-orientation', - id : 13 - } - ], - encodings : - [ - { ssrc: 22222222, rtx: { ssrc: 22222223 }, scalabilityMode: 'L1T3' }, - { ssrc: 22222224, rtx: { ssrc: 22222225 } }, - { ssrc: 22222226, rtx: { ssrc: 22222227 } }, - { ssrc: 22222228, rtx: { ssrc: 22222229 } } - ], - rtcp : - { - cname : 'video-1' - } - }, - appData : { foo: 1, bar: '2' } - }); - - expect(onObserverNewProducer).toHaveBeenCalledTimes(1); - expect(onObserverNewProducer).toHaveBeenCalledWith(videoProducer); - expect(typeof videoProducer.id).toBe('string'); - expect(videoProducer.closed).toBe(false); - expect(videoProducer.kind).toBe('video'); - expect(typeof videoProducer.rtpParameters).toBe('object'); - expect(videoProducer.type).toBe('simulcast'); - // Private API. - expect(typeof videoProducer.consumableRtpParameters).toBe('object'); - expect(videoProducer.paused).toBe(false); - expect(videoProducer.score).toEqual([]); - expect(videoProducer.appData).toEqual({ foo: 1, bar: '2' }); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - mapProducerIdConsumerIds : { [videoProducer.id]: [] }, - mapConsumerIdProducerId : {} - }); - - await expect(transport2.dump()) - .resolves - .toMatchObject( - { - id : transport2.id, - producerIds : [ videoProducer.id ], - consumerIds : [] - }); -}, 2000); - -test('transport1.produce() with wrong arguments rejects with TypeError', async () => -{ - await expect(transport1.produce( - { - // @ts-ignore - kind : 'chicken', - // @ts-ignore - rtpParameters : {} - })) - .rejects - .toThrow(TypeError); - - await expect(transport1.produce( - { - kind : 'audio', - // @ts-ignore - rtpParameters : {} - })) - .rejects - .toThrow(TypeError); - - // Invalid ssrc. - await expect(transport1.produce( - { - kind : 'audio', - rtpParameters : - { - codecs : [], - headerExtensions : [], - // @ts-ignore - encodings : [ { ssrc: '1111' } ], - rtcp : { cname: 'qwerty' } - } - })) - .rejects - .toThrow(TypeError); - - // Missing or empty rtpParameters.encodings. - await expect(transport1.produce( - { - kind : 'video', - rtpParameters : - { - codecs : - [ - { - mimeType : 'video/h264', - payloadType : 112, - clockRate : 90000, - parameters : - { - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - } - }, - { - mimeType : 'video/rtx', - payloadType : 113, - clockRate : 90000, - parameters : { apt: 112 } - } - ], - headerExtensions : [], - encodings : [], - rtcp : { cname: 'qwerty' } - } - })) - .rejects - .toThrow(TypeError); - - // Wrong apt in RTX codec. - await expect(transport1.produce( - { - kind : 'audio', - rtpParameters : - { - codecs : - [ - { - mimeType : 'video/h264', - payloadType : 112, - clockRate : 90000, - parameters : - { - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - } - }, - { - mimeType : 'video/rtx', - payloadType : 113, - clockRate : 90000, - parameters : { apt: 111 } - } - ], - headerExtensions : [], - encodings : - [ - { ssrc: 6666, rtx: { ssrc: 6667 } } - ], - rtcp : - { - cname : 'video-1' - } - } - })) - .rejects - .toThrow(TypeError); -}, 2000); - -test('transport1.produce() with unsupported codecs rejects with UnsupportedError', async () => -{ - await expect(transport1.produce( - { - kind : 'audio', - rtpParameters : - { - codecs : - [ - { - mimeType : 'audio/ISAC', - payloadType : 108, - clockRate : 32000 - } - ], - headerExtensions : [], - encodings : [ { ssrc: 1111 } ], - rtcp : { cname: 'audio' } - } - })) - .rejects - .toThrow(UnsupportedError); - - // Invalid H264 profile-level-id. - await expect(transport1.produce( - { - kind : 'video', - rtpParameters : - { - codecs : - [ - { - mimeType : 'video/h264', - payloadType : 112, - clockRate : 90000, - parameters : - { - 'packetization-mode' : 1, - 'profile-level-id' : 'CHICKEN' - } - }, - { - mimeType : 'video/rtx', - payloadType : 113, - clockRate : 90000, - parameters : { apt: 112 } - } - ], - headerExtensions : [], - encodings : - [ - { ssrc: 6666, rtx: { ssrc: 6667 } } - ] - } - })) - .rejects - .toThrow(UnsupportedError); -}, 2000); - -test('transport.produce() with already used MID or SSRC rejects with Error', async () => -{ - await expect(transport1.produce( - { - kind : 'audio', - rtpParameters : - { - mid : 'AUDIO', - codecs : - [ - { - mimeType : 'audio/opus', - payloadType : 0, - clockRate : 48000, - channels : 2 - } - ], - headerExtensions : [], - encodings : [ { ssrc: 33333333 } ], - rtcp : - { - cname : 'audio-2' - } - } - })) - .rejects - .toThrow(Error); - - await expect(transport2.produce( - { - kind : 'video', - rtpParameters : - { - mid : 'VIDEO2', - codecs : - [ - { - mimeType : 'video/h264', - payloadType : 112, - clockRate : 90000, - parameters : - { - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - } - } - ], - headerExtensions : - [ - { - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - id : 10 - } - ], - encodings : - [ - { ssrc: 22222222 } - ], - rtcp : - { - cname : 'video-1' - } - } - })) - .rejects - .toThrow(Error); -}, 2000); - -test('transport.produce() with no MID and with single encoding without RID or SSRC rejects with Error', async () => -{ - await expect(transport1.produce( - { - kind : 'audio', - rtpParameters : - { - codecs : - [ - { - mimeType : 'audio/opus', - payloadType : 111, - clockRate : 48000, - channels : 2 - } - ], - encodings : [ {} ], - rtcp : - { - cname : 'audio-2' - } - } - })) - .rejects - .toThrow(Error); -}, 2000); - -test('producer.dump() succeeds', async () => -{ - let data; - - data = await audioProducer.dump(); - - expect(data.id).toBe(audioProducer.id); - expect(data.kind).toBe(audioProducer.kind); - expect(typeof data.rtpParameters).toBe('object'); - expect(Array.isArray(data.rtpParameters.codecs)).toBe(true); - expect(data.rtpParameters.codecs.length).toBe(1); - expect(data.rtpParameters.codecs[0].mimeType).toBe('audio/opus'); - expect(data.rtpParameters.codecs[0].payloadType).toBe(0); - expect(data.rtpParameters.codecs[0].clockRate).toBe(48000); - expect(data.rtpParameters.codecs[0].channels).toBe(2); - expect(data.rtpParameters.codecs[0].parameters) - .toEqual( - { - useinbandfec : 1, - usedtx : 1, - foo : 222.222, - bar : '333' - }); - expect(data.rtpParameters.codecs[0].rtcpFeedback).toEqual([]); - expect(Array.isArray(data.rtpParameters.headerExtensions)).toBe(true); - expect(data.rtpParameters.headerExtensions.length).toBe(2); - expect(data.rtpParameters.headerExtensions).toEqual( - [ - { - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - id : 10, - parameters : {}, - encrypt : false - }, - { - uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', - id : 12, - parameters : {}, - encrypt : false - } - ]); - expect(Array.isArray(data.rtpParameters.encodings)).toBe(true); - expect(data.rtpParameters.encodings.length).toBe(1); - expect(data.rtpParameters.encodings).toEqual( - [ - { codecPayloadType: 0 } - ]); - expect(data.type).toBe('simple'); - - data = await videoProducer.dump(); - - expect(data.id).toBe(videoProducer.id); - expect(data.kind).toBe(videoProducer.kind); - expect(typeof data.rtpParameters).toBe('object'); - expect(Array.isArray(data.rtpParameters.codecs)).toBe(true); - expect(data.rtpParameters.codecs.length).toBe(2); - expect(data.rtpParameters.codecs[0].mimeType).toBe('video/H264'); - expect(data.rtpParameters.codecs[0].payloadType).toBe(112); - expect(data.rtpParameters.codecs[0].clockRate).toBe(90000); - expect(data.rtpParameters.codecs[0].channels).toBeUndefined(); - expect(data.rtpParameters.codecs[0].parameters) - .toEqual( - { - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - }); - expect(data.rtpParameters.codecs[0].rtcpFeedback) - .toEqual( - [ - { type: 'nack' }, - { type: 'nack', parameter: 'pli' }, - { type: 'goog-remb' } - ]); - expect(data.rtpParameters.codecs[1].mimeType).toBe('video/rtx'); - expect(data.rtpParameters.codecs[1].payloadType).toBe(113); - expect(data.rtpParameters.codecs[1].clockRate).toBe(90000); - expect(data.rtpParameters.codecs[1].channels).toBeUndefined(); - expect(data.rtpParameters.codecs[1].parameters).toEqual({ apt: 112 }); - expect(data.rtpParameters.codecs[1].rtcpFeedback).toEqual([]); - expect(Array.isArray(data.rtpParameters.headerExtensions)).toBe(true); - expect(data.rtpParameters.headerExtensions.length).toBe(2); - expect(data.rtpParameters.headerExtensions).toEqual( - [ - { - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - id : 10, - parameters : {}, - encrypt : false - }, - { - uri : 'urn:3gpp:video-orientation', - id : 13, - parameters : {}, - encrypt : false - } - ]); - expect(Array.isArray(data.rtpParameters.encodings)).toBe(true); - expect(data.rtpParameters.encodings.length).toBe(4); - expect(data.rtpParameters.encodings).toMatchObject( - [ - { - codecPayloadType : 112, - ssrc : 22222222, - rtx : { ssrc: 22222223 }, - scalabilityMode : 'L1T3' - }, - { codecPayloadType: 112, ssrc: 22222224, rtx: { ssrc: 22222225 } }, - { codecPayloadType: 112, ssrc: 22222226, rtx: { ssrc: 22222227 } }, - { codecPayloadType: 112, ssrc: 22222228, rtx: { ssrc: 22222229 } } - ]); - expect(data.type).toBe('simulcast'); -}, 2000); - -test('producer.getStats() succeeds', async () => -{ - await expect(audioProducer.getStats()) - .resolves - .toEqual([]); - - await expect(videoProducer.getStats()) - .resolves - .toEqual([]); -}, 2000); - -test('producer.pause() and resume() succeed', async () => -{ - await audioProducer.pause(); - expect(audioProducer.paused).toBe(true); - - await expect(audioProducer.dump()) - .resolves - .toMatchObject({ paused: true }); - - await audioProducer.resume(); - expect(audioProducer.paused).toBe(false); - - await expect(audioProducer.dump()) - .resolves - .toMatchObject({ paused: false }); -}, 2000); - -test('producer.enableTraceEvent() succeed', async () => -{ - await audioProducer.enableTraceEvent([ 'rtp', 'pli' ]); - await expect(audioProducer.dump()) - .resolves - .toMatchObject({ traceEventTypes: 'rtp,pli' }); - - await audioProducer.enableTraceEvent([]); - await expect(audioProducer.dump()) - .resolves - .toMatchObject({ traceEventTypes: '' }); - - // @ts-ignore - await audioProducer.enableTraceEvent([ 'nack', 'FOO', 'fir' ]); - await expect(audioProducer.dump()) - .resolves - .toMatchObject({ traceEventTypes: 'nack,fir' }); - - await audioProducer.enableTraceEvent(); - await expect(audioProducer.dump()) - .resolves - .toMatchObject({ traceEventTypes: '' }); -}, 2000); - -test('producer.enableTraceEvent() with wrong arguments rejects with TypeError', async () => -{ - // @ts-ignore - await expect(audioProducer.enableTraceEvent(123)) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(audioProducer.enableTraceEvent('rtp')) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(audioProducer.enableTraceEvent([ 'fir', 123.123 ])) - .rejects - .toThrow(TypeError); -}, 2000); - -test('Producer emits "score"', async () => -{ - // Private API. - const channel = videoProducer.channelForTesting; - const onScore = jest.fn(); - - videoProducer.on('score', onScore); - - channel.emit(videoProducer.id, 'score', [ { ssrc: 11, score: 10 } ]); - channel.emit(videoProducer.id, 'score', [ { ssrc: 11, score: 9 }, { ssrc: 22, score: 8 } ]); - channel.emit(videoProducer.id, 'score', [ { ssrc: 11, score: 9 }, { ssrc: 22, score: 9 } ]); - - expect(onScore).toHaveBeenCalledTimes(3); - expect(videoProducer.score).toEqual([ { ssrc: 11, score: 9 }, { ssrc: 22, score: 9 } ]); -}, 2000); - -test('producer.close() succeeds', async () => -{ - const onObserverClose = jest.fn(); - - audioProducer.observer.once('close', onObserverClose); - audioProducer.close(); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(audioProducer.closed).toBe(true); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - mapProducerIdConsumerIds : {}, - mapConsumerIdProducerId : {} - }); - - await expect(transport1.dump()) - .resolves - .toMatchObject( - { - id : transport1.id, - producerIds : [], - consumerIds : [] - }); -}, 2000); - -test('Producer methods reject if closed', async () => -{ - await expect(audioProducer.dump()) - .rejects - .toThrow(Error); - - await expect(audioProducer.getStats()) - .rejects - .toThrow(Error); - - await expect(audioProducer.pause()) - .rejects - .toThrow(Error); - - await expect(audioProducer.resume()) - .rejects - .toThrow(Error); -}, 2000); - -test('Producer emits "transportclose" if Transport is closed', async () => -{ - const onObserverClose = jest.fn(); - - videoProducer.observer.once('close', onObserverClose); - - await new Promise((resolve) => - { - videoProducer.on('transportclose', resolve); - transport2.close(); - }); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(videoProducer.closed).toBe(true); -}, 2000); diff --git a/node/src/tests/test-Router.ts b/node/src/tests/test-Router.ts deleted file mode 100644 index c9316ee1f4..0000000000 --- a/node/src/tests/test-Router.ts +++ /dev/null @@ -1,161 +0,0 @@ -import * as mediasoup from '../'; -import { InvalidStateError } from '../errors'; - -const { createWorker } = mediasoup; - -let worker: mediasoup.types.Worker; - -beforeEach(() => worker && !worker.closed && worker.close()); -afterEach(() => worker && !worker.closed && worker.close()); - -const mediaCodecs: mediasoup.types.RtpCodecCapability[] = -[ - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 2, - parameters : - { - useinbandfec : 1, - foo : 'bar' - } - }, - { - kind : 'video', - mimeType : 'video/VP8', - clockRate : 90000 - }, - { - kind : 'video', - mimeType : 'video/H264', - clockRate : 90000, - parameters : - { - 'level-asymmetry-allowed' : 1, - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - }, - rtcpFeedback : [] // Will be ignored. - } -]; - -test('worker.createRouter() succeeds', async () => -{ - worker = await createWorker(); - - const onObserverNewRouter = jest.fn(); - - worker.observer.once('newrouter', onObserverNewRouter); - - const router = await worker.createRouter({ mediaCodecs, appData: { foo: 123 } }); - - expect(onObserverNewRouter).toHaveBeenCalledTimes(1); - expect(onObserverNewRouter).toHaveBeenCalledWith(router); - expect(typeof router.id).toBe('string'); - expect(router.closed).toBe(false); - expect(typeof router.rtpCapabilities).toBe('object'); - expect(Array.isArray(router.rtpCapabilities.codecs)).toBe(true); - expect(Array.isArray(router.rtpCapabilities.headerExtensions)).toBe(true); - expect(router.appData).toEqual({ foo: 123 }); - - await expect(worker.dump()) - .resolves - .toEqual( - { - pid : worker.pid, - webRtcServerIds : [], - routerIds : [ router.id ], - channelMessageHandlers : - { - channelRequestHandlers : [ router.id ], - payloadChannelRequestHandlers : [], - payloadChannelNotificationHandlers : [] - } - }); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - id : router.id, - transportIds : [], - rtpObserverIds : [], - mapProducerIdConsumerIds : {}, - mapConsumerIdProducerId : {}, - mapProducerIdObserverIds : {}, - mapDataProducerIdDataConsumerIds : {}, - mapDataConsumerIdDataProducerId : {} - }); - - // Private API. - expect(worker.routersForTesting.size).toBe(1); - - worker.close(); - - expect(router.closed).toBe(true); - - // Private API. - expect(worker.routersForTesting.size).toBe(0); -}, 2000); - -test('worker.createRouter() with wrong arguments rejects with TypeError', async () => -{ - worker = await createWorker(); - - // @ts-ignore - await expect(worker.createRouter({ mediaCodecs: {} })) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(worker.createRouter({ appData: 'NOT-AN-OBJECT' })) - .rejects - .toThrow(TypeError); - - worker.close(); -}, 2000); - -test('worker.createRouter() rejects with InvalidStateError if Worker is closed', async () => -{ - worker = await createWorker(); - - worker.close(); - - await expect(worker.createRouter({ mediaCodecs })) - .rejects - .toThrow(InvalidStateError); -}, 2000); - -test('router.close() succeeds', async () => -{ - worker = await createWorker(); - - const router = await worker.createRouter({ mediaCodecs }); - const onObserverClose = jest.fn(); - - router.observer.once('close', onObserverClose); - router.close(); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(router.closed).toBe(true); -}, 2000); - -test('Router emits "workerclose" if Worker is closed', async () => -{ - worker = await createWorker(); - - const router = await worker.createRouter({ mediaCodecs }); - const onObserverClose = jest.fn(); - - router.observer.once('close', onObserverClose); - - await new Promise((resolve) => - { - router.on('workerclose', resolve); - worker.close(); - }); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(router.closed).toBe(true); -}, 2000); diff --git a/node/src/tests/test-WebRtcServer.ts b/node/src/tests/test-WebRtcServer.ts deleted file mode 100644 index febdea95bf..0000000000 --- a/node/src/tests/test-WebRtcServer.ts +++ /dev/null @@ -1,584 +0,0 @@ -// @ts-ignore -import * as pickPort from 'pick-port'; -import * as mediasoup from '../'; -import { InvalidStateError } from '../errors'; - -const { createWorker } = mediasoup; - -let worker: mediasoup.types.Worker; - -beforeEach(() => worker && !worker.closed && worker.close()); -afterEach(() => worker && !worker.closed && worker.close()); - -test('worker.createWebRtcServer() succeeds', async () => -{ - worker = await createWorker(); - - const onObserverNewWebRtcServer = jest.fn(); - - worker.observer.once('newwebrtcserver', onObserverNewWebRtcServer); - - const port1 = await pickPort({ ip: '127.0.0.1', reserveTimeout: 0 }); - const port2 = await pickPort({ type: 'tcp', ip: '127.0.0.1', reserveTimeout: 0 }); - - const webRtcServer = await worker.createWebRtcServer( - { - listenInfos : - [ - { - protocol : 'udp', - ip : '127.0.0.1', - port : port1 - }, - { - protocol : 'tcp', - ip : '127.0.0.1', - announcedIp : '1.2.3.4', - port : port2 - } - ], - appData : { foo: 123 } - }); - - expect(onObserverNewWebRtcServer).toHaveBeenCalledTimes(1); - expect(onObserverNewWebRtcServer).toHaveBeenCalledWith(webRtcServer); - expect(typeof webRtcServer.id).toBe('string'); - expect(webRtcServer.closed).toBe(false); - expect(webRtcServer.appData).toEqual({ foo: 123 }); - - await expect(worker.dump()) - .resolves - .toEqual( - { - pid : worker.pid, - webRtcServerIds : [ webRtcServer.id ], - routerIds : [], - channelMessageHandlers : - { - channelRequestHandlers : [ webRtcServer.id ], - payloadChannelRequestHandlers : [], - payloadChannelNotificationHandlers : [] - } - }); - - await expect(webRtcServer.dump()) - .resolves - .toMatchObject( - { - id : webRtcServer.id, - udpSockets : - [ - { ip: '127.0.0.1', port: port1 } - ], - tcpServers : - [ - { ip: '127.0.0.1', port: port2 } - ], - webRtcTransportIds : [], - localIceUsernameFragments : [], - tupleHashes : [] - }); - - // Private API. - expect(worker.webRtcServersForTesting.size).toBe(1); - - worker.close(); - - expect(webRtcServer.closed).toBe(true); - - // Private API. - expect(worker.webRtcServersForTesting.size).toBe(0); -}, 2000); - -test('worker.createWebRtcServer() without specifying port succeeds', async () => -{ - worker = await createWorker(); - - const onObserverNewWebRtcServer = jest.fn(); - - worker.observer.once('newwebrtcserver', onObserverNewWebRtcServer); - - const webRtcServer = await worker.createWebRtcServer( - { - listenInfos : - [ - { - protocol : 'udp', - ip : '127.0.0.1' - }, - { - protocol : 'tcp', - ip : '127.0.0.1', - announcedIp : '1.2.3.4' - } - ], - appData : { foo: 123 } - }); - - expect(onObserverNewWebRtcServer).toHaveBeenCalledTimes(1); - expect(onObserverNewWebRtcServer).toHaveBeenCalledWith(webRtcServer); - expect(typeof webRtcServer.id).toBe('string'); - expect(webRtcServer.closed).toBe(false); - expect(webRtcServer.appData).toEqual({ foo: 123 }); - - await expect(worker.dump()) - .resolves - .toEqual( - { - pid : worker.pid, - webRtcServerIds : [ webRtcServer.id ], - routerIds : [], - channelMessageHandlers : - { - channelRequestHandlers : [ webRtcServer.id ], - payloadChannelRequestHandlers : [], - payloadChannelNotificationHandlers : [] - } - }); - - await expect(webRtcServer.dump()) - .resolves - .toMatchObject( - { - id : webRtcServer.id, - udpSockets : - [ - { ip: '127.0.0.1', port: expect.any(Number) } - ], - tcpServers : - [ - { ip: '127.0.0.1', port: expect.any(Number) } - ], - webRtcTransportIds : [], - localIceUsernameFragments : [], - tupleHashes : [] - }); - - // Private API. - expect(worker.webRtcServersForTesting.size).toBe(1); - - worker.close(); - - expect(webRtcServer.closed).toBe(true); - - // Private API. - expect(worker.webRtcServersForTesting.size).toBe(0); -}, 2000); - -test('worker.createWebRtcServer() with wrong arguments rejects with TypeError', async () => -{ - worker = await createWorker(); - - // @ts-ignore - await expect(worker.createWebRtcServer({})) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(worker.createWebRtcServer({ listenInfos: 'NOT-AN-ARRAY' })) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(worker.createWebRtcServer({ listenInfos: [ 'NOT-AN-OBJECT' ] })) - .rejects - .toThrow(TypeError); - - // Empty listenInfos so should fail. - await expect(worker.createWebRtcServer({ listenInfos: [] })) - .rejects - .toThrow(TypeError); - - worker.close(); -}, 2000); - -test('worker.createWebRtcServer() with unavailable listenInfos rejects with Error', async () => -{ - const worker1 = await createWorker(); - const worker2 = await createWorker(); - const port1 = await pickPort({ ip: '127.0.0.1', reserveTimeout: 0 }); - const port2 = await pickPort({ ip: '127.0.0.1', reserveTimeout: 0 }); - - // Using an unavailable listen IP. - await expect(worker1.createWebRtcServer( - { - listenInfos : - [ - { - protocol : 'udp', - ip : '127.0.0.1', - port : port1 - }, - { - protocol : 'udp', - ip : '1.2.3.4', - port : port2 - } - ] - })) - .rejects - .toThrow(Error); - - // Using the same UDP port in two listenInfos. - await expect(worker1.createWebRtcServer( - { - listenInfos : - [ - { - protocol : 'udp', - ip : '127.0.0.1', - port : port1 - }, - { - protocol : 'udp', - ip : '127.0.0.1', - announcedIp : '1.2.3.4', - port : port1 - } - ] - })) - .rejects - .toThrow(Error); - - await worker1.createWebRtcServer( - { - listenInfos : - [ - { - protocol : 'udp', - ip : '127.0.0.1', - port : port1 - } - ] - }); - - // Using the same UDP port in a second Worker. - await expect(worker2.createWebRtcServer( - { - listenInfos : - [ - { - protocol : 'udp', - ip : '127.0.0.1', - port : port1 - } - ] - })) - .rejects - .toThrow(Error); - - worker1.close(); - worker2.close(); -}, 2000); - -test('worker.createWebRtcServer() rejects with InvalidStateError if Worker is closed', async () => -{ - worker = await createWorker(); - - worker.close(); - - const port = await pickPort({ ip: '127.0.0.1', reserveTimeout: 0 }); - - await expect(worker.createWebRtcServer( - { - listenInfos : [ { protocol: 'udp', ip: '127.0.0.1', port } ] - })) - .rejects - .toThrow(InvalidStateError); -}, 2000); - -test('webRtcServer.close() succeeds', async () => -{ - worker = await createWorker(); - - const port = await pickPort({ ip: '127.0.0.1', reserveTimeout: 0 }); - const webRtcServer = await worker.createWebRtcServer( - { - listenInfos : [ { protocol: 'udp', ip: '127.0.0.1', port } ] - }); - const onObserverClose = jest.fn(); - - webRtcServer.observer.once('close', onObserverClose); - webRtcServer.close(); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(webRtcServer.closed).toBe(true); -}, 2000); - -test('WebRtcServer emits "workerclose" if Worker is closed', async () => -{ - worker = await createWorker(); - - const port = await pickPort({ ip: '127.0.0.1', reserveTimeout: 0 }); - const webRtcServer = await worker.createWebRtcServer( - { - listenInfos : [ { protocol: 'tcp', ip: '127.0.0.1', port } ] - }); - const onObserverClose = jest.fn(); - - webRtcServer.observer.once('close', onObserverClose); - - await new Promise((resolve) => - { - webRtcServer.on('workerclose', resolve); - worker.close(); - }); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(webRtcServer.closed).toBe(true); -}, 2000); - -test('router.createWebRtcTransport() with webRtcServer succeeds and transport is closed', async () => -{ - worker = await createWorker(); - - const port1 = await pickPort({ ip: '127.0.0.1', reserveTimeout: 0 }); - const port2 = await pickPort({ type: 'tcp', ip: '127.0.0.1', reserveTimeout: 0 }); - const webRtcServer = await worker.createWebRtcServer( - { - listenInfos : - [ - { protocol: 'udp', ip: '127.0.0.1', port: port1 }, - { protocol: 'tcp', ip: '127.0.0.1', port: port2 } - ] - }); - - const onObserverWebRtcTransportHandled = jest.fn(); - const onObserverWebRtcTransportUnhandled = jest.fn(); - - webRtcServer.observer.once('webrtctransporthandled', onObserverWebRtcTransportHandled); - webRtcServer.observer.once('webrtctransportunhandled', onObserverWebRtcTransportUnhandled); - - const router = await worker.createRouter(); - - const onObserverNewTransport = jest.fn(); - - router.observer.once('newtransport', onObserverNewTransport); - - const transport = await router.createWebRtcTransport( - { - webRtcServer, - enableTcp : false, - appData : { foo: 'bar' } - }); - - await expect(router.dump()) - .resolves - .toMatchObject({ transportIds: [ transport.id ] }); - - expect(onObserverWebRtcTransportHandled).toHaveBeenCalledTimes(1); - expect(onObserverWebRtcTransportHandled).toHaveBeenCalledWith(transport); - expect(onObserverNewTransport).toHaveBeenCalledTimes(1); - expect(onObserverNewTransport).toHaveBeenCalledWith(transport); - expect(typeof transport.id).toBe('string'); - expect(transport.closed).toBe(false); - expect(transport.appData).toEqual({ foo: 'bar' }); - - const iceCandidates = transport.iceCandidates; - - expect(iceCandidates.length).toBe(1); - expect(iceCandidates[0].ip).toBe('127.0.0.1'); - expect(iceCandidates[0].port).toBe(port1); - expect(iceCandidates[0].protocol).toBe('udp'); - expect(iceCandidates[0].type).toBe('host'); - expect(iceCandidates[0].tcpType).toBeUndefined(); - - expect(transport.iceState).toBe('new'); - expect(transport.iceSelectedTuple).toBeUndefined(); - - expect(webRtcServer.webRtcTransportsForTesting.size).toBe(1); - expect(router.transportsForTesting.size).toBe(1); - - await expect(webRtcServer.dump()) - .resolves - .toMatchObject( - { - id : webRtcServer.id, - udpSockets : - [ - { ip: '127.0.0.1', port: port1 } - ], - tcpServers : - [ - { ip: '127.0.0.1', port: port2 } - ], - webRtcTransportIds : [ transport.id ], - localIceUsernameFragments : - [ - { /* localIceUsernameFragment: xxx, */ webRtcTransportId: transport.id } - ], - tupleHashes : [] - }); - - transport.close(); - - expect(transport.closed).toBe(true); - expect(onObserverWebRtcTransportUnhandled).toHaveBeenCalledTimes(1); - expect(onObserverWebRtcTransportUnhandled).toHaveBeenCalledWith(transport); - expect(webRtcServer.webRtcTransportsForTesting.size).toBe(0); - expect(router.transportsForTesting.size).toBe(0); - - await expect(webRtcServer.dump()) - .resolves - .toMatchObject( - { - id : webRtcServer.id, - udpSockets : - [ - { ip: '127.0.0.1', port: port1 } - ], - tcpServers : - [ - { ip: '127.0.0.1', port: port2 } - ], - webRtcTransportIds : [], - localIceUsernameFragments : [], - tupleHashes : [] - }); -}, 2000); - -test('router.createWebRtcTransport() with webRtcServer succeeds and webRtcServer is closed', async () => -{ - worker = await createWorker(); - - const port1 = await pickPort({ ip: '127.0.0.1', reserveTimeout: 0 }); - const port2 = await pickPort({ type: 'tcp', ip: '127.0.0.1', reserveTimeout: 0 }); - const webRtcServer = await worker.createWebRtcServer( - { - listenInfos : - [ - { protocol: 'udp', ip: '127.0.0.1', port: port1 }, - { protocol: 'tcp', ip: '127.0.0.1', port: port2 } - ] - }); - - const onObserverWebRtcTransportHandled = jest.fn(); - const onObserverWebRtcTransportUnhandled = jest.fn(); - - webRtcServer.observer.once('webrtctransporthandled', onObserverWebRtcTransportHandled); - webRtcServer.observer.once('webrtctransportunhandled', onObserverWebRtcTransportUnhandled); - - const router = await worker.createRouter(); - const transport = await router.createWebRtcTransport( - { - webRtcServer, - enableUdp : false, - enableTcp : true, - appData : { foo: 'bar' } - }); - - expect(onObserverWebRtcTransportHandled).toHaveBeenCalledTimes(1); - expect(onObserverWebRtcTransportHandled).toHaveBeenCalledWith(transport); - - await expect(router.dump()) - .resolves - .toMatchObject({ transportIds: [ transport.id ] }); - - expect(typeof transport.id).toBe('string'); - expect(transport.closed).toBe(false); - expect(transport.appData).toEqual({ foo: 'bar' }); - - const iceCandidates = transport.iceCandidates; - - expect(iceCandidates.length).toBe(1); - expect(iceCandidates[0].ip).toBe('127.0.0.1'); - expect(iceCandidates[0].port).toBe(port2); - expect(iceCandidates[0].protocol).toBe('tcp'); - expect(iceCandidates[0].type).toBe('host'); - expect(iceCandidates[0].tcpType).toBe('passive'); - - expect(transport.iceState).toBe('new'); - expect(transport.iceSelectedTuple).toBeUndefined(); - - expect(webRtcServer.webRtcTransportsForTesting.size).toBe(1); - expect(router.transportsForTesting.size).toBe(1); - - await expect(webRtcServer.dump()) - .resolves - .toMatchObject( - { - id : webRtcServer.id, - udpSockets : - [ - { ip: '127.0.0.1', port: port1 } - ], - tcpServers : - [ - { ip: '127.0.0.1', port: port2 } - ], - webRtcTransportIds : [ transport.id ], - localIceUsernameFragments : - [ - { /* localIceUsernameFragment: xxx, */ webRtcTransportId: transport.id } - ], - tupleHashes : [] - }); - - // Let's restart ICE in the transport so it should add a new entry in - // localIceUsernameFragments in the WebRtcServer. - await transport.restartIce(); - - await expect(webRtcServer.dump()) - .resolves - .toMatchObject( - { - id : webRtcServer.id, - udpSockets : - [ - { ip: '127.0.0.1', port: port1 } - ], - tcpServers : - [ - { ip: '127.0.0.1', port: port2 } - ], - webRtcTransportIds : [ transport.id ], - localIceUsernameFragments : - [ - { /* localIceUsernameFragment: xxx, */ webRtcTransportId: transport.id }, - { /* localIceUsernameFragment: yyy, */ webRtcTransportId: transport.id } - ], - tupleHashes : [] - }); - - const onObserverClose = jest.fn(); - - webRtcServer.observer.once('close', onObserverClose); - - const onListenServerClose = jest.fn(); - - transport.once('listenserverclose', onListenServerClose); - - webRtcServer.close(); - - expect(webRtcServer.closed).toBe(true); - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(onListenServerClose).toHaveBeenCalledTimes(1); - expect(onObserverWebRtcTransportUnhandled).toHaveBeenCalledTimes(1); - expect(onObserverWebRtcTransportUnhandled).toHaveBeenCalledWith(transport); - expect(transport.closed).toBe(true); - expect(webRtcServer.webRtcTransportsForTesting.size).toBe(0); - expect(router.transportsForTesting.size).toBe(0); - - await expect(worker.dump()) - .resolves - .toEqual( - { - pid : worker.pid, - webRtcServerIds : [], - routerIds : [ router.id ], - channelMessageHandlers : - { - channelRequestHandlers : [ router.id ], - payloadChannelRequestHandlers : [], - payloadChannelNotificationHandlers : [] - } - }); - - await expect(router.dump()) - .resolves - .toMatchObject( - { - id : router.id, - transportIds : [] - }); -}, 2000); diff --git a/node/src/tests/test-WebRtcTransport.ts b/node/src/tests/test-WebRtcTransport.ts deleted file mode 100644 index 2ad1b9f1ad..0000000000 --- a/node/src/tests/test-WebRtcTransport.ts +++ /dev/null @@ -1,568 +0,0 @@ -// @ts-ignore -import * as pickPort from 'pick-port'; -import * as mediasoup from '../'; - -const { createWorker } = mediasoup; - -let worker: mediasoup.types.Worker; -let router: mediasoup.types.Router; -let transport: mediasoup.types.WebRtcTransport; - -const mediaCodecs: mediasoup.types.RtpCodecCapability[] = -[ - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 2, - parameters : - { - useinbandfec : 1, - foo : 'bar' - } - }, - { - kind : 'video', - mimeType : 'video/VP8', - clockRate : 90000 - }, - { - kind : 'video', - mimeType : 'video/H264', - clockRate : 90000, - parameters : - { - 'level-asymmetry-allowed' : 1, - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032', - foo : 'bar' - } - } -]; - -beforeAll(async () => -{ - worker = await createWorker(); - router = await worker.createRouter({ mediaCodecs }); -}); - -afterAll(() => worker.close()); - -beforeEach(async () => -{ - transport = await router.createWebRtcTransport( - { - listenIps : [ { ip: '127.0.0.1', announcedIp: '9.9.9.1' } ], - enableTcp : false - }); -}); - -afterEach(() => transport.close()); - -test('router.createWebRtcTransport() succeeds', async () => -{ - await expect(router.dump()) - .resolves - .toMatchObject({ transportIds: [ transport.id ] }); - - const onObserverNewTransport = jest.fn(); - - router.observer.once('newtransport', onObserverNewTransport); - - // Create a separate transport here. - const transport1 = await router.createWebRtcTransport( - { - listenIps : - [ - { ip: '127.0.0.1', announcedIp: '9.9.9.1' }, - { ip: '0.0.0.0', announcedIp: '9.9.9.2' }, - { ip: '127.0.0.1', announcedIp: undefined } - ], - enableTcp : true, - preferUdp : true, - enableSctp : true, - numSctpStreams : { OS: 2048, MIS: 2048 }, - maxSctpMessageSize : 1000000, - appData : { foo: 'bar' } - }); - - expect(onObserverNewTransport).toHaveBeenCalledTimes(1); - expect(onObserverNewTransport).toHaveBeenCalledWith(transport1); - expect(typeof transport1.id).toBe('string'); - expect(transport1.closed).toBe(false); - expect(transport1.appData).toEqual({ foo: 'bar' }); - expect(transport1.iceRole).toBe('controlled'); - expect(typeof transport1.iceParameters).toBe('object'); - expect(transport1.iceParameters.iceLite).toBe(true); - expect(typeof transport1.iceParameters.usernameFragment).toBe('string'); - expect(typeof transport1.iceParameters.password).toBe('string'); - expect(transport1.sctpParameters).toMatchObject( - { - port : 5000, - OS : 2048, - MIS : 2048, - maxMessageSize : 1000000 - }); - expect(Array.isArray(transport1.iceCandidates)).toBe(true); - expect(transport1.iceCandidates.length).toBe(6); - - const iceCandidates = transport1.iceCandidates; - - expect(iceCandidates[0].ip).toBe('9.9.9.1'); - expect(iceCandidates[0].protocol).toBe('udp'); - expect(iceCandidates[0].type).toBe('host'); - expect(iceCandidates[0].tcpType).toBeUndefined(); - expect(iceCandidates[1].ip).toBe('9.9.9.1'); - expect(iceCandidates[1].protocol).toBe('tcp'); - expect(iceCandidates[1].type).toBe('host'); - expect(iceCandidates[1].tcpType).toBe('passive'); - expect(iceCandidates[2].ip).toBe('9.9.9.2'); - expect(iceCandidates[2].protocol).toBe('udp'); - expect(iceCandidates[2].type).toBe('host'); - expect(iceCandidates[2].tcpType).toBeUndefined(); - expect(iceCandidates[3].ip).toBe('9.9.9.2'); - expect(iceCandidates[3].protocol).toBe('tcp'); - expect(iceCandidates[3].type).toBe('host'); - expect(iceCandidates[3].tcpType).toBe('passive'); - expect(iceCandidates[4].ip).toBe('127.0.0.1'); - expect(iceCandidates[4].protocol).toBe('udp'); - expect(iceCandidates[4].type).toBe('host'); - expect(iceCandidates[4].tcpType).toBeUndefined(); - expect(iceCandidates[5].ip).toBe('127.0.0.1'); - expect(iceCandidates[5].protocol).toBe('tcp'); - expect(iceCandidates[5].type).toBe('host'); - expect(iceCandidates[5].tcpType).toBe('passive'); - expect(iceCandidates[0].priority).toBeGreaterThan(iceCandidates[1].priority); - expect(iceCandidates[2].priority).toBeGreaterThan(iceCandidates[1].priority); - expect(iceCandidates[2].priority).toBeGreaterThan(iceCandidates[3].priority); - expect(iceCandidates[4].priority).toBeGreaterThan(iceCandidates[3].priority); - expect(iceCandidates[4].priority).toBeGreaterThan(iceCandidates[5].priority); - - expect(transport1.iceState).toBe('new'); - expect(transport1.iceSelectedTuple).toBeUndefined(); - expect(typeof transport1.dtlsParameters).toBe('object'); - expect(Array.isArray(transport1.dtlsParameters.fingerprints)).toBe(true); - expect(transport1.dtlsParameters.role).toBe('auto'); - expect(transport1.dtlsState).toBe('new'); - expect(transport1.dtlsRemoteCert).toBeUndefined(); - expect(transport1.sctpState).toBe('new'); - - const data1 = await transport1.dump(); - - expect(data1.id).toBe(transport1.id); - expect(data1.direct).toBe(false); - expect(data1.producerIds).toEqual([]); - expect(data1.consumerIds).toEqual([]); - expect(data1.iceRole).toBe(transport1.iceRole); - expect(data1.iceParameters).toEqual(transport1.iceParameters); - expect(data1.iceCandidates).toEqual(transport1.iceCandidates); - expect(data1.iceState).toBe(transport1.iceState); - expect(data1.iceSelectedTuple).toEqual(transport1.iceSelectedTuple); - expect(data1.dtlsParameters).toEqual(transport1.dtlsParameters); - expect(data1.dtlsState).toBe(transport1.dtlsState); - expect(data1.sctpParameters).toEqual(transport1.sctpParameters); - expect(data1.sctpState).toBe(transport1.sctpState); - expect(typeof data1.recvRtpHeaderExtensions).toBe('object'); - expect(typeof data1.rtpListener).toBe('object'); - - transport1.close(); - expect(transport1.closed).toBe(true); - - const anotherTransport = await router.createWebRtcTransport( - { listenIps: [ '127.0.0.1' ] }); - - expect(typeof anotherTransport).toBe('object'); -}, 2000); - -test('router.createWebRtcTransport() with wrong arguments rejects with TypeError', async () => -{ - // @ts-ignore - await expect(router.createWebRtcTransport({})) - .rejects - .toThrow(TypeError); - - await expect(router.createWebRtcTransport({ listenIps: [] })) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(router.createWebRtcTransport({ listenIps: [ 123 ] })) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(router.createWebRtcTransport({ listenIps: '127.0.0.1' })) - .rejects - .toThrow(TypeError); - - await expect(router.createWebRtcTransport( - { - listenIps : [ '127.0.0.1' ], - // @ts-ignore - appData : 'NOT-AN-OBJECT' - })) - .rejects - .toThrow(TypeError); - - await expect(router.createWebRtcTransport( - { - listenIps : [ '127.0.0.1' ], - enableSctp : true, - // @ts-ignore - numSctpStreams : 'foo' - })) - .rejects - .toThrow(TypeError); -}, 2000); - -test('router.createWebRtcTransport() with non bindable IP rejects with Error', async () => -{ - await expect(router.createWebRtcTransport({ listenIps: [ '8.8.8.8' ] })) - .rejects - .toThrow(Error); -}, 2000); - -test('webRtcTransport.getStats() succeeds', async () => -{ - const data = await transport.getStats(); - - expect(Array.isArray(data)).toBe(true); - expect(data.length).toBe(1); - expect(data[0].type).toBe('webrtc-transport'); - expect(data[0].transportId).toBe(transport.id); - expect(typeof data[0].timestamp).toBe('number'); - expect(data[0].iceRole).toBe('controlled'); - expect(data[0].iceState).toBe('new'); - expect(data[0].dtlsState).toBe('new'); - expect(data[0].sctpState).toBeUndefined(); - expect(data[0].bytesReceived).toBe(0); - expect(data[0].recvBitrate).toBe(0); - expect(data[0].bytesSent).toBe(0); - expect(data[0].sendBitrate).toBe(0); - expect(data[0].rtpBytesReceived).toBe(0); - expect(data[0].rtpRecvBitrate).toBe(0); - expect(data[0].rtpBytesSent).toBe(0); - expect(data[0].rtpSendBitrate).toBe(0); - expect(data[0].rtxBytesReceived).toBe(0); - expect(data[0].rtxRecvBitrate).toBe(0); - expect(data[0].rtxBytesSent).toBe(0); - expect(data[0].rtxSendBitrate).toBe(0); - expect(data[0].probationBytesSent).toBe(0); - expect(data[0].probationSendBitrate).toBe(0); - expect(data[0].iceSelectedTuple).toBeUndefined(); - expect(data[0].maxIncomingBitrate).toBeUndefined(); -}, 2000); - -test('webRtcTransport.connect() succeeds', async () => -{ - const dtlsRemoteParameters: mediasoup.types.DtlsParameters = - { - fingerprints : - [ - { - algorithm : 'sha-256', - value : '82:5A:68:3D:36:C3:0A:DE:AF:E7:32:43:D2:88:83:57:AC:2D:65:E5:80:C4:B6:FB:AF:1A:A0:21:9F:6D:0C:AD' - } - ], - role : 'client' - }; - - await expect(transport.connect({ dtlsParameters: dtlsRemoteParameters })) - .resolves - .toBeUndefined(); - - // Must fail if connected. - await expect(transport.connect({ dtlsParameters: dtlsRemoteParameters })) - .rejects - .toThrow(Error); - - expect(transport.dtlsParameters.role).toBe('server'); -}, 2000); - -test('webRtcTransport.connect() with wrong arguments rejects with TypeError', async () => -{ - let dtlsRemoteParameters: mediasoup.types.DtlsParameters; - - // @ts-ignore - await expect(transport.connect({})) - .rejects - .toThrow(TypeError); - - dtlsRemoteParameters = - { - fingerprints : - [ - { - algorithm : 'sha-256000', - value : '82:5A:68:3D:36:C3:0A:DE:AF:E7:32:43:D2:88:83:57:AC:2D:65:E5:80:C4:B6:FB:AF:1A:A0:21:9F:6D:0C:AD' - } - ], - role : 'client' - }; - - await expect(transport.connect({ dtlsParameters: dtlsRemoteParameters })) - .rejects - .toThrow(TypeError); - - dtlsRemoteParameters = - { - fingerprints : - [ - { - algorithm : 'sha-256', - value : '82:5A:68:3D:36:C3:0A:DE:AF:E7:32:43:D2:88:83:57:AC:2D:65:E5:80:C4:B6:FB:AF:1A:A0:21:9F:6D:0C:AD' - } - ], - // @ts-ignore - role : 'chicken' - }; - - await expect(transport.connect({ dtlsParameters: dtlsRemoteParameters })) - .rejects - .toThrow(TypeError); - - dtlsRemoteParameters = - { - fingerprints : [], - role : 'client' - }; - - await expect(transport.connect({ dtlsParameters: dtlsRemoteParameters })) - .rejects - .toThrow(TypeError); - - await expect(transport.connect({ dtlsParameters: dtlsRemoteParameters })) - .rejects - .toThrow(TypeError); - - expect(transport.dtlsParameters.role).toBe('auto'); -}, 2000); - -test('webRtcTransport.setMaxIncomingBitrate() succeeds', async () => -{ - await expect(transport.setMaxIncomingBitrate(100000)) - .resolves - .toBeUndefined(); -}, 2000); - -test('webRtcTransport.setMaxOutgoingBitrate() succeeds', async () => -{ - await expect(transport.setMaxIncomingBitrate(100000)) - .resolves - .toBeUndefined(); -}, 2000); - -test('webRtcTransport.restartIce() succeeds', async () => -{ - const previousIceUsernameFragment = transport.iceParameters.usernameFragment; - const previousIcePassword = transport.iceParameters.password; - - await expect(transport.restartIce()) - .resolves - .toMatchObject( - { - usernameFragment : expect.any(String), - password : expect.any(String), - iceLite : true - }); - - expect(typeof transport.iceParameters.usernameFragment).toBe('string'); - expect(typeof transport.iceParameters.password).toBe('string'); - expect(transport.iceParameters.usernameFragment) - .not.toBe(previousIceUsernameFragment); - expect(transport.iceParameters.password).not.toBe(previousIcePassword); -}, 2000); - -test('transport.enableTraceEvent() succeed', async () => -{ - // @ts-ignore - await transport.enableTraceEvent([ 'foo', 'probation' ]); - await expect(transport.dump()) - .resolves - .toMatchObject({ traceEventTypes: 'probation' }); - - await transport.enableTraceEvent([]); - await expect(transport.dump()) - .resolves - .toMatchObject({ traceEventTypes: '' }); - - // @ts-ignore - await transport.enableTraceEvent([ 'probation', 'FOO', 'bwe', 'BAR' ]); - await expect(transport.dump()) - .resolves - .toMatchObject({ traceEventTypes: 'probation,bwe' }); - - await transport.enableTraceEvent(); - await expect(transport.dump()) - .resolves - .toMatchObject({ traceEventTypes: '' }); -}, 2000); - -test('transport.enableTraceEvent() with wrong arguments rejects with TypeError', async () => -{ - // @ts-ignore - await expect(transport.enableTraceEvent(123)) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(transport.enableTraceEvent('probation')) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(transport.enableTraceEvent([ 'probation', 123.123 ])) - .rejects - .toThrow(TypeError); -}, 2000); - -test('WebRtcTransport events succeed', async () => -{ - // Private API. - const channel = transport.channelForTesting; - const onIceStateChange = jest.fn(); - - transport.on('icestatechange', onIceStateChange); - - channel.emit(transport.id, 'icestatechange', { iceState: 'completed' }); - - expect(onIceStateChange).toHaveBeenCalledTimes(1); - expect(onIceStateChange).toHaveBeenCalledWith('completed'); - expect(transport.iceState).toBe('completed'); - - const onIceSelectedTuple = jest.fn(); - const iceSelectedTuple = - { - localIp : '1.1.1.1', - localPort : 1111, - remoteIp : '2.2.2.2', - remotePort : 2222, - protocol : 'udp' - }; - - transport.on('iceselectedtuplechange', onIceSelectedTuple); - channel.emit(transport.id, 'iceselectedtuplechange', { iceSelectedTuple }); - - expect(onIceSelectedTuple).toHaveBeenCalledTimes(1); - expect(onIceSelectedTuple).toHaveBeenCalledWith(iceSelectedTuple); - expect(transport.iceSelectedTuple).toEqual(iceSelectedTuple); - - const onDtlsStateChange = jest.fn(); - - transport.on('dtlsstatechange', onDtlsStateChange); - channel.emit(transport.id, 'dtlsstatechange', { dtlsState: 'connecting' }); - - expect(onDtlsStateChange).toHaveBeenCalledTimes(1); - expect(onDtlsStateChange).toHaveBeenCalledWith('connecting'); - expect(transport.dtlsState).toBe('connecting'); - - channel.emit( - transport.id, 'dtlsstatechange', { dtlsState: 'connected', dtlsRemoteCert: 'ABCD' }); - - expect(onDtlsStateChange).toHaveBeenCalledTimes(2); - expect(onDtlsStateChange).toHaveBeenCalledWith('connected'); - expect(transport.dtlsState).toBe('connected'); - expect(transport.dtlsRemoteCert).toBe('ABCD'); -}, 2000); - -test('WebRtcTransport methods reject if closed', async () => -{ - const onObserverClose = jest.fn(); - - transport.observer.once('close', onObserverClose); - transport.close(); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(transport.closed).toBe(true); - expect(transport.iceState).toBe('closed'); - expect(transport.iceSelectedTuple).toBeUndefined(); - expect(transport.dtlsState).toBe('closed'); - expect(transport.sctpState).toBeUndefined(); - - await expect(transport.dump()) - .rejects - .toThrow(Error); - - await expect(transport.getStats()) - .rejects - .toThrow(Error); - - // @ts-ignore - await expect(transport.connect({})) - .rejects - .toThrow(Error); - - await expect(transport.setMaxIncomingBitrate(200000)) - .rejects - .toThrow(Error); - - await expect(transport.setMaxOutgoingBitrate(200000)) - .rejects - .toThrow(Error); - - await expect(transport.restartIce()) - .rejects - .toThrow(Error); -}, 2000); - -test('router.createWebRtcTransport() with fixed port succeeds', async () => -{ - - const port = await pickPort({ ip: '127.0.0.1', reserveTimeout: 0 }); - const webRtcTransport = await router.createWebRtcTransport( - { - listenIps : [ '127.0.0.1' ], - port - }); - - expect(webRtcTransport.iceCandidates[0].port).toEqual(port); - - webRtcTransport.close(); -}, 2000); - -test('WebRtcTransport emits "routerclose" if Router is closed', async () => -{ - // We need different Router and WebRtcTransport instances here. - const router2 = await worker.createRouter({ mediaCodecs }); - const transport2 = await router2.createWebRtcTransport( - { - listenIps : [ '127.0.0.1' ], - enableSctp : true - }); - const onObserverClose = jest.fn(); - - transport2.observer.once('close', onObserverClose); - - await new Promise((resolve) => - { - transport2.on('routerclose', resolve); - router2.close(); - }); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(transport2.closed).toBe(true); - expect(transport2.iceState).toBe('closed'); - expect(transport2.iceSelectedTuple).toBeUndefined(); - expect(transport2.dtlsState).toBe('closed'); - expect(transport2.sctpState).toBe('closed'); -}, 2000); - -test('WebRtcTransport emits "routerclose" if Worker is closed', async () => -{ - const onObserverClose = jest.fn(); - - transport.observer.once('close', onObserverClose); - - await new Promise((resolve) => - { - transport.on('routerclose', resolve); - worker.close(); - }); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(transport.closed).toBe(true); - expect(transport.iceState).toBe('closed'); - expect(transport.iceSelectedTuple).toBeUndefined(); - expect(transport.dtlsState).toBe('closed'); - expect(transport.sctpState).toBeUndefined(); -}, 2000); diff --git a/node/src/tests/test-Worker.ts b/node/src/tests/test-Worker.ts deleted file mode 100644 index 006bdd8be2..0000000000 --- a/node/src/tests/test-Worker.ts +++ /dev/null @@ -1,322 +0,0 @@ -import * as os from 'os'; -import * as process from 'process'; -import * as path from 'path'; -import * as mediasoup from '../'; -import { InvalidStateError } from '../errors'; - -const { createWorker, observer } = mediasoup; - -let worker: mediasoup.types.Worker; - -beforeEach(() => worker && !worker.closed && worker.close()); -afterEach(() => worker && !worker.closed && worker.close()); - -test('createWorker() succeeds', async () => -{ - const onObserverNewWorker = jest.fn(); - - observer.once('newworker', onObserverNewWorker); - - worker = await createWorker(); - - expect(onObserverNewWorker).toHaveBeenCalledTimes(1); - expect(onObserverNewWorker).toHaveBeenCalledWith(worker); - expect(worker.constructor.name).toBe('Worker'); - expect(typeof worker.pid).toBe('number'); - expect(worker.closed).toBe(false); - expect(worker.died).toBe(false); - - worker.close(); - - expect(worker.closed).toBe(true); - expect(worker.died).toBe(false); - - // eslint-disable-next-line require-atomic-updates - worker = await createWorker( - { - logLevel : 'debug', - logTags : [ 'info' ], - rtcMinPort : 0, - rtcMaxPort : 9999, - dtlsCertificateFile : path.join(__dirname, 'data', 'dtls-cert.pem'), - dtlsPrivateKeyFile : path.join(__dirname, 'data', 'dtls-key.pem'), - libwebrtcFieldTrials : 'WebRTC-Bwe-AlrLimitedBackoff/Disabled/', - appData : { bar: 456 } - }); - expect(worker.constructor.name).toBe('Worker'); - expect(typeof worker.pid).toBe('number'); - expect(worker.closed).toBe(false); - expect(worker.died).toBe(false); - expect(worker.appData).toEqual({ bar: 456 }); - - worker.close(); - - expect(worker.closed).toBe(true); - expect(worker.died).toBe(false); -}, 2000); - -test('createWorker() with wrong settings rejects with TypeError', async () => -{ - // @ts-ignore - await expect(createWorker({ logLevel: 'chicken' })) - .rejects - .toThrow(TypeError); - - await expect(createWorker({ rtcMinPort: 1000, rtcMaxPort: 999 })) - .rejects - .toThrow(TypeError); - - // Port is from 0 to 65535. - await expect(createWorker({ rtcMinPort: 1000, rtcMaxPort: 65536 })) - .rejects - .toThrow(TypeError); - - await expect(createWorker({ dtlsCertificateFile: '/notfound/cert.pem' })) - .rejects - .toThrow(TypeError); - - await expect(createWorker({ dtlsPrivateKeyFile: '/notfound/priv.pem' })) - .rejects - .toThrow(TypeError); - - // @ts-ignore - await expect(createWorker({ appData: 'NOT-AN-OBJECT' })) - .rejects - .toThrow(TypeError); -}, 2000); - -test('worker.updateSettings() succeeds', async () => -{ - worker = await createWorker(); - - await expect(worker.updateSettings({ logLevel: 'debug', logTags: [ 'ice' ] })) - .resolves - .toBeUndefined(); - - worker.close(); -}, 2000); - -test('worker.updateSettings() with wrong settings rejects with TypeError', async () => -{ - worker = await createWorker(); - - // @ts-ignore - await expect(worker.updateSettings({ logLevel: 'chicken' })) - .rejects - .toThrow(TypeError); - - worker.close(); -}, 2000); - -test('worker.updateSettings() rejects with InvalidStateError if closed', async () => -{ - worker = await createWorker(); - worker.close(); - - await expect(worker.updateSettings({ logLevel: 'error' })) - .rejects - .toThrow(InvalidStateError); - - worker.close(); -}, 2000); - -test('worker.dump() succeeds', async () => -{ - worker = await createWorker(); - - await expect(worker.dump()) - .resolves - .toEqual( - { - pid : worker.pid, - webRtcServerIds : [], - routerIds : [], - channelMessageHandlers : - { - channelRequestHandlers : [], - payloadChannelRequestHandlers : [], - payloadChannelNotificationHandlers : [] - } - }); - - worker.close(); -}, 2000); - -test('worker.dump() rejects with InvalidStateError if closed', async () => -{ - worker = await createWorker(); - worker.close(); - - await expect(worker.dump()) - .rejects - .toThrow(InvalidStateError); - - worker.close(); -}, 2000); - -test('worker.getResourceUsage() succeeds', async () => -{ - worker = await createWorker(); - - await expect(worker.getResourceUsage()) - .resolves - .toMatchObject({}); - - worker.close(); -}, 2000); - -test('worker.close() succeeds', async () => -{ - worker = await createWorker({ logLevel: 'warn' }); - - const onObserverClose = jest.fn(); - - worker.observer.once('close', onObserverClose); - worker.close(); - - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(worker.closed).toBe(true); - expect(worker.died).toBe(false); -}, 2000); - -test('Worker emits "died" if worker process died unexpectedly', async () => -{ - let onDied: ReturnType; - let onObserverClose: ReturnType; - - worker = await createWorker({ logLevel: 'warn' }); - onDied = jest.fn(); - onObserverClose = jest.fn(); - - worker.observer.once('close', onObserverClose); - - await new Promise((resolve, reject) => - { - worker.on('died', () => - { - onDied(); - - if (onObserverClose.mock.calls.length > 0) - { - reject( - new Error('observer "close" event emitted before worker "died" event')); - } - else if (worker.closed) - { - resolve(); - } - else - { - reject(new Error('worker.closed is false')); - } - }); - - process.kill(worker.pid, 'SIGINT'); - }); - - expect(onDied).toHaveBeenCalledTimes(1); - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(worker.closed).toBe(true); - expect(worker.died).toBe(true); - - // eslint-disable-next-line require-atomic-updates - worker = await createWorker({ logLevel: 'warn' }); - onDied = jest.fn(); - onObserverClose = jest.fn(); - - worker.observer.once('close', onObserverClose); - - await new Promise((resolve, reject) => - { - worker.on('died', () => - { - onDied(); - - if (onObserverClose.mock.calls.length > 0) - { - reject( - new Error('observer "close" event emitted before worker "died" event')); - } - else if (worker.closed) - { - resolve(); - } - else - { - reject(new Error('worker.closed is false')); - } - }); - - process.kill(worker.pid, 'SIGTERM'); - }); - - expect(onDied).toHaveBeenCalledTimes(1); - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(worker.closed).toBe(true); - expect(worker.died).toBe(true); - - // eslint-disable-next-line require-atomic-updates - worker = await createWorker({ logLevel: 'warn' }); - onDied = jest.fn(); - onObserverClose = jest.fn(); - - worker.observer.once('close', onObserverClose); - - await new Promise((resolve, reject) => - { - worker.on('died', () => - { - onDied(); - - if (onObserverClose.mock.calls.length > 0) - { - reject( - new Error('observer "close" event emitted before worker "died" event')); - } - else if (worker.closed) - { - resolve(); - } - else - { - reject(new Error('worker.closed is false')); - } - }); - - process.kill(worker.pid, 'SIGKILL'); - }); - - expect(onDied).toHaveBeenCalledTimes(1); - expect(onObserverClose).toHaveBeenCalledTimes(1); - expect(worker.closed).toBe(true); - expect(worker.died).toBe(true); -}, 5000); - -test('worker process ignores PIPE, HUP, ALRM, USR1 and USR2 signals', async () => -{ - // Windows doesn't have some signals such as SIGPIPE, SIGALRM, SIGUSR1, SIGUSR2 - // so we just skip this test in Windows. - if (os.platform() === 'win32') - return; - - worker = await createWorker({ logLevel: 'warn' }); - - await new Promise((resolve, reject) => - { - worker.on('died', reject); - - process.kill(worker.pid, 'SIGPIPE'); - process.kill(worker.pid, 'SIGHUP'); - process.kill(worker.pid, 'SIGALRM'); - process.kill(worker.pid, 'SIGUSR1'); - process.kill(worker.pid, 'SIGUSR2'); - - setTimeout(() => - { - expect(worker.closed).toBe(false); - - worker.close(); - resolve(); - }, 2000); - }); -}, 3000); diff --git a/node/src/tests/test-mediasoup.ts b/node/src/tests/test-mediasoup.ts deleted file mode 100644 index 706341748e..0000000000 --- a/node/src/tests/test-mediasoup.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as mediasoup from '../'; - -const { - getSupportedRtpCapabilities, - parseScalabilityMode -} = mediasoup; - -test('mediasoup.getSupportedRtpCapabilities() returns the mediasoup RTP capabilities', () => -{ - const rtpCapabilities = getSupportedRtpCapabilities(); - - expect(typeof rtpCapabilities).toBe('object'); - - // Mangle retrieved codecs to check that, if called again, - // getSupportedRtpCapabilities() returns a cloned object. - // @ts-ignore - rtpCapabilities.codecs = 'bar'; - - const rtpCapabilities2 = getSupportedRtpCapabilities(); - - expect(rtpCapabilities2).not.toEqual(rtpCapabilities); -}, 500); - -test('parseScalabilityMode() works', () => -{ - expect(parseScalabilityMode('L1T3')) - .toEqual({ spatialLayers: 1, temporalLayers: 3, ksvc: false }); - - expect(parseScalabilityMode('L3T2_KEY')) - .toEqual({ spatialLayers: 3, temporalLayers: 2, ksvc: true }); - - expect(parseScalabilityMode('S2T3')) - .toEqual({ spatialLayers: 2, temporalLayers: 3, ksvc: false }); - - expect(parseScalabilityMode('foo')) - .toEqual({ spatialLayers: 1, temporalLayers: 1, ksvc: false }); - - expect(parseScalabilityMode(undefined)) - .toEqual({ spatialLayers: 1, temporalLayers: 1, ksvc: false }); - - expect(parseScalabilityMode('S0T3')) - .toEqual({ spatialLayers: 1, temporalLayers: 1, ksvc: false }); - - expect(parseScalabilityMode('S1T0')) - .toEqual({ spatialLayers: 1, temporalLayers: 1, ksvc: false }); - - expect(parseScalabilityMode('L20T3')) - .toEqual({ spatialLayers: 20, temporalLayers: 3, ksvc: false }); - - expect(parseScalabilityMode('S200T3')) - .toEqual({ spatialLayers: 1, temporalLayers: 1, ksvc: false }); - - expect(parseScalabilityMode('L4T7_KEY_SHIFT')) - .toEqual({ spatialLayers: 4, temporalLayers: 7, ksvc: true }); -}, 500); diff --git a/node/src/tests/test-multiopus.ts b/node/src/tests/test-multiopus.ts deleted file mode 100644 index b1e3a260ed..0000000000 --- a/node/src/tests/test-multiopus.ts +++ /dev/null @@ -1,259 +0,0 @@ -import * as mediasoup from '../'; -import { UnsupportedError } from '../errors'; - -const { createWorker } = mediasoup; - -let worker: mediasoup.types.Worker; -let router: mediasoup.types.Router; -let transport: mediasoup.types.WebRtcTransport; - -const mediaCodecs: mediasoup.types.RtpCodecCapability[] = -[ - { - kind : 'audio', - mimeType : 'audio/multiopus', - clockRate : 48000, - channels : 6, - parameters : - { - useinbandfec : 1, - 'channel_mapping' : '0,4,1,2,3,5', - 'num_streams' : 4, - 'coupled_streams' : 2 - } - } -]; - -const audioProducerParameters: mediasoup.types.ProducerOptions = -{ - kind : 'audio', - rtpParameters : - { - mid : 'AUDIO', - codecs : - [ - { - mimeType : 'audio/multiopus', - payloadType : 0, - clockRate : 48000, - channels : 6, - parameters : - { - useinbandfec : 1, - 'channel_mapping' : '0,4,1,2,3,5', - 'num_streams' : 4, - 'coupled_streams' : 2 - } - } - ], - headerExtensions : - [ - { - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - id : 10 - }, - { - uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', - id : 12 - } - ] - } -}; - -const consumerDeviceCapabilities: mediasoup.types.RtpCapabilities = -{ - codecs : - [ - { - mimeType : 'audio/multiopus', - kind : 'audio', - preferredPayloadType : 100, - clockRate : 48000, - channels : 6, - parameters : - { - 'channel_mapping' : '0,4,1,2,3,5', - 'num_streams' : 4, - 'coupled_streams' : 2 - } - } - ], - headerExtensions : - [ - { - kind : 'audio', - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - preferredId : 1, - preferredEncrypt : false - }, - { - kind : 'audio', - uri : 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time', // eslint-disable-line max-len - preferredId : 4, - preferredEncrypt : false - }, - { - kind : 'audio', - uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', - preferredId : 10, - preferredEncrypt : false - } - ] -}; - -beforeAll(async () => -{ - worker = await createWorker(); - router = await worker.createRouter({ mediaCodecs }); - transport = await router.createWebRtcTransport( - { - listenIps : [ '127.0.0.1' ] - }); -}); - -afterAll(() => worker.close()); - -test('produce/consume succeeds', async () => -{ - const audioProducer = await transport.produce(audioProducerParameters); - - expect(audioProducer.rtpParameters.codecs).toEqual([ - { - mimeType : 'audio/multiopus', - payloadType : 0, - clockRate : 48000, - channels : 6, - parameters : - { - useinbandfec : 1, - 'channel_mapping' : '0,4,1,2,3,5', - 'num_streams' : 4, - 'coupled_streams' : 2 - }, - rtcpFeedback : [] - } - ]); - - expect(router.canConsume({ - producerId : audioProducer.id, - rtpCapabilities : consumerDeviceCapabilities - })) - .toBe(true); - - const audioConsumer = await transport.consume({ - producerId : audioProducer.id, - rtpCapabilities : consumerDeviceCapabilities - }); - - expect(audioConsumer.rtpParameters.codecs).toEqual([ - { - mimeType : 'audio/multiopus', - payloadType : 100, - clockRate : 48000, - channels : 6, - parameters : - { - useinbandfec : 1, - 'channel_mapping' : '0,4,1,2,3,5', - 'num_streams' : 4, - 'coupled_streams' : 2 - }, - rtcpFeedback : [] - } - ]); - - audioProducer.close(); - audioConsumer.close(); -}, 2000); - -test('fails to produce wrong parameters', async () => -{ - await expect(transport.produce({ - kind : 'audio', - rtpParameters : - { - mid : 'AUDIO', - codecs : - [ - { - mimeType : 'audio/multiopus', - payloadType : 0, - clockRate : 48000, - channels : 6, - parameters : - { - 'channel_mapping' : '0,4,1,2,3,5', - 'num_streams' : 2, - 'coupled_streams' : 2 - } - } - ] - } - })) - .rejects - .toThrow(UnsupportedError); - - await expect(transport.produce({ - kind : 'audio', - rtpParameters : - { - mid : 'AUDIO', - codecs : - [ - { - mimeType : 'audio/multiopus', - payloadType : 0, - clockRate : 48000, - channels : 6, - parameters : - { - 'channel_mapping' : '0,4,1,2,3,5', - 'num_streams' : 4, - 'coupled_streams' : 1 - } - } - ] - } - })) - .rejects - .toThrow(UnsupportedError); -}, 2000); - -test('fails to consume wrong channels', async () => -{ - const audioProducer = await transport.produce(audioProducerParameters); - - const localConsumerDeviceCapabilities: mediasoup.types.RtpCapabilities = { - codecs : - [ - { - mimeType : 'audio/multiopus', - kind : 'audio', - preferredPayloadType : 100, - clockRate : 48000, - channels : 8, - parameters : - { - 'channel_mapping' : '0,4,1,2,3,5', - 'num_streams' : 4, - 'coupled_streams' : 2 - } - } - ] - }; - - expect(!router.canConsume({ - producerId : audioProducer.id, - rtpCapabilities : localConsumerDeviceCapabilities - })) - .toBe(true); - - await expect(transport.consume({ - producerId : audioProducer.id, - rtpCapabilities : localConsumerDeviceCapabilities - })) - .rejects - .toThrow(Error); - - audioProducer.close(); -}, 2000); diff --git a/node/src/tests/test-node-sctp.ts b/node/src/tests/test-node-sctp.ts deleted file mode 100644 index 2aca0a4cfb..0000000000 --- a/node/src/tests/test-node-sctp.ts +++ /dev/null @@ -1,235 +0,0 @@ -import * as dgram from 'dgram'; -// @ts-ignore -import * as sctp from 'sctp'; -import * as mediasoup from '../'; - -const { createWorker } = mediasoup; - -// Set node-sctp default PMTU to 1200. -sctp.defaults({ PMTU: 1200 }); - -let worker: mediasoup.types.Worker; -let router: mediasoup.types.Router; -let transport: mediasoup.types.PlainTransport; -let dataProducer: mediasoup.types.DataProducer; -let dataConsumer: mediasoup.types.DataConsumer; -let udpSocket: dgram.Socket; -let sctpSocket: any; -let sctpSendStreamId: number; -let sctpSendStream: any; - -beforeAll(async () => -{ - worker = await createWorker(); - router = await worker.createRouter(); - transport = await router.createPlainTransport( - { - listenIp : '127.0.0.1', // https://github.com/nodejs/node/issues/14900 - comedia : true, // So we don't need to call transport.connect(). - enableSctp : true, - numSctpStreams : { OS: 256, MIS: 256 } - }); - - // Node UDP socket for SCTP. - udpSocket = dgram.createSocket({ type: 'udp4' }); - - await new Promise((resolve) => udpSocket.bind(0, '127.0.0.1', resolve)); - - const remoteUdpIp = transport.tuple.localIp; - const remoteUdpPort = transport.tuple.localPort; - const { OS, MIS } = transport.sctpParameters!; - - // Use UDP connected socket if Node >= 12. - if (typeof udpSocket.connect === 'function') - { - await new Promise((resolve, reject) => - { - // @ts-ignore - udpSocket.connect(remoteUdpPort, remoteUdpIp, (error: Error) => - { - if (error) - { - reject(error); - - return; - } - - sctpSocket = sctp.connect( - { - localPort : 5000, // Required for SCTP over UDP in mediasoup. - port : 5000, // Required for SCTP over UDP in mediasoup. - OS : OS, - MIS : MIS, - udpTransport : udpSocket - }); - - resolve(); - }); - }); - } - // Use UDP disconnected socket if Node < 12. - else - { - sctpSocket = sctp.connect( - { - localPort : 5000, // Required for SCTP over UDP in mediasoup. - port : 5000, // Required for SCTP over UDP in mediasoup. - OS : OS, - MIS : MIS, - udpTransport : udpSocket, - udpPeer : - { - address : remoteUdpIp, - port : remoteUdpPort - } - }); - } - - // Wait for the SCTP association to be open. - await Promise.race( - [ - new Promise((resolve) => sctpSocket.on('connect', resolve)), - new Promise((resolve, reject) => ( - setTimeout(() => reject(new Error('SCTP connection timeout')), 3000) - )) - ]); - - // Create an explicit SCTP outgoing stream with id 123 (id 0 is already used - // by the implicit SCTP outgoing stream built-in the SCTP socket). - sctpSendStreamId = 123; - sctpSendStream = sctpSocket.createStream(sctpSendStreamId); - - // Create a DataProducer with the corresponding SCTP stream id. - dataProducer = await transport.produceData( - { - sctpStreamParameters : - { - streamId : sctpSendStreamId, - ordered : true - }, - label : 'node-sctp', - protocol : 'foo & bar 😀😀😀' - }); - - // Create a DataConsumer to receive messages from the DataProducer over the - // same transport. - dataConsumer = await transport.consumeData({ dataProducerId: dataProducer.id }); -}); - -afterAll(() => -{ - udpSocket.close(); - sctpSocket.end(); - worker.close(); -}); - -test('ordered DataProducer delivers all SCTP messages to the DataConsumer', async () => -{ - const onStream = jest.fn(); - const numMessages = 200; - let sentMessageBytes = 0; - let recvMessageBytes = 0; - let lastSentMessageId = 0; - let lastRecvMessageId = 0; - - // It must be zero because it's the first DataConsumer on the transport. - expect(dataConsumer.sctpStreamParameters?.streamId).toBe(0); - - await new Promise((resolve) => - { - // Send SCTP messages over the sctpSendStream created above. - const interval = setInterval(() => - { - const id = ++lastSentMessageId; - const data = Buffer.from(String(id)); - - // Set ppid of type WebRTC DataChannel string. - if (id < numMessages / 2) - { - // @ts-ignore - data.ppid = sctp.PPID.WEBRTC_STRING; - } - // Set ppid of type WebRTC DataChannel binary. - else - { - // @ts-ignore - data.ppid = sctp.PPID.WEBRTC_BINARY; - } - - sctpSendStream.write(data); - sentMessageBytes += data.byteLength; - - if (id === numMessages) - clearInterval(interval); - }, 10); - - sctpSocket.on('stream', onStream); - - // Handle the generated SCTP incoming stream and SCTP messages receives on it. - // @ts-ignore - sctpSocket.on('stream', (stream, streamId) => - { - // It must be zero because it's the first SCTP incoming stream (so first - // DataConsumer). - expect(streamId).toBe(0); - - // @ts-ignore - stream.on('data', (data: Buffer) => - { - recvMessageBytes += data.byteLength; - - const id = Number(data.toString('utf8')); - - if (id === numMessages) - { - clearInterval(interval); - resolve(); - } - - if (id < numMessages / 2) - { - // @ts-ignore - expect(data.ppid).toBe(sctp.PPID.WEBRTC_STRING); - } - else - { - // @ts-ignore - expect(data.ppid).toBe(sctp.PPID.WEBRTC_BINARY); - } - - expect(id).toBe(++lastRecvMessageId); - }); - }); - }); - - expect(onStream).toHaveBeenCalledTimes(1); - expect(lastSentMessageId).toBe(numMessages); - expect(lastRecvMessageId).toBe(numMessages); - expect(recvMessageBytes).toBe(sentMessageBytes); - - await expect(dataProducer.getStats()) - .resolves - .toMatchObject( - [ - { - type : 'data-producer', - label : dataProducer.label, - protocol : dataProducer.protocol, - messagesReceived : numMessages, - bytesReceived : sentMessageBytes - } - ]); - - await expect(dataConsumer.getStats()) - .resolves - .toMatchObject( - [ - { - type : 'data-consumer', - label : dataConsumer.label, - protocol : dataConsumer.protocol, - messagesSent : numMessages, - bytesSent : recvMessageBytes - } - ]); -}, 10000); diff --git a/node/src/tests/test-ortc.ts b/node/src/tests/test-ortc.ts deleted file mode 100644 index abe9558956..0000000000 --- a/node/src/tests/test-ortc.ts +++ /dev/null @@ -1,601 +0,0 @@ -import * as mediasoup from '../'; -import * as ortc from '../ortc'; -import { UnsupportedError } from '../errors'; - -test('generateRouterRtpCapabilities() succeeds', () => -{ - const mediaCodecs: mediasoup.types.RtpCodecCapability[] = - [ - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 2, - parameters : - { - useinbandfec : 1, - foo : 'bar' - } - }, - { - kind : 'video', - mimeType : 'video/VP8', - preferredPayloadType : 125, // Let's force it. - clockRate : 90000 - }, - { - kind : 'video', - mimeType : 'video/H264', - clockRate : 90000, - parameters : - { - 'level-asymmetry-allowed' : 1, - 'profile-level-id' : '42e01f', - foo : 'bar' - }, - rtcpFeedback : [] // Will be ignored. - } - ]; - - const rtpCapabilities = ortc.generateRouterRtpCapabilities(mediaCodecs); - - expect(rtpCapabilities.codecs?.length).toBe(5); - - // opus. - expect(rtpCapabilities.codecs?.[0]).toEqual( - { - kind : 'audio', - mimeType : 'audio/opus', - preferredPayloadType : 100, // 100 is the first available dynamic PT. - clockRate : 48000, - channels : 2, - parameters : - { - useinbandfec : 1, - foo : 'bar' - }, - rtcpFeedback : - [ - { type: 'transport-cc', parameter: '' } - ] - }); - - // VP8. - expect(rtpCapabilities.codecs?.[1]).toEqual( - { - kind : 'video', - mimeType : 'video/VP8', - preferredPayloadType : 125, - clockRate : 90000, - parameters : {}, - rtcpFeedback : - [ - { type: 'nack', parameter: '' }, - { type: 'nack', parameter: 'pli' }, - { type: 'ccm', parameter: 'fir' }, - { type: 'goog-remb', parameter: '' }, - { type: 'transport-cc', parameter: '' } - ] - }); - - // VP8 RTX. - expect(rtpCapabilities.codecs?.[2]).toEqual( - { - kind : 'video', - mimeType : 'video/rtx', - preferredPayloadType : 101, // 101 is the second available dynamic PT. - clockRate : 90000, - parameters : - { - apt : 125 - }, - rtcpFeedback : [] - }); - - // H264. - expect(rtpCapabilities.codecs?.[3]).toEqual( - { - kind : 'video', - mimeType : 'video/H264', - preferredPayloadType : 102, // 102 is the third available dynamic PT. - clockRate : 90000, - parameters : - { - // Since packetization-mode param was not included in the H264 codec - // and it's default value is 0, it's not added by ortc file. - // 'packetization-mode' : 0, - 'level-asymmetry-allowed' : 1, - 'profile-level-id' : '42e01f', - foo : 'bar' - }, - rtcpFeedback : - [ - { type: 'nack', parameter: '' }, - { type: 'nack', parameter: 'pli' }, - { type: 'ccm', parameter: 'fir' }, - { type: 'goog-remb', parameter: '' }, - { type: 'transport-cc', parameter: '' } - ] - }); - - // H264 RTX. - expect(rtpCapabilities.codecs?.[4]).toEqual( - { - kind : 'video', - mimeType : 'video/rtx', - preferredPayloadType : 103, - clockRate : 90000, - parameters : - { - apt : 102 - }, - rtcpFeedback : [] - }); -}); - -test('generateRouterRtpCapabilities() with unsupported codecs throws UnsupportedError', () => -{ - let mediaCodecs: mediasoup.types.RtpCodecCapability[]; - - mediaCodecs = - [ - { - kind : 'audio', - mimeType : 'audio/chicken', - clockRate : 8000, - channels : 4 - } - ]; - - expect(() => ortc.generateRouterRtpCapabilities(mediaCodecs)) - .toThrow(UnsupportedError); - - mediaCodecs = - [ - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 1 - } - ]; - - expect(() => ortc.generateRouterRtpCapabilities(mediaCodecs)) - .toThrow(UnsupportedError); -}); - -test('generateRouterRtpCapabilities() with too many codecs throws', () => -{ - const mediaCodecs: mediasoup.types.RtpCodecCapability[] = []; - - for (let i = 0; i < 100; ++i) - { - mediaCodecs.push( - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 2 - }); - } - - expect(() => ortc.generateRouterRtpCapabilities(mediaCodecs)) - .toThrow('cannot allocate'); -}); - -test('getProducerRtpParametersMapping(), getConsumableRtpParameters(), getConsumerRtpParameters() and getPipeConsumerRtpParameters() succeed', () => -{ - const mediaCodecs: mediasoup.types.RtpCodecCapability[] = - [ - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 2 - }, - { - kind : 'video', - mimeType : 'video/H264', - clockRate : 90000, - parameters : - { - 'level-asymmetry-allowed' : 1, - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032', - bar : 'lalala' - } - } - ]; - - const routerRtpCapabilities = ortc.generateRouterRtpCapabilities(mediaCodecs); - - const rtpParameters: mediasoup.types.RtpParameters = - { - codecs : - [ - { - mimeType : 'video/H264', - payloadType : 111, - clockRate : 90000, - parameters : - { - foo : 1234, - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - }, - rtcpFeedback : - [ - { type: 'nack', parameter: '' }, - { type: 'nack', parameter: 'pli' }, - { type: 'goog-remb', parameter: '' } - ] - }, - { - mimeType : 'video/rtx', - payloadType : 112, - clockRate : 90000, - parameters : - { - apt : 111 - }, - rtcpFeedback : [] - } - ], - headerExtensions : - [ - { - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - id : 1 - }, - { - uri : 'urn:3gpp:video-orientation', - id : 2 - } - ], - encodings : - [ - { - ssrc : 11111111, - rtx : { ssrc: 11111112 }, - maxBitrate : 111111, - scalabilityMode : 'L1T3' - }, - { - ssrc : 21111111, - rtx : { ssrc: 21111112 }, - maxBitrate : 222222, - scalabilityMode : 'L1T3' - }, - { - rid : 'high', - maxBitrate : 333333, - scalabilityMode : 'L1T3' - } - ], - rtcp : - { - cname : 'qwerty1234' - } - }; - - const rtpMapping = - ortc.getProducerRtpParametersMapping(rtpParameters, routerRtpCapabilities); - - expect(rtpMapping.codecs).toEqual( - [ - { payloadType: 111, mappedPayloadType: 101 }, - { payloadType: 112, mappedPayloadType: 102 } - ]); - - expect(rtpMapping.encodings[0].ssrc).toBe(11111111); - expect(rtpMapping.encodings[0].rid).toBeUndefined(); - expect(typeof rtpMapping.encodings[0].mappedSsrc).toBe('number'); - expect(rtpMapping.encodings[1].ssrc).toBe(21111111); - expect(rtpMapping.encodings[1].rid).toBeUndefined(); - expect(typeof rtpMapping.encodings[1].mappedSsrc).toBe('number'); - expect(rtpMapping.encodings[2].ssrc).toBeUndefined(); - expect(rtpMapping.encodings[2].rid).toBe('high'); - expect(typeof rtpMapping.encodings[2].mappedSsrc).toBe('number'); - - const consumableRtpParameters = ortc.getConsumableRtpParameters( - 'video', rtpParameters, routerRtpCapabilities, rtpMapping); - - expect(consumableRtpParameters.codecs[0].mimeType).toBe('video/H264'); - expect(consumableRtpParameters.codecs[0].payloadType).toBe(101); - expect(consumableRtpParameters.codecs[0].clockRate).toBe(90000); - expect(consumableRtpParameters.codecs[0].parameters).toEqual( - { - foo : 1234, - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - }); - - expect(consumableRtpParameters.codecs[1].mimeType).toBe('video/rtx'); - expect(consumableRtpParameters.codecs[1].payloadType).toBe(102); - expect(consumableRtpParameters.codecs[1].clockRate).toBe(90000); - expect(consumableRtpParameters.codecs[1].parameters).toEqual({ apt: 101 }); - - expect(consumableRtpParameters.encodings?.[0]).toEqual( - { - ssrc : rtpMapping.encodings[0].mappedSsrc, - maxBitrate : 111111, - scalabilityMode : 'L1T3' - }); - expect(consumableRtpParameters.encodings?.[1]).toEqual( - { - ssrc : rtpMapping.encodings[1].mappedSsrc, - maxBitrate : 222222, - scalabilityMode : 'L1T3' - }); - expect(consumableRtpParameters.encodings?.[2]).toEqual( - { - ssrc : rtpMapping.encodings[2].mappedSsrc, - maxBitrate : 333333, - scalabilityMode : 'L1T3' - }); - - expect(consumableRtpParameters.rtcp).toEqual( - { - cname : rtpParameters.rtcp?.cname, - reducedSize : true, - mux : true - }); - - const remoteRtpCapabilities: mediasoup.types.RtpCapabilities = - { - codecs : - [ - { - kind : 'audio', - mimeType : 'audio/opus', - preferredPayloadType : 100, - clockRate : 48000, - channels : 2, - parameters : {}, - rtcpFeedback : [] - }, - { - kind : 'video', - mimeType : 'video/H264', - preferredPayloadType : 101, - clockRate : 90000, - parameters : - { - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032', - baz : 'LOLOLO' - }, - rtcpFeedback : - [ - { type: 'nack', parameter: '' }, - { type: 'nack', parameter: 'pli' }, - { type: 'foo', parameter: 'FOO' } - ] - }, - { - kind : 'video', - mimeType : 'video/rtx', - preferredPayloadType : 102, - clockRate : 90000, - parameters : - { - apt : 101 - }, - rtcpFeedback : [] - } - ], - headerExtensions : - [ - { - kind : 'audio', - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - preferredId : 1, - preferredEncrypt : false, - direction : 'sendrecv' - }, - { - kind : 'video', - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - preferredId : 1, - preferredEncrypt : false, - direction : 'sendrecv' - }, - { - kind : 'video', - uri : 'urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id', - preferredId : 2, - preferredEncrypt : false, - direction : 'sendrecv' - }, - { - kind : 'audio', - uri : 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', - preferredId : 8, - preferredEncrypt : false, - direction : 'sendrecv' - }, - { - kind : 'video', - uri : 'urn:3gpp:video-orientation', - preferredId : 11, - preferredEncrypt : false, - direction : 'sendrecv' - }, - { - kind : 'video', - uri : 'urn:ietf:params:rtp-hdrext:toffset', - preferredId : 12, - preferredEncrypt : false, - direction : 'sendrecv' - } - ] - }; - - const consumerRtpParameters = - ortc.getConsumerRtpParameters(consumableRtpParameters, remoteRtpCapabilities, false); - - expect(consumerRtpParameters.codecs.length).toEqual(2); - expect(consumerRtpParameters.codecs[0]).toEqual( - { - mimeType : 'video/H264', - payloadType : 101, - clockRate : 90000, - parameters : - { - foo : 1234, - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - }, - rtcpFeedback : - [ - { type: 'nack', parameter: '' }, - { type: 'nack', parameter: 'pli' }, - { type: 'foo', parameter: 'FOO' } - ] - }); - expect(consumerRtpParameters.codecs[1]).toEqual( - { - mimeType : 'video/rtx', - payloadType : 102, - clockRate : 90000, - parameters : - { - apt : 101 - }, - rtcpFeedback : [] - }); - - expect(consumerRtpParameters.encodings?.length).toBe(1); - expect(typeof consumerRtpParameters.encodings?.[0].ssrc).toBe('number'); - expect(typeof consumerRtpParameters.encodings?.[0].rtx).toBe('object'); - expect(typeof consumerRtpParameters.encodings?.[0].rtx?.ssrc).toBe('number'); - expect(consumerRtpParameters.encodings?.[0].scalabilityMode).toBe('S3T3'); - expect(consumerRtpParameters.encodings?.[0].maxBitrate).toBe(333333); - - expect(consumerRtpParameters.headerExtensions).toEqual( - [ - { - uri : 'urn:ietf:params:rtp-hdrext:sdes:mid', - id : 1, - encrypt : false, - parameters : {} - }, - { - uri : 'urn:3gpp:video-orientation', - id : 11, - encrypt : false, - parameters : {} - }, - { - uri : 'urn:ietf:params:rtp-hdrext:toffset', - id : 12, - encrypt : false, - parameters : {} - } - ]); - - expect(consumerRtpParameters.rtcp).toEqual( - { - cname : rtpParameters.rtcp?.cname, - reducedSize : true, - mux : true - }); - - const pipeConsumerRtpParameters = - ortc.getPipeConsumerRtpParameters(consumableRtpParameters); - - expect(pipeConsumerRtpParameters.codecs.length).toEqual(1); - expect(pipeConsumerRtpParameters.codecs[0]).toEqual( - { - mimeType : 'video/H264', - payloadType : 101, - clockRate : 90000, - parameters : - { - foo : 1234, - 'packetization-mode' : 1, - 'profile-level-id' : '4d0032' - }, - rtcpFeedback : - [ - { type: 'nack', parameter: 'pli' }, - { type: 'ccm', parameter: 'fir' } - ] - }); - - expect(pipeConsumerRtpParameters.encodings?.length).toBe(3); - expect(typeof pipeConsumerRtpParameters.encodings?.[0].ssrc).toBe('number'); - expect(pipeConsumerRtpParameters.encodings?.[0].rtx).toBeUndefined(); - expect(typeof pipeConsumerRtpParameters.encodings?.[0].maxBitrate).toBe('number'); - expect(pipeConsumerRtpParameters.encodings?.[0].scalabilityMode).toBe('L1T3'); - expect(typeof pipeConsumerRtpParameters.encodings?.[1].ssrc).toBe('number'); - expect(pipeConsumerRtpParameters.encodings?.[1].rtx).toBeUndefined(); - expect(typeof pipeConsumerRtpParameters.encodings?.[1].maxBitrate).toBe('number'); - expect(pipeConsumerRtpParameters.encodings?.[1].scalabilityMode).toBe('L1T3'); - expect(typeof pipeConsumerRtpParameters.encodings?.[2].ssrc).toBe('number'); - expect(pipeConsumerRtpParameters.encodings?.[2].rtx).toBeUndefined(); - expect(typeof pipeConsumerRtpParameters.encodings?.[2].maxBitrate).toBe('number'); - expect(pipeConsumerRtpParameters.encodings?.[2].scalabilityMode).toBe('L1T3'); - - expect(pipeConsumerRtpParameters.rtcp).toEqual( - { - cname : rtpParameters.rtcp?.cname, - reducedSize : true, - mux : true - }); -}); - -test('getProducerRtpParametersMapping() with incompatible params throws UnsupportedError', () => -{ - const mediaCodecs: mediasoup.types.RtpCodecCapability[] = - [ - { - kind : 'audio', - mimeType : 'audio/opus', - clockRate : 48000, - channels : 2 - }, - { - kind : 'video', - mimeType : 'video/H264', - clockRate : 90000, - parameters : - { - 'packetization-mode' : 1, - 'profile-level-id' : '640032' - } - } - ]; - - const routerRtpCapabilities = ortc.generateRouterRtpCapabilities(mediaCodecs); - - const rtpParameters = - { - codecs : - [ - { - mimeType : 'video/VP8', - payloadType : 120, - clockRate : 90000, - rtcpFeedback : - [ - { type: 'nack', parameter: '' }, - { type: 'nack', parameter: 'fir' } - ] - } - ], - headerExtensions : [], - encodings : - [ - { ssrc: 11111111 } - ], - rtcp : - { - cname : 'qwerty1234' - } - }; - - expect( - () => ortc.getProducerRtpParametersMapping(rtpParameters, routerRtpCapabilities)) - .toThrow(UnsupportedError); -}); diff --git a/node/src/types.ts b/node/src/types.ts index b3fe195f77..53fda44fc1 100644 --- a/node/src/types.ts +++ b/node/src/types.ts @@ -1,20 +1,46 @@ -export * from './Worker'; -export * from './WebRtcServer'; -export * from './Router'; -export * from './Transport'; -export * from './WebRtcTransport'; -export * from './PlainTransport'; -export * from './PipeTransport'; -export * from './DirectTransport'; -export * from './Producer'; -export * from './Consumer'; -export * from './DataProducer'; -export * from './DataConsumer'; -export * from './RtpObserver'; -export * from './ActiveSpeakerObserver'; -export * from './AudioLevelObserver'; -export * from './RtpParameters'; -export * from './SctpParameters'; -export * from './SrtpParameters'; +export type { Observer, ObserverEvents } from './index'; +export type * from './WorkerTypes'; +export type * from './WebRtcServerTypes'; +export type * from './RouterTypes'; +export type * from './TransportTypes'; +export type * from './WebRtcTransportTypes'; +export type * from './PlainTransportTypes'; +export type * from './PipeTransportTypes'; +export type * from './DirectTransportTypes'; +export type * from './ProducerTypes'; +export type * from './ConsumerTypes'; +export type * from './DataProducerTypes'; +export type * from './DataConsumerTypes'; +export type * from './RtpObserverTypes'; +export type * from './ActiveSpeakerObserverTypes'; +export type * from './AudioLevelObserverTypes'; +export type * from './rtpParametersTypes'; +export type * from './rtpStreamStatsTypes'; +export type * from './sctpParametersTypes'; +export type * from './srtpParametersTypes'; +export type * from './scalabilityModesTypes'; + +// TODO: Here we are exporting real classes rather than types. This should +// be exported somehow else rather than in mediasoup.types namespace. export * from './errors'; -export { ScalabilityMode } from './scalabilityModes'; + +type Only = { + [P in keyof T]: T[P]; +} & { + [P in keyof U]?: never; +}; + +export type Either = Only | Only; + +export type AppData = { + [key: string]: unknown; +}; + +/** + * Event listeners for mediasoup generated logs. + */ +export type LogEventListeners = { + ondebug?: (namespace: string, log: string) => void; + onwarn?: (namespace: string, log: string) => void; + onerror?: (namespace: string, log: string, error?: Error) => void; +}; diff --git a/node/src/utils.ts b/node/src/utils.ts index cc1a00d2bf..e3cba37a8f 100644 --- a/node/src/utils.ts +++ b/node/src/utils.ts @@ -1,28 +1,51 @@ -import { randomInt } from 'crypto'; +import { randomUUID, randomInt } from 'node:crypto'; /** - * Clones the given object/array. + * Clones the given value. */ -export function clone(data: any): any -{ - if (typeof data !== 'object') - return {}; +export function clone(value: T): T { + if (value === undefined) { + return undefined as unknown as T; + } else if (Number.isNaN(value)) { + return NaN as unknown as T; + } else if (typeof structuredClone === 'function') { + // Available in Node >= 18. + return structuredClone(value); + } else { + return JSON.parse(JSON.stringify(value)); + } +} - return JSON.parse(JSON.stringify(data)); +/** + * Generates a random UUID v4. + */ +export function generateUUIDv4(): string { + return randomUUID(); } /** * Generates a random positive integer. */ -export function generateRandomNumber() -{ +export function generateRandomNumber(): number { return randomInt(100_000_000, 999_999_999); } -type Only = { - [P in keyof T]: T[P]; -} & { - [P in keyof U]?: never; -}; +/** + * Make an object or array recursively immutable. + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze. + */ +export function deepFreeze(object: T): T { + // Retrieve the property names defined on object. + const propNames = Reflect.ownKeys(object as any); + + // Freeze properties before freezing self. + for (const name of propNames) { + const value = (object as any)[name]; -export type Either = Only | Only; + if ((value && typeof value === 'object') || typeof value === 'function') { + deepFreeze(value); + } + } + + return Object.freeze(object); +} diff --git a/node/tsconfig.json b/node/tsconfig.json deleted file mode 100644 index 9196896ea0..0000000000 --- a/node/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compileOnSave": true, - "compilerOptions": - { - "lib": [ "es2021" ], - "target": "esnext", - "module": "commonjs", - "moduleResolution": "node", - "strict": true, - "outDir": "lib", - "declaration": true, - "declarationMap": true - }, - "include": [ "src" ] -} diff --git a/npm-scripts.js b/npm-scripts.js deleted file mode 100644 index 66ce1e5001..0000000000 --- a/npm-scripts.js +++ /dev/null @@ -1,325 +0,0 @@ -/* eslint-disable no-console */ - -const process = require('process'); -const os = require('os'); -const fs = require('fs'); -const { execSync } = require('child_process'); -const { version } = require('./package.json'); - -const isFreeBSD = os.platform() === 'freebsd'; -const isWindows = os.platform() === 'win32'; -const task = process.argv.slice(2).join(' '); - -// mediasoup mayor version. -const MAYOR_VERSION = version.split('.')[0]; - -// make command to use. -const MAKE = process.env.MAKE || (isFreeBSD ? 'gmake' : 'make'); - -console.log(`npm-scripts.js [INFO] running task "${task}"`); - -switch (task) -{ - // As per NPM documentation (https://docs.npmjs.com/cli/v9/using-npm/scripts) - // `prepare` script: - // - // - Runs BEFORE the package is packed, i.e. during `npm publish` and `npm pack`. - // - Runs on local `npm install` without any arguments. - // - NOTE: If a package being installed through git contains a `prepare` script, - // its dependencies and devDependencies will be installed, and the `prepare` - // script will be run, before the package is packaged and installed. - // - // So here we compile TypeScript and flatbuffers to JavaScript. - case 'prepare': - { - buildTypescript(/* force */ false); - - // TODO: Compile flatbuffers. - - break; - } - - case 'postinstall': - { - if (!process.env.MEDIASOUP_WORKER_BIN) - { - buildWorker(); - - if (!process.env.MEDIASOUP_LOCAL_DEV) - { - cleanWorker(); - } - } - - break; - } - - case 'typescript:build': - { - installNodeDeps(); - buildTypescript(/* force */ true); - replaceVersion(); - - break; - } - - case 'typescript:watch': - { - deleteNodeLib(); - - const { TscWatchClient } = require('tsc-watch/client'); - const watch = new TscWatchClient(); - - watch.on('success', replaceVersion); - watch.start('--project', 'node', '--pretty'); - - break; - } - - case 'worker:build': - { - buildWorker(); - - break; - } - - case 'lint:node': - { - lintNode(); - - break; - } - - case 'lint:worker': - { - lintWorker(); - - break; - } - - case 'format:worker': - { - executeCmd(`${MAKE} format -C worker`); - - break; - } - - case 'test:node': - { - buildTypescript(/* force */ false); - replaceVersion(); - testNode(); - - break; - } - - case 'test:worker': - { - testWorker(); - - break; - } - - case 'coverage:node': - { - buildTypescript(/* force */ false); - replaceVersion(); - executeCmd('jest --coverage'); - executeCmd('open-cli coverage/lcov-report/index.html'); - - break; - } - - case 'install-deps:node': - { - installNodeDeps(); - - break; - } - - case 'install-clang-tools': - { - executeCmd('npm ci --prefix worker/scripts'); - - break; - } - - case 'release:check': - { - checkRelease(); - - break; - } - - case 'release': - { - checkRelease(); - executeCmd(`git commit -am '${version}'`); - executeCmd(`git tag -a ${version} -m '${version}'`); - executeCmd(`git push origin v${MAYOR_VERSION}`); - executeCmd(`git push origin '${version}'`); - executeCmd('npm publish'); - - break; - } - - default: - { - throw new TypeError(`unknown task "${task}"`); - } -} - -function replaceVersion() -{ - console.log('npm-scripts.js [INFO] replaceVersion()'); - - const files = - [ - 'node/lib/index.js', - 'node/lib/index.d.ts', - 'node/lib/Worker.js' - ]; - - for (const file of files) - { - const text = fs.readFileSync(file, { encoding: 'utf8' }); - const result = text.replace(/__MEDIASOUP_VERSION__/g, version); - - fs.writeFileSync(file, result, { encoding: 'utf8' }); - } -} - -function deleteNodeLib() -{ - if (!fs.existsSync('node/lib')) - { - return; - } - - console.log('npm-scripts.js [INFO] deleteNodeLib()'); - - if (!isWindows) - { - executeCmd('rm -rf node/lib'); - } - else - { - // NOTE: This command fails in Windows if the dir doesn't exist. - executeCmd('rmdir /s /q "node/lib"', /* exitOnError */ false); - } -} - -function buildTypescript(force = false) -{ - if (!force && fs.existsSync('node/lib')) - { - return; - } - - console.log('npm-scripts.js [INFO] buildTypescript()'); - - deleteNodeLib(); - - executeCmd('tsc --project node'); -} - -function buildWorker() -{ - console.log('npm-scripts.js [INFO] buildWorker()'); - - executeCmd(`${MAKE} -C worker`); -} - -function cleanWorker() -{ - console.log('npm-scripts.js [INFO] cleanWorker()'); - - // Clean build artifacts except `mediasoup-worker`. - executeCmd(`${MAKE} clean-build -C worker`); - // Clean downloaded dependencies. - executeCmd(`${MAKE} clean-subprojects -C worker`); - // Clean PIP/Meson/Ninja. - executeCmd(`${MAKE} clean-pip -C worker`); -} - -function lintNode() -{ - console.log('npm-scripts.js [INFO] lintNode()'); - - executeCmd('eslint -c node/.eslintrc.js --max-warnings 0 node/src node/.eslintrc.js npm-scripts.js worker/scripts/gulpfile.js'); -} - -function lintWorker() -{ - console.log('npm-scripts.js [INFO] lintWorker()'); - - executeCmd(`${MAKE} lint -C worker`); -} - -function testNode() -{ - console.log('npm-scripts.js [INFO] testNode()'); - - if (!process.env.TEST_FILE) - { - executeCmd('jest'); - } - else - { - executeCmd(`jest --testPathPattern ${process.env.TEST_FILE}`); - } -} - -function testWorker() -{ - console.log('npm-scripts.js [INFO] testWorker()'); - - executeCmd(`${MAKE} test -C worker`); -} - -function installNodeDeps() -{ - console.log('npm-scripts.js [INFO] installNodeDeps()'); - - // Install/update Node deps. - executeCmd('npm ci --ignore-scripts'); - // Update package-lock.json. - executeCmd('npm install --package-lock-only --ignore-scripts'); -} - -function checkRelease() -{ - console.log('npm-scripts.js [INFO] checkRelease()'); - - installNodeDeps(); - buildTypescript(/* force */ true); - replaceVersion(); - buildWorker(); - lintNode(); - lintWorker(); - testNode(); - testWorker(); -} - -function executeCmd(command, exitOnError = true) -{ - console.log(`npm-scripts.js [INFO] executeCmd(): ${command}`); - - try - { - execSync(command, { stdio: [ 'ignore', process.stdout, process.stderr ] }); - } - catch (error) - { - if (exitOnError) - { - console.error(`npm-scripts.js [ERROR] executeCmd() failed, exiting: ${error}`); - - process.exit(1); - } - else - { - console.log(`npm-scripts.js [INFO] executeCmd() failed, ignoring: ${error}`); - } - } -} diff --git a/npm-scripts.mjs b/npm-scripts.mjs new file mode 100644 index 0000000000..1612a46229 --- /dev/null +++ b/npm-scripts.mjs @@ -0,0 +1,699 @@ +import * as process from 'node:process'; +import * as os from 'node:os'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { execSync } from 'node:child_process'; +import fetch from 'node-fetch'; +import * as tar from 'tar'; +import * as ini from 'ini'; + +const PKG = JSON.parse( + fs.readFileSync('./package.json', { encoding: 'utf-8' }) +); +const IS_WINDOWS = os.platform() === 'win32'; +const MAYOR_VERSION = PKG.version.split('.')[0]; +const PYTHON = getPython(); +const PIP_INVOKE_DIR = path.resolve('worker/pip_invoke'); +const WORKER_RELEASE_DIR = 'worker/out/Release'; +const WORKER_RELEASE_BIN = IS_WINDOWS + ? 'mediasoup-worker.exe' + : 'mediasoup-worker'; +const WORKER_RELEASE_BIN_PATH = `${WORKER_RELEASE_DIR}/${WORKER_RELEASE_BIN}`; +const WORKER_PREBUILD_DIR = 'worker/prebuild'; +const WORKER_PREBUILD_TAR = getWorkerPrebuildTarName(); +const WORKER_PREBUILD_TAR_PATH = `${WORKER_PREBUILD_DIR}/${WORKER_PREBUILD_TAR}`; +const GH_OWNER = 'versatica'; +const GH_REPO = 'mediasoup'; + +// Paths for ESLint to check. Converted to string for convenience. +const ESLINT_PATHS = [ + 'eslint.config.mjs', + 'node/src', + 'npm-scripts.mjs', + 'worker/scripts', +].join(' '); +// Paths for ESLint to ignore. Converted to string argument for convenience. +const ESLINT_IGNORE_PATTERN_ARGS = ['node/src/fbs'] + .map(entry => `--ignore-pattern ${entry}`) + .join(' '); +// Paths for Prettier to check/write. Converted to string for convenience. +// NOTE: Prettier ignores paths in .gitignore so we don't need to care about +// node/src/fbs. +const PRETTIER_PATHS = [ + 'CHANGELOG.md', + 'CONTRIBUTING.md', + 'README.md', + 'doc', + 'node/src', + 'npm-scripts.mjs', + 'package.json', + 'tsconfig.json', + 'worker/scripts', +].join(' '); + +const task = process.argv[2]; +const args = process.argv.slice(3).join(' '); + +// PYTHONPATH env must be updated now so all invoke calls below will find the +// pip invoke module. +if (process.env.PYTHONPATH) { + if (IS_WINDOWS) { + process.env.PYTHONPATH = `${PIP_INVOKE_DIR};${process.env.PYTHONPATH}`; + } else { + process.env.PYTHONPATH = `${PIP_INVOKE_DIR}:${process.env.PYTHONPATH}`; + } +} else { + process.env.PYTHONPATH = PIP_INVOKE_DIR; +} + +run(); + +async function run() { + logInfo(args ? `[args:"${args}"]` : ''); + + switch (task) { + // As per NPM documentation (https://docs.npmjs.com/cli/v9/using-npm/scripts) + // `prepare` script: + // + // - Runs BEFORE the package is packed, i.e. during `npm publish` and `npm pack`. + // - Runs on local `npm install` without any arguments. + // - NOTE: If a package being installed through git contains a `prepare` script, + // its dependencies and devDependencies will be installed, and the `prepare` + // script will be run, before the package is packaged and installed. + // + // So here we generate flatbuffers definitions for TypeScript and compile + // TypeScript to JavaScript. + case 'prepare': { + flatcNode(); + buildTypescript({ force: false }); + + break; + } + + case 'postinstall': { + // If the user/app provides us with a custom mediasoup-worker binary then + // don't do anything. + if (process.env.MEDIASOUP_WORKER_BIN) { + logInfo('MEDIASOUP_WORKER_BIN environment variable given, skipping'); + + break; + } + // If MEDIASOUP_LOCAL_DEV is given, or if MEDIASOUP_SKIP_WORKER_PREBUILT_DOWNLOAD + // env is given, or if mediasoup package is being installed via git+ssh + // (instead of via npm), and if MEDIASOUP_FORCE_PREBUILT_WORKER_DOWNLOAD env is + // not set, then skip mediasoup-worker prebuilt download. + else if ( + (process.env.MEDIASOUP_LOCAL_DEV || + process.env.MEDIASOUP_SKIP_WORKER_PREBUILT_DOWNLOAD || + process.env.npm_package_resolved?.startsWith('git+ssh://')) && + !process.env.MEDIASOUP_FORCE_WORKER_PREBUILT_DOWNLOAD + ) { + logInfo( + 'skipping mediasoup-worker prebuilt download, building it locally' + ); + + buildWorker(); + + if (!process.env.MEDIASOUP_LOCAL_DEV) { + cleanWorkerArtifacts(); + } + } + // Attempt to download a prebuilt binary. Fallback to building locally. + else if (!(await downloadPrebuiltWorker())) { + logInfo( + `couldn't fetch any mediasoup-worker prebuilt binary, building it locally` + ); + + buildWorker(); + + if (!process.env.MEDIASOUP_LOCAL_DEV) { + cleanWorkerArtifacts(); + } + } + + break; + } + + case 'typescript:build': { + installNodeDeps(); + buildTypescript({ force: true }); + + break; + } + + case 'typescript:watch': { + deleteNodeLib(); + executeCmd(`tsc --watch ${args}`); + + break; + } + + case 'worker:build': { + buildWorker(); + + break; + } + + case 'worker:prebuild': { + await prebuildWorker(); + + break; + } + + case 'lint:node': { + lintNode(); + + break; + } + + case 'lint:worker': { + lintWorker(); + + break; + } + + case 'format:node': { + formatNode(); + + break; + } + + case 'format:worker': { + installInvoke(); + + executeCmd(`"${PYTHON}" -m invoke -r worker format`); + + break; + } + + case 'flatc:node': { + flatcNode(); + + break; + } + + case 'flatc:worker': { + flatcWorker(); + + break; + } + + case 'test:node': { + buildTypescript({ force: false }); + testNode(); + + break; + } + + case 'test:worker': { + testWorker(); + + break; + } + + case 'coverage:node': { + buildTypescript({ force: false }); + executeCmd(`jest --coverage ${args}`); + executeCmd('open-cli coverage/lcov-report/index.html'); + + break; + } + + case 'release:check': { + checkRelease(); + + break; + } + + case 'release': { + let octokit; + let versionChanges; + + try { + octokit = await getOctokit(); + versionChanges = await getVersionChanges(); + } catch (error) { + logError(error.message); + + exitWithError(); + } + + checkRelease(); + executeCmd(`git commit -am '${PKG.version}'`); + executeCmd(`git tag -a ${PKG.version} -m '${PKG.version}'`); + executeCmd(`git push origin v${MAYOR_VERSION}`); + executeCmd(`git push origin '${PKG.version}'`); + + logInfo('creating release in GitHub'); + + await octokit.repos.createRelease({ + owner: GH_OWNER, + repo: GH_REPO, + name: PKG.version, + body: versionChanges, + tag_name: PKG.version, + draft: false, + }); + + executeCmd('npm publish'); + + break; + } + + default: { + logError('unknown task'); + + exitWithError(); + } + } +} + +function getPython() { + let python = process.env.PYTHON; + + if (!python) { + try { + execSync('python3 --version', { stdio: ['ignore', 'ignore', 'ignore'] }); + python = 'python3'; + } catch (error) { + python = 'python'; + } + } + + return python; +} + +function getWorkerPrebuildTarName() { + let name = `mediasoup-worker-${PKG.version}-${os.platform()}-${os.arch()}`; + + // In Linux we want to know about kernel version since kernel >= 6 supports + // io-uring. + if (os.platform() === 'linux') { + const kernelMajorVersion = Number(os.release().split('.')[0]); + + name += `-kernel${kernelMajorVersion}`; + } + + return `${name}.tgz`; +} + +function installInvoke() { + if (fs.existsSync(PIP_INVOKE_DIR)) { + return; + } + + logInfo('installInvoke()'); + + // Install pip invoke into custom location, so we don't depend on system-wide + // installation. + executeCmd( + `"${PYTHON}" -m pip install --upgrade --no-user --target "${PIP_INVOKE_DIR}" invoke`, + /* exitOnError */ true + ); +} + +function deleteNodeLib() { + if (!fs.existsSync('node/lib')) { + return; + } + + logInfo('deleteNodeLib()'); + + fs.rmSync('node/lib', { recursive: true, force: true }); +} + +function buildTypescript({ force = false } = { force: false }) { + if (!force && fs.existsSync('node/lib')) { + return; + } + + logInfo('buildTypescript()'); + + deleteNodeLib(); + executeCmd('tsc'); +} + +function buildWorker() { + logInfo('buildWorker()'); + + installInvoke(); + + executeCmd(`"${PYTHON}" -m invoke -r worker mediasoup-worker`); +} + +function cleanWorkerArtifacts() { + logInfo('cleanWorkerArtifacts()'); + + installInvoke(); + + // Clean build artifacts except `mediasoup-worker`. + executeCmd(`"${PYTHON}" -m invoke -r worker clean-build`); + // Clean downloaded dependencies. + executeCmd(`"${PYTHON}" -m invoke -r worker clean-subprojects`); + // Clean PIP/Meson/Ninja. + executeCmd(`"${PYTHON}" -m invoke -r worker clean-pip`); +} + +function lintNode() { + logInfo('lintNode()'); + + // Ensure there are no rules that are unnecessary or conflict with Prettier + // rules. + executeCmd('eslint-config-prettier eslint.config.mjs'); + + executeCmd( + `eslint -c eslint.config.mjs --max-warnings 0 ${ESLINT_IGNORE_PATTERN_ARGS} ${ESLINT_PATHS}` + ); + + executeCmd(`prettier --check ${PRETTIER_PATHS}`); +} + +function lintWorker() { + logInfo('lintWorker()'); + + installInvoke(); + + executeCmd(`"${PYTHON}" -m invoke -r worker lint`); +} + +function formatNode() { + logInfo('formatNode()'); + + executeCmd(`prettier --write ${PRETTIER_PATHS}`); +} + +function flatcNode() { + logInfo('flatcNode()'); + + installInvoke(); + + // Build flatc if needed. + executeCmd(`"${PYTHON}" -m invoke -r worker flatc`); + + const buildType = process.env.MEDIASOUP_BUILDTYPE || 'Release'; + const extension = IS_WINDOWS ? '.exe' : ''; + const flatbuffersWrapFilePath = path.join( + 'worker', + 'subprojects', + 'flatbuffers.wrap' + ); + const flatbuffersWrap = ini.parse( + fs.readFileSync(flatbuffersWrapFilePath, { + encoding: 'utf-8', + }) + ); + const flatbuffersDir = flatbuffersWrap['wrap-file']['directory']; + + const flatc = path.resolve( + path.join( + 'worker', + 'out', + buildType, + 'build', + 'subprojects', + flatbuffersDir, + `flatc${extension}` + ) + ); + + const out = path.resolve(path.join('node', 'src')); + + for (const dirent of fs.readdirSync(path.join('worker', 'fbs'), { + withFileTypes: true, + })) { + if (!dirent.isFile() || path.parse(dirent.name).ext !== '.fbs') { + continue; + } + + const filePath = path.resolve(path.join('worker', 'fbs', dirent.name)); + + executeCmd( + `"${flatc}" --ts --ts-no-import-ext --gen-object-api -o "${out}" "${filePath}"` + ); + } +} + +function flatcWorker() { + logInfo('flatcWorker()'); + + installInvoke(); + + executeCmd(`"${PYTHON}" -m invoke -r worker flatc`); +} + +function testNode() { + logInfo('testNode()'); + + executeCmd(`jest --silent false --detectOpenHandles ${args}`); +} + +function testWorker() { + logInfo('testWorker()'); + + installInvoke(); + + executeCmd(`"${PYTHON}" -m invoke -r worker test`); +} + +function installNodeDeps() { + logInfo('installNodeDeps()'); + + // Install/update Node deps. + executeCmd('npm ci --ignore-scripts'); + // Update package-lock.json. + executeCmd('npm install --package-lock-only --ignore-scripts'); +} + +function checkRelease() { + logInfo('checkRelease()'); + + installNodeDeps(); + flatcNode(); + buildTypescript({ force: true }); + buildWorker(); + lintNode(); + lintWorker(); + testNode(); + testWorker(); +} + +function ensureDir(dir) { + logInfo(`ensureDir() [dir:${dir}]`); + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } +} + +async function prebuildWorker() { + logInfo('prebuildWorker()'); + + ensureDir(WORKER_PREBUILD_DIR); + + return new Promise((resolve, reject) => { + // Generate a gzip file which just contains mediasoup-worker binary without + // any folder. + tar + .create( + { + cwd: WORKER_RELEASE_DIR, + gzip: true, + }, + [WORKER_RELEASE_BIN] + ) + .pipe(fs.createWriteStream(WORKER_PREBUILD_TAR_PATH)) + .on('finish', resolve) + .on('error', reject); + }); +} + +// Returns a Promise resolving to true if a mediasoup-worker prebuilt binary +// was downloaded and uncompressed, false otherwise. +async function downloadPrebuiltWorker() { + const releaseBase = + process.env.MEDIASOUP_WORKER_PREBUILT_DOWNLOAD_BASE_URL || + `${PKG.repository.url + .replace(/^git\+/, '') + .replace(/\.git$/, '')}/releases/download`; + + const tarUrl = `${releaseBase}/${PKG.version}/${WORKER_PREBUILD_TAR}`; + + logInfo(`downloadPrebuiltWorker() [tarUrl:${tarUrl}]`); + + ensureDir(WORKER_PREBUILD_DIR); + + let res; + + try { + res = await fetch(tarUrl); + + if (res.status === 404) { + logInfo( + 'downloadPrebuiltWorker() | no available mediasoup-worker prebuilt binary for current architecture' + ); + + return false; + } else if (!res.ok) { + logError( + `downloadPrebuiltWorker() | failed to download mediasoup-worker prebuilt binary: ${res.status} ${res.statusText}` + ); + + return false; + } + } catch (error) { + logError( + `downloadPrebuiltWorker() | failed to download mediasoup-worker prebuilt binary: ${error}` + ); + + return false; + } + + ensureDir(WORKER_RELEASE_DIR); + + return new Promise(resolve => { + // Extract mediasoup-worker in the official mediasoup-worker path. + res.body + .pipe( + tar.extract({ + newer: false, + cwd: WORKER_RELEASE_DIR, + }) + ) + .on('finish', () => { + logInfo( + 'downloadPrebuiltWorker() | got mediasoup-worker prebuilt binary' + ); + + try { + // Give execution permission to the binary. + fs.chmodSync(WORKER_RELEASE_BIN_PATH, 0o775); + } catch (error) { + logWarn( + `downloadPrebuiltWorker() | failed to give execution permissions to the mediasoup-worker prebuilt binary: ${error}` + ); + } + + // Let's confirm that the fetched mediasoup-worker prebuit binary does + // run in current host. This is to prevent weird issues related to + // different versions of libc in the system and so on. + // So run mediasoup-worker without the required MEDIASOUP_VERSION env and + // expect exit code 41 (see main.cpp). + + logInfo( + 'downloadPrebuiltWorker() | checking fetched mediasoup-worker prebuilt binary in current host' + ); + + try { + const resolvedBinPath = path.resolve(WORKER_RELEASE_BIN_PATH); + + // This will always fail on purpose, but if status code is 41 then + // it's good. + execSync(`"${resolvedBinPath}"`, { + stdio: ['ignore', 'ignore', 'ignore'], + // Ensure no env is passed to avoid accidents. + env: {}, + }); + } catch (error) { + if (error.status === 41) { + logInfo( + 'downloadPrebuiltWorker() | fetched mediasoup-worker prebuilt binary is valid for current host' + ); + + resolve(true); + } else { + logError( + `downloadPrebuiltWorker() | fetched mediasoup-worker prebuilt binary fails to run in this host [status:${error.status}]` + ); + + try { + fs.unlinkSync(WORKER_RELEASE_BIN_PATH); + } catch (error2) {} + + resolve(false); + } + } + }) + .on('error', error => { + logError( + `downloadPrebuiltWorker() | failed to uncompress downloaded mediasoup-worker prebuilt binary: ${error}` + ); + + resolve(false); + }); + }); +} + +async function getOctokit() { + if (!process.env.GITHUB_TOKEN) { + throw new Error('missing GITHUB_TOKEN environment variable'); + } + + // NOTE: Load dep on demand since it's a devDependency. + const { Octokit } = await import('@octokit/rest'); + + const octokit = new Octokit({ + auth: process.env.GITHUB_TOKEN, + }); + + return octokit; +} + +async function getVersionChanges() { + logInfo('getVersionChanges()'); + + // NOTE: Load dep on demand since it's a devDependency. + const marked = await import('marked'); + + const changelog = fs.readFileSync('./CHANGELOG.md', { encoding: 'utf-8' }); + const entries = marked.lexer(changelog); + + for (let idx = 0; idx < entries.length; ++idx) { + const entry = entries[idx]; + + if (entry.type === 'heading' && entry.text === PKG.version) { + const changes = entries[idx + 1].raw; + + return changes; + } + } + + // This should not happen (unless author forgot to update CHANGELOG). + throw new Error( + `no entry found in CHANGELOG.md for version '${PKG.version}'` + ); +} + +function executeCmd(command, exitOnError = true) { + logInfo(`executeCmd(): ${command}`); + + try { + execSync(command, { stdio: ['ignore', process.stdout, process.stderr] }); + } catch (error) { + if (exitOnError) { + logError(`executeCmd() failed, exiting: ${error}`); + + exitWithError(); + } else { + logInfo(`executeCmd() failed, ignoring: ${error}`); + } + } +} + +function logInfo(message) { + // eslint-disable-next-line no-console + console.log(`npm-scripts.mjs \x1b[36m[INFO] [${task}]\x1b[0m`, message); +} + +function logWarn(message) { + // eslint-disable-next-line no-console + console.warn(`npm-scripts.mjs \x1b[33m[WARN] [${task}]\x1b\0m`, message); +} + +function logError(message) { + // eslint-disable-next-line no-console + console.error(`npm-scripts.mjs \x1b[31m[ERROR] [${task}]\x1b[0m`, message); +} + +function exitWithError() { + process.exit(1); +} diff --git a/package-lock.json b/package-lock.json index a868d9e406..12844415af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9896 +1,10852 @@ { - "name": "mediasoup", - "version": "3.11.8", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "mediasoup", - "version": "3.11.8", - "hasInstallScript": true, - "license": "ISC", - "dependencies": { - "debug": "^4.3.4", - "h264-profile-level-id": "^1.0.1", - "supports-color": "^9.3.1", - "uuid": "^9.0.0" - }, - "devDependencies": { - "@types/debug": "^4.1.7", - "@types/jest": "^29.4.0", - "@types/node": "^18.11.18", - "@types/uuid": "^9.0.0", - "@typescript-eslint/eslint-plugin": "^5.50.0", - "@typescript-eslint/parser": "^5.50.0", - "eslint": "^8.33.0", - "eslint-plugin-jest": "^27.2.1", - "jest": "^29.4.1", - "open-cli": "^7.1.0", - "pick-port": "^1.0.1", - "sctp": "^1.0.0", - "ts-jest": "^29.0.5", - "tsc-watch": "^6.0.0", - "typescript": "^4.9.5" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mediasoup" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", - "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.2.tgz", - "integrity": "sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.2", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-module-transforms": "^7.20.2", - "@babel/helpers": "^7.20.1", - "@babel/parser": "^7.20.2", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.4.tgz", - "integrity": "sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.2", - "@jridgewell/gen-mapping": "^0.3.2", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "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/@babel/helper-compilation-targets": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", - "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.20.0", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", - "dev": true, - "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", - "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", - "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/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/@babel/highlight/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/@babel/highlight/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/@babel/highlight/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/@babel/highlight/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/@babel/highlight/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/@babel/parser": { - "version": "7.20.13", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.13.tgz", - "integrity": "sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", - "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", - "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/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/@babel/types": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", - "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "node_modules/@eslint/eslintrc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", - "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.4.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/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.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": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.4.1.tgz", - "integrity": "sha512-m+XpwKSi3PPM9znm5NGS8bBReeAJJpSkL1OuFCqaMaJL2YX9YXLkkI+MBchMPwu+ZuM2rynL51sgfkQteQ1CKQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.4.1", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.4.1", - "jest-util": "^29.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.4.1.tgz", - "integrity": "sha512-RXFTohpBqpaTebNdg5l3I5yadnKo9zLBajMT0I38D0tDhreVBYv3fA8kywthI00sWxPztWLD3yjiUkewwu/wKA==", - "dev": true, - "dependencies": { - "@jest/console": "^29.4.1", - "@jest/reporters": "^29.4.1", - "@jest/test-result": "^29.4.1", - "@jest/transform": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.4.0", - "jest-config": "^29.4.1", - "jest-haste-map": "^29.4.1", - "jest-message-util": "^29.4.1", - "jest-regex-util": "^29.2.0", - "jest-resolve": "^29.4.1", - "jest-resolve-dependencies": "^29.4.1", - "jest-runner": "^29.4.1", - "jest-runtime": "^29.4.1", - "jest-snapshot": "^29.4.1", - "jest-util": "^29.4.1", - "jest-validate": "^29.4.1", - "jest-watcher": "^29.4.1", - "micromatch": "^4.0.4", - "pretty-format": "^29.4.1", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.4.1.tgz", - "integrity": "sha512-pJ14dHGSQke7Q3mkL/UZR9ZtTOxqskZaC91NzamEH4dlKRt42W+maRBXiw/LWkdJe+P0f/zDR37+SPMplMRlPg==", - "dev": true, - "dependencies": { - "@jest/fake-timers": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/node": "*", - "jest-mock": "^29.4.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.4.1.tgz", - "integrity": "sha512-ZxKJP5DTUNF2XkpJeZIzvnzF1KkfrhEF6Rz0HGG69fHl6Bgx5/GoU3XyaeFYEjuuKSOOsbqD/k72wFvFxc3iTw==", - "dev": true, - "dependencies": { - "expect": "^29.4.1", - "jest-snapshot": "^29.4.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.4.1.tgz", - "integrity": "sha512-w6YJMn5DlzmxjO00i9wu2YSozUYRBhIoJ6nQwpMYcBMtiqMGJm1QBzOf6DDgRao8dbtpDoaqLg6iiQTvv0UHhQ==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.2.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.4.1.tgz", - "integrity": "sha512-/1joI6rfHFmmm39JxNfmNAO3Nwm6Y0VoL5fJDy7H1AtWrD1CgRtqJbN9Ld6rhAkGO76qqp4cwhhxJ9o9kYjQMw==", - "dev": true, - "dependencies": { - "@jest/types": "^29.4.1", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.4.1", - "jest-mock": "^29.4.1", - "jest-util": "^29.4.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.4.1.tgz", - "integrity": "sha512-znoK2EuFytbHH0ZSf2mQK2K1xtIgmaw4Da21R2C/NE/+NnItm5mPEFQmn8gmF3f0rfOlmZ3Y3bIf7bFj7DHxAA==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.4.1", - "@jest/expect": "^29.4.1", - "@jest/types": "^29.4.1", - "jest-mock": "^29.4.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.4.1.tgz", - "integrity": "sha512-AISY5xpt2Xpxj9R6y0RF1+O6GRy9JsGa8+vK23Lmzdy1AYcpQn5ItX79wJSsTmfzPKSAcsY1LNt/8Y5Xe5LOSg==", - "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.4.1", - "@jest/test-result": "^29.4.1", - "@jest/transform": "^29.4.1", - "@jest/types": "^29.4.1", - "@jridgewell/trace-mapping": "^0.3.15", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.4.1", - "jest-util": "^29.4.1", - "jest-worker": "^29.4.1", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.0.tgz", - "integrity": "sha512-0E01f/gOZeNTG76i5eWWSupvSHaIINrTie7vCyjiYFKgzNdyEGd12BUv4oNBFHOqlHDbtoJi3HrQ38KCC90NsQ==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.25.16" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.2.0.tgz", - "integrity": "sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.15", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.4.1.tgz", - "integrity": "sha512-WRt29Lwt+hEgfN8QDrXqXGgCTidq1rLyFqmZ4lmJOpVArC8daXrZWkWjiaijQvgd3aOUj2fM8INclKHsQW9YyQ==", - "dev": true, - "dependencies": { - "@jest/console": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.4.1.tgz", - "integrity": "sha512-v5qLBNSsM0eHzWLXsQ5fiB65xi49A3ILPSFQKPXzGL4Vyux0DPZAIN7NAFJa9b4BiTDP9MBF/Zqc/QA1vuiJ0w==", - "dev": true, - "dependencies": { - "@jest/test-result": "^29.4.1", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.4.1.tgz", - "integrity": "sha512-5w6YJrVAtiAgr0phzKjYd83UPbCXsBRTeYI4BXokv9Er9CcrH9hfXL/crCvP2d2nGOcovPUnlYiLPFLZrkG5Hg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.4.1", - "@jridgewell/trace-mapping": "^0.3.15", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.4.1", - "jest-regex-util": "^29.2.0", - "jest-util": "^29.4.1", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.4.1.tgz", - "integrity": "sha512-zbrAXDUOnpJ+FMST2rV7QZOgec8rskg2zv8g2ajeqitp4tvZiyqTCYXANrKsM+ryj5o+LI+ZN2EgU9drrkiwSA==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.4.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "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/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "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/@sinclair/typebox": { - "version": "0.25.21", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.21.tgz", - "integrity": "sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g==", - "dev": true - }, - "node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", - "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^2.0.0" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "dev": true - }, - "node_modules/@types/babel__core": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", - "integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==", - "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.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", - "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.3.0" - } - }, - "node_modules/@types/debug": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", - "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", - "dev": true, - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", - "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.4.0.tgz", - "integrity": "sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==", - "dev": true, - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true - }, - "node_modules/@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", - "dev": true - }, - "node_modules/@types/ms": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", - "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", - "dev": true - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", - "dev": true - }, - "node_modules/@types/prettier": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", - "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", - "dev": true - }, - "node_modules/@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", - "dev": true - }, - "node_modules/@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, - "node_modules/@types/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==", - "dev": true - }, - "node_modules/@types/yargs": { - "version": "17.0.13", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", - "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", - "dev": true - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.50.0.tgz", - "integrity": "sha512-vwksQWSFZiUhgq3Kv7o1Jcj0DUNylwnIlGvKvLLYsq8pAWha6/WCnXUeaSoNNha/K7QSf2+jvmkxggC1u3pIwQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "5.50.0", - "@typescript-eslint/type-utils": "5.50.0", - "@typescript-eslint/utils": "5.50.0", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "regexpp": "^3.2.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.50.0.tgz", - "integrity": "sha512-KCcSyNaogUDftK2G9RXfQyOCt51uB5yqC6pkUYqhYh8Kgt+DwR5M0EwEAxGPy/+DH6hnmKeGsNhiZRQxjH71uQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "5.50.0", - "@typescript-eslint/types": "5.50.0", - "@typescript-eslint/typescript-estree": "5.50.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.50.0.tgz", - "integrity": "sha512-rt03kaX+iZrhssaT974BCmoUikYtZI24Vp/kwTSy841XhiYShlqoshRFDvN1FKKvU2S3gK+kcBW1EA7kNUrogg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.50.0", - "@typescript-eslint/visitor-keys": "5.50.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.50.0.tgz", - "integrity": "sha512-dcnXfZ6OGrNCO7E5UY/i0ktHb7Yx1fV6fnQGGrlnfDhilcs6n19eIRcvLBqx6OQkrPaFlDPk3OJ0WlzQfrV0bQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "5.50.0", - "@typescript-eslint/utils": "5.50.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.50.0.tgz", - "integrity": "sha512-atruOuJpir4OtyNdKahiHZobPKFvZnBnfDiyEaBf6d9vy9visE7gDjlmhl+y29uxZ2ZDgvXijcungGFjGGex7w==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.50.0.tgz", - "integrity": "sha512-Gq4zapso+OtIZlv8YNAStFtT6d05zyVCK7Fx3h5inlLBx2hWuc/0465C2mg/EQDDU2LKe52+/jN4f0g9bd+kow==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.50.0", - "@typescript-eslint/visitor-keys": "5.50.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.50.0.tgz", - "integrity": "sha512-v/AnUFImmh8G4PH0NDkf6wA8hujNNcrwtecqW4vtQ1UOSNBaZl49zP1SHoZ/06e+UiwzHpgb5zP5+hwlYYWYAw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.50.0", - "@typescript-eslint/types": "5.50.0", - "@typescript-eslint/typescript-estree": "5.50.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.50.0.tgz", - "integrity": "sha512-cdMeD9HGu6EXIeGOh2yVW6oGf9wq8asBgZx7nsR/D36gTfQ0odE5kcRYe5M81vjEFAcPeugXrHg78Imu55F6gg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.50.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", - "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/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/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/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/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/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/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/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-jest": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.4.1.tgz", - "integrity": "sha512-xBZa/pLSsF/1sNpkgsiT3CmY7zV1kAsZ9OxxtrFqYucnOuRftXAfcJqcDVyOPeN4lttWTwhLdu0T9f8uvoPEUg==", - "dev": true, - "dependencies": { - "@jest/transform": "^29.4.1", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.4.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.4.0.tgz", - "integrity": "sha512-a/sZRLQJEmsmejQ2rPEUe35nO1+C9dc9O1gplH1SXmJxveQSRUYdBk8yGZG/VOUuZs1u2aHZJusEGoRMbhhwCg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.4.0.tgz", - "integrity": "sha512-fUB9vZflUSM3dO/6M2TCAepTzvA4VkOvl67PjErcrQMGt9Eve7uazaeyCZ2th3UtI7ljpiBJES0F7A1vBRsLZA==", - "dev": true, - "dependencies": { - "babel-plugin-jest-hoist": "^29.4.0", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.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/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.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "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/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": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-keys": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz", - "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", - "dev": true, - "dependencies": { - "camelcase": "^6.3.0", - "map-obj": "^4.1.0", - "quick-lru": "^5.1.1", - "type-fest": "^1.2.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-keys/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-keys/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001431", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz", - "integrity": "sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - } - ] - }, - "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/chalk/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/chalk/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/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz", - "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", - "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", - "dev": true - }, - "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==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, - "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/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/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/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/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/crypto-random-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", - "dev": true, - "dependencies": { - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/crypto-random-string/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz", - "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dev": true, - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "dev": 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/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/diff-sequences": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.3.1.tgz", - "integrity": "sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "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/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/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, - "node_modules/electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", - "dev": true - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "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/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "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": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz", - "integrity": "sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==", - "dev": true, - "dependencies": { - "@eslint/eslintrc": "^1.4.1", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "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.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", - "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", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "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.1", - "regexpp": "^3.2.0", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "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-plugin-jest": { - "version": "27.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz", - "integrity": "sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==", - "dev": true, - "dependencies": { - "@typescript-eslint/utils": "^5.10.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^5.0.0", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "jest": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint/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/espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", - "dev": true, - "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" - }, - "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.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esquery/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/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/esrecurse/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/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "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/event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", - "dev": true, - "dependencies": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.4.1.tgz", - "integrity": "sha512-OKrGESHOaMxK3b6zxIq9SOW8kEXztKff/Dvg88j4xIJxur1hspEbedVkR3GpHe5LO+WB2Qw7OWN0RMTdp6as5A==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^29.4.1", - "jest-get-type": "^29.2.0", - "jest-matcher-utils": "^29.4.1", - "jest-message-util": "^29.4.1", - "jest-util": "^29.4.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "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==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "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-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/fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "dependencies": { - "bser": "2.1.1" - } - }, - "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/file-type": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.0.0.tgz", - "integrity": "sha512-jjMwFpnW8PKofLE/4ohlhqwDk5k0NC6iy0UHAJFKoY1fQeGMN0GDdLgHQrvCbSpMwbqzoCZhRI5dETCZna5qVA==", - "dev": true, - "dependencies": { - "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0", - "token-types": "^5.0.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "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.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "dev": true - }, - "node_modules/from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "dev": true - }, - "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.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "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.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "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==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stdin": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", - "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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": "13.19.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", - "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true - }, - "node_modules/h264-profile-level-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/h264-profile-level-id/-/h264-profile-level-id-1.0.1.tgz", - "integrity": "sha512-D3Rln/jKNjKDW5ZTJTK3niSoOGE+pFqPvRHHVgQN3G7umcn/zWGPUo8Q8VpDj16x3hKz++zVviRNRmXu5cpN+Q==", - "dependencies": { - "debug": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "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/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.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==", - "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/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "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/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true - }, - "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/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "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, - "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/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "dependencies": { - "is-docker": "^2.0.0" - }, - "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/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/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/istanbul-lib-report/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/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.4.1.tgz", - "integrity": "sha512-cknimw7gAXPDOmj0QqztlxVtBVCw2lYY9CeIE5N6kD+kET1H4H79HSNISJmijb1HF+qk+G+ploJgiDi5k/fRlg==", - "dev": true, - "dependencies": { - "@jest/core": "^29.4.1", - "@jest/types": "^29.4.1", - "import-local": "^3.0.2", - "jest-cli": "^29.4.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.4.0.tgz", - "integrity": "sha512-rnI1oPxgFghoz32Y8eZsGJMjW54UlqT17ycQeCEktcxxwqqKdlj9afl8LNeO0Pbu+h2JQHThQP0BzS67eTRx4w==", - "dev": true, - "dependencies": { - "execa": "^5.0.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.4.1.tgz", - "integrity": "sha512-v02NuL5crMNY4CGPHBEflLzl4v91NFb85a+dH9a1pUNx6Xjggrd8l9pPy4LZ1VYNRXlb+f65+7O/MSIbLir6pA==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.4.1", - "@jest/expect": "^29.4.1", - "@jest/test-result": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.4.1", - "jest-matcher-utils": "^29.4.1", - "jest-message-util": "^29.4.1", - "jest-runtime": "^29.4.1", - "jest-snapshot": "^29.4.1", - "jest-util": "^29.4.1", - "p-limit": "^3.1.0", - "pretty-format": "^29.4.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.4.1.tgz", - "integrity": "sha512-jz7GDIhtxQ37M+9dlbv5K+/FVcIo1O/b1sX3cJgzlQUf/3VG25nvuWzlDC4F1FLLzUThJeWLu8I7JF9eWpuURQ==", - "dev": true, - "dependencies": { - "@jest/core": "^29.4.1", - "@jest/test-result": "^29.4.1", - "@jest/types": "^29.4.1", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^29.4.1", - "jest-util": "^29.4.1", - "jest-validate": "^29.4.1", - "prompts": "^2.0.1", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.4.1.tgz", - "integrity": "sha512-g7p3q4NuXiM4hrS4XFATTkd+2z0Ml2RhFmFPM8c3WyKwVDNszbl4E7cV7WIx1YZeqqCtqbtTtZhGZWJlJqngzg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.4.1", - "@jest/types": "^29.4.1", - "babel-jest": "^29.4.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.4.1", - "jest-environment-node": "^29.4.1", - "jest-get-type": "^29.2.0", - "jest-regex-util": "^29.2.0", - "jest-resolve": "^29.4.1", - "jest-runner": "^29.4.1", - "jest-util": "^29.4.1", - "jest-validate": "^29.4.1", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.4.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.4.1.tgz", - "integrity": "sha512-uazdl2g331iY56CEyfbNA0Ut7Mn2ulAG5vUaEHXycf1L6IPyuImIxSz4F0VYBKi7LYIuxOwTZzK3wh5jHzASMw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.3.1", - "jest-get-type": "^29.2.0", - "pretty-format": "^29.4.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.2.0.tgz", - "integrity": "sha512-bkxUsxTgWQGbXV5IENmfiIuqZhJcyvF7tU4zJ/7ioTutdz4ToB5Yx6JOFBpgI+TphRY4lhOyCWGNH/QFQh5T6A==", - "dev": true, - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.4.1.tgz", - "integrity": "sha512-QlYFiX3llJMWUV0BtWht/esGEz9w+0i7BHwODKCze7YzZzizgExB9MOfiivF/vVT0GSQ8wXLhvHXh3x2fVD4QQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.4.1", - "chalk": "^4.0.0", - "jest-get-type": "^29.2.0", - "jest-util": "^29.4.1", - "pretty-format": "^29.4.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.4.1.tgz", - "integrity": "sha512-x/H2kdVgxSkxWAIlIh9MfMuBa0hZySmfsC5lCsWmWr6tZySP44ediRKDUiNggX/eHLH7Cd5ZN10Rw+XF5tXsqg==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.4.1", - "@jest/fake-timers": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/node": "*", - "jest-mock": "^29.4.1", - "jest-util": "^29.4.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz", - "integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.4.1.tgz", - "integrity": "sha512-imTjcgfVVTvg02khXL11NNLTx9ZaofbAWhilrMg/G8dIkp+HYCswhxf0xxJwBkfhWb3e8dwbjuWburvxmcr58w==", - "dev": true, - "dependencies": { - "@jest/types": "^29.4.1", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.2.0", - "jest-util": "^29.4.1", - "jest-worker": "^29.4.1", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.4.1.tgz", - "integrity": "sha512-akpZv7TPyGMnH2RimOCgy+hPmWZf55EyFUvymQ4LMsQP8xSPlZumCPtXGoDhFNhUE2039RApZkTQDKU79p/FiQ==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.2.0", - "pretty-format": "^29.4.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.4.1.tgz", - "integrity": "sha512-k5h0u8V4nAEy6lSACepxL/rw78FLDkBnXhZVgFneVpnJONhb2DhZj/Gv4eNe+1XqQ5IhgUcqj745UwH0HJmMnA==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.4.1", - "jest-get-type": "^29.2.0", - "pretty-format": "^29.4.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.4.1.tgz", - "integrity": "sha512-H4/I0cXUaLeCw6FM+i4AwCnOwHRgitdaUFOdm49022YD5nfyr8C/DrbXOBEyJaj+w/y0gGJ57klssOaUiLLQGQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.4.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.4.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.4.1.tgz", - "integrity": "sha512-MwA4hQ7zBOcgVCVnsM8TzaFLVUD/pFWTfbkY953Y81L5ret3GFRZtmPmRFAjKQSdCKoJvvqOu6Bvfpqlwwb0dQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.4.1", - "@types/node": "*", - "jest-util": "^29.4.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.2.0.tgz", - "integrity": "sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.4.1.tgz", - "integrity": "sha512-j/ZFNV2lm9IJ2wmlq1uYK0Y/1PiyDq9g4HEGsNTNr3viRbJdV+8Lf1SXIiLZXFvyiisu0qUyIXGBnw+OKWkJwQ==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.4.1", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.4.1", - "jest-validate": "^29.4.1", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.4.1.tgz", - "integrity": "sha512-Y3QG3M1ncAMxfjbYgtqNXC5B595zmB6e//p/qpA/58JkQXu/IpLDoLeOa8YoYfsSglBKQQzNUqtfGJJT/qLmJg==", - "dev": true, - "dependencies": { - "jest-regex-util": "^29.2.0", - "jest-snapshot": "^29.4.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.4.1.tgz", - "integrity": "sha512-8d6XXXi7GtHmsHrnaqBKWxjKb166Eyj/ksSaUYdcBK09VbjPwIgWov1VwSmtupCIz8q1Xv4Qkzt/BTo3ZqiCeg==", - "dev": true, - "dependencies": { - "@jest/console": "^29.4.1", - "@jest/environment": "^29.4.1", - "@jest/test-result": "^29.4.1", - "@jest/transform": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.2.0", - "jest-environment-node": "^29.4.1", - "jest-haste-map": "^29.4.1", - "jest-leak-detector": "^29.4.1", - "jest-message-util": "^29.4.1", - "jest-resolve": "^29.4.1", - "jest-runtime": "^29.4.1", - "jest-util": "^29.4.1", - "jest-watcher": "^29.4.1", - "jest-worker": "^29.4.1", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.4.1.tgz", - "integrity": "sha512-UXTMU9uKu2GjYwTtoAw5rn4STxWw/nadOfW7v1sx6LaJYa3V/iymdCLQM6xy3+7C6mY8GfX22vKpgxY171UIoA==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.4.1", - "@jest/fake-timers": "^29.4.1", - "@jest/globals": "^29.4.1", - "@jest/source-map": "^29.2.0", - "@jest/test-result": "^29.4.1", - "@jest/transform": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.4.1", - "jest-message-util": "^29.4.1", - "jest-mock": "^29.4.1", - "jest-regex-util": "^29.2.0", - "jest-resolve": "^29.4.1", - "jest-snapshot": "^29.4.1", - "jest-util": "^29.4.1", - "semver": "^7.3.5", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.4.1.tgz", - "integrity": "sha512-l4iV8EjGgQWVz3ee/LR9sULDk2pCkqb71bjvlqn+qp90lFwpnulHj4ZBT8nm1hA1C5wowXLc7MGnw321u0tsYA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.4.1", - "@jest/transform": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.4.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.4.1", - "jest-get-type": "^29.2.0", - "jest-haste-map": "^29.4.1", - "jest-matcher-utils": "^29.4.1", - "jest-message-util": "^29.4.1", - "jest-util": "^29.4.1", - "natural-compare": "^1.4.0", - "pretty-format": "^29.4.1", - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.4.1.tgz", - "integrity": "sha512-bQy9FPGxVutgpN4VRc0hk6w7Hx/m6L53QxpDreTZgJd9gfx/AV2MjyPde9tGyZRINAUrSv57p2inGBu2dRLmkQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.4.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.4.1.tgz", - "integrity": "sha512-qNZXcZQdIQx4SfUB/atWnI4/I2HUvhz8ajOSYUu40CSmf9U5emil8EDHgE7M+3j9/pavtk3knlZBDsgFvv/SWw==", - "dev": true, - "dependencies": { - "@jest/types": "^29.4.1", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.2.0", - "leven": "^3.1.0", - "pretty-format": "^29.4.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.4.1.tgz", - "integrity": "sha512-vFOzflGFs27nU6h8dpnVRER3O2rFtL+VMEwnG0H3KLHcllLsU8y9DchSh0AL/Rg5nN1/wSiQ+P4ByMGpuybaVw==", - "dev": true, - "dependencies": { - "@jest/test-result": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.4.1", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.4.1.tgz", - "integrity": "sha512-O9doU/S1EBe+yp/mstQ0VpPwpv0Clgn68TkNwGxL6/usX/KUW9Arnn4ag8C3jc6qHcXznhsT5Na1liYzAsuAbQ==", - "dev": true, - "dependencies": { - "@types/node": "*", - "jest-util": "^29.4.1", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/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/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-sdsl": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", - "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", - "dev": true - }, - "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==", - "dev": true - }, - "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/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-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "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/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/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "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/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/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.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true - }, - "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/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/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", - "dev": true - }, - "node_modules/meow": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/meow/-/meow-10.1.5.tgz", - "integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==", - "dev": true, - "dependencies": { - "@types/minimist": "^1.2.2", - "camelcase-keys": "^7.0.0", - "decamelize": "^5.0.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.2", - "read-pkg-up": "^8.0.0", - "redent": "^4.0.0", - "trim-newlines": "^4.0.2", - "type-fest": "^1.2.2", - "yargs-parser": "^20.2.9" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "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/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/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "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/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, - "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==" - }, - "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/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, - "node_modules/node-cleanup": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", - "integrity": "sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==", - "dev": true - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", - "dev": true - }, - "node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "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/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=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/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", - "dev": true, - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open-cli": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/open-cli/-/open-cli-7.1.0.tgz", - "integrity": "sha512-Xnn/B7WY9ygV47oK+LlYp5WU8xr0tEL6SEw9jMX8n6ceElOs2AzVXFXI87/O0+b+LwLokQBZVxBMzGZHCYVppw==", - "dev": true, - "dependencies": { - "file-type": "^18.0.0", - "get-stdin": "^9.0.0", - "meow": "^10.1.5", - "open": "^8.4.0", - "tempy": "^3.0.0" - }, - "bin": { - "open-cli": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "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/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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-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/pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", - "dev": true, - "dependencies": { - "through": "~2.3" - } - }, - "node_modules/peek-readable": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", - "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/pick-port": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pick-port/-/pick-port-1.0.1.tgz", - "integrity": "sha512-JzjRIkfG/4pG3tYLl1LwdmFtnlW+Rsxe200DevHZzZLYDUgfnx8LuOFnLwy5Dt59JY1HIN3JXyMXRbUvERkh/g==", - "dev": true, - "dependencies": { - "debug": "^4.3.1" - } - }, - "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/pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/polycrc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/polycrc/-/polycrc-0.1.0.tgz", - "integrity": "sha512-pBjdz8Gj0ixRkR80acjWl6bxiHf23MTI6chIKbQqphF2SrXXtYSPlftCSL31bD3veSWJCaTsM1QhT6zlIebqlg==", - "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/pretty-format": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.4.1.tgz", - "integrity": "sha512-dt/Z761JUVsrIKaY215o1xQJBGlSmTx/h4cSqXqjHLnU1+Kt+mavVE7UgqJJO5ukx5HjSswHfmXz4LjS2oIJfg==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.4.0", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/ps-tree": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", - "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", - "dev": true, - "dependencies": { - "event-stream": "=3.3.4" - }, - "bin": { - "ps-tree": "bin/ps-tree.js" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "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/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "node_modules/read-pkg": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz", - "integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^3.0.2", - "parse-json": "^5.2.0", - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-8.0.0.tgz", - "integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==", - "dev": true, - "dependencies": { - "find-up": "^5.0.0", - "read-pkg": "^6.0.0", - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", - "dev": true, - "dependencies": { - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/redent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", - "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", - "dev": true, - "dependencies": { - "indent-string": "^5.0.0", - "strip-indent": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.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-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "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/resolve.exports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.0.tgz", - "integrity": "sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "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/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/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/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==", - "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/sctp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/sctp/-/sctp-1.0.0.tgz", - "integrity": "sha512-wceaBrz55a0dbYG3c2zfJ1adUASLJhntQYZNVZKlGfKH1ExMckZB0sOxroWgpJLflcAB/k6wMOAPOrmylsRU4A==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "ip": "^1.1.5", - "polycrc": "^0.1.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "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/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/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "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/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/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", - "dev": true - }, - "node_modules/split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", - "dev": true, - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, - "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-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", - "dev": true, - "dependencies": { - "duplexer": "~0.1.1" - } - }, - "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==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-argv": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", - "dev": true, - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "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/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-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", - "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", - "dev": true, - "dependencies": { - "min-indent": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/strtok3": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", - "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", - "dev": true, - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/supports-color": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.3.1.tgz", - "integrity": "sha512-knBY82pjmnIzK3NifMo3RxEIRD9E0kIzV4BKcyTZ9+9kWgLMxd4PrsTSMoFQUabgRBbF8KOLRDCyKgNV+iK44Q==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "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/temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/tempy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.0.0.tgz", - "integrity": "sha512-B2I9X7+o2wOaW4r/CWMkpOO9mdiTRCxXNgob6iGvPmfPWgH/KyUD6Uy5crtWBxIBe3YrNZKR2lSzv1JJKWD4vA==", - "dev": true, - "dependencies": { - "is-stream": "^3.0.0", - "temp-dir": "^2.0.0", - "type-fest": "^2.12.2", - "unique-string": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "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/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "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, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/token-types": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", - "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", - "dev": true, - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/trim-newlines": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.0.2.tgz", - "integrity": "sha512-GJtWyq9InR/2HRiLZgpIKv+ufIKrVrvjQWEj7PxAXNc5dwbNJkqhAUoAGgzRmULAnoOM5EIpveYd3J2VeSAIew==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ts-jest": { - "version": "29.0.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.5.tgz", - "integrity": "sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==", - "dev": true, - "dependencies": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", - "jest-util": "^29.0.0", - "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "7.x", - "yargs-parser": "^21.0.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", - "typescript": ">=4.3" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/ts-jest/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==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/tsc-watch": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tsc-watch/-/tsc-watch-6.0.0.tgz", - "integrity": "sha512-zgpju+/z5z29/kK5V28Nz16CMkX2voFOUxkTlCim/R25hxzbyUqu2NfTnmJBQfESBSPbEQUGqDdB9A8opAcB4A==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "node-cleanup": "^2.1.2", - "ps-tree": "^1.2.0", - "string-argv": "^0.3.1" - }, - "bin": { - "tsc-watch": "dist/lib/tsc-watch.js" - }, - "engines": { - "node": ">=12.12.0" - }, - "peerDependencies": { - "typescript": "*" - } - }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "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-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "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/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/unique-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", - "dev": true, - "dependencies": { - "crypto-random-string": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist-lint": "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/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-to-istanbul": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", - "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/v8-to-istanbul/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "dependencies": { - "makeerror": "1.0.12" - } - }, - "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/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "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/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/write-file-atomic": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.0.tgz", - "integrity": "sha512-R7NYMnHSlV42K54lwY9lvW6MnSm1HSJqZL3xiSgi9E7//FYaI74r2G0rd+/X6VAMkHEdzxQaU5HUOXWUz5kA/w==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "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/yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", - "dev": true, - "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": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs/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==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "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" - } - } - }, - "dependencies": { - "@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, - "requires": { - "@babel/highlight": "^7.18.6" - } - }, - "@babel/compat-data": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", - "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==", - "dev": true - }, - "@babel/core": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.2.tgz", - "integrity": "sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.2", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-module-transforms": "^7.20.2", - "@babel/helpers": "^7.20.1", - "@babel/parser": "^7.20.2", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "dependencies": { - "convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.4.tgz", - "integrity": "sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA==", - "dev": true, - "requires": { - "@babel/types": "^7.20.2", - "@jridgewell/gen-mapping": "^0.3.2", - "jsesc": "^2.5.1" - }, - "dependencies": { - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - } - } - }, - "@babel/helper-compilation-targets": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", - "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.20.0", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true - }, - "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", - "dev": true, - "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-module-transforms": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", - "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", - "dev": true - }, - "@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", - "dev": true, - "requires": { - "@babel/types": "^7.20.2" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", - "dev": true - }, - "@babel/helpers": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", - "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", - "dev": true, - "requires": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.0" - } - }, - "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "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, - "requires": { - "color-convert": "^1.9.0" - } - }, - "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, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "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, - "requires": { - "color-name": "1.1.3" - } - }, - "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 - }, - "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 - }, - "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, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.20.13", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.13.tgz", - "integrity": "sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw==", - "dev": true - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", - "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-typescript": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", - "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.19.0" - } - }, - "@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" - } - }, - "@babel/traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "dependencies": { - "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 - } - } - }, - "@babel/types": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", - "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" - } - }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "@eslint/eslintrc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", - "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.4.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" - } - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@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 - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true - }, - "@jest/console": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.4.1.tgz", - "integrity": "sha512-m+XpwKSi3PPM9znm5NGS8bBReeAJJpSkL1OuFCqaMaJL2YX9YXLkkI+MBchMPwu+ZuM2rynL51sgfkQteQ1CKQ==", - "dev": true, - "requires": { - "@jest/types": "^29.4.1", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.4.1", - "jest-util": "^29.4.1", - "slash": "^3.0.0" - } - }, - "@jest/core": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.4.1.tgz", - "integrity": "sha512-RXFTohpBqpaTebNdg5l3I5yadnKo9zLBajMT0I38D0tDhreVBYv3fA8kywthI00sWxPztWLD3yjiUkewwu/wKA==", - "dev": true, - "requires": { - "@jest/console": "^29.4.1", - "@jest/reporters": "^29.4.1", - "@jest/test-result": "^29.4.1", - "@jest/transform": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.4.0", - "jest-config": "^29.4.1", - "jest-haste-map": "^29.4.1", - "jest-message-util": "^29.4.1", - "jest-regex-util": "^29.2.0", - "jest-resolve": "^29.4.1", - "jest-resolve-dependencies": "^29.4.1", - "jest-runner": "^29.4.1", - "jest-runtime": "^29.4.1", - "jest-snapshot": "^29.4.1", - "jest-util": "^29.4.1", - "jest-validate": "^29.4.1", - "jest-watcher": "^29.4.1", - "micromatch": "^4.0.4", - "pretty-format": "^29.4.1", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "@jest/environment": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.4.1.tgz", - "integrity": "sha512-pJ14dHGSQke7Q3mkL/UZR9ZtTOxqskZaC91NzamEH4dlKRt42W+maRBXiw/LWkdJe+P0f/zDR37+SPMplMRlPg==", - "dev": true, - "requires": { - "@jest/fake-timers": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/node": "*", - "jest-mock": "^29.4.1" - } - }, - "@jest/expect": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.4.1.tgz", - "integrity": "sha512-ZxKJP5DTUNF2XkpJeZIzvnzF1KkfrhEF6Rz0HGG69fHl6Bgx5/GoU3XyaeFYEjuuKSOOsbqD/k72wFvFxc3iTw==", - "dev": true, - "requires": { - "expect": "^29.4.1", - "jest-snapshot": "^29.4.1" - } - }, - "@jest/expect-utils": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.4.1.tgz", - "integrity": "sha512-w6YJMn5DlzmxjO00i9wu2YSozUYRBhIoJ6nQwpMYcBMtiqMGJm1QBzOf6DDgRao8dbtpDoaqLg6iiQTvv0UHhQ==", - "dev": true, - "requires": { - "jest-get-type": "^29.2.0" - } - }, - "@jest/fake-timers": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.4.1.tgz", - "integrity": "sha512-/1joI6rfHFmmm39JxNfmNAO3Nwm6Y0VoL5fJDy7H1AtWrD1CgRtqJbN9Ld6rhAkGO76qqp4cwhhxJ9o9kYjQMw==", - "dev": true, - "requires": { - "@jest/types": "^29.4.1", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.4.1", - "jest-mock": "^29.4.1", - "jest-util": "^29.4.1" - } - }, - "@jest/globals": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.4.1.tgz", - "integrity": "sha512-znoK2EuFytbHH0ZSf2mQK2K1xtIgmaw4Da21R2C/NE/+NnItm5mPEFQmn8gmF3f0rfOlmZ3Y3bIf7bFj7DHxAA==", - "dev": true, - "requires": { - "@jest/environment": "^29.4.1", - "@jest/expect": "^29.4.1", - "@jest/types": "^29.4.1", - "jest-mock": "^29.4.1" - } - }, - "@jest/reporters": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.4.1.tgz", - "integrity": "sha512-AISY5xpt2Xpxj9R6y0RF1+O6GRy9JsGa8+vK23Lmzdy1AYcpQn5ItX79wJSsTmfzPKSAcsY1LNt/8Y5Xe5LOSg==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.4.1", - "@jest/test-result": "^29.4.1", - "@jest/transform": "^29.4.1", - "@jest/types": "^29.4.1", - "@jridgewell/trace-mapping": "^0.3.15", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.4.1", - "jest-util": "^29.4.1", - "jest-worker": "^29.4.1", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - } - }, - "@jest/schemas": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.0.tgz", - "integrity": "sha512-0E01f/gOZeNTG76i5eWWSupvSHaIINrTie7vCyjiYFKgzNdyEGd12BUv4oNBFHOqlHDbtoJi3HrQ38KCC90NsQ==", - "dev": true, - "requires": { - "@sinclair/typebox": "^0.25.16" - } - }, - "@jest/source-map": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.2.0.tgz", - "integrity": "sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.15", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - } - }, - "@jest/test-result": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.4.1.tgz", - "integrity": "sha512-WRt29Lwt+hEgfN8QDrXqXGgCTidq1rLyFqmZ4lmJOpVArC8daXrZWkWjiaijQvgd3aOUj2fM8INclKHsQW9YyQ==", - "dev": true, - "requires": { - "@jest/console": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.4.1.tgz", - "integrity": "sha512-v5qLBNSsM0eHzWLXsQ5fiB65xi49A3ILPSFQKPXzGL4Vyux0DPZAIN7NAFJa9b4BiTDP9MBF/Zqc/QA1vuiJ0w==", - "dev": true, - "requires": { - "@jest/test-result": "^29.4.1", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.4.1", - "slash": "^3.0.0" - } - }, - "@jest/transform": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.4.1.tgz", - "integrity": "sha512-5w6YJrVAtiAgr0phzKjYd83UPbCXsBRTeYI4BXokv9Er9CcrH9hfXL/crCvP2d2nGOcovPUnlYiLPFLZrkG5Hg==", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.4.1", - "@jridgewell/trace-mapping": "^0.3.15", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.4.1", - "jest-regex-util": "^29.2.0", - "jest-util": "^29.4.1", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.0" - } - }, - "@jest/types": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.4.1.tgz", - "integrity": "sha512-zbrAXDUOnpJ+FMST2rV7QZOgec8rskg2zv8g2ajeqitp4tvZiyqTCYXANrKsM+ryj5o+LI+ZN2EgU9drrkiwSA==", - "dev": true, - "requires": { - "@jest/schemas": "^29.4.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true - }, - "@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 - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "@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, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@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 - }, - "@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, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@sinclair/typebox": { - "version": "0.25.21", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.21.tgz", - "integrity": "sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g==", - "dev": true - }, - "@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", - "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^2.0.0" - } - }, - "@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "dev": true - }, - "@types/babel__core": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", - "integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==", - "dev": true, - "requires": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", - "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", - "dev": true, - "requires": { - "@babel/types": "^7.3.0" - } - }, - "@types/debug": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", - "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", - "dev": true, - "requires": { - "@types/ms": "*" - } - }, - "@types/graceful-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", - "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, - "@types/jest": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.4.0.tgz", - "integrity": "sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==", - "dev": true, - "requires": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true - }, - "@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", - "dev": true - }, - "@types/ms": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", - "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", - "dev": true - }, - "@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", - "dev": true - }, - "@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", - "dev": true - }, - "@types/prettier": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", - "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", - "dev": true - }, - "@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", - "dev": true - }, - "@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, - "@types/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==", - "dev": true - }, - "@types/yargs": { - "version": "17.0.13", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", - "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.50.0.tgz", - "integrity": "sha512-vwksQWSFZiUhgq3Kv7o1Jcj0DUNylwnIlGvKvLLYsq8pAWha6/WCnXUeaSoNNha/K7QSf2+jvmkxggC1u3pIwQ==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.50.0", - "@typescript-eslint/type-utils": "5.50.0", - "@typescript-eslint/utils": "5.50.0", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "regexpp": "^3.2.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.50.0.tgz", - "integrity": "sha512-KCcSyNaogUDftK2G9RXfQyOCt51uB5yqC6pkUYqhYh8Kgt+DwR5M0EwEAxGPy/+DH6hnmKeGsNhiZRQxjH71uQ==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.50.0", - "@typescript-eslint/types": "5.50.0", - "@typescript-eslint/typescript-estree": "5.50.0", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.50.0.tgz", - "integrity": "sha512-rt03kaX+iZrhssaT974BCmoUikYtZI24Vp/kwTSy841XhiYShlqoshRFDvN1FKKvU2S3gK+kcBW1EA7kNUrogg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.50.0", - "@typescript-eslint/visitor-keys": "5.50.0" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.50.0.tgz", - "integrity": "sha512-dcnXfZ6OGrNCO7E5UY/i0ktHb7Yx1fV6fnQGGrlnfDhilcs6n19eIRcvLBqx6OQkrPaFlDPk3OJ0WlzQfrV0bQ==", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.50.0", - "@typescript-eslint/utils": "5.50.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.50.0.tgz", - "integrity": "sha512-atruOuJpir4OtyNdKahiHZobPKFvZnBnfDiyEaBf6d9vy9visE7gDjlmhl+y29uxZ2ZDgvXijcungGFjGGex7w==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.50.0.tgz", - "integrity": "sha512-Gq4zapso+OtIZlv8YNAStFtT6d05zyVCK7Fx3h5inlLBx2hWuc/0465C2mg/EQDDU2LKe52+/jN4f0g9bd+kow==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.50.0", - "@typescript-eslint/visitor-keys": "5.50.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.50.0.tgz", - "integrity": "sha512-v/AnUFImmh8G4PH0NDkf6wA8hujNNcrwtecqW4vtQ1UOSNBaZl49zP1SHoZ/06e+UiwzHpgb5zP5+hwlYYWYAw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.50.0", - "@typescript-eslint/types": "5.50.0", - "@typescript-eslint/typescript-estree": "5.50.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0", - "semver": "^7.3.7" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.50.0.tgz", - "integrity": "sha512-cdMeD9HGu6EXIeGOh2yVW6oGf9wq8asBgZx7nsR/D36gTfQ0odE5kcRYe5M81vjEFAcPeugXrHg78Imu55F6gg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.50.0", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", - "dev": true - }, - "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, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - }, - "dependencies": { - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true - } - } - }, - "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 - }, - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "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, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "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 - }, - "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 - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true - }, - "babel-jest": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.4.1.tgz", - "integrity": "sha512-xBZa/pLSsF/1sNpkgsiT3CmY7zV1kAsZ9OxxtrFqYucnOuRftXAfcJqcDVyOPeN4lttWTwhLdu0T9f8uvoPEUg==", - "dev": true, - "requires": { - "@jest/transform": "^29.4.1", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.4.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - } - }, - "babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - } - }, - "babel-plugin-jest-hoist": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.4.0.tgz", - "integrity": "sha512-a/sZRLQJEmsmejQ2rPEUe35nO1+C9dc9O1gplH1SXmJxveQSRUYdBk8yGZG/VOUuZs1u2aHZJusEGoRMbhhwCg==", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - } - }, - "babel-preset-jest": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.4.0.tgz", - "integrity": "sha512-fUB9vZflUSM3dO/6M2TCAepTzvA4VkOvl67PjErcrQMGt9Eve7uazaeyCZ2th3UtI7ljpiBJES0F7A1vBRsLZA==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^29.4.0", - "babel-preset-current-node-syntax": "^1.0.0" - } - }, - "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 - }, - "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, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" - } - }, - "bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "requires": { - "fast-json-stable-stringify": "2.x" - } - }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "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 - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "camelcase-keys": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz", - "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", - "dev": true, - "requires": { - "camelcase": "^6.3.0", - "map-obj": "^4.1.0", - "quick-lru": "^5.1.1", - "type-fest": "^1.2.1" - }, - "dependencies": { - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - }, - "type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true - } - } - }, - "caniuse-lite": { - "version": "1.0.30001431", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz", - "integrity": "sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "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 - }, - "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, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true - }, - "ci-info": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz", - "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==", - "dev": true - }, - "cjs-module-lexer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", - "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", - "dev": true - }, - "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==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true - }, - "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, - "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, - "requires": { - "color-name": "~1.1.4" - } - }, - "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 - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "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 - }, - "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, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "crypto-random-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", - "dev": true, - "requires": { - "type-fest": "^1.0.1" - }, - "dependencies": { - "type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true - } - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "decamelize": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz", - "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", - "dev": true - }, - "decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dev": true, - "requires": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dev": true - } - } - }, - "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "dev": true - }, - "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 - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - }, - "define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "dev": true - }, - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - }, - "diff-sequences": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.3.1.tgz", - "integrity": "sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==", - "dev": true - }, - "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, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", - "dev": true - }, - "emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true - }, - "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 - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "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 - }, - "eslint": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz", - "integrity": "sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==", - "dev": true, - "requires": { - "@eslint/eslintrc": "^1.4.1", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "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.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", - "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", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "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.1", - "regexpp": "^3.2.0", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "dependencies": { - "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "eslint-plugin-jest": { - "version": "27.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz", - "integrity": "sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==", - "dev": true, - "requires": { - "@typescript-eslint/utils": "^5.10.0" - } - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - } - } - }, - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true - }, - "espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" - } - }, - "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 - }, - "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "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, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", - "dev": true, - "requires": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - } - }, - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true - }, - "expect": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.4.1.tgz", - "integrity": "sha512-OKrGESHOaMxK3b6zxIq9SOW8kEXztKff/Dvg88j4xIJxur1hspEbedVkR3GpHe5LO+WB2Qw7OWN0RMTdp6as5A==", - "dev": true, - "requires": { - "@jest/expect-utils": "^29.4.1", - "jest-get-type": "^29.2.0", - "jest-matcher-utils": "^29.4.1", - "jest-message-util": "^29.4.1", - "jest-util": "^29.4.1" - } - }, - "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==", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "requires": { - "@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" - }, - "dependencies": { - "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, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "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 - }, - "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 - }, - "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "requires": { - "bser": "2.1.1" - } - }, - "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, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "file-type": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.0.0.tgz", - "integrity": "sha512-jjMwFpnW8PKofLE/4ohlhqwDk5k0NC6iy0UHAJFKoY1fQeGMN0GDdLgHQrvCbSpMwbqzoCZhRI5dETCZna5qVA==", - "dev": true, - "requires": { - "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0", - "token-types": "^5.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "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, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "dev": true - }, - "from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "dev": true - }, - "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 - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "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 - }, - "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==", - "dev": true - }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true - }, - "get-stdin": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", - "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", - "dev": true - }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "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" - } - }, - "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, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.19.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", - "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "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" - } - }, - "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - }, - "grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true - }, - "h264-profile-level-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/h264-profile-level-id/-/h264-profile-level-id-1.0.1.tgz", - "integrity": "sha512-D3Rln/jKNjKDW5ZTJTK3niSoOGE+pFqPvRHHVgQN3G7umcn/zWGPUo8Q8VpDj16x3hKz++zVviRNRmXu5cpN+Q==", - "requires": { - "debug": "^4.1.1" - } - }, - "hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "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 - }, - "hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - }, - "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, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true - }, - "indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true - }, - "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, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "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==", - "dev": true - }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true - }, - "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, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "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 - }, - "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 - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "requires": { - "is-docker": "^2.0.0" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "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 - }, - "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, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - } - }, - "istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jest": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.4.1.tgz", - "integrity": "sha512-cknimw7gAXPDOmj0QqztlxVtBVCw2lYY9CeIE5N6kD+kET1H4H79HSNISJmijb1HF+qk+G+ploJgiDi5k/fRlg==", - "dev": true, - "requires": { - "@jest/core": "^29.4.1", - "@jest/types": "^29.4.1", - "import-local": "^3.0.2", - "jest-cli": "^29.4.1" - } - }, - "jest-changed-files": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.4.0.tgz", - "integrity": "sha512-rnI1oPxgFghoz32Y8eZsGJMjW54UlqT17ycQeCEktcxxwqqKdlj9afl8LNeO0Pbu+h2JQHThQP0BzS67eTRx4w==", - "dev": true, - "requires": { - "execa": "^5.0.0", - "p-limit": "^3.1.0" - } - }, - "jest-circus": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.4.1.tgz", - "integrity": "sha512-v02NuL5crMNY4CGPHBEflLzl4v91NFb85a+dH9a1pUNx6Xjggrd8l9pPy4LZ1VYNRXlb+f65+7O/MSIbLir6pA==", - "dev": true, - "requires": { - "@jest/environment": "^29.4.1", - "@jest/expect": "^29.4.1", - "@jest/test-result": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.4.1", - "jest-matcher-utils": "^29.4.1", - "jest-message-util": "^29.4.1", - "jest-runtime": "^29.4.1", - "jest-snapshot": "^29.4.1", - "jest-util": "^29.4.1", - "p-limit": "^3.1.0", - "pretty-format": "^29.4.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - } - }, - "jest-cli": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.4.1.tgz", - "integrity": "sha512-jz7GDIhtxQ37M+9dlbv5K+/FVcIo1O/b1sX3cJgzlQUf/3VG25nvuWzlDC4F1FLLzUThJeWLu8I7JF9eWpuURQ==", - "dev": true, - "requires": { - "@jest/core": "^29.4.1", - "@jest/test-result": "^29.4.1", - "@jest/types": "^29.4.1", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^29.4.1", - "jest-util": "^29.4.1", - "jest-validate": "^29.4.1", - "prompts": "^2.0.1", - "yargs": "^17.3.1" - } - }, - "jest-config": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.4.1.tgz", - "integrity": "sha512-g7p3q4NuXiM4hrS4XFATTkd+2z0Ml2RhFmFPM8c3WyKwVDNszbl4E7cV7WIx1YZeqqCtqbtTtZhGZWJlJqngzg==", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.4.1", - "@jest/types": "^29.4.1", - "babel-jest": "^29.4.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.4.1", - "jest-environment-node": "^29.4.1", - "jest-get-type": "^29.2.0", - "jest-regex-util": "^29.2.0", - "jest-resolve": "^29.4.1", - "jest-runner": "^29.4.1", - "jest-util": "^29.4.1", - "jest-validate": "^29.4.1", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.4.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - } - }, - "jest-diff": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.4.1.tgz", - "integrity": "sha512-uazdl2g331iY56CEyfbNA0Ut7Mn2ulAG5vUaEHXycf1L6IPyuImIxSz4F0VYBKi7LYIuxOwTZzK3wh5jHzASMw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^29.3.1", - "jest-get-type": "^29.2.0", - "pretty-format": "^29.4.1" - } - }, - "jest-docblock": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.2.0.tgz", - "integrity": "sha512-bkxUsxTgWQGbXV5IENmfiIuqZhJcyvF7tU4zJ/7ioTutdz4ToB5Yx6JOFBpgI+TphRY4lhOyCWGNH/QFQh5T6A==", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.4.1.tgz", - "integrity": "sha512-QlYFiX3llJMWUV0BtWht/esGEz9w+0i7BHwODKCze7YzZzizgExB9MOfiivF/vVT0GSQ8wXLhvHXh3x2fVD4QQ==", - "dev": true, - "requires": { - "@jest/types": "^29.4.1", - "chalk": "^4.0.0", - "jest-get-type": "^29.2.0", - "jest-util": "^29.4.1", - "pretty-format": "^29.4.1" - } - }, - "jest-environment-node": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.4.1.tgz", - "integrity": "sha512-x/H2kdVgxSkxWAIlIh9MfMuBa0hZySmfsC5lCsWmWr6tZySP44ediRKDUiNggX/eHLH7Cd5ZN10Rw+XF5tXsqg==", - "dev": true, - "requires": { - "@jest/environment": "^29.4.1", - "@jest/fake-timers": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/node": "*", - "jest-mock": "^29.4.1", - "jest-util": "^29.4.1" - } - }, - "jest-get-type": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz", - "integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==", - "dev": true - }, - "jest-haste-map": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.4.1.tgz", - "integrity": "sha512-imTjcgfVVTvg02khXL11NNLTx9ZaofbAWhilrMg/G8dIkp+HYCswhxf0xxJwBkfhWb3e8dwbjuWburvxmcr58w==", - "dev": true, - "requires": { - "@jest/types": "^29.4.1", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.2.0", - "jest-util": "^29.4.1", - "jest-worker": "^29.4.1", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - } - }, - "jest-leak-detector": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.4.1.tgz", - "integrity": "sha512-akpZv7TPyGMnH2RimOCgy+hPmWZf55EyFUvymQ4LMsQP8xSPlZumCPtXGoDhFNhUE2039RApZkTQDKU79p/FiQ==", - "dev": true, - "requires": { - "jest-get-type": "^29.2.0", - "pretty-format": "^29.4.1" - } - }, - "jest-matcher-utils": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.4.1.tgz", - "integrity": "sha512-k5h0u8V4nAEy6lSACepxL/rw78FLDkBnXhZVgFneVpnJONhb2DhZj/Gv4eNe+1XqQ5IhgUcqj745UwH0HJmMnA==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^29.4.1", - "jest-get-type": "^29.2.0", - "pretty-format": "^29.4.1" - } - }, - "jest-message-util": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.4.1.tgz", - "integrity": "sha512-H4/I0cXUaLeCw6FM+i4AwCnOwHRgitdaUFOdm49022YD5nfyr8C/DrbXOBEyJaj+w/y0gGJ57klssOaUiLLQGQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.4.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.4.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - } - }, - "jest-mock": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.4.1.tgz", - "integrity": "sha512-MwA4hQ7zBOcgVCVnsM8TzaFLVUD/pFWTfbkY953Y81L5ret3GFRZtmPmRFAjKQSdCKoJvvqOu6Bvfpqlwwb0dQ==", - "dev": true, - "requires": { - "@jest/types": "^29.4.1", - "@types/node": "*", - "jest-util": "^29.4.1" - } - }, - "jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "requires": {} - }, - "jest-regex-util": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.2.0.tgz", - "integrity": "sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA==", - "dev": true - }, - "jest-resolve": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.4.1.tgz", - "integrity": "sha512-j/ZFNV2lm9IJ2wmlq1uYK0Y/1PiyDq9g4HEGsNTNr3viRbJdV+8Lf1SXIiLZXFvyiisu0qUyIXGBnw+OKWkJwQ==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.4.1", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.4.1", - "jest-validate": "^29.4.1", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - } - }, - "jest-resolve-dependencies": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.4.1.tgz", - "integrity": "sha512-Y3QG3M1ncAMxfjbYgtqNXC5B595zmB6e//p/qpA/58JkQXu/IpLDoLeOa8YoYfsSglBKQQzNUqtfGJJT/qLmJg==", - "dev": true, - "requires": { - "jest-regex-util": "^29.2.0", - "jest-snapshot": "^29.4.1" - } - }, - "jest-runner": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.4.1.tgz", - "integrity": "sha512-8d6XXXi7GtHmsHrnaqBKWxjKb166Eyj/ksSaUYdcBK09VbjPwIgWov1VwSmtupCIz8q1Xv4Qkzt/BTo3ZqiCeg==", - "dev": true, - "requires": { - "@jest/console": "^29.4.1", - "@jest/environment": "^29.4.1", - "@jest/test-result": "^29.4.1", - "@jest/transform": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.2.0", - "jest-environment-node": "^29.4.1", - "jest-haste-map": "^29.4.1", - "jest-leak-detector": "^29.4.1", - "jest-message-util": "^29.4.1", - "jest-resolve": "^29.4.1", - "jest-runtime": "^29.4.1", - "jest-util": "^29.4.1", - "jest-watcher": "^29.4.1", - "jest-worker": "^29.4.1", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - } - }, - "jest-runtime": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.4.1.tgz", - "integrity": "sha512-UXTMU9uKu2GjYwTtoAw5rn4STxWw/nadOfW7v1sx6LaJYa3V/iymdCLQM6xy3+7C6mY8GfX22vKpgxY171UIoA==", - "dev": true, - "requires": { - "@jest/environment": "^29.4.1", - "@jest/fake-timers": "^29.4.1", - "@jest/globals": "^29.4.1", - "@jest/source-map": "^29.2.0", - "@jest/test-result": "^29.4.1", - "@jest/transform": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.4.1", - "jest-message-util": "^29.4.1", - "jest-mock": "^29.4.1", - "jest-regex-util": "^29.2.0", - "jest-resolve": "^29.4.1", - "jest-snapshot": "^29.4.1", - "jest-util": "^29.4.1", - "semver": "^7.3.5", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - } - }, - "jest-snapshot": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.4.1.tgz", - "integrity": "sha512-l4iV8EjGgQWVz3ee/LR9sULDk2pCkqb71bjvlqn+qp90lFwpnulHj4ZBT8nm1hA1C5wowXLc7MGnw321u0tsYA==", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.4.1", - "@jest/transform": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.4.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.4.1", - "jest-get-type": "^29.2.0", - "jest-haste-map": "^29.4.1", - "jest-matcher-utils": "^29.4.1", - "jest-message-util": "^29.4.1", - "jest-util": "^29.4.1", - "natural-compare": "^1.4.0", - "pretty-format": "^29.4.1", - "semver": "^7.3.5" - } - }, - "jest-util": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.4.1.tgz", - "integrity": "sha512-bQy9FPGxVutgpN4VRc0hk6w7Hx/m6L53QxpDreTZgJd9gfx/AV2MjyPde9tGyZRINAUrSv57p2inGBu2dRLmkQ==", - "dev": true, - "requires": { - "@jest/types": "^29.4.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - } - }, - "jest-validate": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.4.1.tgz", - "integrity": "sha512-qNZXcZQdIQx4SfUB/atWnI4/I2HUvhz8ajOSYUu40CSmf9U5emil8EDHgE7M+3j9/pavtk3knlZBDsgFvv/SWw==", - "dev": true, - "requires": { - "@jest/types": "^29.4.1", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.2.0", - "leven": "^3.1.0", - "pretty-format": "^29.4.1" - }, - "dependencies": { - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - } - } - }, - "jest-watcher": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.4.1.tgz", - "integrity": "sha512-vFOzflGFs27nU6h8dpnVRER3O2rFtL+VMEwnG0H3KLHcllLsU8y9DchSh0AL/Rg5nN1/wSiQ+P4ByMGpuybaVw==", - "dev": true, - "requires": { - "@jest/test-result": "^29.4.1", - "@jest/types": "^29.4.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.4.1", - "string-length": "^4.0.1" - } - }, - "jest-worker": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.4.1.tgz", - "integrity": "sha512-O9doU/S1EBe+yp/mstQ0VpPwpv0Clgn68TkNwGxL6/usX/KUW9Arnn4ag8C3jc6qHcXznhsT5Na1liYzAsuAbQ==", - "dev": true, - "requires": { - "@types/node": "*", - "jest-util": "^29.4.1", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "dependencies": { - "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 - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "js-sdsl": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", - "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "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, - "requires": { - "argparse": "^2.0.1" - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "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 - }, - "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 - }, - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true - }, - "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, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "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 - }, - "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, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true - }, - "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 - }, - "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, - "requires": { - "yallist": "^4.0.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "requires": { - "tmpl": "1.0.5" - } - }, - "map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true - }, - "map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", - "dev": true - }, - "meow": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/meow/-/meow-10.1.5.tgz", - "integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.2", - "camelcase-keys": "^7.0.0", - "decamelize": "^5.0.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.2", - "read-pkg-up": "^8.0.0", - "redent": "^4.0.0", - "trim-newlines": "^4.0.2", - "type-fest": "^1.2.2", - "yargs-parser": "^20.2.9" - }, - "dependencies": { - "type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true - } - } - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, - "node-cleanup": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", - "integrity": "sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==", - "dev": true - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true - }, - "node-releases": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", - "dev": true - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - } - }, - "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 - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "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, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "open": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", - "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", - "dev": true, - "requires": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - } - }, - "open-cli": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/open-cli/-/open-cli-7.1.0.tgz", - "integrity": "sha512-Xnn/B7WY9ygV47oK+LlYp5WU8xr0tEL6SEw9jMX8n6ceElOs2AzVXFXI87/O0+b+LwLokQBZVxBMzGZHCYVppw==", - "dev": true, - "requires": { - "file-type": "^18.0.0", - "get-stdin": "^9.0.0", - "meow": "^10.1.5", - "open": "^8.4.0", - "tempy": "^3.0.0" - } - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "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, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "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, - "requires": { - "p-limit": "^3.0.2" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "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, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "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 - }, - "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 - }, - "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 - }, - "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 - }, - "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 - }, - "pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", - "dev": true, - "requires": { - "through": "~2.3" - } - }, - "peek-readable": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", - "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", - "dev": true - }, - "pick-port": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pick-port/-/pick-port-1.0.1.tgz", - "integrity": "sha512-JzjRIkfG/4pG3tYLl1LwdmFtnlW+Rsxe200DevHZzZLYDUgfnx8LuOFnLwy5Dt59JY1HIN3JXyMXRbUvERkh/g==", - "dev": true, - "requires": { - "debug": "^4.3.1" - } - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - } - } - }, - "polycrc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/polycrc/-/polycrc-0.1.0.tgz", - "integrity": "sha512-pBjdz8Gj0ixRkR80acjWl6bxiHf23MTI6chIKbQqphF2SrXXtYSPlftCSL31bD3veSWJCaTsM1QhT6zlIebqlg==", - "dev": true - }, - "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 - }, - "pretty-format": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.4.1.tgz", - "integrity": "sha512-dt/Z761JUVsrIKaY215o1xQJBGlSmTx/h4cSqXqjHLnU1+Kt+mavVE7UgqJJO5ukx5HjSswHfmXz4LjS2oIJfg==", - "dev": true, - "requires": { - "@jest/schemas": "^29.4.0", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - } - }, - "ps-tree": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", - "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", - "dev": true, - "requires": { - "event-stream": "=3.3.4" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "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 - }, - "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "read-pkg": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz", - "integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^3.0.2", - "parse-json": "^5.2.0", - "type-fest": "^1.0.1" - }, - "dependencies": { - "type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-8.0.0.tgz", - "integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==", - "dev": true, - "requires": { - "find-up": "^5.0.0", - "read-pkg": "^6.0.0", - "type-fest": "^1.0.1" - }, - "dependencies": { - "type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true - } - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", - "dev": true, - "requires": { - "readable-stream": "^3.6.0" - } - }, - "redent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", - "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", - "dev": true, - "requires": { - "indent-string": "^5.0.0", - "strip-indent": "^4.0.0" - } - }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, - "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==", - "dev": true - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "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 - }, - "resolve.exports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.0.tgz", - "integrity": "sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg==", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "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, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "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==", - "dev": true - }, - "sctp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/sctp/-/sctp-1.0.0.tgz", - "integrity": "sha512-wceaBrz55a0dbYG3c2zfJ1adUASLJhntQYZNVZKlGfKH1ExMckZB0sOxroWgpJLflcAB/k6wMOAPOrmylsRU4A==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "ip": "^1.1.5", - "polycrc": "^0.1.0" - } - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "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, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "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 - }, - "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 - }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "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 - }, - "source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", - "dev": true - }, - "split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", - "dev": true, - "requires": { - "through": "2" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "requires": { - "escape-string-regexp": "^2.0.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - } - } - }, - "stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", - "dev": true, - "requires": { - "duplexer": "~0.1.1" - } - }, - "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==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "string-argv": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", - "dev": true - }, - "string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "requires": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - } - }, - "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, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "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, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, - "strip-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", - "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", - "dev": true, - "requires": { - "min-indent": "^1.0.1" - } - }, - "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 - }, - "strtok3": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", - "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", - "dev": true, - "requires": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.0.0" - } - }, - "supports-color": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.3.1.tgz", - "integrity": "sha512-knBY82pjmnIzK3NifMo3RxEIRD9E0kIzV4BKcyTZ9+9kWgLMxd4PrsTSMoFQUabgRBbF8KOLRDCyKgNV+iK44Q==" - }, - "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 - }, - "temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", - "dev": true - }, - "tempy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.0.0.tgz", - "integrity": "sha512-B2I9X7+o2wOaW4r/CWMkpOO9mdiTRCxXNgob6iGvPmfPWgH/KyUD6Uy5crtWBxIBe3YrNZKR2lSzv1JJKWD4vA==", - "dev": true, - "requires": { - "is-stream": "^3.0.0", - "temp-dir": "^2.0.0", - "type-fest": "^2.12.2", - "unique-string": "^3.0.0" - }, - "dependencies": { - "is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true - }, - "type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true - } - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "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 - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, - "tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "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 - }, - "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, - "requires": { - "is-number": "^7.0.0" - } - }, - "token-types": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", - "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", - "dev": true, - "requires": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - } - }, - "trim-newlines": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.0.2.tgz", - "integrity": "sha512-GJtWyq9InR/2HRiLZgpIKv+ufIKrVrvjQWEj7PxAXNc5dwbNJkqhAUoAGgzRmULAnoOM5EIpveYd3J2VeSAIew==", - "dev": true - }, - "ts-jest": { - "version": "29.0.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.5.tgz", - "integrity": "sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==", - "dev": true, - "requires": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", - "jest-util": "^29.0.0", - "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "7.x", - "yargs-parser": "^21.0.1" - }, - "dependencies": { - "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==", - "dev": true - } - } - }, - "tsc-watch": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tsc-watch/-/tsc-watch-6.0.0.tgz", - "integrity": "sha512-zgpju+/z5z29/kK5V28Nz16CMkX2voFOUxkTlCim/R25hxzbyUqu2NfTnmJBQfESBSPbEQUGqDdB9A8opAcB4A==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "node-cleanup": "^2.1.2", - "ps-tree": "^1.2.0", - "string-argv": "^0.3.1" - } - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "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, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "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 - }, - "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true - }, - "unique-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", - "dev": true, - "requires": { - "crypto-random-string": "^4.0.0" - } - }, - "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "dev": true, - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "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 - }, - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - }, - "v8-to-istanbul": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", - "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" - }, - "dependencies": { - "convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - } - } - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "requires": { - "makeerror": "1.0.12" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "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, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "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 - }, - "write-file-atomic": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.0.tgz", - "integrity": "sha512-R7NYMnHSlV42K54lwY9lvW6MnSm1HSJqZL3xiSgi9E7//FYaI74r2G0rd+/X6VAMkHEdzxQaU5HUOXWUz5kA/w==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - } - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", - "dev": true, - "requires": { - "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" - }, - "dependencies": { - "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==", - "dev": true - } - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - }, - "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 - } - } + "name": "mediasoup", + "version": "3.15.2", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "mediasoup", + "version": "3.15.2", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "@types/ini": "^4.1.1", + "debug": "^4.3.7", + "flatbuffers": "^24.3.25", + "h264-profile-level-id": "^2.0.0", + "ini": "^5.0.0", + "node-fetch": "^3.3.2", + "supports-color": "^9.4.0", + "tar": "^7.4.3" + }, + "devDependencies": { + "@eslint/js": "^9.15.0", + "@octokit/rest": "^21.0.2", + "@types/debug": "^4.1.12", + "@types/jest": "^29.5.14", + "@types/node": "^22.9.1", + "eslint": "^9.15.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jest": "^28.9.0", + "eslint-plugin-prettier": "^5.2.1", + "globals": "^15.12.0", + "jest": "^29.7.0", + "marked": "^15.0.2", + "open-cli": "^8.0.0", + "pick-port": "^2.1.0", + "prettier": "^3.3.3", + "sctp": "^1.0.0", + "ts-jest": "^29.2.5", + "typescript": "^5.6.3", + "typescript-eslint": "^8.15.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mediasoup" + } + }, + "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/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/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/@babel/code-frame/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/@babel/code-frame/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/@babel/code-frame/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/@babel/code-frame/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/@babel/code-frame/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/@babel/compat-data": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", + "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.2.tgz", + "integrity": "sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.2", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-module-transforms": "^7.20.2", + "@babel/helpers": "^7.20.1", + "@babel/parser": "^7.20.2", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.2", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/@babel/core/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/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "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/@babel/helper-compilation-targets": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", + "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.0", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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/@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.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", + "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.2" + }, + "engines": { + "node": ">=6.9.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.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.2" + }, + "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.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "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.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", + "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "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/highlight/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/@babel/highlight/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/@babel/highlight/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/@babel/highlight/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/@babel/highlight/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/@babel/highlight/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/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "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-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "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/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.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@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.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/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/@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "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.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", + "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", + "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.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": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", + "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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==", + "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==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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==" + }, + "node_modules/@isaacs/cliui/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==", + "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/@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==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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==", + "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/@isaacs/fs-minipass": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.0.tgz", + "integrity": "sha512-S00nN1Qt3z3dSP6Db45fj/mksrAq5XWNIJ/SWXGP8XPT2jrzEuYRCSEx08JpJwBcG2F1xgiOtBMGDU0AZHmxew==", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.0.tgz", + "integrity": "sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "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/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", + "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", + "dev": true, + "peer": true, + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", + "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", + "dev": true, + "peer": true, + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz", + "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==", + "dev": true, + "peer": true, + "dependencies": { + "@octokit/request": "^8.3.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", + "dev": true + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.1.tgz", + "integrity": "sha512-ryqobs26cLtM1kQxqeZui4v8FeznirUsksiA+RYemMPJ7Micju0WSkv50dBksTuZks9O5cg4wp+t8fZ/cLY56g==", + "dev": true, + "dependencies": { + "@octokit/types": "^13.5.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.2.tgz", + "integrity": "sha512-EI7kXWidkt3Xlok5uN43suK99VWqc8OaIMktY9d9+RNKl69juoTyxmLoWPIZgJYzi41qj/9zU7G/ljnNOJ5AFA==", + "dev": true, + "dependencies": { + "@octokit/types": "^13.5.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" + } + }, + "node_modules/@octokit/request": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.3.1.tgz", + "integrity": "sha512-fin4cl5eHN5Ybmb/gtn7YZ+ycyUlcyqqkg5lfxeSChqj7sUt6TNaJPehREi+0PABKLREYL8pfaUhH3TicEWNoA==", + "dev": true, + "peer": true, + "dependencies": { + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", + "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "dev": true, + "peer": true, + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.0.2.tgz", + "integrity": "sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==", + "dev": true, + "dependencies": { + "@octokit/core": "^6.1.2", + "@octokit/plugin-paginate-rest": "^11.0.0", + "@octokit/plugin-request-log": "^5.3.1", + "@octokit/plugin-rest-endpoint-methods": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", + "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/core": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", + "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", + "dev": true, + "dependencies": { + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.0.0", + "@octokit/request": "^9.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/endpoint": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", + "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", + "dev": true, + "dependencies": { + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/graphql": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", + "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", + "dev": true, + "dependencies": { + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/plugin-request-log": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", + "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", + "dev": true, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/request": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", + "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", + "dev": true, + "dependencies": { + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/@octokit/request-error": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz", + "integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==", + "dev": true, + "dependencies": { + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest/node_modules/before-after-hook": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "dev": true + }, + "node_modules/@octokit/rest/node_modules/universal-user-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", + "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", + "dev": true + }, + "node_modules/@octokit/types": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", + "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", + "dev": true, + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "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==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", + "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "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.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", + "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz", + "integrity": "sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "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/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, + "node_modules/@types/node": { + "version": "22.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz", + "integrity": "sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.13", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", + "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz", + "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/type-utils": "8.15.0", + "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", + "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", + "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz", + "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/utils": "8.15.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", + "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", + "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz", + "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", + "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.15.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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, + "license": "MIT", + "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/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/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/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, + "license": "Python-2.0" + }, + "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/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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==" + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true, + "peer": true + }, + "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, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "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/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001431", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz", + "integrity": "sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "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/chalk/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/chalk/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/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "engines": { + "node": ">=18" + } + }, + "node_modules/ci-info": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz", + "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "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==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "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/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/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/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/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dev": true, + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "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/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true, + "peer": true + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.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==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.284", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", + "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", + "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.15.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.5", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "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-jest": { + "version": "28.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.9.0.tgz", + "integrity": "sha512-rLu1s1Wf96TgUUxSw6loVIkNtUjq1Re7A9QdCCHSohnvXEBAjuL420h0T/fMmkQlNsQP2GhQzEUpYHPfxBkvYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "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-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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, + "license": "BSD-2-Clause", + "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/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "18.7.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.7.0.tgz", + "integrity": "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==", + "dev": true, + "dependencies": { + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0", + "token-types": "^5.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/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/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatbuffers": { + "version": "24.3.25", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-24.3.25.tgz", + "integrity": "sha512-3HDgPbgiwWMI9zVB7VYBHaMrbOO7Gm0v+yD2FV/sCKj+9NDeVL7BOBYUuhWAQGKWOzBo8S9WdMvV0eixO233XQ==" + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": 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==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/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==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "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==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "15.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", + "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "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/h264-profile-level-id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/h264-profile-level-id/-/h264-profile-level-id-2.0.0.tgz", + "integrity": "sha512-X4CLryVbVA0CtjTExS4G5U1gb2Z4wa32AF8ukVmFuLdw2JRq2aHisor7SY5SYTUUrUSqq0KdPIO18sql6IWIQw==", + "dependencies": { + "@types/debug": "^4.1.12", + "debug": "^4.3.4" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mediasoup" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "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/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.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==", + "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/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "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, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/ip": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/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/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/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/istanbul-lib-report/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/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/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/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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==", + "dev": true + }, + "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, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "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, + "license": "MIT" + }, + "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/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/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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/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/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.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "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/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marked": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.2.tgz", + "integrity": "sha512-85RUkoYKIVB21PbMKrnD6aCl9ws+XKEyhJNMbLn206NyD3jbBo7Ec7Wi4Jrsn4dV1a2ng7K/jfkmIN0DNoS41w==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=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.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/minizlib/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==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/minizlib/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minizlib/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minizlib/node_modules/rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "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/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=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/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/open/-/open-10.0.3.tgz", + "integrity": "sha512-dtbI5oW7987hwC9qjJTyABldTaa19SuyJse1QboWv3b0qCcrrLNVDqBx1XgELAjh9QTVQaP/C5b1nhQebd1H2A==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open-cli": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/open-cli/-/open-cli-8.0.0.tgz", + "integrity": "sha512-3muD3BbfLyzl+aMVSEfn2FfOqGdPYR0O4KNnxXsLEPE2q9OSjBfJAaB6XKbrUzLgymoSMejvb5jpXJfru/Ko2A==", + "dev": true, + "dependencies": { + "file-type": "^18.7.0", + "get-stdin": "^9.0.0", + "meow": "^12.1.1", + "open": "^10.0.0", + "tempy": "^3.1.0" + }, + "bin": { + "open-cli": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "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.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dependencies": { + "lru-cache": "^10.2.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.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/peek-readable": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", + "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pick-port": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pick-port/-/pick-port-2.1.0.tgz", + "integrity": "sha512-nqdK+0cmJLGMHKZCNh6PfA/ZeIhHYKSlLLwyfH/IFQVv9SqwNUdbm+08olnd+PbmLqrHk8Twhq6yO9viqOGkkw==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=18" + } + }, + "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/pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/polycrc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/polycrc/-/polycrc-0.1.0.tgz", + "integrity": "sha512-pBjdz8Gj0ixRkR80acjWl6bxiHf23MTI6chIKbQqphF2SrXXtYSPlftCSL31bD3veSWJCaTsM1QhT6zlIebqlg==", + "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.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "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/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.3.tgz", + "integrity": "sha512-KddyFewCsO0j3+np81IQ+SweXLDnDQTs5s67BOnrYmYe/yNmUhttQyGsYzy8yUnoljGAQ9sl38YB4vH8ur7Y+w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "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" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "dev": true, + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "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-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "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==", + "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/sctp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sctp/-/sctp-1.0.0.tgz", + "integrity": "sha512-wceaBrz55a0dbYG3c2zfJ1adUASLJhntQYZNVZKlGfKH1ExMckZB0sOxroWgpJLflcAB/k6wMOAPOrmylsRU4A==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "ip": "^1.1.5", + "polycrc": "^0.1.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "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==", + "engines": { + "node": ">=8" + } + }, + "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/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "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/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/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.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-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=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==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/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==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/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==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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/strtok3": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", + "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", + "dev": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "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/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "dev": true, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/tempy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", + "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", + "dev": true, + "dependencies": { + "is-stream": "^3.0.0", + "temp-dir": "^3.0.0", + "type-fest": "^2.12.2", + "unique-string": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "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, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/token-types": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", + "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", + "dev": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, + "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-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.15.0.tgz", + "integrity": "sha512-wY4FRGl0ZI+ZU4Jo/yjdBu0lVTSML58pu6PgGtJmCufvzfV565pUF6iACQt092uFOd49iLOTX/sEVmHtbSrS+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.15.0", + "@typescript-eslint/parser": "8.15.0", + "@typescript-eslint/utils": "8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "dev": true, + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true, + "peer": true + }, + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "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, + "license": "BSD-2-Clause", + "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/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "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": { + "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==", + "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/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/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "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==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "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" + } + } + }, + "dependencies": { + "@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 + }, + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "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, + "requires": { + "color-convert": "^1.9.0" + } + }, + "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, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "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, + "requires": { + "color-name": "1.1.3" + } + }, + "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 + }, + "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 + }, + "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, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/compat-data": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", + "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==", + "dev": true + }, + "@babel/core": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.2.tgz", + "integrity": "sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.2", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-module-transforms": "^7.20.2", + "@babel/helpers": "^7.20.1", + "@babel/parser": "^7.20.2", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.2", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "dependencies": { + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "requires": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, + "@babel/helper-compilation-targets": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", + "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.20.0", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@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 + }, + "@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, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@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, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-transforms": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", + "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.2" + } + }, + "@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 + }, + "@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "dev": true, + "requires": { + "@babel/types": "^7.20.2" + } + }, + "@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, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@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 + }, + "@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", + "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", + "dev": true, + "requires": { + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.0" + } + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "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, + "requires": { + "color-convert": "^1.9.0" + } + }, + "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, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "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, + "requires": { + "color-name": "1.1.3" + } + }, + "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 + }, + "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 + }, + "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, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@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.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "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 + } + } + }, + "@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@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, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true + }, + "@eslint/config-array": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", + "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", + "dev": true, + "requires": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + } + }, + "@eslint/core": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", + "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.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" + }, + "dependencies": { + "globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true + } + } + }, + "@eslint/js": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", + "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", + "dev": true + }, + "@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true + }, + "@eslint/plugin-kit": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", + "dev": true, + "requires": { + "levn": "^0.4.1" + } + }, + "@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true + }, + "@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "requires": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "dependencies": { + "@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true + } + } + }, + "@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 + }, + "@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "requires": { + "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" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@isaacs/fs-minipass": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.0.tgz", + "integrity": "sha512-S00nN1Qt3z3dSP6Db45fj/mksrAq5XWNIJ/SWXGP8XPT2jrzEuYRCSEx08JpJwBcG2F1xgiOtBMGDU0AZHmxew==", + "requires": { + "minipass": "^7.0.4" + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + } + }, + "@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "requires": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + } + }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + } + }, + "@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.0.tgz", + "integrity": "sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + } + } + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@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 + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "@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, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@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 + }, + "@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, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "peer": true + }, + "@octokit/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", + "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", + "dev": true, + "peer": true, + "requires": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/endpoint": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", + "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", + "dev": true, + "peer": true, + "requires": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/graphql": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz", + "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==", + "dev": true, + "peer": true, + "requires": { + "@octokit/request": "^8.3.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", + "dev": true + }, + "@octokit/plugin-paginate-rest": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.1.tgz", + "integrity": "sha512-ryqobs26cLtM1kQxqeZui4v8FeznirUsksiA+RYemMPJ7Micju0WSkv50dBksTuZks9O5cg4wp+t8fZ/cLY56g==", + "dev": true, + "requires": { + "@octokit/types": "^13.5.0" + } + }, + "@octokit/plugin-rest-endpoint-methods": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.2.tgz", + "integrity": "sha512-EI7kXWidkt3Xlok5uN43suK99VWqc8OaIMktY9d9+RNKl69juoTyxmLoWPIZgJYzi41qj/9zU7G/ljnNOJ5AFA==", + "dev": true, + "requires": { + "@octokit/types": "^13.5.0" + } + }, + "@octokit/request": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.3.1.tgz", + "integrity": "sha512-fin4cl5eHN5Ybmb/gtn7YZ+ycyUlcyqqkg5lfxeSChqj7sUt6TNaJPehREi+0PABKLREYL8pfaUhH3TicEWNoA==", + "dev": true, + "peer": true, + "requires": { + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/request-error": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", + "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "dev": true, + "peer": true, + "requires": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "@octokit/rest": { + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.0.2.tgz", + "integrity": "sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==", + "dev": true, + "requires": { + "@octokit/core": "^6.1.2", + "@octokit/plugin-paginate-rest": "^11.0.0", + "@octokit/plugin-request-log": "^5.3.1", + "@octokit/plugin-rest-endpoint-methods": "^13.0.0" + }, + "dependencies": { + "@octokit/auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", + "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", + "dev": true + }, + "@octokit/core": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", + "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", + "dev": true, + "requires": { + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.0.0", + "@octokit/request": "^9.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" + } + }, + "@octokit/endpoint": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", + "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", + "dev": true, + "requires": { + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.2" + } + }, + "@octokit/graphql": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", + "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", + "dev": true, + "requires": { + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" + } + }, + "@octokit/plugin-request-log": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", + "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", + "dev": true, + "requires": {} + }, + "@octokit/request": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", + "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", + "dev": true, + "requires": { + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^7.0.2" + } + }, + "@octokit/request-error": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz", + "integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==", + "dev": true, + "requires": { + "@octokit/types": "^13.0.0" + } + }, + "before-after-hook": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "dev": true + }, + "universal-user-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", + "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", + "dev": true + } + } + }, + "@octokit/types": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", + "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", + "dev": true, + "requires": { + "@octokit/openapi-types": "^22.2.0" + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true + }, + "@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true + }, + "@types/babel__core": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", + "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", + "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "requires": { + "@types/ms": "*" + } + }, + "@types/eslint": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz", + "integrity": "sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==" + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "@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 + }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, + "@types/node": { + "version": "22.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz", + "integrity": "sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg==", + "dev": true, + "requires": { + "undici-types": "~6.19.8" + } + }, + "@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "@types/yargs": { + "version": "17.0.13", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", + "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz", + "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/type-utils": "8.15.0", + "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + } + }, + "@typescript-eslint/parser": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", + "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", + "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz", + "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/utils": "8.15.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + } + }, + "@typescript-eslint/types": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", + "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", + "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "dependencies": { + "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, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@typescript-eslint/utils": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz", + "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", + "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.15.0", + "eslint-visitor-keys": "^4.2.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true + } + } + }, + "acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true + }, + "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, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "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, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "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 + }, + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "requires": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true, + "peer": true + }, + "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, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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, + "requires": { + "fill-range": "^7.1.1" + } + }, + "browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "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 + }, + "bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "requires": { + "run-applescript": "^7.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001431", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz", + "integrity": "sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "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 + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, + "chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==" + }, + "ci-info": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.1.tgz", + "integrity": "sha512-up5ggbaDqOqJ4UqLKZ2naVkyqSJQgJi5lwD6b6mM748ysrghDBX0bx/qJTUHzw7zu6Mq4gycviSF5hJnwceD8w==", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "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==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "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==", + "requires": { + "color-name": "~1.1.4" + } + }, + "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==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "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 + }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + } + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dev": true, + "requires": { + "type-fest": "^1.0.1" + }, + "dependencies": { + "type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true + } + } + }, + "data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" + }, + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + }, + "dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "requires": {} + }, + "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 + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, + "default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "requires": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + } + }, + "default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true + }, + "define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true + }, + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true, + "peer": true + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, + "electron-to-chromium": { + "version": "1.4.284", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", + "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true + }, + "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==" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "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 + }, + "eslint": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", + "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.15.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.5", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true + } + } + }, + "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, + "requires": {} + }, + "eslint-plugin-jest": { + "version": "28.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.9.0.tgz", + "integrity": "sha512-rLu1s1Wf96TgUUxSw6loVIkNtUjq1Re7A9QdCCHSohnvXEBAjuL420h0T/fMmkQlNsQP2GhQzEUpYHPfxBkvYQ==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + } + }, + "eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "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 + }, + "espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "requires": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true + } + } + }, + "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 + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "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, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "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==", + "dev": true + }, + "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 + }, + "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, + "requires": { + "@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" + }, + "dependencies": { + "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, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "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 + }, + "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 + }, + "fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, + "file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "requires": { + "flat-cache": "^4.0.0" + } + }, + "file-type": { + "version": "18.7.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.7.0.tgz", + "integrity": "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==", + "dev": true, + "requires": { + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0", + "token-types": "^5.0.1" + } + }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "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, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "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, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "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, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + } + }, + "flatbuffers": { + "version": "24.3.25", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-24.3.25.tgz", + "integrity": "sha512-3HDgPbgiwWMI9zVB7VYBHaMrbOO7Gm0v+yD2FV/sCKj+9NDeVL7BOBYUuhWAQGKWOzBo8S9WdMvV0eixO233XQ==" + }, + "flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "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==", + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + } + } + }, + "formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "requires": { + "fetch-blob": "^3.1.2" + } + }, + "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 + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "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 + }, + "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==", + "dev": true + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "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" + } + }, + "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, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "15.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", + "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "h264-profile-level-id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/h264-profile-level-id/-/h264-profile-level-id-2.0.0.tgz", + "integrity": "sha512-X4CLryVbVA0CtjTExS4G5U1gb2Z4wa32AF8ukVmFuLdw2JRq2aHisor7SY5SYTUUrUSqq0KdPIO18sql6IWIQw==", + "requires": { + "@types/debug": "^4.1.12", + "debug": "^4.3.4" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "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 + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true + }, + "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, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "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, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==" + }, + "ip": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "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==" + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "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, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "requires": { + "is-docker": "^3.0.0" + } + }, + "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 + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "requires": { + "is-inside-container": "^1.0.0" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "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 + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "jake": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", + "dev": true, + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + } + }, + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + } + }, + "jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + } + }, + "jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + } + }, + "jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + } + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + } + }, + "jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + } + }, + "jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "requires": {} + }, + "jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true + }, + "jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "requires": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + } + }, + "jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + } + }, + "jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "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 + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "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, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "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 + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "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 + }, + "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 + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "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, + "requires": { + "json-buffer": "3.0.1" + } + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "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, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "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 + }, + "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, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "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 + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, + "marked": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.2.tgz", + "integrity": "sha512-85RUkoYKIVB21PbMKrnD6aCl9ws+XKEyhJNMbLn206NyD3jbBo7Ec7Wi4Jrsn4dV1a2ng7K/jfkmIN0DNoS41w==", + "dev": true + }, + "meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, + "minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "requires": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + } + }, + "minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "requires": { + "glob": "^10.3.7" + } + } + } + }, + "mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "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-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + }, + "node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "dev": true + }, + "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 + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "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, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/open/-/open-10.0.3.tgz", + "integrity": "sha512-dtbI5oW7987hwC9qjJTyABldTaa19SuyJse1QboWv3b0qCcrrLNVDqBx1XgELAjh9QTVQaP/C5b1nhQebd1H2A==", + "dev": true, + "requires": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + } + }, + "open-cli": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/open-cli/-/open-cli-8.0.0.tgz", + "integrity": "sha512-3muD3BbfLyzl+aMVSEfn2FfOqGdPYR0O4KNnxXsLEPE2q9OSjBfJAaB6XKbrUzLgymoSMejvb5jpXJfru/Ko2A==", + "dev": true, + "requires": { + "file-type": "^18.7.0", + "get-stdin": "^9.0.0", + "meow": "^12.1.1", + "open": "^10.0.0", + "tempy": "^3.1.0" + } + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@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" + } + }, + "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, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "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, + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "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, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "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 + }, + "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 + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "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 + }, + "path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==" + } + } + }, + "peek-readable": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", + "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", + "dev": true + }, + "pick-port": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pick-port/-/pick-port-2.1.0.tgz", + "integrity": "sha512-nqdK+0cmJLGMHKZCNh6PfA/ZeIhHYKSlLLwyfH/IFQVv9SqwNUdbm+08olnd+PbmLqrHk8Twhq6yO9viqOGkkw==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, + "polycrc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/polycrc/-/polycrc-0.1.0.tgz", + "integrity": "sha512-pBjdz8Gj0ixRkR80acjWl6bxiHf23MTI6chIKbQqphF2SrXXtYSPlftCSL31bD3veSWJCaTsM1QhT6zlIebqlg==", + "dev": true + }, + "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 + }, + "prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true + }, + "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, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "pure-rand": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.3.tgz", + "integrity": "sha512-KddyFewCsO0j3+np81IQ+SweXLDnDQTs5s67BOnrYmYe/yNmUhttQyGsYzy8yUnoljGAQ9sl38YB4vH8ur7Y+w==", + "dev": true + }, + "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 + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "dev": true, + "requires": { + "readable-stream": "^3.6.0" + } + }, + "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==", + "dev": true + }, + "resolve": { + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "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 + }, + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true + }, + "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, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "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==", + "dev": true + }, + "sctp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sctp/-/sctp-1.0.0.tgz", + "integrity": "sha512-wceaBrz55a0dbYG3c2zfJ1adUASLJhntQYZNVZKlGfKH1ExMckZB0sOxroWgpJLflcAB/k6wMOAPOrmylsRU4A==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "ip": "^1.1.5", + "polycrc": "^0.1.0" + } + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + }, + "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==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "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 + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "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 + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, + "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==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "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==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "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 + }, + "strtok3": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", + "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", + "dev": true, + "requires": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.0.0" + } + }, + "supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==" + }, + "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 + }, + "synckit": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "dev": true, + "requires": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + } + }, + "tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "requires": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "dependencies": { + "yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==" + } + } + }, + "temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "dev": true + }, + "tempy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", + "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", + "dev": true, + "requires": { + "is-stream": "^3.0.0", + "temp-dir": "^3.0.0", + "type-fest": "^2.12.2", + "unique-string": "^3.0.0" + }, + "dependencies": { + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true + } + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "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 + }, + "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, + "requires": { + "is-number": "^7.0.0" + } + }, + "token-types": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", + "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", + "dev": true, + "requires": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + } + }, + "ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "requires": {} + }, + "ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "requires": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + } + }, + "tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, + "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, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true + }, + "typescript-eslint": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.15.0.tgz", + "integrity": "sha512-wY4FRGl0ZI+ZU4Jo/yjdBu0lVTSML58pu6PgGtJmCufvzfV565pUF6iACQt092uFOd49iLOTX/sEVmHtbSrS+w==", + "dev": true, + "requires": { + "@typescript-eslint/eslint-plugin": "8.15.0", + "@typescript-eslint/parser": "8.15.0", + "@typescript-eslint/utils": "8.15.0" + } + }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "dev": true, + "requires": { + "crypto-random-string": "^4.0.0" + } + }, + "universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true, + "peer": true + }, + "update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "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 + }, + "v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "dependencies": { + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + } + } + }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, + "web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "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, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "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 + }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "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" + } + }, + "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==", + "dev": true + }, + "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 + } + } } diff --git a/package.json b/package.json index f84904edb3..da573333b8 100644 --- a/package.json +++ b/package.json @@ -1,110 +1,132 @@ { - "name": "mediasoup", - "version": "3.11.8", - "description": "Cutting Edge WebRTC Video Conferencing", - "contributors": [ - "IÃąaki Baz Castillo (https://inakibaz.me)", - "JosÊ Luis MillÃĄn (https://github.com/jmillan)", - "Nazar Mokynskyi (https://github.com/nazar-pc)" - ], - "homepage": "https://mediasoup.org", - "license": "ISC", - "repository": { - "type": "git", - "url": "https://github.com/versatica/mediasoup.git" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mediasoup" - }, - "main": "node/lib/index.js", - "types": "node/lib/index.d.ts", - "files": [ - "npm-scripts.js", - "node/lib", - "worker/Makefile", - "worker/meson.build", - "worker/meson_options.txt", - "worker/src", - "worker/include", - "worker/test/src", - "worker/test/include", - "worker/fuzzer/src", - "worker/fuzzer/include", - "worker/subprojects/*.wrap", - "worker/deps/libwebrtc", - "worker/scripts/*.py", - "worker/scripts/*.sh", - "worker/scripts/*.js", - "worker/scripts/*.json" - ], - "keywords": [ - "webrtc", - "ortc", - "sfu", - "nodejs" - ], - "engines": { - "node": ">=16" - }, - "scripts": { - "prepare": "node npm-scripts.js prepare", - "postinstall": "node npm-scripts.js postinstall", - "typescript:build": "node npm-scripts.js typescript:build", - "typescript:watch": "node npm-scripts.js typescript:watch", - "worker:build": "node npm-scripts.js worker:build", - "lint": "npm run lint:node && npm run lint:worker", - "lint:node": "node npm-scripts.js lint:node", - "lint:worker": "node npm-scripts.js lint:worker", - "format:worker": "node npm-scripts.js format:worker", - "test": "npm run test:node && npm run test:worker", - "test:node": "node npm-scripts.js test:node", - "test:worker": "node npm-scripts.js test:worker", - "coverage:node": "node npm-scripts.js coverage:node", - "install-deps:node": "node npm-scripts.js install-deps:node", - "install-clang-tools": "node npm-scripts.js install-clang-tools", - "release:check": "node npm-scripts.js release:check", - "release": "node npm-scripts.js release" - }, - "jest": { - "verbose": true, - "testEnvironment": "node", - "testRegex": "node/src/tests/test-.*\\.ts", - "transform": { - "^.*\\.ts$": [ - "ts-jest", - { - "diagnostics": { - "ignoreCodes": [ - "TS151001" - ] - } - } - ] - }, - "cacheDirectory": ".cache/jest" - }, - "dependencies": { - "debug": "^4.3.4", - "h264-profile-level-id": "^1.0.1", - "supports-color": "^9.3.1", - "uuid": "^9.0.0" - }, - "devDependencies": { - "@types/debug": "^4.1.7", - "@types/jest": "^29.4.0", - "@types/node": "^18.11.18", - "@types/uuid": "^9.0.0", - "@typescript-eslint/eslint-plugin": "^5.50.0", - "@typescript-eslint/parser": "^5.50.0", - "eslint": "^8.33.0", - "eslint-plugin-jest": "^27.2.1", - "jest": "^29.4.1", - "open-cli": "^7.1.0", - "pick-port": "^1.0.1", - "sctp": "^1.0.0", - "ts-jest": "^29.0.5", - "tsc-watch": "^6.0.0", - "typescript": "^4.9.5" - } + "name": "mediasoup", + "version": "3.15.2", + "description": "Cutting Edge WebRTC Video Conferencing", + "contributors": [ + "IÃąaki Baz Castillo (https://inakibaz.me)", + "JosÊ Luis MillÃĄn (https://github.com/jmillan)", + "Nazar Mokynskyi (https://github.com/nazar-pc)" + ], + "license": "ISC", + "homepage": "https://mediasoup.org", + "repository": { + "type": "git", + "url": "git+https://github.com/versatica/mediasoup.git" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mediasoup" + }, + "main": "node/lib/index.js", + "types": "node/lib/index.d.ts", + "files": [ + "node/lib", + "worker/deps/libwebrtc", + "worker/fbs", + "worker/fuzzer/include", + "worker/fuzzer/src", + "worker/include", + "worker/src", + "worker/scripts/*.json", + "worker/scripts/*.mjs", + "worker/scripts/*.py", + "worker/scripts/*.sh", + "worker/subprojects/*.wrap", + "worker/test/include", + "worker/test/src", + "worker/meson.build", + "worker/meson_options.txt", + "worker/tasks.py", + "npm-scripts.mjs" + ], + "engines": { + "node": ">=18" + }, + "keywords": [ + "webrtc", + "ortc", + "sfu", + "nodejs" + ], + "scripts": { + "prepare": "node npm-scripts.mjs prepare", + "postinstall": "node npm-scripts.mjs postinstall", + "typescript:build": "node npm-scripts.mjs typescript:build", + "typescript:watch": "node npm-scripts.mjs typescript:watch", + "worker:build": "node npm-scripts.mjs worker:build", + "worker:prebuild": "node npm-scripts.mjs worker:prebuild", + "lint": "npm run lint:node && npm run lint:worker", + "lint:node": "node npm-scripts.mjs lint:node", + "lint:worker": "node npm-scripts.mjs lint:worker", + "format": "npm run format:node && npm run format:worker", + "format:node": "node npm-scripts.mjs format:node", + "format:worker": "node npm-scripts.mjs format:worker", + "flatc": "npm run flatc:node && npm run flatc:worker", + "flatc:node": "node npm-scripts.mjs flatc:node", + "flatc:worker": "node npm-scripts.mjs flatc:worker", + "test": "npm run test:node && npm run test:worker", + "test:node": "node npm-scripts.mjs test:node", + "test:worker": "node npm-scripts.mjs test:worker", + "coverage:node": "node npm-scripts.mjs coverage:node", + "release:check": "node npm-scripts.mjs release:check", + "release": "node npm-scripts.mjs release" + }, + "jest": { + "verbose": true, + "testEnvironment": "node", + "testRegex": "node/src/test/test-.*\\.ts", + "transform": { + "^.*\\.ts$": [ + "ts-jest", + { + "diagnostics": { + "ignoreCodes": [ + "TS151001" + ] + } + } + ] + }, + "coveragePathIgnorePatterns": [ + "node/src/fbs", + "node/src/test" + ], + "cacheDirectory": ".cache/jest", + "modulePathIgnorePatterns": [ + "worker", + "rust", + "target" + ] + }, + "dependencies": { + "@types/ini": "^4.1.1", + "debug": "^4.3.7", + "flatbuffers": "^24.3.25", + "h264-profile-level-id": "^2.0.0", + "ini": "^5.0.0", + "node-fetch": "^3.3.2", + "supports-color": "^9.4.0", + "tar": "^7.4.3" + }, + "devDependencies": { + "@eslint/js": "^9.15.0", + "@octokit/rest": "^21.0.2", + "@types/debug": "^4.1.12", + "@types/jest": "^29.5.14", + "@types/node": "^22.9.1", + "eslint": "^9.15.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jest": "^28.9.0", + "eslint-plugin-prettier": "^5.2.1", + "globals": "^15.12.0", + "jest": "^29.7.0", + "marked": "^15.0.2", + "open-cli": "^8.0.0", + "pick-port": "^2.1.0", + "prettier": "^3.3.3", + "sctp": "^1.0.0", + "ts-jest": "^29.2.5", + "typescript": "^5.6.3", + "typescript-eslint": "^8.15.0" + } } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index b42ddcf07f..874939176e 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.67.0" +channel = "1.79.0" components = ["rustfmt", "clippy"] diff --git a/rust/CHANGELOG.md b/rust/CHANGELOG.md index 5d501f9b8a..1ba60e0256 100644 --- a/rust/CHANGELOG.md +++ b/rust/CHANGELOG.md @@ -1,114 +1,178 @@ # Changelog +# NEXT + +# 0.17.1 + +- Update Rust toolchain channel to version 1.79.0 (PR #1409). +- Updates from mediasoup TypeScript `3.14.7..=3.14.10`. +- General mediasoup changes: + - Worker: Add `enable_liburing` boolean option (`true` by default) to disable `io_uring` even if it's supported by the prebuilt `mediasoup-worker` and by current host (PR #1442). + +# 0.17.0 + +- Updates from mediasoup TypeScript `3.13.18..=3.14.6`. +- General mediasoup changes: + - Worker: Fix crash when closing `WebRtcServer` with active `WebRtcTransports` (PR #1390). + - `Worker: Fix memory leak when using `WebRtcServer` with TCP enabled (PR #1389). + - OPUS: Fix DTX detection (PR #1357). + - `TransportListenInfo`: Add `portRange` (deprecate worker port range) (PR #1365). + - Update worker FlatBuffers to 24.3.6-1 (fix cannot set temporal layer 0) (PR #1348). + - Fix DTLS packets do not honor configured DTLS MTU (attempt 3) (PR #1345). + - Add server side ICE consent checks to detect silent WebRTC disconnections (PR #1332). + - `TransportListenInfo`: "announced ip" can also be a hostname (PR #1322). + - `TransportListenInfo`: Rename "announced ip" to "announced address" (PR #1324). + +# 0.16.0 + +- Updates from mediasoup TypeScript `3.13.13..=3.13.17`. +- General mediasoup changes: + - `TransportListenInfo.announced_ip` can also be a hostname (PR #1322). + - `TransportListenInfo.announced_ip` is now `announced_address`, `IceCandidate.ip` is now `IceCandidate.address` and `TransportTuple.local_ip` is not `TransportTuple.local_address` (PR #1324). + +# 0.15.0 + +- Expose DataChannel string message as binary (PR #1289). + +# 0.14.0 + +- Updates from mediasoup TypeScript `3.13.8..=3.13.12`. +- Update h264-profile-level-id dependency to 0.2.0. +- Fix docs build (PR #1271). +- Rename `data_consumer::on_producer_resume` to `data_consumer::on_data_producer_resume` (PR #1271). + +# 0.13.0 + +- Updates from mediasoup TypeScript `3.13.0..=3.13.7`. +- General mediasoup changes: + - Switch from JSON based messages to `flatbuffers` (PR #1064). + - Enable `liburing` usage for Linux (kernel versions >= 6) (PR #1218). + - Add pause/resume API in `DataProducer` and `DataConsumer` (PR #1104). + - DataChannel subchannels feature (PR #1152). + - `Worker`: Make DTLS fragment stay within MTU size range (PR #1156). + - Replace make + Makefile with Python Invoke library + tasks.py (also fix installation under path with whitespaces) (PR #1239). + +# 0.12.0 + +- Updates from mediasoup TypeScript `3.11.9..=3.12.16`. + +# 0.11.4 + +- Fix consuming data producer from direct transport by data consumer on non-direct transport. + +# 0.11.3 + +- Updates from mediasoup TypeScript `3.11.3..=3.11.8`. + # 0.11.2 -* Updates from mediasoup TypeScript `3.10.11..=3.11.2`. +- Updates from mediasoup TypeScript `3.10.11..=3.11.2`. # 0.11.1 -* Updates from mediasoup TypeScript `3.10.7..=3.10.10`. +- Updates from mediasoup TypeScript `3.10.7..=3.10.10`. # 0.11.0 -* Updates from mediasoup TypeScript `3.10.2..=3.10.6`. +- Updates from mediasoup TypeScript `3.10.2..=3.10.6`. # 0.10.0 -* Updates from mediasoup TypeScript `3.9.10..=3.10.1`. -* `WebRtcServer`: A new class that brings to `WebRtcTransports` the ability to listen on a single UDP/TCP port (PR #834, PR #845). -* Minor API breaking changes. +- Updates from mediasoup TypeScript `3.9.10..=3.10.1`. +- `WebRtcServer`: A new class that brings to `WebRtcTransports` the ability to listen on a single UDP/TCP port (PR #834, PR #845). +- Minor API breaking changes. # 0.9.3 -* Fix a segfaults in tests and under multithreaded executor -* Fix another racy deadlock situation -* Expose hierarchical dependencies of ownership of Rust data structures, now it is possible to call `consumer.transport().router().worker().worker_manager()` -* General mediasoup changes: - * ICE renomination support (PR #756). - * Update `libuv` to 1.43.0. - * TCC client optimizations for faster and more stable BWE (PR #712 by @ggarber). - * Added support for RTP abs-capture-time header (PR #761 by @oto313). - * Fix VP9 kSVC forwarding logic to not forward lower unneded layers (PR #778 by @ggarber). - * Fix update bandwidth estimation configuration and available bitrate when updating max outgoing bitrate (PR #779 by @ggarber). - * Optimize RTP header extension handling (PR #786). - * `RateCalculator`: Reset optimization (PR #785). - * Fix frozen video due to double call to `Consumer::UserOnTransportDisconnected()` (PR #788, thanks to @ggarber for exposing this issue in PR #787). +- Fix a segfaults in tests and under multithreaded executor. +- Fix another racy deadlock situation. +- Expose hierarchical dependencies of ownership of Rust data structures, now it is possible to call `consumer.transport().router().worker().worker_manager()`. +- General mediasoup changes: + - ICE renomination support (PR #756). + - Update `libuv` to 1.43.0. + - TCC client optimizations for faster and more stable BWE (PR #712 by @ggarber). + - Added support for RTP abs-capture-time header (PR #761 by @oto313). + - Fix VP9 kSVC forwarding logic to not forward lower unneded layers (PR #778 by @ggarber). + - Fix update bandwidth estimation configuration and available bitrate when updating max outgoing bitrate (PR #779 by @ggarber). + - Optimize RTP header extension handling (PR #786). + - `RateCalculator`: Reset optimization (PR #785). + - Fix frozen video due to double call to `Consumer::UserOnTransportDisconnected()` (PR #788, thanks to @ggarber for exposing this issue in PR #787). # 0.9.2 -* Update `lru` dependency to fix security vulnerability +- Update `lru` dependency to fix security vulnerability # 0.9.1 -* Fix cleanup of build artifacts -* Make `Transport` implement `Send` -* Another fix to rare deadlock -* Improved Windows support (doesn't require MSVS activation) +- Fix cleanup of build artifacts. +- Make `Transport` implement `Send`. +- Another fix to rare deadlock. +- Improved Windows support (doesn't require MSVS activation). # 0.9.0 -* Fix for receiving data over payload channel -* Support thread initializer function for worker threads, can be used for pinning worker threads to CPU cores -* Significant worker communication optimizations (especially latency) -* Switch from file descriptors to function calls when communicating with worker -* Various optimizations that caused minor breaking changes to public API -* Requests no longer have internal timeout, but they can now be cancelled, add your own timeouts on top if needed -* Windows support -* General mediasoup changes: - * Replaces GYP build system with fully-functional Meson build system (PR #622). - * `Consumer`: Modification of bitrate allocation algorithm (PR #708). - * Single H264/H265 codec configuration in `supportedRtpCapabilities` (PR #718). +- Fix for receiving data over payload channel. +- Support thread initializer function for worker threads, can be used for pinning worker threads to CPU cores. +- Significant worker communication optimizations (especially latency). +- Switch from file descriptors to function calls when communicating with worker. +- Various optimizations that caused minor breaking changes to public API. +- Requests no longer have internal timeout, but they can now be cancelled, add your own timeouts on top if needed. +- Windows support. +- General mediasoup changes: + - Replaces GYP build system with fully-functional Meson build system (PR #622). + - `Consumer`: Modification of bitrate allocation algorithm (PR #708). + - Single H264/H265 codec configuration in `supportedRtpCapabilities` (PR #718). # 0.8.5 -* Fix types for `round_trip_time` and `bitrate_by_layer` fields `ProducerStat` and `ConsumerStat` -* Accumulation of worker fixes +- Fix types for `round_trip_time` and `bitrate_by_layer` fields `ProducerStat` and `ConsumerStat`. +- Accumulation of worker fixes. # 0.8.4 -* Add Active Speaker Observer to prelude -* Fix consumers preventing producers from being closed (regression introduced in 0.8.3) +- Add Active Speaker Observer to prelude. +- Fix consumers preventing producers from being closed (regression introduced in 0.8.3). # 0.8.3 -* prelude module containing traits and structs that should be sufficient for most basic mediasoup-based apps -* Dominant Speaker Event (PR #603 by @SteveMcFarlin). +- prelude module containing traits and structs that should be sufficient for most basic mediasoup-based apps. +- Dominant Speaker Event (PR #603 by @SteveMcFarlin). ### 0.8.2 -* Support for optional fixed port on transports +- Support for optional fixed port on transports. ### 0.8.1 -* Add convenience methods for getting information from `TransportTuple` enum, especially local IP/port -* Add `mid` option in `ConsumerOptions` to provide way to override MID -* Add convenience method `ConsumerStats::consumer_stat()` +- Add convenience methods for getting information from `TransportTuple` enum, especially local IP/port. +- Add `mid` option in `ConsumerOptions` to provide way to override MID +- Add convenience method `ConsumerStats::consumer_stat()`. ### 0.8.0 -* `NonClosingProducer` removed (use `PipedProducer` instead, they were identical) -* `RtpHeaderExtensionUri::as_str()` now takes `self` instead of `&self` -* `kind` field of `RtpHeaderExtension` is no longer optional -* Refactor `ScalabilityMode` from being a string to enum, make sure layers are not zero on type system level -* Concrete types for info field of tracing events +- `NonClosingProducer` removed (use `PipedProducer` instead, they were identical). +- `RtpHeaderExtensionUri::as_str()` now takes `self` instead of `&self`. +- `kind` field of `RtpHeaderExtension` is no longer optional. +- Refactor `ScalabilityMode` from being a string to enum, make sure layers are not zero on type system level. +- Concrete types for info field of tracing events. ### 0.7.2 -* Thread and memory safety fixes in mediasoup-sys -* macOS support -* `NonClosingProducer` renamed into `PipedProducer` with better docs -* Internal restructuring of modules for better compatibility with IDEs -* Feature level updated to mediasoup `3.7.6` +- Thread and memory safety fixes in mediasoup-sys. +- macOS support. +- `NonClosingProducer` renamed into `PipedProducer` with better docs. +- Internal restructuring of modules for better compatibility with IDEs. +- Feature level updated to mediasoup `3.7.6`. ### 0.7.0 -* Switch from running C++ worker processes to worker threads using mediasoup-sys that wraps mediasoup-worker into library -* Simplify `WorkerManager::new()` and `WorkerManager::with_executor()` API as the result of above -* Support `rtxPacketsDiscarded` in `Producer` stats -* Enable Rust 2018 idioms warnings -* Make sure all public types have `Debug` implementation on them -* Enforce docs on public types and add missing documentation -* Remove `RtpCodecParametersParameters::new()` (`RtpCodecParametersParameters::default()` does the same thing) +- Switch from running C++ worker processes to worker threads using mediasoup-sys that wraps mediasoup-worker into library. +- Simplify `WorkerManager::new()` and `WorkerManager::with_executor()` API as the result of above. +- Support `rtxPacketsDiscarded` in `Producer` stats. +- Enable Rust 2018 idioms warnings. +- Make sure all public types have `Debug` implementation on them. +- Enforce docs on public types and add missing documentation. +- Remove `RtpCodecParametersParameters::new()` (`RtpCodecParametersParameters::default()` does the same thing). ### 0.6.0 diff --git a/rust/Cargo.toml b/rust/Cargo.toml index c635ad067b..909ba49228 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mediasoup" -version = "0.11.2" +version = "0.17.1" description = "Cutting Edge WebRTC Video Conferencing in Rust" categories = ["api-bindings", "multimedia", "network-programming"] authors = ["Nazar Mokrynskyi "] @@ -30,11 +30,12 @@ atomic-take = "1.0.0" event-listener-primitives = "2.0.1" fastrand = "1.8.0" futures-lite = "1.12.0" -h264-profile-level-id = "0.1.1" +h264-profile-level-id = "0.2.0" hash_hasher = "2.0.3" log = "0.4.17" nohash-hasher = "0.2.0" once_cell = "1.16.0" +planus = "0.4.0" serde_json = "1.0.87" serde_repr = "0.1.9" thiserror = "1.0.37" @@ -45,7 +46,7 @@ version = "0.8.1" [dependencies.mediasoup-sys] path = "../worker" -version = "0.5.3" +version = "0.9.1" [dependencies.parking_lot] version = "0.12.1" @@ -58,7 +59,7 @@ version = "1.6.0" [dependencies.serde] features = ["derive"] -version = "1.0.147" +version = "1.0.190" [dependencies.uuid] features = ["serde", "v4"] @@ -80,3 +81,6 @@ version = "4.2.1" [[bench]] name = "direct_data" harness = false +[[bench]] +name = "producer" +harness = false diff --git a/rust/benches/direct_data.rs b/rust/benches/direct_data.rs index 8406a7acd3..0cea4d8fe0 100644 --- a/rust/benches/direct_data.rs +++ b/rust/benches/direct_data.rs @@ -19,7 +19,7 @@ async fn create_data_producer_consumer_pair( .produce_data(DataProducerOptions::new_direct()) .await?; let data_consumer = direct_transport - .consume_data(DataConsumerOptions::new_direct(data_producer.id())) + .consume_data(DataConsumerOptions::new_direct(data_producer.id(), None)) .await?; Ok((data_producer, data_consumer)) @@ -51,7 +51,8 @@ pub fn criterion_benchmark(c: &mut Criterion) { let _ = sender.send(()); }); - let _ = direct_data_producer.send(WebRtcMessage::Binary(Cow::from(data))); + let _ = + direct_data_producer.send(WebRtcMessage::Binary(Cow::from(data)), None, None); let _ = receiver.recv(); }) diff --git a/rust/benches/producer.rs b/rust/benches/producer.rs new file mode 100644 index 0000000000..260a00f189 --- /dev/null +++ b/rust/benches/producer.rs @@ -0,0 +1,288 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use mediasoup::prelude::*; +use std::env; +use std::net::{IpAddr, Ipv4Addr}; +use std::num::{NonZeroU32, NonZeroU8}; + +fn create_ssrc() -> u32 { + fastrand::u32(100_000_000..999_999_999) +} + +fn media_codecs() -> Vec { + vec![ + RtpCodecCapability::Audio { + mime_type: MimeTypeAudio::Opus, + preferred_payload_type: None, + clock_rate: NonZeroU32::new(48000).unwrap(), + channels: NonZeroU8::new(2).unwrap(), + parameters: RtpCodecParametersParameters::from([("foo", "111".into())]), + rtcp_feedback: vec![], + }, + RtpCodecCapability::Video { + mime_type: MimeTypeVideo::Vp8, + preferred_payload_type: None, + clock_rate: NonZeroU32::new(90000).unwrap(), + parameters: RtpCodecParametersParameters::default(), + rtcp_feedback: vec![], + }, + RtpCodecCapability::Video { + mime_type: MimeTypeVideo::H264, + preferred_payload_type: None, + clock_rate: NonZeroU32::new(90000).unwrap(), + parameters: RtpCodecParametersParameters::from([ + ("level-asymmetry-allowed", 1_u32.into()), + ("packetization-mode", 1_u32.into()), + ("profile-level-id", "4d0032".into()), + ("foo", "bar".into()), + ]), + rtcp_feedback: vec![], + }, + ] +} + +async fn init() -> (Worker, Router, WebRtcTransport, WebRtcTransport) { + { + let mut builder = env_logger::builder(); + if env::var(env_logger::DEFAULT_FILTER_ENV).is_err() { + builder.filter_level(log::LevelFilter::Off); + } + let _ = builder.is_test(true).try_init(); + } + + let worker_manager = WorkerManager::new(); + + let worker = worker_manager + .create_worker(WorkerSettings::default()) + .await + .expect("Failed to create worker"); + + let router = worker + .create_router(RouterOptions::new(media_codecs())) + .await + .expect("Failed to create router"); + + let transport_options = + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })); + + let transport_1 = router + .create_webrtc_transport(transport_options.clone()) + .await + .expect("Failed to create transport1"); + + let transport_2 = router + .create_webrtc_transport(transport_options) + .await + .expect("Failed to create transport2"); + + (worker, router, transport_1, transport_2) +} + +fn audio_producer_options() -> ProducerOptions { + ProducerOptions::new( + MediaKind::Audio, + RtpParameters { + mid: Some(fastrand::u32(100_000_000..999_999_999).to_string()), + codecs: vec![RtpCodecParameters::Audio { + mime_type: MimeTypeAudio::Opus, + payload_type: 0, + clock_rate: NonZeroU32::new(48000).unwrap(), + channels: NonZeroU8::new(2).unwrap(), + parameters: RtpCodecParametersParameters::from([ + ("useinbandfec", 1_u32.into()), + ("usedtx", 1_u32.into()), + ("foo", "222.222".into()), + ("bar", "333".into()), + ]), + rtcp_feedback: vec![], + }], + header_extensions: vec![ + RtpHeaderExtensionParameters { + uri: RtpHeaderExtensionUri::Mid, + id: 10, + encrypt: false, + }, + RtpHeaderExtensionParameters { + uri: RtpHeaderExtensionUri::AudioLevel, + id: 12, + encrypt: false, + }, + ], + // Missing encodings on purpose. + encodings: vec![], + rtcp: RtcpParameters { + cname: Some("audio-1".to_string()), + ..RtcpParameters::default() + }, + }, + ) +} + +fn video_producer_options() -> ProducerOptions { + ProducerOptions::new( + MediaKind::Video, + RtpParameters { + mid: Some(fastrand::u32(100_000_000..999_999_999).to_string()), + codecs: vec![ + RtpCodecParameters::Video { + mime_type: MimeTypeVideo::H264, + payload_type: 112, + clock_rate: NonZeroU32::new(90000).unwrap(), + parameters: RtpCodecParametersParameters::from([ + ("packetization-mode", 1_u32.into()), + ("profile-level-id", "4d0032".into()), + ]), + rtcp_feedback: vec![ + RtcpFeedback::Nack, + RtcpFeedback::NackPli, + RtcpFeedback::GoogRemb, + ], + }, + RtpCodecParameters::Video { + mime_type: MimeTypeVideo::Rtx, + payload_type: 113, + clock_rate: NonZeroU32::new(90000).unwrap(), + parameters: RtpCodecParametersParameters::from([("apt", 112u32.into())]), + rtcp_feedback: vec![], + }, + ], + header_extensions: vec![ + RtpHeaderExtensionParameters { + uri: RtpHeaderExtensionUri::Mid, + id: 10, + encrypt: false, + }, + RtpHeaderExtensionParameters { + uri: RtpHeaderExtensionUri::VideoOrientation, + id: 13, + encrypt: false, + }, + ], + encodings: vec![ + RtpEncodingParameters { + ssrc: Some(create_ssrc()), + rtx: Some(RtpEncodingParametersRtx { + ssrc: create_ssrc(), + }), + scalability_mode: "L1T3".parse().unwrap(), + ..RtpEncodingParameters::default() + }, + RtpEncodingParameters { + ssrc: Some(create_ssrc()), + rtx: Some(RtpEncodingParametersRtx { + ssrc: create_ssrc(), + }), + ..RtpEncodingParameters::default() + }, + RtpEncodingParameters { + ssrc: Some(create_ssrc()), + rtx: Some(RtpEncodingParametersRtx { + ssrc: create_ssrc(), + }), + ..RtpEncodingParameters::default() + }, + RtpEncodingParameters { + ssrc: Some(create_ssrc()), + rtx: Some(RtpEncodingParametersRtx { + ssrc: create_ssrc(), + }), + ..RtpEncodingParameters::default() + }, + ], + rtcp: RtcpParameters { + cname: Some("video-1".to_string()), + ..RtcpParameters::default() + }, + }, + ) +} + +pub fn criterion_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("producer"); + + { + let (_worker, _router, transport_1, _transport_2) = + futures_lite::future::block_on(async { init().await }); + + { + let audio_producer = futures_lite::future::block_on(async { + let (_worker, _router, transport_1, _transport_2) = init().await; + transport_1 + .produce(audio_producer_options()) + .await + .expect("Failed to produce audio") + }); + + group.bench_function("create/audio", |b| { + b.iter(|| { + let _ = futures_lite::future::block_on(async { + transport_1 + .produce(audio_producer_options()) + .await + .expect("Failed to produce audio") + }); + }) + }); + + group.bench_function("dump/audio", |b| { + b.iter(|| { + let _ = futures_lite::future::block_on(async { audio_producer.dump().await }); + }) + }); + + group.bench_function("stats/audio", |b| { + b.iter(|| { + let _ = + futures_lite::future::block_on(async { audio_producer.get_stats().await }); + }) + }); + } + + { + let video_producer = futures_lite::future::block_on(async { + let (_worker, _router, transport_1, _transport_2) = init().await; + transport_1 + .produce(video_producer_options()) + .await + .expect("Failed to produce video") + }); + + group.bench_function("create/video", |b| { + b.iter(|| { + let _ = futures_lite::future::block_on(async { + transport_1 + .produce(video_producer_options()) + .await + .expect("Failed to produce video") + }); + }) + }); + + group.bench_function("dump/video", |b| { + b.iter(|| { + let _ = futures_lite::future::block_on(async { video_producer.dump().await }); + }) + }); + + group.bench_function("stats/video", |b| { + b.iter(|| { + let _ = + futures_lite::future::block_on(async { video_producer.get_stats().await }); + }) + }); + } + } + + group.finish(); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/rust/examples/echo.rs b/rust/examples/echo.rs index 0a0c688b9f..98a39e582d 100644 --- a/rust/examples/echo.rs +++ b/rust/examples/echo.rs @@ -181,10 +181,17 @@ impl EchoConnection { // We know that for echo example we'll need 2 transports, so we can create both right away. // This may not be the case for real-world applications or you may create this at a // different time and/or in different order. - let transport_options = WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - })); + let transport_options = + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })); let producer_transport = router .create_webrtc_transport(transport_options.clone()) .await diff --git a/rust/examples/multiopus.rs b/rust/examples/multiopus.rs index 67e784335d..9f959c1999 100644 --- a/rust/examples/multiopus.rs +++ b/rust/examples/multiopus.rs @@ -145,9 +145,15 @@ impl EchoConnection { // For simplicity we will create plain transport for audio producer right away let plain_transport = router .create_plain_transport({ - let mut options = PlainTransportOptions::new(ListenIp { + let mut options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); options.comedia = true; @@ -193,9 +199,9 @@ impl EchoConnection { RTCP listening on {}:{}\n \ PT=100\n \ SSRC=1111", - plain_transport.tuple().local_ip(), + plain_transport.tuple().local_address(), plain_transport.tuple().local_port(), - plain_transport.rtcp_tuple().unwrap().local_ip(), + plain_transport.rtcp_tuple().unwrap().local_address(), plain_transport.rtcp_tuple().unwrap().local_port(), ); @@ -215,9 +221,9 @@ impl EchoConnection { rtpbin.send_rtp_sink_0 \\\n \ rtpbin.send_rtp_src_0 ! udpsink host={} port={} sync=false async=false \\\n \ rtpbin.send_rtcp_src_0 ! udpsink host={} port={} sync=false async=false", - plain_transport.tuple().local_ip(), + plain_transport.tuple().local_address(), plain_transport.tuple().local_port(), - plain_transport.rtcp_tuple().unwrap().local_ip(), + plain_transport.rtcp_tuple().unwrap().local_address(), plain_transport.rtcp_tuple().unwrap().local_port(), ); @@ -225,12 +231,18 @@ impl EchoConnection { // it right away. This may not be the case for real-world applications or you may create // this at a different time and/or in different order. let consumer_transport = router - .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - ListenIp { + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - }, - ))) + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) .await .map_err(|error| format!("Failed to create consumer transport: {error}"))?; diff --git a/rust/examples/svc-simulcast.rs b/rust/examples/svc-simulcast.rs index 465bddac88..b33b4f095d 100644 --- a/rust/examples/svc-simulcast.rs +++ b/rust/examples/svc-simulcast.rs @@ -201,10 +201,17 @@ impl SvcSimulcastConnection { // right away. // This may not be the case for real-world applications or you may create this at a // different time and/or in different order. - let transport_options = WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - })); + let transport_options = + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })); let producer_transport = router .create_webrtc_transport(transport_options.clone()) .await diff --git a/rust/examples/videoroom.rs b/rust/examples/videoroom.rs index 369ae4f0da..d475372735 100644 --- a/rust/examples/videoroom.rs +++ b/rust/examples/videoroom.rs @@ -496,9 +496,15 @@ mod participant { // right away. This may not be the case for real-world applications or you may create // this at a different time and/or in different order. let transport_options = - WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, })); let producer_transport = room .router() diff --git a/rust/src/data_structures.rs b/rust/src/data_structures.rs index 798de4bf9d..a2043f3c08 100644 --- a/rust/src/data_structures.rs +++ b/rust/src/data_structures.rs @@ -3,6 +3,9 @@ #[cfg(test)] mod tests; +use mediasoup_sys::fbs::{ + common, producer, rtp_packet, sctp_association, transport, web_rtc_transport, +}; use serde::de::{MapAccess, Visitor}; use serde::ser::SerializeStruct; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; @@ -10,7 +13,7 @@ use std::any::Any; use std::borrow::Cow; use std::fmt; use std::net::IpAddr; -use std::ops::{Deref, DerefMut}; +use std::ops::{Deref, DerefMut, RangeInclusive}; use std::sync::Arc; /// Container for arbitrary data attached to mediasoup entities. @@ -44,18 +47,88 @@ impl AppData { } } -/// IP to listen on. +/// Listening protocol, IP and port for [`WebRtcServer`](crate::webrtc_server::WebRtcServer) to listen on. /// /// # Notes on usage -/// If you use "0.0.0.0" or "::" as ip value, then you need to also provide `announced_ip`. -#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)] +/// If you use "0.0.0.0" or "::" as ip value, then you need to also provide +/// `announced_address`. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct ListenIp { +pub struct ListenInfo { + /// Network protocol. + pub protocol: Protocol, /// Listening IPv4 or IPv6. pub ip: IpAddr, - /// Announced IPv4 or IPv6 (useful when running mediasoup behind NAT with private IP). + /// Announced IPv4, IPv6 or hostname (useful when running mediasoup behind + /// NAT with private IP). + #[serde(skip_serializing_if = "Option::is_none")] + pub announced_address: Option, + /// Listening port. + #[serde(skip_serializing_if = "Option::is_none")] + pub port: Option, + /// Listening port range. If given then |port| will be ignored. + #[serde(skip_serializing_if = "Option::is_none")] + pub port_range: Option>, + /// Socket flags. #[serde(skip_serializing_if = "Option::is_none")] - pub announced_ip: Option, + pub flags: Option, + /// Send buffer size (bytes). + #[serde(skip_serializing_if = "Option::is_none")] + pub send_buffer_size: Option, + /// Recv buffer size (bytes). + #[serde(skip_serializing_if = "Option::is_none")] + pub recv_buffer_size: Option, +} + +impl ListenInfo { + pub(crate) fn to_fbs(&self) -> transport::ListenInfo { + transport::ListenInfo { + protocol: match self.protocol { + Protocol::Tcp => transport::Protocol::Tcp, + Protocol::Udp => transport::Protocol::Udp, + }, + ip: self.ip.to_string(), + announced_address: self + .announced_address + .as_ref() + .map(|address| address.to_string()), + port: self.port.unwrap_or(0), + port_range: match &self.port_range { + Some(port_range) => Box::new(transport::PortRange { + min: *port_range.start(), + max: *port_range.end(), + }), + None => Box::new(transport::PortRange { min: 0, max: 0 }), + }, + flags: Box::new(self.flags.unwrap_or_default().to_fbs()), + send_buffer_size: self.send_buffer_size.unwrap_or(0), + recv_buffer_size: self.recv_buffer_size.unwrap_or(0), + } + } +} + +/// UDP/TCP socket flags. +#[derive( + Default, Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize, +)] +#[serde(rename_all = "camelCase")] +pub struct SocketFlags { + /// Disable dual-stack support so only IPv6 is used (only if ip is IPv6). + /// Defaults to false. + pub ipv6_only: bool, + /// Make different transports bind to the same ip and port (only for UDP). + /// Useful for multicast scenarios with plain transport. Use with caution. + /// Defaults to false. + pub udp_reuse_port: bool, +} + +impl SocketFlags { + pub(crate) fn to_fbs(self) -> transport::SocketFlags { + transport::SocketFlags { + ipv6_only: self.ipv6_only, + udp_reuse_port: self.udp_reuse_port, + } + } } /// ICE role. @@ -68,6 +141,15 @@ pub enum IceRole { Controlling, } +impl IceRole { + pub(crate) fn from_fbs(role: web_rtc_transport::IceRole) -> Self { + match role { + web_rtc_transport::IceRole::Controlled => IceRole::Controlled, + web_rtc_transport::IceRole::Controlling => IceRole::Controlling, + } + } +} + /// ICE parameters. #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -80,18 +162,28 @@ pub struct IceParameters { pub ice_lite: Option, } +impl IceParameters { + pub(crate) fn from_fbs(parameters: web_rtc_transport::IceParameters) -> Self { + Self { + username_fragment: parameters.username_fragment.to_string(), + password: parameters.password.to_string(), + ice_lite: Some(parameters.ice_lite), + } + } +} + /// ICE candidate type. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum IceCandidateType { /// The candidate is a host candidate, whose IP address as specified in the - /// [`IceCandidate::ip`] property is in fact the true address of the remote peer. + /// [`IceCandidate::address`] property is in fact the true address of the remote peer. Host, - /// The candidate is a server reflexive candidate; the [`IceCandidate::ip`] indicates an + /// The candidate is a server reflexive candidate; the [`IceCandidate::address`] indicates an /// intermediary address assigned by the STUN server to represent the candidate's peer /// anonymously. Srflx, - /// The candidate is a peer reflexive candidate; the [`IceCandidate::ip`] is an intermediary + /// The candidate is a peer reflexive candidate; the [`IceCandidate::address`] is an intermediary /// address assigned by the STUN server to represent the candidate's peer anonymously. Prflx, /// The candidate is a relay candidate, obtained from a TURN server. The relay candidate's IP @@ -100,6 +192,14 @@ pub enum IceCandidateType { Relay, } +impl IceCandidateType { + pub(crate) fn from_fbs(candidate_type: web_rtc_transport::IceCandidateType) -> Self { + match candidate_type { + web_rtc_transport::IceCandidateType::Host => IceCandidateType::Host, + } + } +} + /// ICE candidate TCP type (always `Passive`). #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] @@ -108,6 +208,14 @@ pub enum IceCandidateTcpType { Passive, } +impl IceCandidateTcpType { + pub(crate) fn from_fbs(candidate_type: web_rtc_transport::IceCandidateTcpType) -> Self { + match candidate_type { + web_rtc_transport::IceCandidateTcpType::Passive => IceCandidateTcpType::Passive, + } + } +} + /// Transport protocol. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] @@ -118,6 +226,15 @@ pub enum Protocol { Udp, } +impl Protocol { + pub(crate) fn from_fbs(protocol: transport::Protocol) -> Self { + match protocol { + transport::Protocol::Tcp => Protocol::Tcp, + transport::Protocol::Udp => Protocol::Udp, + } + } +} + /// ICE candidate #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -127,8 +244,8 @@ pub struct IceCandidate { pub foundation: String, /// The assigned priority of the candidate. pub priority: u32, - /// The IP address of the candidate. - pub ip: IpAddr, + /// The IP address or hostname of the candidate. + pub address: String, /// The protocol of the candidate. pub protocol: Protocol, /// The port for the candidate. @@ -140,6 +257,20 @@ pub struct IceCandidate { pub tcp_type: Option, } +impl IceCandidate { + pub(crate) fn from_fbs(candidate: &web_rtc_transport::IceCandidate) -> Self { + Self { + foundation: candidate.foundation.clone(), + priority: candidate.priority, + address: candidate.address.clone(), + protocol: Protocol::from_fbs(candidate.protocol), + port: candidate.port, + r#type: IceCandidateType::from_fbs(candidate.type_), + tcp_type: candidate.tcp_type.map(IceCandidateTcpType::from_fbs), + } + } +} + /// ICE state. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -155,8 +286,17 @@ pub enum IceState { /// ICE was `Connected` or `Completed` but it has suddenly failed (this can just happen if the /// selected tuple has `Tcp` protocol). Disconnected, - /// ICE state when the transport has been closed. - Closed, +} + +impl IceState { + pub(crate) fn from_fbs(state: web_rtc_transport::IceState) -> Self { + match state { + web_rtc_transport::IceState::New => IceState::New, + web_rtc_transport::IceState::Connected => IceState::Connected, + web_rtc_transport::IceState::Completed => IceState::Completed, + web_rtc_transport::IceState::Disconnected => IceState::Disconnected, + } + } } /// Tuple of local IP/port/protocol + optional remote IP/port. @@ -167,14 +307,14 @@ pub enum IceState { /// `PipeTransport`, or via dynamic detection as it happens in `WebRtcTransport` (in which the /// remote media address is detected by ICE means), or in `PlainTransport` (when using `comedia` /// mode). -#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)] +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(untagged)] pub enum TransportTuple { /// Transport tuple with remote endpoint info. #[serde(rename_all = "camelCase")] WithRemote { - /// Local IP address. - local_ip: IpAddr, + /// Local IP address or hostname. + local_address: String, /// Local port. local_port: u16, /// Remote IP address. @@ -187,8 +327,8 @@ pub enum TransportTuple { /// Transport tuple without remote endpoint info. #[serde(rename_all = "camelCase")] LocalOnly { - /// Local IP address. - local_ip: IpAddr, + /// Local IP address or hostname. + local_address: String, /// Local port. local_port: u16, /// Protocol @@ -197,10 +337,10 @@ pub enum TransportTuple { } impl TransportTuple { - /// Local IP address. - pub fn local_ip(&self) -> IpAddr { - let (Self::WithRemote { local_ip, .. } | Self::LocalOnly { local_ip, .. }) = self; - *local_ip + /// Local IP address or hostname. + pub fn local_address(&self) -> &String { + let (Self::WithRemote { local_address, .. } | Self::LocalOnly { local_address, .. }) = self; + local_address } /// Local port. @@ -232,6 +372,34 @@ impl TransportTuple { None } } + + pub(crate) fn from_fbs(tuple: &transport::Tuple) -> TransportTuple { + match &tuple.remote_ip { + Some(_remote_ip) => TransportTuple::WithRemote { + local_address: tuple + .local_address + .parse() + .expect("Error parsing local address"), + local_port: tuple.local_port, + remote_ip: tuple + .remote_ip + .as_ref() + .unwrap() + .parse() + .expect("Error parsing remote IP address"), + remote_port: tuple.remote_port, + protocol: Protocol::from_fbs(tuple.protocol), + }, + None => TransportTuple::LocalOnly { + local_address: tuple + .local_address + .parse() + .expect("Error parsing local address"), + local_port: tuple.local_port, + protocol: Protocol::from_fbs(tuple.protocol), + }, + } + } } /// DTLS state. @@ -250,6 +418,18 @@ pub enum DtlsState { Closed, } +impl DtlsState { + pub(crate) fn from_fbs(state: web_rtc_transport::DtlsState) -> Self { + match state { + web_rtc_transport::DtlsState::New => DtlsState::New, + web_rtc_transport::DtlsState::Connecting => DtlsState::Connecting, + web_rtc_transport::DtlsState::Connected => DtlsState::Connected, + web_rtc_transport::DtlsState::Failed => DtlsState::Failed, + web_rtc_transport::DtlsState::Closed => DtlsState::Closed, + } + } +} + /// SCTP state. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -266,6 +446,18 @@ pub enum SctpState { Closed, } +impl SctpState { + pub(crate) fn from_fbs(state: &sctp_association::SctpState) -> Self { + match state { + sctp_association::SctpState::New => Self::New, + sctp_association::SctpState::Connecting => Self::Connecting, + sctp_association::SctpState::Connected => Self::Connected, + sctp_association::SctpState::Failed => Self::Failed, + sctp_association::SctpState::Closed => Self::Closed, + } + } +} + /// DTLS role. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -280,6 +472,24 @@ pub enum DtlsRole { Server, } +impl DtlsRole { + pub(crate) fn to_fbs(self) -> web_rtc_transport::DtlsRole { + match self { + DtlsRole::Auto => web_rtc_transport::DtlsRole::Auto, + DtlsRole::Client => web_rtc_transport::DtlsRole::Client, + DtlsRole::Server => web_rtc_transport::DtlsRole::Server, + } + } + + pub(crate) fn from_fbs(role: web_rtc_transport::DtlsRole) -> Self { + match role { + web_rtc_transport::DtlsRole::Auto => DtlsRole::Auto, + web_rtc_transport::DtlsRole::Client => DtlsRole::Client, + web_rtc_transport::DtlsRole::Server => DtlsRole::Server, + } + } +} + impl Default for DtlsRole { fn default() -> Self { Self::Auto @@ -318,6 +528,84 @@ pub enum DtlsFingerprint { }, } +fn hex_as_bytes(input: &str) -> [u8; N] { + let mut output = [0_u8; N]; + for (i, o) in input.split(':').zip(&mut output.iter_mut()) { + *o = u8::from_str_radix(i, 16).unwrap_or_else(|error| { + panic!("Failed to parse value {i} as series of hex bytes: {error}") + }); + } + + output +} + +impl DtlsFingerprint { + pub(crate) fn to_fbs(self) -> web_rtc_transport::Fingerprint { + match self { + DtlsFingerprint::Sha1 { .. } => web_rtc_transport::Fingerprint { + algorithm: web_rtc_transport::FingerprintAlgorithm::Sha1, + value: self.value_string(), + }, + DtlsFingerprint::Sha224 { .. } => web_rtc_transport::Fingerprint { + algorithm: web_rtc_transport::FingerprintAlgorithm::Sha224, + value: self.value_string(), + }, + DtlsFingerprint::Sha256 { .. } => web_rtc_transport::Fingerprint { + algorithm: web_rtc_transport::FingerprintAlgorithm::Sha256, + value: self.value_string(), + }, + DtlsFingerprint::Sha384 { .. } => web_rtc_transport::Fingerprint { + algorithm: web_rtc_transport::FingerprintAlgorithm::Sha384, + value: self.value_string(), + }, + DtlsFingerprint::Sha512 { .. } => web_rtc_transport::Fingerprint { + algorithm: web_rtc_transport::FingerprintAlgorithm::Sha512, + value: self.value_string(), + }, + } + } + + pub(crate) fn from_fbs(fingerprint: &web_rtc_transport::Fingerprint) -> DtlsFingerprint { + match fingerprint.algorithm { + web_rtc_transport::FingerprintAlgorithm::Sha1 => { + let value_result = hex_as_bytes::<20>(fingerprint.value.as_str()); + + DtlsFingerprint::Sha1 { + value: value_result, + } + } + web_rtc_transport::FingerprintAlgorithm::Sha224 => { + let value_result = hex_as_bytes::<28>(fingerprint.value.as_str()); + + DtlsFingerprint::Sha224 { + value: value_result, + } + } + web_rtc_transport::FingerprintAlgorithm::Sha256 => { + let value_result = hex_as_bytes::<32>(fingerprint.value.as_str()); + + DtlsFingerprint::Sha256 { + value: value_result, + } + } + web_rtc_transport::FingerprintAlgorithm::Sha384 => { + let value_result = hex_as_bytes::<48>(fingerprint.value.as_str()); + + DtlsFingerprint::Sha384 { + value: value_result, + } + } + web_rtc_transport::FingerprintAlgorithm::Sha512 => { + let value_result = hex_as_bytes::<64>(fingerprint.value.as_str()); + + DtlsFingerprint::Sha512 { + value: value_result, + } + } + } + } +} + impl fmt::Debug for DtlsFingerprint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = match self { @@ -755,6 +1043,30 @@ pub struct DtlsParameters { pub fingerprints: Vec, } +impl DtlsParameters { + pub(crate) fn to_fbs(&self) -> web_rtc_transport::DtlsParameters { + web_rtc_transport::DtlsParameters { + role: self.role.to_fbs(), + fingerprints: self + .fingerprints + .iter() + .map(|fingerprint| fingerprint.to_fbs()) + .collect(), + } + } + + pub(crate) fn from_fbs(parameters: web_rtc_transport::DtlsParameters) -> DtlsParameters { + DtlsParameters { + role: DtlsRole::from_fbs(parameters.role), + fingerprints: parameters + .fingerprints + .iter() + .map(DtlsFingerprint::from_fbs) + .collect(), + } + } +} + /// Trace event direction #[derive(Debug, Copy, Clone, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] @@ -765,12 +1077,21 @@ pub enum TraceEventDirection { Out, } +impl TraceEventDirection { + pub(crate) fn from_fbs(event_type: common::TraceDirection) -> Self { + match event_type { + common::TraceDirection::DirectionIn => TraceEventDirection::In, + common::TraceDirection::DirectionOut => TraceEventDirection::Out, + } + } +} + /// Container used for sending/receiving messages using `DirectTransport` data producers and data /// consumers. #[derive(Debug, Clone)] pub enum WebRtcMessage<'a> { /// String - String(String), + String(Cow<'a, [u8]>), /// Binary Binary(Cow<'a, [u8]>), /// EmptyString @@ -793,9 +1114,7 @@ impl<'a> WebRtcMessage<'a> { pub(crate) fn new(ppid: u32, payload: Cow<'a, [u8]>) -> Result { match ppid { - 51 => Ok(WebRtcMessage::String( - String::from_utf8(payload.to_vec()).unwrap(), - )), + 51 => Ok(WebRtcMessage::String(payload)), 53 => Ok(WebRtcMessage::Binary(payload)), 56 => Ok(WebRtcMessage::EmptyString), 57 => Ok(WebRtcMessage::EmptyBinary), @@ -805,12 +1124,36 @@ impl<'a> WebRtcMessage<'a> { pub(crate) fn into_ppid_and_payload(self) -> (u32, Cow<'a, [u8]>) { match self { - WebRtcMessage::String(string) => (51_u32, Cow::from(string.into_bytes())), + WebRtcMessage::String(binary) => (51_u32, binary), WebRtcMessage::Binary(binary) => (53_u32, binary), - WebRtcMessage::EmptyString => (56_u32, Cow::from(b" ".as_ref())), + WebRtcMessage::EmptyString => (56_u32, Cow::from(vec![0_u8])), WebRtcMessage::EmptyBinary => (57_u32, Cow::from(vec![0_u8])), } } + + /// Convert to owned message + pub fn into_owned(self) -> OwnedWebRtcMessage { + match self { + WebRtcMessage::String(binary) => OwnedWebRtcMessage::String(binary.into_owned()), + WebRtcMessage::Binary(binary) => OwnedWebRtcMessage::Binary(binary.into_owned()), + WebRtcMessage::EmptyString => OwnedWebRtcMessage::EmptyString, + WebRtcMessage::EmptyBinary => OwnedWebRtcMessage::EmptyBinary, + } + } +} + +/// Similar to WebRtcMessage but represents +/// messages that have ownership over the data +#[derive(Debug, Clone)] +pub enum OwnedWebRtcMessage { + /// String + String(Vec), + /// Binary + Binary(Vec), + /// EmptyString + EmptyString, + /// EmptyBinary + EmptyBinary, } /// RTP packet info in trace event. @@ -830,9 +1173,9 @@ pub struct RtpPacketTraceInfo { /// Whether packet contains a key frame. pub is_key_frame: bool, /// Packet size. - pub size: usize, + pub size: u64, /// Payload size. - pub payload_size: usize, + pub payload_size: u64, /// The spatial layer index (from 0 to N). pub spatial_layer: u8, /// The temporal layer index (from 0 to N). @@ -850,11 +1193,33 @@ pub struct RtpPacketTraceInfo { pub is_rtx: bool, } +impl RtpPacketTraceInfo { + pub(crate) fn from_fbs(rtp_packet: rtp_packet::Dump, is_rtx: bool) -> Self { + Self { + payload_type: rtp_packet.payload_type, + sequence_number: rtp_packet.sequence_number, + timestamp: rtp_packet.timestamp, + marker: rtp_packet.marker, + ssrc: rtp_packet.ssrc, + is_key_frame: rtp_packet.is_key_frame, + size: rtp_packet.size, + payload_size: rtp_packet.payload_size, + spatial_layer: rtp_packet.spatial_layer, + temporal_layer: rtp_packet.temporal_layer, + mid: rtp_packet.mid, + rid: rtp_packet.rid, + rrid: rtp_packet.rrid, + wide_sequence_number: rtp_packet.wide_sequence_number, + is_rtx, + } + } +} + /// SSRC info in trace event. #[derive(Debug, Copy, Clone, Deserialize, Serialize)] pub struct SsrcTraceInfo { /// RTP stream SSRC. - ssrc: u32, + pub ssrc: u32, } /// Bandwidth estimation type. @@ -868,6 +1233,15 @@ pub enum BweType { Remb, } +impl BweType { + pub(crate) fn from_fbs(info: transport::BweType) -> Self { + match info { + transport::BweType::TransportCc => BweType::TransportCc, + transport::BweType::Remb => BweType::Remb, + } + } +} + /// BWE info in trace event. #[derive(Debug, Copy, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -889,3 +1263,49 @@ pub struct BweTraceInfo { /// Available bitrate. available_bitrate: u32, } + +impl BweTraceInfo { + pub(crate) fn from_fbs(info: transport::BweTraceInfo) -> Self { + Self { + r#type: BweType::from_fbs(info.bwe_type), + desired_bitrate: info.desired_bitrate, + effective_desired_bitrate: info.effective_desired_bitrate, + min_bitrate: info.min_bitrate, + max_bitrate: info.max_bitrate, + start_bitrate: info.start_bitrate, + max_padding_bitrate: info.max_padding_bitrate, + available_bitrate: info.available_bitrate, + } + } +} + +/// RTCP Sender Report info in trace event. +#[derive(Debug, Copy, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SrTraceInfo { + /// Stream SSRC + ssrc: u32, + /// NTP : most significant word + ntp_sec: u32, + /// NTP : least significant word + ntp_frac: u32, + /// RTP timestamp + rtp_ts: u32, + /// Sender packet count + packet_count: u32, + /// Sender octet count + octet_count: u32, +} + +impl SrTraceInfo { + pub(crate) fn from_fbs(info: producer::SrTraceInfo) -> Self { + Self { + ssrc: info.ssrc, + ntp_sec: info.ntp_sec, + ntp_frac: info.ntp_frac, + rtp_ts: info.rtp_ts, + packet_count: info.packet_count, + octet_count: info.octet_count, + } + } +} diff --git a/rust/src/macros.rs b/rust/src/macros.rs index 5fd5d4d128..f331a0ba00 100644 --- a/rust/src/macros.rs +++ b/rust/src/macros.rs @@ -18,7 +18,7 @@ macro_rules! uuid_based_wrapper_type { Eq, PartialEq, )] - pub struct $struct_name(uuid::Uuid); + pub struct $struct_name(::uuid::Uuid); impl std::fmt::Display for $struct_name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -26,7 +26,15 @@ macro_rules! uuid_based_wrapper_type { } } - impl From<$struct_name> for uuid::Uuid { + impl ::std::str::FromStr for $struct_name { + type Err = ::uuid::Error; + + fn from_str(s: &str) -> Result { + ::uuid::Uuid::from_str(s).map(Self) + } + } + + impl From<$struct_name> for ::uuid::Uuid { fn from(id: $struct_name) -> Self { id.0 } @@ -34,7 +42,7 @@ macro_rules! uuid_based_wrapper_type { impl $struct_name { pub(super) fn new() -> Self { - $struct_name(uuid::Uuid::new_v4()) + $struct_name(::uuid::Uuid::new_v4()) } } diff --git a/rust/src/messages.rs b/rust/src/messages.rs index f933c18290..d06fcaaf52 100644 --- a/rust/src/messages.rs +++ b/rust/src/messages.rs @@ -1,226 +1,559 @@ use crate::active_speaker_observer::ActiveSpeakerObserverOptions; use crate::audio_level_observer::AudioLevelObserverOptions; use crate::consumer::{ - ConsumerDump, ConsumerId, ConsumerLayers, ConsumerScore, ConsumerStats, ConsumerTraceEventType, - ConsumerType, + ConsumerId, ConsumerLayers, ConsumerScore, ConsumerTraceEventType, ConsumerType, }; -use crate::data_consumer::{DataConsumerDump, DataConsumerId, DataConsumerStat, DataConsumerType}; -use crate::data_producer::{DataProducerDump, DataProducerId, DataProducerStat, DataProducerType}; +use crate::data_consumer::{DataConsumerId, DataConsumerType}; +use crate::data_producer::{DataProducerId, DataProducerType}; use crate::data_structures::{ - DtlsParameters, DtlsRole, DtlsState, IceCandidate, IceParameters, IceRole, IceState, ListenIp, - SctpState, TransportTuple, + DtlsParameters, DtlsRole, DtlsState, IceCandidate, IceParameters, IceRole, IceState, + ListenInfo, SctpState, TransportTuple, }; use crate::direct_transport::DirectTransportOptions; use crate::ortc::RtpMapping; use crate::pipe_transport::PipeTransportOptions; use crate::plain_transport::PlainTransportOptions; -use crate::producer::{ - ProducerDump, ProducerId, ProducerStat, ProducerTraceEventType, ProducerType, -}; +use crate::producer::{ProducerId, ProducerTraceEventType, ProducerType}; +use crate::router::consumer::ConsumerDump; +use crate::router::producer::ProducerDump; use crate::router::{RouterDump, RouterId}; use crate::rtp_observer::RtpObserverId; use crate::rtp_parameters::{MediaKind, RtpEncodingParameters, RtpParameters}; use crate::sctp_parameters::{NumSctpStreams, SctpParameters, SctpStreamParameters}; use crate::srtp_parameters::{SrtpCryptoSuite, SrtpParameters}; use crate::transport::{TransportId, TransportTraceEventType}; -use crate::webrtc_server::{WebRtcServerDump, WebRtcServerId, WebRtcServerListenInfos}; -use crate::webrtc_transport::{TransportListenIps, WebRtcTransportListen, WebRtcTransportOptions}; -use crate::worker::{WorkerDump, WorkerUpdateSettings}; +use crate::webrtc_server::{ + WebRtcServerDump, WebRtcServerIceUsernameFragment, WebRtcServerId, WebRtcServerIpPort, + WebRtcServerListenInfos, WebRtcServerTupleHash, +}; +use crate::webrtc_transport::{ + WebRtcTransportListen, WebRtcTransportListenInfos, WebRtcTransportOptions, +}; +use crate::worker::{ChannelMessageHandlers, LibUringDump, WorkerDump, WorkerUpdateSettings}; +use mediasoup_sys::fbs::{ + active_speaker_observer, audio_level_observer, consumer, data_consumer, data_producer, + direct_transport, message, notification, pipe_transport, plain_transport, producer, request, + response, router, rtp_observer, transport, web_rtc_server, web_rtc_transport, worker, +}; use parking_lot::Mutex; -use serde::de::DeserializeOwned; +use planus::Builder; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use std::error::Error; use std::fmt::{Debug, Display}; use std::net::IpAddr; use std::num::NonZeroU16; pub(crate) trait Request where - Self: Debug + Serialize, + Self: Debug, { + /// Request method to call on worker. + const METHOD: request::Method; type HandlerId: Display; - type Response: DeserializeOwned; + type Response; - /// Request method to call on worker. - fn as_method(&self) -> &'static str; + /// Get a serialized message out of this request. + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec; /// Default response to return in case of soft error, such as channel already closed, entity /// doesn't exist on worker during closing. fn default_for_soft_error() -> Option { None } + + /// Convert generic response into specific type of this request. + fn convert_response( + response: Option>, + ) -> Result>; } -pub(crate) trait Notification: Debug + Serialize { +pub(crate) trait Notification: Debug { + /// Notification event to call on worker. + const EVENT: notification::Event; type HandlerId: Display; - /// Request event to call on worker. - fn as_event(&self) -> &'static str; -} - -macro_rules! request_response { - ( - $handler_id_type: ty, - $method: literal, - $request_struct_name: ident { $( $(#[$request_field_name_attributes: meta])? $request_field_name: ident: $request_field_type: ty$(,)? )* }, - $existing_response_type: ty, - $default_for_soft_error: expr $(,)? - ) => { - #[derive(Debug, Serialize)] - #[serde(rename_all = "camelCase")] - pub(crate) struct $request_struct_name { - $( - $(#[$request_field_name_attributes])* - pub(crate) $request_field_name: $request_field_type, - )* - } + /// Get a serialized message out of this notification. + fn into_bytes(self, handler_id: Self::HandlerId) -> Vec; +} - impl Request for $request_struct_name { - type HandlerId = $handler_id_type; - type Response = $existing_response_type; +#[derive(Debug)] +pub(crate) struct WorkerCloseRequest {} - fn as_method(&self) -> &'static str { - $method - } +impl Request for WorkerCloseRequest { + const METHOD: request::Method = request::Method::WorkerClose; + type HandlerId = &'static str; + type Response = (); - fn default_for_soft_error() -> Option { - $default_for_soft_error - } - } - }; - // Call above macro with no default for soft error - ( - $handler_id_type: ty, - $method: literal, - $request_struct_name: ident $request_struct_impl: tt $(,)? - $existing_response_type: ty $(,)? - ) => { - request_response!( - $handler_id_type, - $method, - $request_struct_name $request_struct_impl, - $existing_response_type, - None, - ); - }; - // Call above macro with unit type as expected response - ( - $handler_id_type: ty, - $method: literal, - $request_struct_name: ident $request_struct_impl: tt $(,)? - ) => { - request_response!( - $handler_id_type, - $method, - $request_struct_name $request_struct_impl, - (), - None, - ); - }; - ( - $handler_id_type: ty, - $method: literal, - $request_struct_name: ident { $( $(#[$request_field_name_attributes: meta])? $request_field_name: ident: $request_field_type: ty$(,)? )* }, - $response_struct_name: ident { $( $response_field_name: ident: $response_field_type: ty$(,)? )* }, - ) => { - #[derive(Debug, Serialize)] - #[serde(rename_all = "camelCase")] - pub(crate) struct $request_struct_name { - $( - $(#[$request_field_name_attributes])* - pub(crate) $request_field_name: $request_field_type, - )* - } + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); - #[derive(Debug, Deserialize)] - #[serde(rename_all = "camelCase")] - pub(crate) struct $response_struct_name { - $( pub(crate) $response_field_name: $response_field_type, )* - } + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); - impl Request for $request_struct_name { - type HandlerId = $handler_id_type; - type Response = $response_struct_name; + builder.finish(message, None).to_vec() + } - fn as_method(&self) -> &'static str { - $method - } - } - }; + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } } -request_response!(&'static str, "worker.close", WorkerCloseRequest {}); +#[derive(Debug)] +pub(crate) struct WorkerDumpRequest {} -request_response!( - &'static str, - "worker.dump", - WorkerDumpRequest {}, - WorkerDump -); +impl Request for WorkerDumpRequest { + const METHOD: request::Method = request::Method::WorkerDump; + type HandlerId = &'static str; + type Response = WorkerDump; -request_response!( - &'static str, - "worker.updateSettings", - WorkerUpdateSettingsRequest { - #[serde(flatten)] - data: WorkerUpdateSettings, - }, -); + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); -request_response!( - &'static str, - "worker.createWebRtcServer", - WorkerCreateWebRtcServerRequest { - #[serde(rename = "webRtcServerId")] - webrtc_server_id: WebRtcServerId, - listen_infos: WebRtcServerListenInfos, - }, -); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); -request_response!( - &'static str, - "worker.closeWebRtcServer", - WebRtcServerCloseRequest { - #[serde(rename = "webRtcServerId")] - webrtc_server_id: WebRtcServerId, - }, - (), - Some(()), -); - -request_response!( - WebRtcServerId, - "webRtcServer.dump", - WebRtcServerDumpRequest {}, - WebRtcServerDump, -); - -request_response!( - &'static str, - "worker.createRouter", - WorkerCreateRouterRequest { - router_id: RouterId, - }, -); + builder.finish(message, None).to_vec() + } -request_response!( - &'static str, - "worker.closeRouter", - RouterCloseRequest { - router_id: RouterId, - }, - (), - Some(()), -); + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::WorkerDumpResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = worker::DumpResponse::try_from(data)?; + + Ok(WorkerDump { + router_ids: data + .router_ids + .into_iter() + .map(|id| id.parse()) + .collect::>()?, + webrtc_server_ids: data + .web_rtc_server_ids + .into_iter() + .map(|id| id.parse()) + .collect::>()?, + channel_message_handlers: ChannelMessageHandlers { + channel_request_handlers: data + .channel_message_handlers + .channel_request_handlers + .into_iter() + .map(|id| id.parse()) + .collect::>()?, + channel_notification_handlers: data + .channel_message_handlers + .channel_notification_handlers + .into_iter() + .map(|id| id.parse()) + .collect::>()?, + }, + liburing: data.liburing.map(|liburing| LibUringDump { + sqe_process_count: liburing.sqe_process_count, + sqe_miss_count: liburing.sqe_miss_count, + user_data_miss_count: liburing.user_data_miss_count, + }), + }) + } +} + +#[derive(Debug)] +pub(crate) struct WorkerUpdateSettingsRequest { + pub(crate) data: WorkerUpdateSettings, +} + +impl Request for WorkerUpdateSettingsRequest { + const METHOD: request::Method = request::Method::WorkerUpdateSettings; + type HandlerId = &'static str; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = worker::UpdateSettingsRequest::create( + &mut builder, + self.data + .log_level + .as_ref() + .map(|log_level| log_level.as_str()), + self.data.log_tags.as_ref().map(|log_tags| { + log_tags + .iter() + .map(|log_tag| log_tag.as_str()) + .collect::>() + }), + ); + let request_body = request::Body::create_worker_update_settings_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct WorkerCreateWebRtcServerRequest { + pub(crate) webrtc_server_id: WebRtcServerId, + pub(crate) listen_infos: WebRtcServerListenInfos, +} + +impl Request for WorkerCreateWebRtcServerRequest { + const METHOD: request::Method = request::Method::WorkerCreateWebrtcserver; + type HandlerId = &'static str; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = worker::CreateWebRtcServerRequest::create( + &mut builder, + self.webrtc_server_id.to_string(), + self.listen_infos.to_fbs(), + ); + let request_body = + request::Body::create_worker_create_web_rtc_server_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct WebRtcServerCloseRequest { + pub(crate) webrtc_server_id: WebRtcServerId, +} + +impl Request for WebRtcServerCloseRequest { + const METHOD: request::Method = request::Method::WorkerWebrtcserverClose; + type HandlerId = &'static str; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = worker::CloseWebRtcServerRequest::create( + &mut builder, + self.webrtc_server_id.to_string(), + ); + let request_body = + request::Body::create_worker_close_web_rtc_server_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct WebRtcServerDumpRequest {} + +impl Request for WebRtcServerDumpRequest { + const METHOD: request::Method = request::Method::WebrtcserverDump; + type HandlerId = WebRtcServerId; + type Response = WebRtcServerDump; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::WebRtcServerDumpResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = web_rtc_server::DumpResponse::try_from(data)?; + + Ok(WebRtcServerDump { + id: data.id.parse()?, + udp_sockets: data + .udp_sockets + .into_iter() + .map(|ip_port| WebRtcServerIpPort { + ip: ip_port.ip.parse().unwrap(), + port: ip_port.port, + }) + .collect(), + tcp_servers: data + .tcp_servers + .into_iter() + .map(|ip_port| WebRtcServerIpPort { + ip: ip_port.ip.parse().unwrap(), + port: ip_port.port, + }) + .collect(), + webrtc_transport_ids: data + .web_rtc_transport_ids + .into_iter() + .map(|id| id.parse()) + .collect::>()?, + local_ice_username_fragments: data + .local_ice_username_fragments + .into_iter() + .map(|username_fragment| WebRtcServerIceUsernameFragment { + local_ice_username_fragment: username_fragment + .local_ice_username_fragment + .parse() + .unwrap(), + webrtc_transport_id: username_fragment.web_rtc_transport_id.parse().unwrap(), + }) + .collect(), + tuple_hashes: data + .tuple_hashes + .into_iter() + .map(|tuple_hash| WebRtcServerTupleHash { + tuple_hash: tuple_hash.tuple_hash, + webrtc_transport_id: tuple_hash.web_rtc_transport_id.parse().unwrap(), + }) + .collect(), + }) + } +} + +#[derive(Debug)] +pub(crate) struct WorkerCreateRouterRequest { + pub(crate) router_id: RouterId, +} + +impl Request for WorkerCreateRouterRequest { + const METHOD: request::Method = request::Method::WorkerCreateRouter; + type HandlerId = &'static str; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = worker::CreateRouterRequest::create(&mut builder, self.router_id.to_string()); + let request_body = request::Body::create_worker_create_router_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct RouterCloseRequest { + pub(crate) router_id: RouterId, +} + +impl Request for RouterCloseRequest { + const METHOD: request::Method = request::Method::WorkerCloseRouter; + type HandlerId = &'static str; + type Response = (); -request_response!(RouterId, "router.dump", RouterDumpRequest {}, RouterDump); + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = worker::CloseRouterRequest::create(&mut builder, self.router_id.to_string()); + let request_body = request::Body::create_worker_close_router_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct RouterDumpRequest {} + +impl Request for RouterDumpRequest { + const METHOD: request::Method = request::Method::RouterDump; + type HandlerId = RouterId; + type Response = RouterDump; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::RouterDumpResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = router::DumpResponse::try_from(data)?; + + Ok(RouterDump { + id: data.id.parse()?, + map_consumer_id_producer_id: data + .map_consumer_id_producer_id + .into_iter() + .map(|key_value| Ok((key_value.key.parse()?, key_value.value.parse()?))) + .collect::>>()?, + map_data_consumer_id_data_producer_id: data + .map_data_consumer_id_data_producer_id + .into_iter() + .map(|key_value| Ok((key_value.key.parse()?, key_value.value.parse()?))) + .collect::>>()?, + map_data_producer_id_data_consumer_ids: data + .map_data_producer_id_data_consumer_ids + .into_iter() + .map(|key_values| { + Ok(( + key_values.key.parse()?, + key_values + .values + .into_iter() + .map(|value| value.parse()) + .collect::>()?, + )) + }) + .collect::>>()?, + map_producer_id_consumer_ids: data + .map_producer_id_consumer_ids + .into_iter() + .map(|key_values| { + Ok(( + key_values.key.parse()?, + key_values + .values + .into_iter() + .map(|value| value.parse()) + .collect::>()?, + )) + }) + .collect::>>()?, + map_producer_id_observer_ids: data + .map_producer_id_observer_ids + .into_iter() + .map(|key_values| { + Ok(( + key_values.key.parse()?, + key_values + .values + .into_iter() + .map(|value| value.parse()) + .collect::>()?, + )) + }) + .collect::>>()?, + rtp_observer_ids: data + .rtp_observer_ids + .into_iter() + .map(|id| id.parse()) + .collect::>()?, + transport_ids: data + .transport_ids + .into_iter() + .map(|id| id.parse()) + .collect::>()?, + }) + } +} #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct RouterCreateDirectTransportData { transport_id: TransportId, direct: bool, - max_message_size: usize, + max_message_size: u32, } impl RouterCreateDirectTransportData { @@ -234,26 +567,68 @@ impl RouterCreateDirectTransportData { max_message_size: direct_transport_options.max_message_size, } } + + pub(crate) fn to_fbs(&self) -> direct_transport::DirectTransportOptions { + direct_transport::DirectTransportOptions { + base: Box::new(transport::Options { + direct: true, + max_message_size: Some(self.max_message_size), + initial_available_outgoing_bitrate: None, + enable_sctp: false, + num_sctp_streams: None, + max_sctp_message_size: 0, + sctp_send_buffer_size: 0, + is_data_channel: false, + }), + } + } } -request_response!( - RouterId, - "router.createDirectTransport", - RouterCreateDirectTransportRequest { - #[serde(flatten)] - data: RouterCreateDirectTransportData, - }, - RouterCreateDirectTransportResponse {}, -); +#[derive(Debug)] +pub(crate) struct RouterCreateDirectTransportRequest { + pub(crate) data: RouterCreateDirectTransportData, +} + +impl Request for RouterCreateDirectTransportRequest { + const METHOD: request::Method = request::Method::RouterCreateDirecttransport; + type HandlerId = RouterId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = router::CreateDirectTransportRequest::create( + &mut builder, + self.data.transport_id.to_string(), + self.data.to_fbs(), + ); + let request_body = + request::Body::create_router_create_direct_transport_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} #[derive(Debug, Serialize)] #[serde(untagged)] enum RouterCreateWebrtcTransportListen { #[serde(rename_all = "camelCase")] Individual { - listen_ips: TransportListenIps, - #[serde(skip_serializing_if = "Option::is_none")] - port: Option, + listen_infos: WebRtcTransportListenInfos, }, Server { #[serde(rename = "webRtcServerId")] @@ -261,17 +636,40 @@ enum RouterCreateWebrtcTransportListen { }, } +impl RouterCreateWebrtcTransportListen { + pub(crate) fn to_fbs(&self) -> web_rtc_transport::Listen { + match self { + RouterCreateWebrtcTransportListen::Individual { listen_infos } => { + web_rtc_transport::Listen::ListenIndividual(Box::new( + web_rtc_transport::ListenIndividual { + listen_infos: listen_infos + .iter() + .map(|listen_info| listen_info.to_fbs()) + .collect(), + }, + )) + } + RouterCreateWebrtcTransportListen::Server { webrtc_server_id } => { + web_rtc_transport::Listen::ListenServer(Box::new(web_rtc_transport::ListenServer { + web_rtc_server_id: webrtc_server_id.to_string(), + })) + } + } + } +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] -pub(crate) struct RouterCreateWebrtcTransportRequest { +pub(crate) struct RouterCreateWebrtcTransportData { transport_id: TransportId, #[serde(flatten)] listen: RouterCreateWebrtcTransportListen, + initial_available_outgoing_bitrate: u32, enable_udp: bool, enable_tcp: bool, prefer_udp: bool, prefer_tcp: bool, - initial_available_outgoing_bitrate: u32, + ice_consent_timeout: u8, enable_sctp: bool, num_sctp_streams: NumSctpStreams, max_sctp_message_size: u32, @@ -279,7 +677,7 @@ pub(crate) struct RouterCreateWebrtcTransportRequest { is_data_channel: bool, } -impl RouterCreateWebrtcTransportRequest { +impl RouterCreateWebrtcTransportData { pub(crate) fn from_options( transport_id: TransportId, webrtc_transport_options: &WebRtcTransportOptions, @@ -287,10 +685,9 @@ impl RouterCreateWebrtcTransportRequest { Self { transport_id, listen: match &webrtc_transport_options.listen { - WebRtcTransportListen::Individual { listen_ips, port } => { + WebRtcTransportListen::Individual { listen_infos } => { RouterCreateWebrtcTransportListen::Individual { - listen_ips: listen_ips.clone(), - port: *port, + listen_infos: listen_infos.clone(), } } WebRtcTransportListen::Server { webrtc_server } => { @@ -299,12 +696,13 @@ impl RouterCreateWebrtcTransportRequest { } } }, + initial_available_outgoing_bitrate: webrtc_transport_options + .initial_available_outgoing_bitrate, enable_udp: webrtc_transport_options.enable_udp, enable_tcp: webrtc_transport_options.enable_tcp, prefer_udp: webrtc_transport_options.prefer_udp, prefer_tcp: webrtc_transport_options.prefer_tcp, - initial_available_outgoing_bitrate: webrtc_transport_options - .initial_available_outgoing_bitrate, + ice_consent_timeout: webrtc_transport_options.ice_consent_timeout, enable_sctp: webrtc_transport_options.enable_sctp, num_sctp_streams: webrtc_transport_options.num_sctp_streams, max_sctp_message_size: webrtc_transport_options.max_sctp_message_size, @@ -312,6 +710,27 @@ impl RouterCreateWebrtcTransportRequest { is_data_channel: true, } } + + pub(crate) fn to_fbs(&self) -> web_rtc_transport::WebRtcTransportOptions { + web_rtc_transport::WebRtcTransportOptions { + base: Box::new(transport::Options { + direct: false, + max_message_size: None, + initial_available_outgoing_bitrate: Some(self.initial_available_outgoing_bitrate), + enable_sctp: self.enable_sctp, + num_sctp_streams: Some(Box::new(self.num_sctp_streams.to_fbs())), + max_sctp_message_size: self.max_sctp_message_size, + sctp_send_buffer_size: self.sctp_send_buffer_size, + is_data_channel: true, + }), + listen: self.listen.to_fbs(), + enable_udp: self.enable_udp, + enable_tcp: self.enable_tcp, + prefer_udp: self.prefer_udp, + prefer_tcp: self.prefer_tcp, + ice_consent_timeout: self.ice_consent_timeout, + } + } } #[derive(Debug, Deserialize)] @@ -329,17 +748,155 @@ pub(crate) struct WebRtcTransportData { pub(crate) sctp_state: Mutex>, } -impl Request for RouterCreateWebrtcTransportRequest { +#[derive(Debug)] +pub(crate) struct RouterCreateWebRtcTransportRequest { + pub(crate) data: RouterCreateWebrtcTransportData, +} + +impl Request for RouterCreateWebRtcTransportRequest { + const METHOD: request::Method = request::Method::RouterCreateWebrtctransport; type HandlerId = RouterId; type Response = WebRtcTransportData; - fn as_method(&self) -> &'static str { - match &self.listen { - RouterCreateWebrtcTransportListen::Individual { .. } => "router.createWebRtcTransport", - RouterCreateWebrtcTransportListen::Server { .. } => { - "router.createWebRtcTransportWithServer" - } - } + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let RouterCreateWebrtcTransportListen::Individual { listen_infos: _ } = self.data.listen + else { + panic!("RouterCreateWebrtcTransportListen variant must be Individual"); + }; + + let mut builder = Builder::new(); + let data = router::CreateWebRtcTransportRequest::create( + &mut builder, + self.data.transport_id.to_string(), + self.data.to_fbs(), + ); + let request_body = + request::Body::create_router_create_web_rtc_transport_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::WebRtcTransportDumpResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = web_rtc_transport::DumpResponse::try_from(data)?; + + Ok(WebRtcTransportData { + ice_role: IceRole::from_fbs(data.ice_role), + ice_parameters: IceParameters::from_fbs(*data.ice_parameters), + ice_candidates: data + .ice_candidates + .iter() + .map(IceCandidate::from_fbs) + .collect(), + ice_state: Mutex::new(IceState::from_fbs(data.ice_state)), + ice_selected_tuple: Mutex::new( + data.ice_selected_tuple + .map(|tuple| TransportTuple::from_fbs(tuple.as_ref())), + ), + dtls_parameters: Mutex::new(DtlsParameters::from_fbs(*data.dtls_parameters)), + dtls_state: Mutex::new(DtlsState::from_fbs(data.dtls_state)), + dtls_remote_cert: Mutex::new(None), + sctp_parameters: data + .base + .sctp_parameters + .map(|parameters| SctpParameters::from_fbs(parameters.as_ref())), + sctp_state: Mutex::new( + data.base + .sctp_state + .map(|state| SctpState::from_fbs(&state)), + ), + }) + } +} + +#[derive(Debug)] +pub(crate) struct RouterCreateWebRtcTransportWithServerRequest { + pub(crate) data: RouterCreateWebrtcTransportData, +} + +impl Request for RouterCreateWebRtcTransportWithServerRequest { + const METHOD: request::Method = request::Method::RouterCreateWebrtctransportWithServer; + type HandlerId = RouterId; + type Response = WebRtcTransportData; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let RouterCreateWebrtcTransportListen::Server { + webrtc_server_id: _, + } = self.data.listen + else { + panic!("RouterCreateWebrtcTransportListen variant must be Server"); + }; + + let mut builder = Builder::new(); + let data = router::CreateWebRtcTransportRequest::create( + &mut builder, + self.data.transport_id.to_string(), + self.data.to_fbs(), + ); + let request_body = + request::Body::create_router_create_web_rtc_transport_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::WebRtcTransportDumpResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = web_rtc_transport::DumpResponse::try_from(data)?; + + Ok(WebRtcTransportData { + ice_role: IceRole::from_fbs(data.ice_role), + ice_parameters: IceParameters::from_fbs(*data.ice_parameters), + ice_candidates: data + .ice_candidates + .iter() + .map(IceCandidate::from_fbs) + .collect(), + ice_state: Mutex::new(IceState::from_fbs(data.ice_state)), + ice_selected_tuple: Mutex::new( + data.ice_selected_tuple + .map(|tuple| TransportTuple::from_fbs(tuple.as_ref())), + ), + dtls_parameters: Mutex::new(DtlsParameters::from_fbs(*data.dtls_parameters)), + dtls_state: Mutex::new(DtlsState::from_fbs(data.dtls_state)), + dtls_remote_cert: Mutex::new(None), + sctp_parameters: data + .base + .sctp_parameters + .map(|parameters| SctpParameters::from_fbs(parameters.as_ref())), + sctp_state: Mutex::new( + data.base + .sctp_state + .map(|state| SctpState::from_fbs(&state)), + ), + }) } } @@ -347,9 +904,8 @@ impl Request for RouterCreateWebrtcTransportRequest { #[serde(rename_all = "camelCase")] pub(crate) struct RouterCreatePlainTransportData { transport_id: TransportId, - listen_ip: ListenIp, - #[serde(skip_serializing_if = "Option::is_none")] - port: Option, + listen_info: ListenInfo, + rtcp_listen_info: Option, rtcp_mux: bool, comedia: bool, enable_sctp: bool, @@ -368,8 +924,8 @@ impl RouterCreatePlainTransportData { ) -> Self { Self { transport_id, - listen_ip: plain_transport_options.listen_ip, - port: plain_transport_options.port, + listen_info: plain_transport_options.listen_info.clone(), + rtcp_listen_info: plain_transport_options.rtcp_listen_info.clone(), rtcp_mux: plain_transport_options.rtcp_mux, comedia: plain_transport_options.comedia, enable_sctp: plain_transport_options.enable_sctp, @@ -381,34 +937,112 @@ impl RouterCreatePlainTransportData { is_data_channel: false, } } + + pub(crate) fn to_fbs(&self) -> plain_transport::PlainTransportOptions { + plain_transport::PlainTransportOptions { + base: Box::new(transport::Options { + direct: false, + max_message_size: None, + initial_available_outgoing_bitrate: None, + enable_sctp: self.enable_sctp, + num_sctp_streams: Some(Box::new(self.num_sctp_streams.to_fbs())), + max_sctp_message_size: self.max_sctp_message_size, + sctp_send_buffer_size: self.sctp_send_buffer_size, + is_data_channel: self.is_data_channel, + }), + listen_info: Box::new(self.listen_info.clone().to_fbs()), + rtcp_listen_info: self + .rtcp_listen_info + .clone() + .map(|listen_info| Box::new(listen_info.to_fbs())), + rtcp_mux: self.rtcp_mux, + comedia: self.comedia, + enable_srtp: self.enable_srtp, + srtp_crypto_suite: Some(SrtpCryptoSuite::to_fbs(self.srtp_crypto_suite)), + } + } } -request_response!( - RouterId, - "router.createPlainTransport", - RouterCreatePlainTransportRequest { - #[serde(flatten)] - data: RouterCreatePlainTransportData, - }, - PlainTransportData { - // The following fields are present, but unused - // rtcp_mux: bool, - // comedia: bool, - tuple: Mutex, - rtcp_tuple: Mutex>, - sctp_parameters: Option, - sctp_state: Mutex>, - srtp_parameters: Mutex>, - }, -); +#[derive(Debug)] +pub(crate) struct RouterCreatePlainTransportRequest { + pub(crate) data: RouterCreatePlainTransportData, +} + +impl Request for RouterCreatePlainTransportRequest { + const METHOD: request::Method = request::Method::RouterCreatePlaintransport; + type HandlerId = RouterId; + type Response = PlainTransportData; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = router::CreatePlainTransportRequest::create( + &mut builder, + self.data.transport_id.to_string(), + self.data.to_fbs(), + ); + let request_body = + request::Body::create_router_create_plain_transport_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::PlainTransportDumpResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = plain_transport::DumpResponse::try_from(data)?; + + Ok(PlainTransportData { + tuple: Mutex::new(TransportTuple::from_fbs(data.tuple.as_ref())), + rtcp_tuple: Mutex::new( + data.rtcp_tuple + .map(|tuple| TransportTuple::from_fbs(tuple.as_ref())), + ), + sctp_parameters: data + .base + .sctp_parameters + .map(|parameters| SctpParameters::from_fbs(parameters.as_ref())), + sctp_state: Mutex::new( + data.base + .sctp_state + .map(|state| SctpState::from_fbs(&state)), + ), + srtp_parameters: Mutex::new( + data.srtp_parameters + .map(|parameters| SrtpParameters::from_fbs(parameters.as_ref())), + ), + }) + } +} + +pub(crate) struct PlainTransportData { + // The following fields are present, but unused + // rtcp_mux: bool, + // comedia: bool, + pub(crate) tuple: Mutex, + pub(crate) rtcp_tuple: Mutex>, + pub(crate) sctp_parameters: Option, + pub(crate) sctp_state: Mutex>, + pub(crate) srtp_parameters: Mutex>, +} #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct RouterCreatePipeTransportData { transport_id: TransportId, - listen_ip: ListenIp, - #[serde(skip_serializing_if = "Option::is_none")] - port: Option, + listen_info: ListenInfo, enable_sctp: bool, num_sctp_streams: NumSctpStreams, max_sctp_message_size: u32, @@ -425,8 +1059,7 @@ impl RouterCreatePipeTransportData { ) -> Self { Self { transport_id, - listen_ip: pipe_transport_options.listen_ip, - port: pipe_transport_options.port, + listen_info: pipe_transport_options.listen_info.clone(), enable_sctp: pipe_transport_options.enable_sctp, num_sctp_streams: pipe_transport_options.num_sctp_streams, max_sctp_message_size: pipe_transport_options.max_sctp_message_size, @@ -436,23 +1069,100 @@ impl RouterCreatePipeTransportData { is_data_channel: false, } } + + pub(crate) fn to_fbs(&self) -> pipe_transport::PipeTransportOptions { + pipe_transport::PipeTransportOptions { + base: Box::new(transport::Options { + direct: false, + max_message_size: None, + initial_available_outgoing_bitrate: None, + enable_sctp: self.enable_sctp, + num_sctp_streams: Some(Box::new(self.num_sctp_streams.to_fbs())), + max_sctp_message_size: self.max_sctp_message_size, + sctp_send_buffer_size: self.sctp_send_buffer_size, + is_data_channel: self.is_data_channel, + }), + listen_info: Box::new(self.listen_info.clone().to_fbs()), + enable_rtx: self.enable_rtx, + enable_srtp: self.enable_srtp, + } + } } -request_response!( - RouterId, - "router.createPipeTransport", - RouterCreatePipeTransportRequest { - #[serde(flatten)] - data: RouterCreatePipeTransportData, - }, - PipeTransportData { - tuple: Mutex, - sctp_parameters: Option, - sctp_state: Mutex>, - rtx: bool, - srtp_parameters: Mutex>, - }, -); +#[derive(Debug)] +pub(crate) struct RouterCreatePipeTransportRequest { + pub(crate) data: RouterCreatePipeTransportData, +} + +impl Request for RouterCreatePipeTransportRequest { + const METHOD: request::Method = request::Method::RouterCreatePipetransport; + type HandlerId = RouterId; + type Response = PipeTransportData; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = router::CreatePipeTransportRequest::create( + &mut builder, + self.data.transport_id.to_string(), + self.data.to_fbs(), + ); + let request_body = + request::Body::create_router_create_pipe_transport_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::PipeTransportDumpResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = pipe_transport::DumpResponse::try_from(data)?; + + Ok(PipeTransportData { + tuple: Mutex::new(TransportTuple::from_fbs(data.tuple.as_ref())), + sctp_parameters: data + .base + .sctp_parameters + .map(|parameters| SctpParameters::from_fbs(parameters.as_ref())), + sctp_state: Mutex::new( + data.base + .sctp_state + .map(|state| SctpState::from_fbs(&state)), + ), + rtx: data.rtx, + srtp_parameters: Mutex::new( + data.srtp_parameters + .map(|parameters| SrtpParameters::from_fbs(parameters.as_ref())), + ), + }) + } +} + +pub(crate) struct PipeTransportData { + pub(crate) tuple: Mutex, + pub(crate) sctp_parameters: Option, + pub(crate) sctp_state: Mutex>, + pub(crate) rtx: bool, + pub(crate) srtp_parameters: Mutex>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RouterCreateAudioLevelObserverRequest { + pub(crate) data: RouterCreateAudioLevelObserverData, +} #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -477,14 +1187,53 @@ impl RouterCreateAudioLevelObserverData { } } -request_response!( - RouterId, - "router.createAudioLevelObserver", - RouterCreateAudioLevelObserverRequest { - #[serde(flatten)] - data: RouterCreateAudioLevelObserverData, - }, -); +impl Request for RouterCreateAudioLevelObserverRequest { + const METHOD: request::Method = request::Method::RouterCreateAudiolevelobserver; + type HandlerId = RouterId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let options = audio_level_observer::AudioLevelObserverOptions::create( + &mut builder, + u16::from(self.data.max_entries), + self.data.threshold, + self.data.interval, + ); + let data = router::CreateAudioLevelObserverRequest::create( + &mut builder, + self.data.rtp_observer_id.to_string(), + options, + ); + let request_body = + request::Body::create_router_create_audio_level_observer_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RouterCreateActiveSpeakerObserverRequest { + pub(crate) data: RouterCreateActiveSpeakerObserverData, +} #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -505,405 +1254,1957 @@ impl RouterCreateActiveSpeakerObserverData { } } -request_response!( - RouterId, - "router.createActiveSpeakerObserver", - RouterCreateActiveSpeakerObserverRequest { - #[serde(flatten)] - data: RouterCreateActiveSpeakerObserverData, - }, -); - -request_response!( - RouterId, - "router.closeTransport", - TransportCloseRequest { - transport_id: TransportId, - }, - (), - Some(()), -); - -request_response!( - TransportId, - "transport.dump", - TransportDumpRequest {}, - Value, -); - -request_response!( - TransportId, - "transport.getStats", - TransportGetStatsRequest {}, - Value, -); - -request_response!( - TransportId, - "transport.connect", - TransportConnectWebRtcRequest { - dtls_parameters: DtlsParameters, - }, - TransportConnectResponseWebRtc { - dtls_local_role: DtlsRole, - }, -); - -request_response!( - TransportId, - "transport.connect", - TransportConnectPipeRequest { - ip: IpAddr, - port: u16, - #[serde(skip_serializing_if = "Option::is_none")] - srtp_parameters: Option, - }, - TransportConnectResponsePipe { - tuple: TransportTuple, - }, -); - -request_response!( - TransportId, - "transport.connect", - TransportConnectPlainRequest { - #[serde(skip_serializing_if = "Option::is_none")] - ip: Option, - #[serde(skip_serializing_if = "Option::is_none")] - port: Option, - #[serde(skip_serializing_if = "Option::is_none")] - rtcp_port: Option, - #[serde(skip_serializing_if = "Option::is_none")] - srtp_parameters: Option, - }, - TransportConnectResponsePlain { - tuple: Option, - rtcp_tuple: Option, - srtp_parameters: Option, - }, -); - -request_response!( - TransportId, - "transport.setMaxIncomingBitrate", - TransportSetMaxIncomingBitrateRequest { bitrate: u32 }, -); - -request_response!( - TransportId, - "transport.setMaxOutgoingBitrate", - TransportSetMaxOutgoingBitrateRequest { bitrate: u32 }, -); - -request_response!( - TransportId, - "transport.restartIce", - TransportRestartIceRequest {}, - TransportRestartIceResponse { - ice_parameters: IceParameters, - }, -); - -request_response!( - TransportId, - "transport.produce", - TransportProduceRequest { - producer_id: ProducerId, - kind: MediaKind, - rtp_parameters: RtpParameters, - rtp_mapping: RtpMapping, - key_frame_request_delay: u32, - paused: bool, - }, - TransportProduceResponse { - r#type: ProducerType, - }, -); - -request_response!( - TransportId, - "transport.consume", - TransportConsumeRequest { - consumer_id: ConsumerId, - producer_id: ProducerId, - kind: MediaKind, - rtp_parameters: RtpParameters, - r#type: ConsumerType, - consumable_rtp_encodings: Vec, - paused: bool, - preferred_layers: Option, - ignore_dtx: bool, - }, - TransportConsumeResponse { - paused: bool, - producer_paused: bool, - score: ConsumerScore, - preferred_layers: Option, - }, -); - -request_response!( - TransportId, - "transport.produceData", - TransportProduceDataRequest { - data_producer_id: DataProducerId, - r#type: DataProducerType, - #[serde(skip_serializing_if = "Option::is_none")] - sctp_stream_parameters: Option, - label: String, - protocol: String, - }, - TransportProduceDataResponse { - r#type: DataProducerType, - sctp_stream_parameters: Option, - label: String, - protocol: String, - }, -); - -request_response!( - TransportId, - "transport.consumeData", - TransportConsumeDataRequest { - data_consumer_id: DataConsumerId, - data_producer_id: DataProducerId, - r#type: DataConsumerType, - #[serde(skip_serializing_if = "Option::is_none")] - sctp_stream_parameters: Option, - label: String, - protocol: String, - }, - TransportConsumeDataResponse { - r#type: DataConsumerType, - sctp_stream_parameters: Option, - label: String, - protocol: String, - }, -); +impl Request for RouterCreateActiveSpeakerObserverRequest { + const METHOD: request::Method = request::Method::RouterCreateActivespeakerobserver; + type HandlerId = RouterId; + type Response = (); -request_response!( - TransportId, - "transport.enableTraceEvent", - TransportEnableTraceEventRequest { - types: Vec, - }, -); + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct TransportSendRtcpNotification {} + let options = active_speaker_observer::ActiveSpeakerObserverOptions::create( + &mut builder, + self.data.interval, + ); + let data = router::CreateActiveSpeakerObserverRequest::create( + &mut builder, + self.data.rtp_observer_id.to_string(), + options, + ); + let request_body = + request::Body::create_router_create_active_speaker_observer_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); -impl Notification for TransportSendRtcpNotification { - type HandlerId = TransportId; + builder.finish(message, None).to_vec() + } - fn as_event(&self) -> &'static str { - "transport.sendRtcp" + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) } } -request_response!( - TransportId, - "transport.closeProducer", - ProducerCloseRequest { - producer_id: ProducerId, - }, - (), - Some(()), -); - -request_response!( - ProducerId, - "producer.dump", - ProducerDumpRequest {}, - ProducerDump -); - -request_response!( - ProducerId, - "producer.getStats", - ProducerGetStatsRequest {}, - Vec, -); - -request_response!(ProducerId, "producer.pause", ProducerPauseRequest {}); - -request_response!(ProducerId, "producer.resume", ProducerResumeRequest {}); - -request_response!( - ProducerId, - "producer.enableTraceEvent", - ProducerEnableTraceEventRequest { - types: Vec, - }, -); +#[derive(Debug)] +pub(crate) struct TransportDumpRequest {} + +impl Request for TransportDumpRequest { + const METHOD: request::Method = request::Method::TransportDump; + type HandlerId = TransportId; + type Response = response::Body; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + match response { + Some(data) => Ok(data.try_into().unwrap()), + _ => { + panic!("Wrong message from worker: {response:?}"); + } + } + } +} +#[derive(Debug)] +pub(crate) struct TransportGetStatsRequest {} + +impl Request for TransportGetStatsRequest { + const METHOD: request::Method = request::Method::TransportGetStats; + type HandlerId = TransportId; + type Response = response::Body; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + match response { + Some(data) => Ok(data.try_into().unwrap()), + _ => { + panic!("Wrong message from worker: {response:?}"); + } + } + } +} + +#[derive(Debug)] +pub(crate) struct TransportCloseRequest { + pub(crate) transport_id: TransportId, +} + +impl Request for TransportCloseRequest { + const METHOD: request::Method = request::Method::RouterCloseTransport; + type HandlerId = RouterId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = + router::CloseTransportRequest::create(&mut builder, self.transport_id.to_string()); + let request_body = request::Body::create_router_close_transport_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct WebRtcTransportConnectResponse { + pub(crate) dtls_local_role: DtlsRole, +} + +#[derive(Debug)] +pub(crate) struct WebRtcTransportConnectRequest { + pub(crate) dtls_parameters: DtlsParameters, +} + +impl Request for WebRtcTransportConnectRequest { + const METHOD: request::Method = request::Method::WebrtctransportConnect; + type HandlerId = TransportId; + type Response = WebRtcTransportConnectResponse; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = + web_rtc_transport::ConnectRequest::create(&mut builder, self.dtls_parameters.to_fbs()); + let request_body = + request::Body::create_web_rtc_transport_connect_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::WebRtcTransportConnectResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = web_rtc_transport::ConnectResponse::try_from(data)?; + + Ok(WebRtcTransportConnectResponse { + dtls_local_role: DtlsRole::from_fbs(data.dtls_local_role), + }) + } +} + +#[derive(Debug)] +pub(crate) struct PipeTransportConnectResponse { + pub(crate) tuple: TransportTuple, +} + +#[derive(Debug)] +pub(crate) struct PipeTransportConnectRequest { + pub(crate) ip: IpAddr, + pub(crate) port: u16, + pub(crate) srtp_parameters: Option, +} + +impl Request for PipeTransportConnectRequest { + const METHOD: request::Method = request::Method::PipetransportConnect; + type HandlerId = TransportId; + type Response = PipeTransportConnectResponse; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = pipe_transport::ConnectRequest::create( + &mut builder, + self.ip.to_string(), + self.port, + self.srtp_parameters.map(|parameters| parameters.to_fbs()), + ); + let request_body = request::Body::create_pipe_transport_connect_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::PipeTransportConnectResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = pipe_transport::ConnectResponse::try_from(data)?; + + Ok(PipeTransportConnectResponse { + tuple: TransportTuple::from_fbs(data.tuple.as_ref()), + }) + } +} + +#[derive(Debug)] +pub(crate) struct PlainTransportConnectResponse { + pub(crate) tuple: TransportTuple, + pub(crate) rtcp_tuple: Option, + pub(crate) srtp_parameters: Option, +} + +#[derive(Debug)] +pub(crate) struct TransportConnectPlainRequest { + pub(crate) ip: Option, + pub(crate) port: Option, + pub(crate) rtcp_port: Option, + pub(crate) srtp_parameters: Option, +} + +impl Request for TransportConnectPlainRequest { + const METHOD: request::Method = request::Method::PlaintransportConnect; + type HandlerId = TransportId; + type Response = PlainTransportConnectResponse; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = plain_transport::ConnectRequest::create( + &mut builder, + self.ip.map(|ip| ip.to_string()), + self.port, + self.rtcp_port, + self.srtp_parameters.map(|parameters| parameters.to_fbs()), + ); + let request_body = + request::Body::create_plain_transport_connect_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::PlainTransportConnectResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = plain_transport::ConnectResponse::try_from(data)?; + + Ok(PlainTransportConnectResponse { + tuple: TransportTuple::from_fbs(data.tuple.as_ref()), + rtcp_tuple: data + .rtcp_tuple + .map(|tuple| TransportTuple::from_fbs(tuple.as_ref())), + srtp_parameters: data + .srtp_parameters + .map(|parameters| SrtpParameters::from_fbs(parameters.as_ref())), + }) + } +} + +#[derive(Debug)] +pub(crate) struct TransportSetMaxIncomingBitrateRequest { + pub(crate) bitrate: u32, +} + +impl Request for TransportSetMaxIncomingBitrateRequest { + const METHOD: request::Method = request::Method::TransportSetMaxIncomingBitrate; + type HandlerId = TransportId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = transport::SetMaxIncomingBitrateRequest::create(&mut builder, self.bitrate); + let request_body = + request::Body::create_transport_set_max_incoming_bitrate_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct TransportSetMaxOutgoingBitrateRequest { + pub(crate) bitrate: u32, +} + +impl Request for TransportSetMaxOutgoingBitrateRequest { + const METHOD: request::Method = request::Method::TransportSetMaxOutgoingBitrate; + type HandlerId = TransportId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = transport::SetMaxOutgoingBitrateRequest::create(&mut builder, self.bitrate); + let request_body = + request::Body::create_transport_set_max_outgoing_bitrate_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct TransportSetMinOutgoingBitrateRequest { + pub(crate) bitrate: u32, +} + +impl Request for TransportSetMinOutgoingBitrateRequest { + const METHOD: request::Method = request::Method::TransportSetMinOutgoingBitrate; + type HandlerId = TransportId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = transport::SetMinOutgoingBitrateRequest::create(&mut builder, self.bitrate); + let request_body = + request::Body::create_transport_set_min_outgoing_bitrate_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct TransportRestartIceRequest {} + +impl Request for TransportRestartIceRequest { + const METHOD: request::Method = request::Method::TransportRestartIce; + type HandlerId = TransportId; + type Response = IceParameters; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::TransportRestartIceResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = transport::RestartIceResponse::try_from(data)?; + + Ok(IceParameters::from_fbs(web_rtc_transport::IceParameters { + username_fragment: data.username_fragment, + password: data.password, + ice_lite: data.ice_lite, + })) + } +} + +#[derive(Debug)] +pub(crate) struct TransportProduceRequest { + pub(crate) producer_id: ProducerId, + pub(crate) kind: MediaKind, + pub(crate) rtp_parameters: RtpParameters, + pub(crate) rtp_mapping: RtpMapping, + pub(crate) key_frame_request_delay: u32, + pub(crate) paused: bool, +} + +#[derive(Debug)] +pub(crate) struct TransportProduceResponse { + pub(crate) r#type: ProducerType, +} + +impl Request for TransportProduceRequest { + const METHOD: request::Method = request::Method::TransportProduce; + type HandlerId = TransportId; + type Response = TransportProduceResponse; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = transport::ProduceRequest::create( + &mut builder, + self.producer_id.to_string(), + self.kind.to_fbs(), + Box::new(self.rtp_parameters.into_fbs()), + Box::new(self.rtp_mapping.to_fbs()), + self.key_frame_request_delay, + self.paused, + ); + let request_body = request::Body::create_transport_produce_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::TransportProduceResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = transport::ProduceResponse::try_from(data)?; + + Ok(TransportProduceResponse { + r#type: ProducerType::from_fbs(data.type_), + }) + } +} + +#[derive(Debug)] +pub(crate) struct TransportConsumeRequest { + pub(crate) consumer_id: ConsumerId, + pub(crate) producer_id: ProducerId, + pub(crate) kind: MediaKind, + pub(crate) rtp_parameters: RtpParameters, + pub(crate) r#type: ConsumerType, + pub(crate) consumable_rtp_encodings: Vec, + pub(crate) paused: bool, + pub(crate) preferred_layers: Option, + pub(crate) ignore_dtx: bool, +} + +#[derive(Debug)] +pub(crate) struct TransportConsumeResponse { + pub(crate) paused: bool, + pub(crate) producer_paused: bool, + pub(crate) score: ConsumerScore, + pub(crate) preferred_layers: Option, +} + +impl Request for TransportConsumeRequest { + const METHOD: request::Method = request::Method::TransportConsume; + type HandlerId = TransportId; + type Response = TransportConsumeResponse; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = transport::ConsumeRequest::create( + &mut builder, + self.consumer_id.to_string(), + self.producer_id.to_string(), + self.kind.to_fbs(), + Box::new(self.rtp_parameters.into_fbs()), + self.r#type.to_fbs(), + self.consumable_rtp_encodings + .iter() + .map(RtpEncodingParameters::to_fbs) + .collect::>(), + self.paused, + self.preferred_layers.map(ConsumerLayers::to_fbs), + self.ignore_dtx, + ); + let request_body = request::Body::create_transport_consume_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::TransportConsumeResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = transport::ConsumeResponse::try_from(data)?; + + Ok(TransportConsumeResponse { + paused: data.paused, + producer_paused: data.producer_paused, + score: ConsumerScore::from_fbs(*data.score), + preferred_layers: data + .preferred_layers + .map(|preferred_layers| ConsumerLayers::from_fbs(*preferred_layers)), + }) + } +} + +#[derive(Debug)] +pub(crate) struct TransportProduceDataRequest { + pub(crate) data_producer_id: DataProducerId, + pub(crate) r#type: DataProducerType, + // #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) sctp_stream_parameters: Option, + pub(crate) label: String, + pub(crate) protocol: String, + pub(crate) paused: bool, +} + +#[derive(Debug)] +pub(crate) struct TransportProduceDataResponse { + pub(crate) r#type: DataProducerType, + pub(crate) sctp_stream_parameters: Option, + pub(crate) label: String, + pub(crate) protocol: String, + pub(crate) paused: bool, +} + +impl Request for TransportProduceDataRequest { + const METHOD: request::Method = request::Method::TransportProduceData; + type HandlerId = TransportId; + type Response = TransportProduceDataResponse; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = transport::ProduceDataRequest::create( + &mut builder, + self.data_producer_id.to_string(), + match self.r#type { + DataProducerType::Sctp => data_producer::Type::Sctp, + DataProducerType::Direct => data_producer::Type::Direct, + }, + self.sctp_stream_parameters + .map(SctpStreamParameters::to_fbs), + if self.label.is_empty() { + None + } else { + Some(self.label) + }, + if self.protocol.is_empty() { + None + } else { + Some(self.protocol) + }, + self.paused, + ); + let request_body = request::Body::create_transport_produce_data_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::DataProducerDumpResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = data_producer::DumpResponse::try_from(data)?; + + Ok(TransportProduceDataResponse { + r#type: match data.type_ { + data_producer::Type::Sctp => DataProducerType::Sctp, + data_producer::Type::Direct => DataProducerType::Direct, + }, + sctp_stream_parameters: data + .sctp_stream_parameters + .map(|stream_parameters| SctpStreamParameters::from_fbs(*stream_parameters)), + label: data.label.to_string(), + protocol: data.protocol.to_string(), + paused: data.paused, + }) + } +} + +#[derive(Debug)] +pub(crate) struct TransportConsumeDataRequest { + pub(crate) data_consumer_id: DataConsumerId, + pub(crate) data_producer_id: DataProducerId, + pub(crate) r#type: DataConsumerType, + pub(crate) sctp_stream_parameters: Option, + pub(crate) label: String, + pub(crate) protocol: String, + pub(crate) paused: bool, + pub(crate) subchannels: Option>, +} + +#[derive(Debug)] +pub(crate) struct TransportConsumeDataResponse { + pub(crate) r#type: DataConsumerType, + pub(crate) sctp_stream_parameters: Option, + pub(crate) label: String, + pub(crate) protocol: String, + pub(crate) paused: bool, + pub(crate) data_producer_paused: bool, + pub(crate) subchannels: Vec, +} + +impl Request for TransportConsumeDataRequest { + const METHOD: request::Method = request::Method::TransportConsumeData; + type HandlerId = TransportId; + type Response = TransportConsumeDataResponse; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = transport::ConsumeDataRequest::create( + &mut builder, + self.data_consumer_id.to_string(), + self.data_producer_id.to_string(), + match self.r#type { + DataConsumerType::Sctp => data_producer::Type::Sctp, + DataConsumerType::Direct => data_producer::Type::Direct, + }, + self.sctp_stream_parameters + .map(SctpStreamParameters::to_fbs), + if self.label.is_empty() { + None + } else { + Some(self.label) + }, + if self.protocol.is_empty() { + None + } else { + Some(self.protocol) + }, + self.paused, + self.subchannels, + ); + let request_body = request::Body::create_transport_consume_data_request(&mut builder, data); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::DataConsumerDumpResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = data_consumer::DumpResponse::try_from(data)?; + + Ok(TransportConsumeDataResponse { + r#type: match data.type_ { + data_producer::Type::Sctp => DataConsumerType::Sctp, + data_producer::Type::Direct => DataConsumerType::Direct, + }, + sctp_stream_parameters: data + .sctp_stream_parameters + .map(|stream_parameters| SctpStreamParameters::from_fbs(*stream_parameters)), + label: data.label.to_string(), + protocol: data.protocol.to_string(), + paused: data.paused, + data_producer_paused: data.data_producer_paused, + subchannels: data.subchannels, + }) + } +} + +#[derive(Debug)] +pub(crate) struct TransportEnableTraceEventRequest { + pub(crate) types: Vec, +} + +impl Request for TransportEnableTraceEventRequest { + const METHOD: request::Method = request::Method::TransportEnableTraceEvent; + type HandlerId = TransportId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = transport::EnableTraceEventRequest { + events: self + .types + .into_iter() + .map(TransportTraceEventType::to_fbs) + .collect(), + }; + + let request_body = request::Body::TransportEnableTraceEventRequest(Box::new(data)); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } + + fn default_for_soft_error() -> Option { + None + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct TransportSendRtcpNotification { + pub(crate) rtcp_packet: Vec, +} + +impl Notification for TransportSendRtcpNotification { + const EVENT: notification::Event = notification::Event::TransportSendRtcp; + type HandlerId = TransportId; + + fn into_bytes(self, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = transport::SendRtcpNotification::create(&mut builder, self.rtcp_packet); + let notification_body = + notification::Body::create_transport_send_rtcp_notification(&mut builder, data); + + let notification = notification::Notification::create( + &mut builder, + handler_id.to_string(), + Self::EVENT, + Some(notification_body), + ); + let message_body = message::Body::create_notification(&mut builder, notification); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } +} + +#[derive(Debug)] +pub(crate) struct ProducerCloseRequest { + pub(crate) producer_id: ProducerId, +} + +impl Request for ProducerCloseRequest { + const METHOD: request::Method = request::Method::TransportCloseProducer; + type HandlerId = TransportId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = + transport::CloseProducerRequest::create(&mut builder, self.producer_id.to_string()); + let request_body = + request::Body::create_transport_close_producer_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct ProducerDumpRequest {} + +impl Request for ProducerDumpRequest { + const METHOD: request::Method = request::Method::ProducerDump; + type HandlerId = ProducerId; + type Response = ProducerDump; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::ProducerDumpResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + ProducerDump::from_fbs_ref(data) + } +} + +#[derive(Debug)] +pub(crate) struct ProducerGetStatsRequest {} + +impl Request for ProducerGetStatsRequest { + const METHOD: request::Method = request::Method::ProducerGetStats; + type HandlerId = ProducerId; + type Response = response::Body; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + match response { + Some(data) => Ok(data.try_into().unwrap()), + _ => { + panic!("Wrong message from worker: {response:?}"); + } + } + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct ProducerPauseRequest {} + +impl Request for ProducerPauseRequest { + const METHOD: request::Method = request::Method::ProducerPause; + type HandlerId = ProducerId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct ProducerResumeRequest {} + +impl Request for ProducerResumeRequest { + const METHOD: request::Method = request::Method::ProducerResume; + type HandlerId = ProducerId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct ProducerEnableTraceEventRequest { + pub(crate) types: Vec, +} + +impl Request for ProducerEnableTraceEventRequest { + const METHOD: request::Method = request::Method::ProducerEnableTraceEvent; + type HandlerId = ProducerId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = producer::EnableTraceEventRequest { + events: self + .types + .into_iter() + .map(ProducerTraceEventType::to_fbs) + .collect(), + }; + + let request_body = request::Body::ProducerEnableTraceEventRequest(Box::new(data)); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } + + fn default_for_soft_error() -> Option { + None + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ProducerSendNotification { + pub(crate) rtp_packet: Vec, +} + +impl Notification for ProducerSendNotification { + const EVENT: notification::Event = notification::Event::ProducerSend; + type HandlerId = ProducerId; + + fn into_bytes(self, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = producer::SendNotification::create(&mut builder, self.rtp_packet); + let notification_body = + notification::Body::create_producer_send_notification(&mut builder, data); + + let notification = notification::Notification::create( + &mut builder, + handler_id.to_string(), + Self::EVENT, + Some(notification_body), + ); + let message_body = message::Body::create_notification(&mut builder, notification); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } +} + +#[derive(Debug)] +pub(crate) struct ConsumerCloseRequest { + pub(crate) consumer_id: ConsumerId, +} + +impl Request for ConsumerCloseRequest { + const METHOD: request::Method = request::Method::TransportCloseConsumer; + type HandlerId = TransportId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = + transport::CloseConsumerRequest::create(&mut builder, self.consumer_id.to_string()); + let request_body = + request::Body::create_transport_close_consumer_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct ConsumerDumpRequest {} + +impl Request for ConsumerDumpRequest { + const METHOD: request::Method = request::Method::ConsumerDump; + type HandlerId = ConsumerId; + type Response = ConsumerDump; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::ConsumerDumpResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + ConsumerDump::from_fbs_ref(data) + } +} + +#[derive(Debug)] +pub(crate) struct ConsumerGetStatsRequest {} + +impl Request for ConsumerGetStatsRequest { + const METHOD: request::Method = request::Method::ConsumerGetStats; + type HandlerId = ConsumerId; + type Response = response::Body; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + match response { + Some(data) => Ok(data.try_into().unwrap()), + _ => { + panic!("Wrong message from worker: {response:?}"); + } + } + } +} + +#[derive(Debug)] +pub(crate) struct ConsumerPauseRequest {} + +impl Request for ConsumerPauseRequest { + const METHOD: request::Method = request::Method::ConsumerPause; + type HandlerId = ConsumerId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct ConsumerResumeRequest {} + +impl Request for ConsumerResumeRequest { + const METHOD: request::Method = request::Method::ConsumerResume; + type HandlerId = ConsumerId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct ConsumerSetPreferredLayersRequest { + pub(crate) data: ConsumerLayers, +} + +impl Request for ConsumerSetPreferredLayersRequest { + const METHOD: request::Method = request::Method::ConsumerSetPreferredLayers; + type HandlerId = ConsumerId; + type Response = Option; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = consumer::SetPreferredLayersRequest::create( + &mut builder, + ConsumerLayers::to_fbs(self.data), + ); + let request_body = + request::Body::create_consumer_set_preferred_layers_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::ConsumerSetPreferredLayersResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = consumer::SetPreferredLayersResponse::try_from(data)?; + + match data.preferred_layers { + Some(preferred_layers) => Ok(Some(ConsumerLayers::from_fbs(*preferred_layers))), + None => Ok(None), + } + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct ConsumerSetPriorityRequest { + pub(crate) priority: u8, +} + +#[derive(Debug, Serialize)] +pub(crate) struct ConsumerSetPriorityResponse { + pub(crate) priority: u8, +} + +impl Request for ConsumerSetPriorityRequest { + const METHOD: request::Method = request::Method::ConsumerSetPriority; + type HandlerId = ConsumerId; + type Response = ConsumerSetPriorityResponse; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = consumer::SetPriorityRequest::create(&mut builder, self.priority); + let request_body = request::Body::create_consumer_set_priority_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::ConsumerSetPriorityResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = consumer::SetPriorityResponse::try_from(data)?; + + Ok(ConsumerSetPriorityResponse { + priority: data.priority, + }) + } +} + +#[derive(Debug)] +pub(crate) struct ConsumerRequestKeyFrameRequest {} + +impl Request for ConsumerRequestKeyFrameRequest { + const METHOD: request::Method = request::Method::ConsumerRequestKeyFrame; + type HandlerId = ConsumerId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct ConsumerEnableTraceEventRequest { + pub(crate) types: Vec, +} + +impl Request for ConsumerEnableTraceEventRequest { + const METHOD: request::Method = request::Method::ConsumerEnableTraceEvent; + type HandlerId = ConsumerId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = consumer::EnableTraceEventRequest { + events: self + .types + .into_iter() + .map(ConsumerTraceEventType::to_fbs) + .collect(), + }; + + let request_body = request::Body::ConsumerEnableTraceEventRequest(Box::new(data)); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } + + fn default_for_soft_error() -> Option { + None + } +} + +#[derive(Debug)] +pub(crate) struct DataProducerCloseRequest { + pub(crate) data_producer_id: DataProducerId, +} + +impl Request for DataProducerCloseRequest { + const METHOD: request::Method = request::Method::TransportCloseDataproducer; + type HandlerId = TransportId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = transport::CloseDataProducerRequest::create( + &mut builder, + self.data_producer_id.to_string(), + ); + let request_body = + request::Body::create_transport_close_data_producer_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct DataProducerDumpRequest {} + +impl Request for DataProducerDumpRequest { + const METHOD: request::Method = request::Method::DataproducerDump; + type HandlerId = DataProducerId; + type Response = response::Body; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + match response { + Some(data) => Ok(data.try_into().unwrap()), + _ => { + panic!("Wrong message from worker: {response:?}"); + } + } + } +} + +#[derive(Debug)] +pub(crate) struct DataProducerGetStatsRequest {} + +impl Request for DataProducerGetStatsRequest { + const METHOD: request::Method = request::Method::DataproducerGetStats; + type HandlerId = DataProducerId; + type Response = response::Body; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + match response { + Some(data) => Ok(data.try_into().unwrap()), + _ => { + panic!("Wrong message from worker: {response:?}"); + } + } + } +} #[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct ProducerSendNotification {} +pub(crate) struct DataProducerPauseRequest {} -impl Notification for ProducerSendNotification { - type HandlerId = ProducerId; +impl Request for DataProducerPauseRequest { + const METHOD: request::Method = request::Method::DataproducerPause; + type HandlerId = DataProducerId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } - fn as_event(&self) -> &'static str { - "producer.send" + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) } } -request_response!( - TransportId, - "transport.closeConsumer", - ConsumerCloseRequest { - consumer_id: ConsumerId, - }, - (), - Some(()), -); - -request_response!( - ConsumerId, - "consumer.dump", - ConsumerDumpRequest {}, - ConsumerDump, -); - -request_response!( - ConsumerId, - "consumer.getStats", - ConsumerGetStatsRequest {}, - ConsumerStats, -); - -request_response!(ConsumerId, "consumer.pause", ConsumerPauseRequest {},); - -request_response!(ConsumerId, "consumer.resume", ConsumerResumeRequest {},); - -request_response!( - ConsumerId, - "consumer.setPreferredLayers", - ConsumerSetPreferredLayersRequest { - #[serde(flatten)] - data: ConsumerLayers, - }, - Option, -); - -request_response!( - ConsumerId, - "consumer.setPriority", - ConsumerSetPriorityRequest { priority: u8 }, - ConsumerSetPriorityResponse { priority: u8 }, -); - -request_response!( - ConsumerId, - "consumer.requestKeyFrame", - ConsumerRequestKeyFrameRequest {}, -); - -request_response!( - ConsumerId, - "consumer.enableTraceEvent", - ConsumerEnableTraceEventRequest { - types: Vec, - }, -); +#[derive(Debug, Serialize)] +pub(crate) struct DataProducerResumeRequest {} -request_response!( - TransportId, - "transport.closeDataProducer", - DataProducerCloseRequest { - data_producer_id: DataProducerId, - }, - (), - Some(()), -); - -request_response!( - DataProducerId, - "dataProducer.dump", - DataProducerDumpRequest {}, - DataProducerDump, -); - -request_response!( - DataProducerId, - "dataProducer.getStats", - DataProducerGetStatsRequest {}, - Vec, -); - -#[derive(Debug, Copy, Clone, Serialize)] -#[serde(into = "u32")] +impl Request for DataProducerResumeRequest { + const METHOD: request::Method = request::Method::DataproducerResume; + type HandlerId = DataProducerId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] pub(crate) struct DataProducerSendNotification { - #[serde(flatten)] pub(crate) ppid: u32, + pub(crate) payload: Vec, + pub(crate) subchannels: Option>, + pub(crate) required_subchannel: Option, } -impl From for u32 { - fn from(notification: DataProducerSendNotification) -> Self { - notification.ppid +impl Notification for DataProducerSendNotification { + const EVENT: notification::Event = notification::Event::DataproducerSend; + type HandlerId = DataProducerId; + + fn into_bytes(self, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = data_producer::SendNotification::create( + &mut builder, + self.ppid, + self.payload, + self.subchannels, + self.required_subchannel, + ); + let notification_body = + notification::Body::create_data_producer_send_notification(&mut builder, data); + + let notification = notification::Notification::create( + &mut builder, + handler_id.to_string(), + Self::EVENT, + Some(notification_body), + ); + let message_body = message::Body::create_notification(&mut builder, notification); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() } } -impl Notification for DataProducerSendNotification { - type HandlerId = DataProducerId; +#[derive(Debug)] +pub(crate) struct DataConsumerCloseRequest { + pub(crate) data_consumer_id: DataConsumerId, +} + +impl Request for DataConsumerCloseRequest { + const METHOD: request::Method = request::Method::TransportCloseDataconsumer; + type HandlerId = TransportId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = transport::CloseDataConsumerRequest::create( + &mut builder, + self.data_consumer_id.to_string(), + ); + let request_body = + request::Body::create_transport_close_data_consumer_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } - fn as_event(&self) -> &'static str { - "dataProducer.send" + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) } } -request_response!( - TransportId, - "transport.closeDataConsumer", - DataConsumerCloseRequest { - data_consumer_id: DataConsumerId, - }, - (), - Some(()), -); - -request_response!( - DataConsumerId, - "dataConsumer.dump", - DataConsumerDumpRequest {}, - DataConsumerDump, -); - -request_response!( - DataConsumerId, - "dataConsumer.getStats", - DataConsumerGetStatsRequest {}, - Vec, -); - -request_response!( - DataConsumerId, - "dataConsumer.getBufferedAmount", - DataConsumerGetBufferedAmountRequest {}, - DataConsumerGetBufferedAmountResponse { - buffered_amount: u32, - }, -); +#[derive(Debug)] +pub(crate) struct DataConsumerDumpRequest {} + +impl Request for DataConsumerDumpRequest { + const METHOD: request::Method = request::Method::DataconsumerDump; + type HandlerId = DataConsumerId; + type Response = response::Body; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + match response { + Some(data) => Ok(data.try_into().unwrap()), + _ => { + panic!("Wrong message from worker: {response:?}"); + } + } + } +} + +#[derive(Debug)] +pub(crate) struct DataConsumerGetStatsRequest {} + +impl Request for DataConsumerGetStatsRequest { + const METHOD: request::Method = request::Method::DataconsumerGetStats; + type HandlerId = DataConsumerId; + type Response = response::Body; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + match response { + Some(data) => Ok(data.try_into().unwrap()), + _ => { + panic!("Wrong message from worker: {response:?}"); + } + } + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct DataConsumerPauseRequest {} + +impl Request for DataConsumerPauseRequest { + const METHOD: request::Method = request::Method::DataconsumerPause; + type HandlerId = DataConsumerId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct DataConsumerResumeRequest {} + +impl Request for DataConsumerResumeRequest { + const METHOD: request::Method = request::Method::DataconsumerResume; + type HandlerId = DataConsumerId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct DataConsumerGetBufferedAmountRequest {} + +#[derive(Debug, Serialize)] +pub(crate) struct DataConsumerGetBufferedAmountResponse { + pub(crate) buffered_amount: u32, +} + +impl Request for DataConsumerGetBufferedAmountRequest { + const METHOD: request::Method = request::Method::DataconsumerGetBufferedAmount; + type HandlerId = DataConsumerId; + type Response = DataConsumerGetBufferedAmountResponse; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::DataConsumerGetBufferedAmountResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = data_consumer::GetBufferedAmountResponse::try_from(data)?; + + Ok(DataConsumerGetBufferedAmountResponse { + buffered_amount: data.buffered_amount, + }) + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct DataConsumerSetBufferedAmountLowThresholdRequest { + pub(crate) threshold: u32, +} + +impl Request for DataConsumerSetBufferedAmountLowThresholdRequest { + const METHOD: request::Method = request::Method::DataconsumerSetBufferedAmountLowThreshold; + type HandlerId = DataConsumerId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); -request_response!( - DataConsumerId, - "dataConsumer.setBufferedAmountLowThreshold", - DataConsumerSetBufferedAmountLowThresholdRequest { threshold: u32 }, -); + let data = data_consumer::SetBufferedAmountLowThresholdRequest::create( + &mut builder, + self.threshold, + ); + let request_body = + request::Body::create_data_consumer_set_buffered_amount_low_threshold_request( + &mut builder, + data, + ); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } -#[derive(Debug, Copy, Clone, Serialize)] + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize)] #[serde(into = "u32")] pub(crate) struct DataConsumerSendRequest { pub(crate) ppid: u32, + pub(crate) payload: Vec, } impl Request for DataConsumerSendRequest { + const METHOD: request::Method = request::Method::DataconsumerSend; type HandlerId = DataConsumerId; type Response = (); - fn as_method(&self) -> &'static str { - "dataConsumer.send" + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = data_consumer::SendRequest::create(&mut builder, self.ppid, self.payload); + let request_body = request::Body::create_data_consumer_send_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) } } @@ -913,40 +3214,327 @@ impl From for u32 { } } -request_response!( - RouterId, - "router.closeRtpObserver", - RtpObserverCloseRequest { - rtp_observer_id: RtpObserverId, - }, - (), - Some(()), -); - -request_response!( - RtpObserverId, - "rtpObserver.pause", - RtpObserverPauseRequest {}, -); - -request_response!( - RtpObserverId, - "rtpObserver.resume", - RtpObserverResumeRequest {}, -); - -request_response!( - RtpObserverId, - "rtpObserver.addProducer", - RtpObserverAddProducerRequest { - producer_id: ProducerId, - }, -); +#[derive(Debug, Clone, Serialize)] +pub(crate) struct DataConsumerSetSubchannelsRequest { + pub(crate) subchannels: Vec, +} -request_response!( - RtpObserverId, - "rtpObserver.removeProducer", - RtpObserverRemoveProducerRequest { - producer_id: ProducerId, - }, -); +#[derive(Debug, Clone, Serialize)] +pub(crate) struct DataConsumerSetSubchannelsResponse { + pub(crate) subchannels: Vec, +} + +impl Request for DataConsumerSetSubchannelsRequest { + const METHOD: request::Method = request::Method::DataconsumerSetSubchannels; + type HandlerId = DataConsumerId; + type Response = DataConsumerSetSubchannelsResponse; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = data_consumer::SetSubchannelsRequest::create(&mut builder, self.subchannels); + let request_body = + request::Body::create_data_consumer_set_subchannels_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::DataConsumerSetSubchannelsResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = data_consumer::SetSubchannelsResponse::try_from(data)?; + + Ok(DataConsumerSetSubchannelsResponse { + subchannels: data.subchannels, + }) + } +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct DataConsumerAddSubchannelRequest { + pub(crate) subchannel: u16, +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct DataConsumerAddSubchannelResponse { + pub(crate) subchannels: Vec, +} + +impl Request for DataConsumerAddSubchannelRequest { + const METHOD: request::Method = request::Method::DataconsumerAddSubchannel; + type HandlerId = DataConsumerId; + type Response = DataConsumerAddSubchannelResponse; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = data_consumer::AddSubchannelRequest::create(&mut builder, self.subchannel); + let request_body = + request::Body::create_data_consumer_add_subchannel_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::DataConsumerAddSubchannelResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = data_consumer::AddSubchannelResponse::try_from(data)?; + + Ok(DataConsumerAddSubchannelResponse { + subchannels: data.subchannels, + }) + } +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct DataConsumerRemoveSubchannelRequest { + pub(crate) subchannel: u16, +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct DataConsumerRemoveSubchannelResponse { + pub(crate) subchannels: Vec, +} + +impl Request for DataConsumerRemoveSubchannelRequest { + const METHOD: request::Method = request::Method::DataconsumerRemoveSubchannel; + type HandlerId = DataConsumerId; + type Response = DataConsumerRemoveSubchannelResponse; + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = data_consumer::RemoveSubchannelRequest::create(&mut builder, self.subchannel); + let request_body = + request::Body::create_data_consumer_remove_subchannel_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + response: Option>, + ) -> Result> { + let Some(response::BodyRef::DataConsumerRemoveSubchannelResponse(data)) = response else { + panic!("Wrong message from worker: {response:?}"); + }; + + let data = data_consumer::RemoveSubchannelResponse::try_from(data)?; + + Ok(DataConsumerRemoveSubchannelResponse { + subchannels: data.subchannels, + }) + } +} + +#[derive(Debug)] +pub(crate) struct RtpObserverCloseRequest { + pub(crate) rtp_observer_id: RtpObserverId, +} + +impl Request for RtpObserverCloseRequest { + const METHOD: request::Method = request::Method::RouterCloseRtpobserver; + type HandlerId = RouterId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let data = + router::CloseRtpObserverRequest::create(&mut builder, self.rtp_observer_id.to_string()); + let request_body = + request::Body::create_router_close_rtp_observer_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct RtpObserverPauseRequest {} + +impl Request for RtpObserverPauseRequest { + const METHOD: request::Method = request::Method::RtpobserverPause; + type HandlerId = RtpObserverId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct RtpObserverResumeRequest {} + +impl Request for RtpObserverResumeRequest { + const METHOD: request::Method = request::Method::RtpobserverResume; + type HandlerId = RtpObserverId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + None::, + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RtpObserverAddProducerRequest { + pub(crate) producer_id: ProducerId, +} + +impl Request for RtpObserverAddProducerRequest { + const METHOD: request::Method = request::Method::RtpobserverAddProducer; + type HandlerId = RtpObserverId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = + rtp_observer::AddProducerRequest::create(&mut builder, self.producer_id.to_string()); + let request_body = + request::Body::create_rtp_observer_add_producer_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RtpObserverRemoveProducerRequest { + pub(crate) producer_id: ProducerId, +} + +impl Request for RtpObserverRemoveProducerRequest { + const METHOD: request::Method = request::Method::RtpobserverRemoveProducer; + type HandlerId = RtpObserverId; + type Response = (); + + fn into_bytes(self, id: u32, handler_id: Self::HandlerId) -> Vec { + let mut builder = Builder::new(); + + let data = + rtp_observer::RemoveProducerRequest::create(&mut builder, self.producer_id.to_string()); + let request_body = + request::Body::create_rtp_observer_remove_producer_request(&mut builder, data); + + let request = request::Request::create( + &mut builder, + id, + Self::METHOD, + handler_id.to_string(), + Some(request_body), + ); + let message_body = message::Body::create_request(&mut builder, request); + let message = message::Message::create(&mut builder, message_body); + + builder.finish(message, None).to_vec() + } + + fn convert_response( + _response: Option>, + ) -> Result> { + Ok(()) + } +} diff --git a/rust/src/ortc.rs b/rust/src/ortc.rs index c626ee47fa..7bec5dc701 100644 --- a/rust/src/ortc.rs +++ b/rust/src/ortc.rs @@ -7,10 +7,12 @@ use crate::rtp_parameters::{ }; use crate::scalability_modes::ScalabilityMode; use crate::supported_rtp_capabilities; +use mediasoup_sys::fbs::rtp_parameters; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::collections::BTreeMap; use std::convert::TryFrom; +use std::error::Error; use std::mem; use std::num::{NonZeroU32, NonZeroU8}; use std::ops::Deref; @@ -52,6 +54,64 @@ pub struct RtpMapping { pub encodings: Vec, } +impl RtpMapping { + pub(crate) fn to_fbs(&self) -> rtp_parameters::RtpMapping { + rtp_parameters::RtpMapping { + codecs: self + .codecs + .iter() + .map(|mapping| rtp_parameters::CodecMapping { + payload_type: mapping.payload_type, + mapped_payload_type: mapping.mapped_payload_type, + }) + .collect(), + encodings: self + .encodings + .iter() + .map(|mapping| rtp_parameters::EncodingMapping { + rid: mapping.rid.clone().map(|rid| rid.to_string()), + ssrc: mapping.ssrc, + scalability_mode: Some(mapping.scalability_mode.to_string()), + mapped_ssrc: mapping.mapped_ssrc, + }) + .collect(), + } + } + + pub(crate) fn from_fbs_ref( + mapping: rtp_parameters::RtpMappingRef<'_>, + ) -> Result> { + Ok(Self { + codecs: mapping + .codecs()? + .iter() + .map(|mapping| { + Ok(RtpMappingCodec { + payload_type: mapping?.payload_type()?, + mapped_payload_type: mapping?.mapped_payload_type()?, + }) + }) + .collect::, Box>>()?, + encodings: mapping + .encodings()? + .iter() + .map(|mapping| { + Ok(RtpMappingEncoding { + rid: mapping?.rid()?.map(|rid| rid.to_string()), + ssrc: mapping?.ssrc()?, + scalability_mode: mapping? + .scalability_mode()? + .map(|maybe_scalability_mode| maybe_scalability_mode.parse()) + .transpose()? + .unwrap_or_default(), + mapped_ssrc: mapping?.mapped_ssrc()?, + }) + }) + .collect::, Box>>()?, + }) + } +} + /// Error caused by invalid RTP parameters. #[derive(Debug, Error, Eq, PartialEq)] pub enum RtpParametersError { @@ -603,7 +663,6 @@ pub(crate) fn get_consumable_rtp_parameters( consumable_params.rtcp = RtcpParameters { cname: params.rtcp.cname.clone(), reduced_size: true, - mux: Some(true), }; consumable_params @@ -622,7 +681,7 @@ pub(crate) fn can_consume( if caps .codecs .iter() - .any(|cap_codec| match_codecs(cap_codec.deref().into(), codec.into(), true).is_ok()) + .any(|cap_codec| match_codecs(cap_codec.into(), codec.into(), true).is_ok()) { matching_codecs.push(codec); } @@ -630,7 +689,7 @@ pub(crate) fn can_consume( // Ensure there is at least one media codec. Ok(matching_codecs - .get(0) + .first() .map(|codec| !codec.is_rtx()) .unwrap_or_default()) } @@ -641,29 +700,40 @@ pub(crate) fn can_consume( /// codecs' RTCP feedback and header extensions, and also enables or disabled RTX. #[allow(clippy::suspicious_operation_groupings)] pub(crate) fn get_consumer_rtp_parameters( - consumable_params: &RtpParameters, - caps: &RtpCapabilities, + consumable_rtp_parameters: &RtpParameters, + remote_rtp_capabilities: &RtpCapabilities, pipe: bool, + enable_rtx: bool, ) -> Result { let mut consumer_params = RtpParameters { - rtcp: consumable_params.rtcp.clone(), + rtcp: consumable_rtp_parameters.rtcp.clone(), ..RtpParameters::default() }; - for cap_codec in &caps.codecs { + for cap_codec in &remote_rtp_capabilities.codecs { validate_rtp_codec_capability(cap_codec) .map_err(ConsumerRtpParametersError::InvalidCapabilities)?; } let mut rtx_supported = false; - for mut codec in consumable_params.codecs.clone() { - if let Some(matched_cap_codec) = caps + for mut codec in consumable_rtp_parameters.codecs.clone() { + if !enable_rtx && codec.is_rtx() { + continue; + } + + if let Some(matched_cap_codec) = remote_rtp_capabilities .codecs .iter() - .find(|cap_codec| match_codecs(cap_codec.deref().into(), (&codec).into(), true).is_ok()) + .find(|cap_codec| match_codecs((*cap_codec).into(), (&codec).into(), true).is_ok()) { - *codec.rtcp_feedback_mut() = matched_cap_codec.rtcp_feedback().clone(); + *codec.rtcp_feedback_mut() = matched_cap_codec + .rtcp_feedback() + .iter() + .filter(|&&fb| enable_rtx || fb != RtcpFeedback::Nack) + .copied() + .collect(); + consumer_params.codecs.push(codec); } } @@ -697,11 +767,12 @@ pub(crate) fn get_consumer_rtp_parameters( return Err(ConsumerRtpParametersError::NoCompatibleMediaCodecs); } - consumer_params.header_extensions = consumable_params + consumer_params.header_extensions = consumable_rtp_parameters .header_extensions .iter() .filter(|ext| { - caps.header_extensions + remote_rtp_capabilities + .header_extensions .iter() .any(|cap_ext| cap_ext.preferred_id == ext.id && cap_ext.uri == ext.uri) }) @@ -738,7 +809,7 @@ pub(crate) fn get_consumer_rtp_parameters( } if pipe { - for ((encoding, ssrc), rtx_ssrc) in consumable_params + for ((encoding, ssrc), rtx_ssrc) in consumable_rtp_parameters .encodings .iter() .zip(generate_ssrc()..) @@ -766,19 +837,19 @@ pub(crate) fn get_consumer_rtp_parameters( }); } - // If any of the consumable_params.encodings has scalability_mode, process it + // If any of the consumable_rtp_parameters.encodings has scalability_mode, process it // (assume all encodings have the same value). - let mut scalability_mode = consumable_params + let mut scalability_mode = consumable_rtp_parameters .encodings - .get(0) + .first() .map(|encoding| encoding.scalability_mode.clone()) .unwrap_or_default(); // If there is simulcast, mangle spatial layers in scalabilityMode. - if consumable_params.encodings.len() > 1 { + if consumable_rtp_parameters.encodings.len() > 1 { scalability_mode = format!( - "S{}T{}", - consumable_params.encodings.len(), + "L{}T{}", + consumable_rtp_parameters.encodings.len(), scalability_mode.temporal_layers() ) .parse() @@ -788,7 +859,7 @@ pub(crate) fn get_consumer_rtp_parameters( consumer_encoding.scalability_mode = scalability_mode; // Use the maximum max_bitrate in any encoding and honor it in the Consumer's encoding. - consumer_encoding.max_bitrate = consumable_params + consumer_encoding.max_bitrate = consumable_rtp_parameters .encodings .iter() .map(|encoding| encoding.max_bitrate) @@ -807,7 +878,7 @@ pub(crate) fn get_consumer_rtp_parameters( /// It keeps all original consumable encodings and removes support for BWE. If /// enableRtx is false, it also removes RTX and NACK support. pub(crate) fn get_pipe_consumer_rtp_parameters( - consumable_params: &RtpParameters, + consumable_rtp_parameters: &RtpParameters, enable_rtx: bool, ) -> RtpParameters { let mut consumer_params = RtpParameters { @@ -815,10 +886,10 @@ pub(crate) fn get_pipe_consumer_rtp_parameters( codecs: vec![], header_extensions: vec![], encodings: vec![], - rtcp: consumable_params.rtcp.clone(), + rtcp: consumable_rtp_parameters.rtcp.clone(), }; - for codec in &consumable_params.codecs { + for codec in &consumable_rtp_parameters.codecs { if !enable_rtx && codec.is_rtx() { continue; } @@ -834,7 +905,7 @@ pub(crate) fn get_pipe_consumer_rtp_parameters( } // Reduce RTP extensions by disabling transport MID and BWE related ones. - consumer_params.header_extensions = consumable_params + consumer_params.header_extensions = consumable_rtp_parameters .header_extensions .iter() .filter(|ext| { @@ -848,7 +919,7 @@ pub(crate) fn get_pipe_consumer_rtp_parameters( .cloned() .collect(); - for ((encoding, ssrc), rtx_ssrc) in consumable_params + for ((encoding, ssrc), rtx_ssrc) in consumable_rtp_parameters .encodings .iter() .zip(generate_ssrc()..) diff --git a/rust/src/ortc/tests.rs b/rust/src/ortc/tests.rs index 8020a11b30..7fb411da9a 100644 --- a/rust/src/ortc/tests.rs +++ b/rust/src/ortc/tests.rs @@ -51,7 +51,7 @@ fn generate_router_rtp_capabilities_succeeds() { ("useinbandfec", 1_u32.into()), ("foo", "bar".into()), ]), - rtcp_feedback: vec![RtcpFeedback::TransportCc], + rtcp_feedback: vec![RtcpFeedback::Nack, RtcpFeedback::TransportCc,], }, RtpCodecCapabilityFinalized::Video { mime_type: MimeTypeVideo::Vp8, @@ -255,8 +255,8 @@ fn get_producer_rtp_parameters_mapping_get_consumable_rtp_parameters_get_consume ] ); - assert_eq!(rtp_mapping.encodings.get(0).unwrap().ssrc, Some(11111111)); - assert_eq!(rtp_mapping.encodings.get(0).unwrap().rid, None); + assert_eq!(rtp_mapping.encodings.first().unwrap().ssrc, Some(11111111)); + assert_eq!(rtp_mapping.encodings.first().unwrap().rid, None); assert_eq!(rtp_mapping.encodings.get(1).unwrap().ssrc, Some(21111111)); assert_eq!(rtp_mapping.encodings.get(1).unwrap().rid, None); assert_eq!(rtp_mapping.encodings.get(2).unwrap().ssrc, None); @@ -303,13 +303,13 @@ fn get_producer_rtp_parameters_mapping_get_consumable_rtp_parameters_get_consume ); assert_eq!( - consumable_rtp_parameters.encodings.get(0).unwrap().ssrc, - Some(rtp_mapping.encodings.get(0).unwrap().mapped_ssrc), + consumable_rtp_parameters.encodings.first().unwrap().ssrc, + Some(rtp_mapping.encodings.first().unwrap().mapped_ssrc), ); assert_eq!( consumable_rtp_parameters .encodings - .get(0) + .first() .unwrap() .max_bitrate, Some(111111), @@ -317,7 +317,7 @@ fn get_producer_rtp_parameters_mapping_get_consumable_rtp_parameters_get_consume assert_eq!( consumable_rtp_parameters .encodings - .get(0) + .first() .unwrap() .scalability_mode, ScalabilityMode::L1T3, @@ -368,7 +368,6 @@ fn get_producer_rtp_parameters_mapping_get_consumable_rtp_parameters_get_consume RtcpParameters { cname: rtp_parameters.rtcp.cname.clone(), reduced_size: true, - mux: Some(true), } ); @@ -451,9 +450,13 @@ fn get_producer_rtp_parameters_mapping_get_consumable_rtp_parameters_get_consume ], }; - let consumer_rtp_parameters = - get_consumer_rtp_parameters(&consumable_rtp_parameters, &remote_rtp_capabilities, false) - .expect("Failed to get consumer RTP parameters"); + let consumer_rtp_parameters = get_consumer_rtp_parameters( + &consumable_rtp_parameters, + &remote_rtp_capabilities, + false, + true, + ) + .expect("Failed to get consumer RTP parameters"); assert_eq!( consumer_rtp_parameters.codecs, @@ -486,28 +489,28 @@ fn get_producer_rtp_parameters_mapping_get_consumable_rtp_parameters_get_consume assert_eq!(consumer_rtp_parameters.encodings.len(), 1); assert!(consumer_rtp_parameters .encodings - .get(0) + .first() .unwrap() .ssrc .is_some()); assert!(consumer_rtp_parameters .encodings - .get(0) + .first() .unwrap() .rtx .is_some()); assert_eq!( consumer_rtp_parameters .encodings - .get(0) + .first() .unwrap() .scalability_mode, - ScalabilityMode::S3T3, + ScalabilityMode::L3T3, ); assert_eq!( consumer_rtp_parameters .encodings - .get(0) + .first() .unwrap() .max_bitrate, Some(333333), @@ -539,7 +542,6 @@ fn get_producer_rtp_parameters_mapping_get_consumable_rtp_parameters_get_consume RtcpParameters { cname: rtp_parameters.rtcp.cname.clone(), reduced_size: true, - mux: Some(true), }, ); @@ -564,26 +566,26 @@ fn get_producer_rtp_parameters_mapping_get_consumable_rtp_parameters_get_consume assert_eq!(pipe_consumer_rtp_parameters.encodings.len(), 3); assert!(pipe_consumer_rtp_parameters .encodings - .get(0) + .first() .unwrap() .ssrc .is_some()); assert!(pipe_consumer_rtp_parameters .encodings - .get(0) + .first() .unwrap() .rtx .is_none()); assert!(pipe_consumer_rtp_parameters .encodings - .get(0) + .first() .unwrap() .max_bitrate .is_some()); assert_eq!( pipe_consumer_rtp_parameters .encodings - .get(0) + .first() .unwrap() .scalability_mode, ScalabilityMode::L1T3, @@ -646,7 +648,6 @@ fn get_producer_rtp_parameters_mapping_get_consumable_rtp_parameters_get_consume RtcpParameters { cname: rtp_parameters.rtcp.cname, reduced_size: true, - mux: Some(true), }, ); } diff --git a/rust/src/prelude.rs b/rust/src/prelude.rs index f23ce3e364..63a010fd5c 100644 --- a/rust/src/prelude.rs +++ b/rust/src/prelude.rs @@ -20,8 +20,7 @@ pub use crate::router::{ }; pub use crate::webrtc_server::{ - WebRtcServer, WebRtcServerId, WebRtcServerListenInfo, WebRtcServerListenInfos, - WebRtcServerOptions, + WebRtcServer, WebRtcServerId, WebRtcServerListenInfos, WebRtcServerOptions, }; pub use crate::direct_transport::{DirectTransport, DirectTransportOptions, WeakDirectTransport}; @@ -36,7 +35,8 @@ pub use crate::transport::{ TransportId, }; pub use crate::webrtc_transport::{ - TransportListenIps, WebRtcTransport, WebRtcTransportOptions, WebRtcTransportRemoteParameters, + WebRtcTransport, WebRtcTransportListenInfos, WebRtcTransportOptions, + WebRtcTransportRemoteParameters, }; pub use crate::active_speaker_observer::{ @@ -60,11 +60,13 @@ pub use crate::data_producer::{ pub use crate::producer::{Producer, ProducerId, ProducerOptions, WeakProducer}; pub use crate::data_structures::{ - AppData, DtlsParameters, IceCandidate, IceParameters, ListenIp, WebRtcMessage, + AppData, DtlsParameters, IceCandidate, IceParameters, ListenInfo, Protocol, WebRtcMessage, }; pub use crate::rtp_parameters::{ - MediaKind, MimeTypeAudio, MimeTypeVideo, RtcpFeedback, RtpCapabilities, - RtpCapabilitiesFinalized, RtpCodecCapability, RtpCodecParametersParameters, RtpParameters, + MediaKind, MimeTypeAudio, MimeTypeVideo, RtcpFeedback, RtcpParameters, RtpCapabilities, + RtpCapabilitiesFinalized, RtpCodecCapability, RtpCodecParameters, RtpCodecParametersParameters, + RtpEncodingParameters, RtpEncodingParametersRtx, RtpHeaderExtensionParameters, + RtpHeaderExtensionUri, RtpParameters, }; pub use crate::sctp_parameters::SctpStreamParameters; pub use crate::srtp_parameters::SrtpCryptoSuite; diff --git a/rust/src/router.rs b/rust/src/router.rs index 4ee29355da..1a5bc219d5 100644 --- a/rust/src/router.rs +++ b/rust/src/router.rs @@ -28,7 +28,7 @@ use crate::data_consumer::{DataConsumer, DataConsumerId, DataConsumerOptions}; use crate::data_producer::{ DataProducer, DataProducerId, DataProducerOptions, NonClosingDataProducer, WeakDataProducer, }; -use crate::data_structures::{AppData, ListenIp}; +use crate::data_structures::{AppData, ListenInfo, Protocol}; use crate::direct_transport::{DirectTransport, DirectTransportOptions}; use crate::messages::{ RouterCloseRequest, RouterCreateActiveSpeakerObserverData, @@ -36,7 +36,9 @@ use crate::messages::{ RouterCreateAudioLevelObserverRequest, RouterCreateDirectTransportData, RouterCreateDirectTransportRequest, RouterCreatePipeTransportData, RouterCreatePipeTransportRequest, RouterCreatePlainTransportData, - RouterCreatePlainTransportRequest, RouterCreateWebrtcTransportRequest, RouterDumpRequest, + RouterCreatePlainTransportRequest, RouterCreateWebRtcTransportRequest, + RouterCreateWebRtcTransportWithServerRequest, RouterCreateWebrtcTransportData, + RouterDumpRequest, }; use crate::pipe_transport::{ PipeTransport, PipeTransportOptions, PipeTransportRemoteParameters, WeakPipeTransport, @@ -51,7 +53,7 @@ use crate::transport::{ TransportId, }; use crate::webrtc_transport::{WebRtcTransport, WebRtcTransportListen, WebRtcTransportOptions}; -use crate::worker::{Channel, PayloadChannel, RequestError, Worker}; +use crate::worker::{Channel, RequestError, Worker}; use crate::{ortc, uuid_based_wrapper_type}; use async_executor::Executor; use async_lock::Mutex as AsyncMutex; @@ -116,8 +118,8 @@ pub struct PipeToRouterOptions { pub router: Router, /// IP used in the PipeTransport pair. /// - /// Default `127.0.0.1`. - listen_ip: ListenIp, + /// Default `{ protocol: 'udp', ip: '127.0.0.1' }`. + listen_info: ListenInfo, /// Create a SCTP association. /// /// Default `true`. @@ -140,9 +142,15 @@ impl PipeToRouterOptions { pub fn new(router: Router) -> Self { Self { router, - listen_ip: ListenIp { + listen_info: ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }, enable_sctp: true, num_sctp_streams: NumSctpStreams::default(), @@ -170,7 +178,7 @@ pub struct PipeProducerToRouterPair { } /// Error that caused [`Router::pipe_producer_to_router()`] to fail. -#[derive(Debug, Error, Eq, PartialEq)] +#[derive(Debug, Error)] pub enum PipeProducerToRouterError { /// Destination router must be different #[error("Destination router must be different")] @@ -367,7 +375,6 @@ struct Inner { executor: Arc>, rtp_capabilities: RtpCapabilitiesFinalized, channel: Channel, - payload_channel: PayloadChannel, handlers: Arc, app_data: AppData, producers: Arc>>, @@ -441,7 +448,6 @@ impl Router { id: RouterId, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, rtp_capabilities: RtpCapabilitiesFinalized, app_data: AppData, worker: Worker, @@ -473,7 +479,6 @@ impl Router { executor, rtp_capabilities, channel, - payload_channel, handlers, producers, data_producers, @@ -576,7 +581,6 @@ impl Router { transport_id, Arc::clone(&self.inner.executor), self.inner.channel.clone(), - self.inner.payload_channel.clone(), direct_transport_options.app_data, self.clone(), ); @@ -601,10 +605,16 @@ impl Router { /// /// # async fn f(router: Router) -> Result<(), Box> { /// let transport = router - /// .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - /// ListenIp { + /// .create_webrtc_transport(WebRtcTransportOptions::new(WebRtcTransportListenInfos::new( + /// ListenInfo { + /// protocol: Protocol::Udp, /// ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - /// announced_ip: Some("9.9.9.1".parse().unwrap()), + /// announced_address: Some("9.9.9.1".to_string()), + /// port: None, + /// port_range: None, + /// flags: None, + /// send_buffer_size: None, + /// recv_buffer_size: None, /// }, /// ))) /// .await?; @@ -621,23 +631,41 @@ impl Router { let _buffer_guard = self.inner.channel.buffer_messages_for(transport_id.into()); - let data = self - .inner - .channel - .request( - self.inner.id, - RouterCreateWebrtcTransportRequest::from_options( - transport_id, - &webrtc_transport_options, - ), - ) - .await?; + let data = match webrtc_transport_options.listen { + WebRtcTransportListen::Individual { listen_infos: _ } => { + self.inner + .channel + .request( + self.inner.id, + RouterCreateWebRtcTransportRequest { + data: RouterCreateWebrtcTransportData::from_options( + transport_id, + &webrtc_transport_options, + ), + }, + ) + .await? + } + WebRtcTransportListen::Server { webrtc_server: _ } => { + self.inner + .channel + .request( + self.inner.id, + RouterCreateWebRtcTransportWithServerRequest { + data: RouterCreateWebrtcTransportData::from_options( + transport_id, + &webrtc_transport_options, + ), + }, + ) + .await? + } + }; let transport = WebRtcTransport::new( transport_id, Arc::clone(&self.inner.executor), self.inner.channel.clone(), - self.inner.payload_channel.clone(), data, webrtc_transport_options.app_data, self.clone(), @@ -667,9 +695,15 @@ impl Router { /// /// # async fn f(router: Router) -> Result<(), Box> { /// let transport = router - /// .create_pipe_transport(PipeTransportOptions::new(ListenIp { + /// .create_pipe_transport(PipeTransportOptions::new(ListenInfo { + /// protocol: Protocol::Udp, /// ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - /// announced_ip: Some("9.9.9.1".parse().unwrap()), + /// announced_address: Some("9.9.9.1".to_string()), + /// port: None, + /// port_range: None, + /// flags: None, + /// send_buffer_size: None, + /// recv_buffer_size: None, /// })) /// .await?; /// # Ok(()) @@ -703,7 +737,6 @@ impl Router { transport_id, Arc::clone(&self.inner.executor), self.inner.channel.clone(), - self.inner.payload_channel.clone(), data, pipe_transport_options.app_data, self.clone(), @@ -729,9 +762,15 @@ impl Router { /// /// # async fn f(router: Router) -> Result<(), Box> { /// let transport = router - /// .create_plain_transport(PlainTransportOptions::new(ListenIp { + /// .create_plain_transport(PlainTransportOptions::new(ListenInfo { + /// protocol: Protocol::Udp, /// ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - /// announced_ip: Some("9.9.9.1".parse().unwrap()), + /// announced_address: Some("9.9.9.1".to_string()), + /// port: None, + /// port_range: None, + /// flags: None, + /// send_buffer_size: None, + /// recv_buffer_size: None, /// })) /// .await?; /// # Ok(()) @@ -765,7 +804,6 @@ impl Router { transport_id, Arc::clone(&self.inner.executor), self.inner.channel.clone(), - self.inner.payload_channel.clone(), data, plain_transport_options.app_data, self.clone(), @@ -935,10 +973,16 @@ impl Router { /// /// // Produce in router1. /// let transport1 = router1 - /// .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - /// ListenIp { + /// .create_webrtc_transport(WebRtcTransportOptions::new(WebRtcTransportListenInfos::new( + /// ListenInfo { + /// protocol: Protocol::Udp, /// ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - /// announced_ip: Some("9.9.9.1".parse().unwrap()), + /// announced_address: Some("9.9.9.1".to_string()), + /// port: None, + /// port_range: None, + /// flags: None, + /// send_buffer_size: None, + /// recv_buffer_size: None, /// }, /// ))) /// .await?; @@ -973,10 +1017,16 @@ impl Router { /// /// // Consume producer1 from router2. /// let transport2 = router2 - /// .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - /// ListenIp { + /// .create_webrtc_transport(WebRtcTransportOptions::new(WebRtcTransportListenInfos::new( + /// ListenInfo { + /// protocol: Protocol::Udp, /// ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - /// announced_ip: Some("9.9.9.1".parse().unwrap()), + /// announced_address: Some("9.9.9.1".to_string()), + /// port: None, + /// port_range: None, + /// flags: None, + /// send_buffer_size: None, + /// recv_buffer_size: None, /// }, /// ))) /// .await?; @@ -1154,10 +1204,16 @@ impl Router { /// // Produce in router1. /// let transport1 = router1 /// .create_webrtc_transport({ - /// let mut options = WebRtcTransportOptions::new(TransportListenIps::new( - /// ListenIp { + /// let mut options = WebRtcTransportOptions::new(WebRtcTransportListenInfos::new( + /// ListenInfo { + /// protocol: Protocol::Udp, /// ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - /// announced_ip: Some("9.9.9.1".parse().unwrap()), + /// announced_address: Some("9.9.9.1".to_string()), + /// port: None, + /// port_range: None, + /// flags: None, + /// send_buffer_size: None, + /// recv_buffer_size: None, /// }, /// )); /// options.enable_sctp = true; @@ -1181,10 +1237,16 @@ impl Router { /// // Consume data_producer1 from router2. /// let transport2 = router2 /// .create_webrtc_transport({ - /// let mut options = WebRtcTransportOptions::new(TransportListenIps::new( - /// ListenIp { + /// let mut options = WebRtcTransportOptions::new(WebRtcTransportListenInfos::new( + /// ListenInfo { + /// protocol: Protocol::Udp, /// ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - /// announced_ip: Some("9.9.9.1".parse().unwrap()), + /// announced_address: Some("9.9.9.1".to_string()), + /// port: None, + /// port_range: None, + /// flags: None, + /// send_buffer_size: None, + /// recv_buffer_size: None, /// }, /// )); /// options.enable_sctp = true; @@ -1241,8 +1303,12 @@ impl Router { // We've created `DataConsumer` with SCTP above, so this should never panic pipe_data_consumer.sctp_stream_parameters().unwrap(), ); - producer_options.label = pipe_data_consumer.label().clone(); - producer_options.protocol = pipe_data_consumer.protocol().clone(); + producer_options + .label + .clone_from(pipe_data_consumer.label()); + producer_options + .protocol + .clone_from(pipe_data_consumer.protocol()); producer_options.app_data = data_producer.app_data().clone(); producer_options @@ -1400,7 +1466,7 @@ impl Router { ) -> Result { let PipeToRouterOptions { router, - listen_ip, + listen_info, enable_sctp, num_sctp_streams, enable_rtx, @@ -1415,7 +1481,7 @@ impl Router { enable_rtx, enable_srtp, app_data: AppData::default(), - ..PipeTransportOptions::new(listen_ip) + ..PipeTransportOptions::new(listen_info) }; let local_pipe_transport_fut = self.create_pipe_transport(transport_options.clone()); @@ -1428,7 +1494,7 @@ impl Router { let tuple = remote_pipe_transport.tuple(); PipeTransportRemoteParameters { - ip: tuple.local_ip(), + ip: tuple.local_address().parse::().unwrap(), port: tuple.local_port(), srtp_parameters: remote_pipe_transport.srtp_parameters(), } @@ -1438,7 +1504,7 @@ impl Router { let tuple = local_pipe_transport.tuple(); PipeTransportRemoteParameters { - ip: tuple.local_ip(), + ip: tuple.local_address().parse::().unwrap(), port: tuple.local_port(), srtp_parameters: local_pipe_transport.srtp_parameters(), } diff --git a/rust/src/router/active_speaker_observer.rs b/rust/src/router/active_speaker_observer.rs index 5999f04526..3707ea9ba0 100644 --- a/rust/src/router/active_speaker_observer.rs +++ b/rust/src/router/active_speaker_observer.rs @@ -9,11 +9,12 @@ use crate::messages::{ use crate::producer::{Producer, ProducerId}; use crate::router::Router; use crate::rtp_observer::{RtpObserver, RtpObserverAddProducerOptions, RtpObserverId}; -use crate::worker::{Channel, RequestError, SubscriptionHandler}; +use crate::worker::{Channel, NotificationParseError, RequestError, SubscriptionHandler}; use async_executor::Executor; use async_trait::async_trait; use event_listener_primitives::{Bag, BagOnce, HandlerId}; use log::{debug, error}; +use mediasoup_sys::fbs::notification; use parking_lot::Mutex; use serde::Deserialize; use std::fmt; @@ -74,6 +75,29 @@ enum Notification { DominantSpeaker(DominantSpeakerNotification), } +impl Notification { + pub(crate) fn from_fbs( + notification: notification::NotificationRef<'_>, + ) -> Result { + match notification.event().unwrap() { + notification::Event::ActivespeakerobserverDominantSpeaker => { + let Ok(Some( + notification::BodyRef::ActiveSpeakerObserverDominantSpeakerNotification(body), + )) = notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let dominant_speaker_notification = DominantSpeakerNotification { + producer_id: body.producer_id().unwrap().parse().unwrap(), + }; + Ok(Notification::DominantSpeaker(dominant_speaker_notification)) + } + _ => Err(NotificationParseError::InvalidEvent), + } + } +} + struct Inner { id: RtpObserverId, executor: Arc>, @@ -294,7 +318,7 @@ impl ActiveSpeakerObserver { let handlers = Arc::clone(&handlers); channel.subscribe_to_notifications(id.into(), move |notification| { - match serde_json::from_slice::(notification) { + match Notification::from_fbs(notification) { Ok(notification) => match notification { Notification::DominantSpeaker(dominant_speaker) => { let DominantSpeakerNotification { producer_id } = dominant_speaker; diff --git a/rust/src/router/audio_level_observer.rs b/rust/src/router/audio_level_observer.rs index cd55b6adff..e696d4c715 100644 --- a/rust/src/router/audio_level_observer.rs +++ b/rust/src/router/audio_level_observer.rs @@ -9,11 +9,12 @@ use crate::messages::{ use crate::producer::{Producer, ProducerId}; use crate::router::Router; use crate::rtp_observer::{RtpObserver, RtpObserverAddProducerOptions, RtpObserverId}; -use crate::worker::{Channel, RequestError, SubscriptionHandler}; +use crate::worker::{Channel, NotificationParseError, RequestError, SubscriptionHandler}; use async_executor::Executor; use async_trait::async_trait; use event_listener_primitives::{Bag, BagOnce, HandlerId}; use log::{debug, error}; +use mediasoup_sys::fbs::{audio_level_observer, notification}; use parking_lot::Mutex; use serde::Deserialize; use std::fmt; @@ -85,6 +86,40 @@ enum Notification { Silence, } +impl Notification { + pub(crate) fn from_fbs( + notification: notification::NotificationRef<'_>, + ) -> Result { + match notification.event().unwrap() { + notification::Event::AudiolevelobserverVolumes => { + let Ok(Some(notification::BodyRef::AudioLevelObserverVolumesNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let volumes_fbs: Vec<_> = body + .volumes() + .unwrap() + .iter() + .map(|volume| audio_level_observer::Volume::try_from(volume.unwrap()).unwrap()) + .collect(); + let volumes = volumes_fbs + .iter() + .map(|volume| VolumeNotification { + producer_id: volume.producer_id.parse().unwrap(), + volume: volume.volume, + }) + .collect(); + + Ok(Notification::Volumes(volumes)) + } + notification::Event::AudiolevelobserverSilence => Ok(Notification::Silence), + _ => Err(NotificationParseError::InvalidEvent), + } + } +} + struct Inner { id: RtpObserverId, executor: Arc>, @@ -305,7 +340,7 @@ impl AudioLevelObserver { let handlers = Arc::clone(&handlers); channel.subscribe_to_notifications(id.into(), move |notification| { - match serde_json::from_slice::(notification) { + match Notification::from_fbs(notification) { Ok(notification) => match notification { Notification::Volumes(volumes) => { let volumes = volumes diff --git a/rust/src/router/consumer.rs b/rust/src/router/consumer.rs index 7e7804c10f..8fa10b6420 100644 --- a/rust/src/router/consumer.rs +++ b/rust/src/router/consumer.rs @@ -8,16 +8,21 @@ use crate::messages::{ ConsumerResumeRequest, ConsumerSetPreferredLayersRequest, ConsumerSetPriorityRequest, }; use crate::producer::{Producer, ProducerId, ProducerStat, ProducerType, WeakProducer}; -use crate::rtp_parameters::{MediaKind, MimeType, RtpCapabilities, RtpParameters}; -use crate::scalability_modes::ScalabilityMode; +use crate::rtp_parameters::{ + MediaKind, MimeType, RtpCapabilities, RtpEncodingParameters, RtpParameters, +}; use crate::transport::Transport; use crate::uuid_based_wrapper_type; -use crate::worker::{Channel, PayloadChannel, RequestError, SubscriptionHandler}; +use crate::worker::{Channel, NotificationParseError, RequestError, SubscriptionHandler}; use async_executor::Executor; use event_listener_primitives::{Bag, BagOnce, HandlerId}; use log::{debug, error}; +use mediasoup_sys::fbs::{ + consumer, notification, response, rtp_parameters, rtp_stream, rtx_stream, +}; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; +use std::error::Error; use std::fmt; use std::fmt::Debug; use std::sync::atomic::{AtomicBool, Ordering}; @@ -38,6 +43,22 @@ pub struct ConsumerLayers { pub temporal_layer: Option, } +impl ConsumerLayers { + pub(crate) fn to_fbs(self) -> consumer::ConsumerLayers { + consumer::ConsumerLayers { + spatial_layer: self.spatial_layer, + temporal_layer: self.temporal_layer, + } + } + + pub(crate) fn from_fbs(consumer_layers: consumer::ConsumerLayers) -> Self { + Self { + spatial_layer: consumer_layers.spatial_layer, + temporal_layer: consumer_layers.temporal_layer, + } + } +} + /// Score of consumer and corresponding producer. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -53,6 +74,16 @@ pub struct ConsumerScore { pub producer_scores: Vec, } +impl ConsumerScore { + pub(crate) fn from_fbs(consumer_score: consumer::ConsumerScore) -> Self { + Self { + score: consumer_score.score, + producer_score: consumer_score.producer_score, + producer_scores: consumer_score.producer_scores.into_iter().collect(), + } + } +} + /// [`Consumer`] options. #[derive(Debug, Clone)] #[non_exhaustive] @@ -77,6 +108,12 @@ pub struct ConsumerOptions { /// Preferred spatial and temporal layer for simulcast or SVC media sources. /// If `None`, the highest ones are selected. pub preferred_layers: Option, + /// Whether this Consumer should enable RTP retransmissions, storing sent RTP and processing the + /// incoming RTCP NACK from the remote Consumer. If not set it's true by default for video codecs + /// and false for audio codecs. If set to true, NACK will be enabled if both endpoints (mediasoup + /// and the remote Consumer) support NACK for this codec. When it comes to audio codecs, just + /// OPUS supports NACK. + pub enable_rtx: Option, /// Whether this Consumer should ignore DTX packets (only valid for Opus codec). /// If set, DTX packets are not forwarded to the remote Consumer. pub ignore_dtx: bool, @@ -96,6 +133,7 @@ impl ConsumerOptions { paused: false, preferred_layers: None, ignore_dtx: false, + enable_rtx: None, pipe: false, mid: None, app_data: AppData::default(), @@ -109,7 +147,7 @@ impl ConsumerOptions { pub struct RtpStreamParams { pub clock_rate: u32, pub cname: String, - pub encoding_idx: usize, + pub encoding_idx: u32, pub mime_type: MimeType, pub payload_type: u8, pub spatial_layers: u8, @@ -120,41 +158,85 @@ pub struct RtpStreamParams { pub use_nack: bool, pub use_pli: bool, pub rid: Option, - pub rtc_ssrc: Option, - pub rtc_payload_type: Option, + pub rtx_ssrc: Option, + pub rtx_payload_type: Option, +} + +impl RtpStreamParams { + pub(crate) fn from_fbs_ref( + params: rtp_stream::ParamsRef<'_>, + ) -> Result> { + Ok(Self { + clock_rate: params.clock_rate()?, + cname: params.cname()?.to_string(), + encoding_idx: params.encoding_idx()?, + mime_type: params.mime_type()?.parse()?, + payload_type: params.payload_type()?, + spatial_layers: params.spatial_layers()?, + ssrc: params.ssrc()?, + temporal_layers: params.temporal_layers()?, + use_dtx: params.use_dtx()?, + use_in_band_fec: params.use_in_band_fec()?, + use_nack: params.use_nack()?, + use_pli: params.use_pli()?, + rid: params.rid()?.map(|rid| rid.to_string()), + rtx_ssrc: params.rtx_ssrc()?, + rtx_payload_type: params.rtx_payload_type()?, + }) + } } #[derive(Debug, Clone, PartialOrd, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[doc(hidden)] -pub struct RtpStream { - pub params: RtpStreamParams, - pub score: u8, +pub struct RtxStreamParams { + pub clock_rate: u32, + pub cname: String, + pub mime_type: MimeType, + pub payload_type: u8, + pub ssrc: u32, + pub rrid: Option, +} + +impl RtxStreamParams { + pub(crate) fn from_fbs_ref( + params: rtx_stream::ParamsRef<'_>, + ) -> Result> { + Ok(Self { + clock_rate: params.clock_rate()?, + cname: params.cname()?.to_string(), + mime_type: params.mime_type()?.parse()?, + payload_type: params.payload_type()?, + ssrc: params.ssrc()?, + rrid: params.rrid()?.map(|rrid| rrid.to_string()), + }) + } } #[derive(Debug, Clone, PartialOrd, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[doc(hidden)] -pub struct RtpRtxParameters { - pub ssrc: Option, +pub struct RtpStream { + pub params: RtpStreamParams, + pub score: u8, +} + +impl RtpStream { + pub(crate) fn from_fbs_ref( + dump: rtp_stream::DumpRef<'_>, + ) -> Result> { + Ok(Self { + params: RtpStreamParams::from_fbs_ref(dump.params()?)?, + score: dump.score()?, + }) + } } -#[derive(Debug, Clone, PartialOrd, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialOrd, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[doc(hidden)] -pub struct ConsumableRtpEncoding { +pub struct RtpRtxParameters { pub ssrc: Option, - pub rid: Option, - pub codec_payload_type: Option, - pub rtx: Option, - pub max_bitrate: Option, - pub max_framerate: Option, - pub dtx: Option, - #[serde(default, skip_serializing_if = "ScalabilityMode::is_none")] - pub scalability_mode: ScalabilityMode, - pub spatial_layers: Option, - pub temporal_layers: Option, - pub ksvc: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] @@ -170,10 +252,10 @@ pub struct ConsumerDump { pub producer_paused: bool, pub rtp_parameters: RtpParameters, pub supported_codec_payload_types: Vec, - pub trace_event_types: String, + pub trace_event_types: Vec, pub r#type: ConsumerType, - pub consumable_rtp_encodings: Vec, - pub rtp_stream: RtpStream, + pub consumable_rtp_encodings: Vec, + pub rtp_streams: Vec, /// Essentially `Option` or `Option<-1>` pub preferred_spatial_layer: Option, /// Essentially `Option` or `Option<-1>` @@ -188,6 +270,53 @@ pub struct ConsumerDump { pub current_temporal_layer: Option, } +impl ConsumerDump { + pub(crate) fn from_fbs_ref( + dump: consumer::DumpResponseRef<'_>, + ) -> Result> { + let dump = dump.data(); + + Ok(Self { + id: dump?.base()?.id()?.parse()?, + kind: MediaKind::from_fbs(dump?.base()?.kind()?), + paused: dump?.base()?.paused()?, + priority: dump?.base()?.priority()?, + producer_id: dump?.base()?.producer_id()?.parse()?, + producer_paused: dump?.base()?.producer_paused()?, + rtp_parameters: RtpParameters::from_fbs_ref(dump?.base()?.rtp_parameters()?)?, + supported_codec_payload_types: Vec::from( + dump?.base()?.supported_codec_payload_types()?, + ), + trace_event_types: dump? + .base()? + .trace_event_types()? + .iter() + .map(|trace_event_type| Ok(ConsumerTraceEventType::from_fbs(trace_event_type?))) + .collect::>>()?, + r#type: ConsumerType::from_fbs(dump?.base()?.type_()?), + consumable_rtp_encodings: dump? + .base()? + .consumable_rtp_encodings()? + .iter() + .map(|encoding_parameters| { + RtpEncodingParameters::from_fbs_ref(encoding_parameters?) + }) + .collect::>>()?, + rtp_streams: dump? + .rtp_streams()? + .iter() + .map(|stream| RtpStream::from_fbs_ref(stream?)) + .collect::>>()?, + preferred_spatial_layer: dump?.preferred_spatial_layer()?, + target_spatial_layer: dump?.target_spatial_layer()?, + current_spatial_layer: dump?.current_spatial_layer()?, + preferred_temporal_layer: dump?.preferred_temporal_layer()?, + target_temporal_layer: dump?.target_temporal_layer()?, + current_temporal_layer: dump?.current_temporal_layer()?, + }) + } +} + /// Consumer type. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] @@ -213,6 +342,26 @@ impl From for ConsumerType { } } +impl ConsumerType { + pub(crate) fn to_fbs(self) -> rtp_parameters::Type { + match self { + ConsumerType::Simple => rtp_parameters::Type::Simple, + ConsumerType::Simulcast => rtp_parameters::Type::Simulcast, + ConsumerType::Svc => rtp_parameters::Type::Svc, + ConsumerType::Pipe => rtp_parameters::Type::Pipe, + } + } + + pub(crate) fn from_fbs(r#type: rtp_parameters::Type) -> ConsumerType { + match r#type { + rtp_parameters::Type::Simple => ConsumerType::Simple, + rtp_parameters::Type::Simulcast => ConsumerType::Simulcast, + rtp_parameters::Type::Svc => ConsumerType::Svc, + rtp_parameters::Type::Pipe => ConsumerType::Pipe, + } + } +} + /// RTC statistics of the consumer alone. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -226,22 +375,56 @@ pub struct ConsumerStat { pub rtx_ssrc: Option, pub kind: MediaKind, pub mime_type: MimeType, - pub packets_lost: u32, + pub packets_lost: u64, pub fraction_lost: u8, - pub packets_discarded: usize, - pub packets_retransmitted: usize, - pub packets_repaired: usize, - pub nack_count: usize, - pub nack_packet_count: usize, - pub pli_count: usize, - pub fir_count: usize, + pub packets_discarded: u64, + pub packets_retransmitted: u64, + pub packets_repaired: u64, + pub nack_count: u64, + pub nack_packet_count: u64, + pub pli_count: u64, + pub fir_count: u64, pub score: u8, - pub packet_count: usize, - pub byte_count: usize, + pub packet_count: u64, + pub byte_count: u64, pub bitrate: u32, pub round_trip_time: Option, } +impl ConsumerStat { + pub(crate) fn from_fbs(stats: &rtp_stream::Stats) -> Self { + let rtp_stream::StatsData::SendStats(ref stats) = stats.data else { + panic!("Wrong message from worker"); + }; + + let rtp_stream::StatsData::BaseStats(ref base) = stats.base.data else { + panic!("Wrong message from worker"); + }; + + Self { + timestamp: base.timestamp, + ssrc: base.ssrc, + rtx_ssrc: base.rtx_ssrc, + kind: MediaKind::from_fbs(base.kind), + mime_type: base.mime_type.to_string().parse().unwrap(), + packets_lost: base.packets_lost, + fraction_lost: base.fraction_lost, + packets_discarded: base.packets_discarded, + packets_retransmitted: base.packets_retransmitted, + packets_repaired: base.packets_repaired, + nack_count: base.nack_count, + nack_packet_count: base.nack_packet_count, + pli_count: base.pli_count, + fir_count: base.fir_count, + score: base.score, + packet_count: stats.packet_count, + byte_count: stats.byte_count, + bitrate: stats.bitrate, + round_trip_time: Some(base.round_trip_time), + } + } +} + /// RTC statistics of the consumer, may or may not include producer statistics. #[allow(clippy::large_enum_variant)] #[derive(Debug, Deserialize, Serialize)] @@ -312,6 +495,61 @@ pub enum ConsumerTraceEventData { }, } +impl ConsumerTraceEventData { + pub(crate) fn from_fbs(data: consumer::TraceNotification) -> Self { + match data.type_ { + consumer::TraceEventType::Rtp => ConsumerTraceEventData::Rtp { + timestamp: data.timestamp, + direction: TraceEventDirection::from_fbs(data.direction), + info: { + let Some(consumer::TraceInfo::RtpTraceInfo(info)) = data.info else { + panic!("Wrong message from worker: {data:?}"); + }; + + RtpPacketTraceInfo::from_fbs(*info.rtp_packet, info.is_rtx) + }, + }, + consumer::TraceEventType::Keyframe => ConsumerTraceEventData::KeyFrame { + timestamp: data.timestamp, + direction: TraceEventDirection::from_fbs(data.direction), + info: { + let Some(consumer::TraceInfo::KeyFrameTraceInfo(info)) = data.info else { + panic!("Wrong message from worker: {data:?}"); + }; + + RtpPacketTraceInfo::from_fbs(*info.rtp_packet, info.is_rtx) + }, + }, + consumer::TraceEventType::Nack => ConsumerTraceEventData::Nack { + timestamp: data.timestamp, + direction: TraceEventDirection::from_fbs(data.direction), + }, + consumer::TraceEventType::Pli => ConsumerTraceEventData::Pli { + timestamp: data.timestamp, + direction: TraceEventDirection::from_fbs(data.direction), + info: { + let Some(consumer::TraceInfo::PliTraceInfo(info)) = data.info else { + panic!("Wrong message from worker: {data:?}"); + }; + + SsrcTraceInfo { ssrc: info.ssrc } + }, + }, + consumer::TraceEventType::Fir => ConsumerTraceEventData::Fir { + timestamp: data.timestamp, + direction: TraceEventDirection::from_fbs(data.direction), + info: { + let Some(consumer::TraceInfo::FirTraceInfo(info)) = data.info else { + panic!("Wrong message from worker: {data:?}"); + }; + + SsrcTraceInfo { ssrc: info.ssrc } + }, + }, + } + } +} + /// Types of consumer trace events. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] @@ -328,21 +566,102 @@ pub enum ConsumerTraceEventType { Fir, } +impl ConsumerTraceEventType { + pub(crate) fn to_fbs(self) -> consumer::TraceEventType { + match self { + ConsumerTraceEventType::Rtp => consumer::TraceEventType::Rtp, + ConsumerTraceEventType::KeyFrame => consumer::TraceEventType::Keyframe, + ConsumerTraceEventType::Nack => consumer::TraceEventType::Nack, + ConsumerTraceEventType::Pli => consumer::TraceEventType::Pli, + ConsumerTraceEventType::Fir => consumer::TraceEventType::Fir, + } + } + + pub(crate) fn from_fbs(event_type: consumer::TraceEventType) -> Self { + match event_type { + consumer::TraceEventType::Rtp => ConsumerTraceEventType::Rtp, + consumer::TraceEventType::Keyframe => ConsumerTraceEventType::KeyFrame, + consumer::TraceEventType::Nack => ConsumerTraceEventType::Nack, + consumer::TraceEventType::Pli => ConsumerTraceEventType::Pli, + consumer::TraceEventType::Fir => ConsumerTraceEventType::Fir, + } + } +} #[derive(Debug, Deserialize)] #[serde(tag = "event", rename_all = "lowercase", content = "data")] enum Notification { ProducerClose, ProducerPause, ProducerResume, + Rtp(Vec), Score(ConsumerScore), LayersChange(Option), Trace(ConsumerTraceEventData), } -#[derive(Debug, Deserialize)] -#[serde(tag = "event", rename_all = "lowercase", content = "data")] -enum PayloadNotification { - Rtp, +impl Notification { + pub(crate) fn from_fbs( + notification: notification::NotificationRef<'_>, + ) -> Result { + match notification.event().unwrap() { + notification::Event::ConsumerProducerClose => Ok(Notification::ProducerClose), + notification::Event::ConsumerProducerPause => Ok(Notification::ProducerPause), + notification::Event::ConsumerProducerResume => Ok(Notification::ProducerResume), + notification::Event::ConsumerRtp => { + let Ok(Some(notification::BodyRef::ConsumerRtpNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let rtp_notification_fbs = consumer::RtpNotification::try_from(body).unwrap(); + + Ok(Notification::Rtp(rtp_notification_fbs.data)) + } + notification::Event::ConsumerScore => { + let Ok(Some(notification::BodyRef::ConsumerScoreNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let score_fbs = consumer::ConsumerScore::try_from(body.score().unwrap()).unwrap(); + let score = ConsumerScore::from_fbs(score_fbs); + + Ok(Notification::Score(score)) + } + notification::Event::ConsumerLayersChange => { + let Ok(Some(notification::BodyRef::ConsumerLayersChangeNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + match body.layers().unwrap() { + Some(layers) => { + let layers_fbs = consumer::ConsumerLayers::try_from(layers).unwrap(); + let layers = ConsumerLayers::from_fbs(layers_fbs); + + Ok(Notification::LayersChange(Some(layers))) + } + None => Ok(Notification::LayersChange(None)), + } + } + notification::Event::ConsumerTrace => { + let Ok(Some(notification::BodyRef::ConsumerTraceNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let trace_notification_fbs = consumer::TraceNotification::try_from(body).unwrap(); + let trace_notification = ConsumerTraceEventData::from_fbs(trace_notification_fbs); + + Ok(Notification::Trace(trace_notification)) + } + _ => Err(NotificationParseError::InvalidEvent), + } + } } #[derive(Default)] @@ -461,7 +780,6 @@ impl Consumer { paused: bool, executor: Arc>, channel: Channel, - payload_channel: &PayloadChannel, producer_paused: bool, score: ConsumerScore, preferred_layers: Option, @@ -490,7 +808,7 @@ impl Consumer { let inner_weak = Arc::clone(&inner_weak); channel.subscribe_to_notifications(id.into(), move |notification| { - match serde_json::from_slice::(notification) { + match Notification::from_fbs(notification) { Ok(notification) => match notification { Notification::ProducerClose => { if !closed.load(Ordering::SeqCst) { @@ -513,27 +831,31 @@ impl Consumer { } Notification::ProducerPause => { let mut producer_paused = producer_paused.lock(); - let was_paused = *paused.lock() || *producer_paused; + let paused = *paused.lock(); *producer_paused = true; handlers.producer_pause.call_simple(); - if !was_paused { + if !paused { handlers.pause.call_simple(); } } Notification::ProducerResume => { let mut producer_paused = producer_paused.lock(); let paused = *paused.lock(); - let was_paused = paused || *producer_paused; *producer_paused = false; handlers.producer_resume.call_simple(); - if was_paused && !paused { + if !paused { handlers.resume.call_simple(); } } + Notification::Rtp(data) => { + handlers.rtp.call(|callback| { + callback(&data); + }); + } Notification::Score(consumer_score) => { *score.lock() = consumer_score.clone(); handlers.score.call_simple(&consumer_score); @@ -553,25 +875,6 @@ impl Consumer { }) }; - let payload_subscription_handler = { - let handlers = Arc::clone(&handlers); - - payload_channel.subscribe_to_notifications(id.into(), move |message, payload| { - match serde_json::from_slice::(message) { - Ok(notification) => match notification { - PayloadNotification::Rtp => { - handlers.rtp.call(|callback| { - callback(payload); - }); - } - }, - Err(error) => { - error!("Failed to parse payload notification: {}", error); - } - } - }) - }; - let on_transport_close_handler = transport.on_close({ let inner_weak = Arc::clone(&inner_weak); @@ -602,10 +905,7 @@ impl Consumer { transport, weak_producer: producer.downgrade(), closed, - _subscription_handlers: Mutex::new(vec![ - subscription_handler, - payload_subscription_handler, - ]), + _subscription_handlers: Mutex::new(vec![subscription_handler]), _on_transport_close_handler: Mutex::new(on_transport_close_handler), }); @@ -723,10 +1023,30 @@ impl Consumer { pub async fn get_stats(&self) -> Result { debug!("get_stats()"); - self.inner + let response = self + .inner .channel .request(self.id(), ConsumerGetStatsRequest {}) - .await + .await?; + + if let response::Body::ConsumerGetStatsResponse(data) = response { + match data.stats.len() { + 0 => panic!("Empty stats response from worker"), + 1 => { + let consumer_stat = ConsumerStat::from_fbs(&data.stats[0]); + + Ok(ConsumerStats::JustConsumer((consumer_stat,))) + } + _ => { + let consumer_stat = ConsumerStat::from_fbs(&data.stats[0]); + let producer_stat = ProducerStat::from_fbs(&data.stats[1]); + + Ok(ConsumerStats::WithProducer((consumer_stat, producer_stat))) + } + } + } else { + panic!("Wrong message from worker"); + } } /// Pauses the consumer (no RTP is sent to the consuming endpoint). diff --git a/rust/src/router/consumer/tests.rs b/rust/src/router/consumer/tests.rs index 62d7a69b4c..eaf36003db 100644 --- a/rust/src/router/consumer/tests.rs +++ b/rust/src/router/consumer/tests.rs @@ -1,5 +1,5 @@ use crate::consumer::ConsumerOptions; -use crate::data_structures::ListenIp; +use crate::data_structures::{ListenInfo, Protocol}; use crate::producer::ProducerOptions; use crate::router::{Router, RouterOptions}; use crate::rtp_parameters::{ @@ -7,7 +7,9 @@ use crate::rtp_parameters::{ RtpCodecParametersParameters, RtpParameters, }; use crate::transport::Transport; -use crate::webrtc_transport::{TransportListenIps, WebRtcTransport, WebRtcTransportOptions}; +use crate::webrtc_transport::{ + WebRtcTransport, WebRtcTransportListenInfos, WebRtcTransportOptions, +}; use crate::worker::WorkerSettings; use crate::worker_manager::WorkerManager; use futures_lite::future; @@ -79,10 +81,17 @@ async fn init() -> (Router, WebRtcTransport, WebRtcTransport) { .await .expect("Failed to create router"); - let transport_options = WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - })); + let transport_options = + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })); let transport_1 = router .create_webrtc_transport(transport_options.clone()) diff --git a/rust/src/router/data_consumer.rs b/rust/src/router/data_consumer.rs index 5477e31400..e813f5e858 100644 --- a/rust/src/router/data_consumer.rs +++ b/rust/src/router/data_consumer.rs @@ -4,20 +4,25 @@ mod tests; use crate::data_producer::{DataProducer, DataProducerId, WeakDataProducer}; use crate::data_structures::{AppData, WebRtcMessage}; use crate::messages::{ - DataConsumerCloseRequest, DataConsumerDumpRequest, DataConsumerGetBufferedAmountRequest, - DataConsumerGetStatsRequest, DataConsumerSendRequest, - DataConsumerSetBufferedAmountLowThresholdRequest, + DataConsumerAddSubchannelRequest, DataConsumerCloseRequest, DataConsumerDumpRequest, + DataConsumerGetBufferedAmountRequest, DataConsumerGetStatsRequest, DataConsumerPauseRequest, + DataConsumerRemoveSubchannelRequest, DataConsumerResumeRequest, DataConsumerSendRequest, + DataConsumerSetBufferedAmountLowThresholdRequest, DataConsumerSetSubchannelsRequest, }; use crate::sctp_parameters::SctpStreamParameters; use crate::transport::Transport; use crate::uuid_based_wrapper_type; -use crate::worker::{Channel, PayloadChannel, RequestError, SubscriptionHandler}; +use crate::worker::{Channel, NotificationParseError, RequestError, SubscriptionHandler}; use async_executor::Executor; use event_listener_primitives::{Bag, BagOnce, HandlerId}; use log::{debug, error}; +use mediasoup_sys::fbs::{data_consumer, data_producer, notification, response}; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use std::borrow::Cow; +use std::error::Error; +// TODO. +// use std::borrow::Cow; use std::fmt; use std::fmt::Debug; use std::sync::atomic::{AtomicBool, Ordering}; @@ -50,6 +55,12 @@ pub struct DataConsumerOptions { /// Defaults to the value in the [`DataProducer`](crate::data_producer::DataProducer) if it /// has type `Sctp` or unset if it has type `Direct`. pub(super) max_retransmits: Option, + /// Whether the DataConsumer must start in paused mode. Default false. + pub paused: bool, + /// Subchannels this DataConsumer initially subscribes to. + /// Only used in case this DataConsumer receives messages from a local DataProducer + /// that specifies subchannel(s) when calling send(). + pub subchannels: Option>, /// Custom application data. pub app_data: AppData, } @@ -64,18 +75,22 @@ impl DataConsumerOptions { ordered: None, max_packet_life_time: None, max_retransmits: None, + subchannels: None, + paused: false, app_data: AppData::default(), } } /// For [`DirectTransport`](crate::direct_transport::DirectTransport). #[must_use] - pub fn new_direct(data_producer_id: DataProducerId) -> Self { + pub fn new_direct(data_producer_id: DataProducerId, subchannels: Option>) -> Self { Self { data_producer_id, ordered: Some(true), max_packet_life_time: None, max_retransmits: None, + paused: false, + subchannels, app_data: AppData::default(), } } @@ -88,6 +103,8 @@ impl DataConsumerOptions { ordered: None, max_packet_life_time: None, max_retransmits: None, + paused: false, + subchannels: None, app_data: AppData::default(), } } @@ -104,6 +121,8 @@ impl DataConsumerOptions { ordered: None, max_packet_life_time: Some(max_packet_life_time), max_retransmits: None, + paused: false, + subchannels: None, app_data: AppData::default(), } } @@ -119,6 +138,8 @@ impl DataConsumerOptions { ordered: None, max_packet_life_time: None, max_retransmits: Some(max_retransmits), + paused: false, + subchannels: None, app_data: AppData::default(), } } @@ -136,6 +157,34 @@ pub struct DataConsumerDump { pub protocol: String, pub sctp_stream_parameters: Option, pub buffered_amount_low_threshold: u32, + pub paused: bool, + pub subchannels: Vec, + pub data_producer_paused: bool, +} + +impl DataConsumerDump { + pub(crate) fn from_fbs( + dump: data_consumer::DumpResponse, + ) -> Result> { + Ok(Self { + id: dump.id.parse()?, + data_producer_id: dump.data_producer_id.parse()?, + r#type: if dump.type_ == data_producer::Type::Sctp { + DataConsumerType::Sctp + } else { + DataConsumerType::Direct + }, + label: dump.label, + protocol: dump.protocol, + sctp_stream_parameters: dump + .sctp_stream_parameters + .map(|parameters| SctpStreamParameters::from_fbs(*parameters)), + buffered_amount_low_threshold: dump.buffered_amount_low_threshold, + paused: dump.paused, + subchannels: dump.subchannels, + data_producer_paused: dump.data_producer_paused, + }) + } } /// RTC statistics of the data consumer. @@ -148,11 +197,24 @@ pub struct DataConsumerStat { pub timestamp: u64, pub label: String, pub protocol: String, - pub messages_sent: usize, - pub bytes_sent: usize, + pub messages_sent: u64, + pub bytes_sent: u64, pub buffered_amount: u32, } +impl DataConsumerStat { + pub(crate) fn from_fbs(stats: &data_consumer::GetStatsResponse) -> Self { + Self { + timestamp: stats.timestamp, + label: stats.label.to_string(), + protocol: stats.protocol.to_string(), + messages_sent: stats.messages_sent, + bytes_sent: stats.bytes_sent, + buffered_amount: stats.buffered_amount, + } + } +} + /// Data consumer type. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] @@ -167,17 +229,63 @@ pub enum DataConsumerType { #[serde(tag = "event", rename_all = "lowercase", content = "data")] enum Notification { DataProducerClose, + DataProducerPause, + DataProducerResume, SctpSendBufferFull, + Message { + ppid: u32, + data: Vec, + }, #[serde(rename_all = "camelCase")] BufferedAmountLow { buffered_amount: u32, }, } -#[derive(Debug, Deserialize)] -#[serde(tag = "event", rename_all = "lowercase", content = "data")] -enum PayloadNotification { - Message { ppid: u32 }, +impl Notification { + pub(crate) fn from_fbs( + notification: notification::NotificationRef<'_>, + ) -> Result { + match notification.event().unwrap() { + notification::Event::DataconsumerDataproducerClose => { + Ok(Notification::DataProducerClose) + } + notification::Event::DataconsumerDataproducerPause => { + Ok(Notification::DataProducerPause) + } + notification::Event::DataconsumerDataproducerResume => { + Ok(Notification::DataProducerResume) + } + notification::Event::DataconsumerSctpSendbufferFull => { + Ok(Notification::SctpSendBufferFull) + } + notification::Event::DataconsumerMessage => { + let Ok(Some(notification::BodyRef::DataConsumerMessageNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + Ok(Notification::Message { + ppid: body.ppid().unwrap(), + data: body.data().unwrap().into(), + }) + } + notification::Event::DataconsumerBufferedAmountLow => { + let Ok(Some(notification::BodyRef::DataConsumerBufferedAmountLowNotification( + body, + ))) = notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + Ok(Notification::BufferedAmountLow { + buffered_amount: body.buffered_amount().unwrap(), + }) + } + _ => Err(NotificationParseError::InvalidEvent), + } + } } #[derive(Default)] @@ -187,6 +295,10 @@ struct Handlers { sctp_send_buffer_full: Bag>, buffered_amount_low: Bag>, data_producer_close: BagOnce>, + pause: Bag>, + resume: Bag>, + data_producer_pause: Bag>, + data_producer_resume: Bag>, transport_close: BagOnce>, close: BagOnce>, } @@ -199,9 +311,11 @@ struct Inner { protocol: String, data_producer_id: DataProducerId, direct: bool, + paused: Arc>, + subchannels: Arc>>, + data_producer_paused: Arc>, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, handlers: Arc, app_data: AppData, transport: Arc, @@ -267,6 +381,9 @@ impl fmt::Debug for RegularDataConsumer { .field("label", &self.inner.label) .field("protocol", &self.inner.protocol) .field("data_producer_id", &self.inner.data_producer_id) + .field("paused", &self.inner.paused) + .field("data_producer_paused", &self.inner.data_producer_paused) + .field("subchannels", &self.inner.subchannels) .field("transport", &self.inner.transport) .field("closed", &self.inner.closed) .finish() @@ -295,6 +412,9 @@ impl fmt::Debug for DirectDataConsumer { .field("label", &self.inner.label) .field("protocol", &self.inner.protocol) .field("data_producer_id", &self.inner.data_producer_id) + .field("paused", &self.inner.paused) + .field("data_producer_paused", &self.inner.data_producer_paused) + .field("subchannels", &self.inner.subchannels) .field("transport", &self.inner.transport) .field("closed", &self.inner.closed) .finish() @@ -342,10 +462,12 @@ impl DataConsumer { sctp_stream_parameters: Option, label: String, protocol: String, + paused: bool, data_producer: DataProducer, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, + data_producer_paused: bool, + subchannels: Vec, app_data: AppData, transport: Arc, direct: bool, @@ -354,15 +476,21 @@ impl DataConsumer { let handlers = Arc::::default(); let closed = Arc::new(AtomicBool::new(false)); + let paused = Arc::new(Mutex::new(paused)); + #[allow(clippy::mutex_atomic)] + let data_producer_paused = Arc::new(Mutex::new(data_producer_paused)); + let subchannels = Arc::new(Mutex::new(subchannels)); let inner_weak = Arc::>>>::default(); let subscription_handler = { let handlers = Arc::clone(&handlers); let closed = Arc::clone(&closed); + let paused = Arc::clone(&paused); + let data_producer_paused = Arc::clone(&data_producer_paused); let inner_weak = Arc::clone(&inner_weak); channel.subscribe_to_notifications(id.into(), move |notification| { - match serde_json::from_slice::(notification) { + match Notification::from_fbs(notification) { Ok(notification) => match notification { Notification::DataProducerClose => { if !closed.load(Ordering::SeqCst) { @@ -383,30 +511,33 @@ impl DataConsumer { } } } - Notification::SctpSendBufferFull => { - handlers.sctp_send_buffer_full.call_simple(); - } - Notification::BufferedAmountLow { buffered_amount } => { - handlers.buffered_amount_low.call(|callback| { - callback(buffered_amount); - }); + Notification::DataProducerPause => { + let mut data_producer_paused = data_producer_paused.lock(); + let paused = *paused.lock(); + *data_producer_paused = true; + + handlers.data_producer_pause.call_simple(); + + if !paused { + handlers.pause.call_simple(); + } } - }, - Err(error) => { - error!("Failed to parse notification: {}", error); - } - } - }) - }; + Notification::DataProducerResume => { + let mut data_producer_paused = data_producer_paused.lock(); + let paused = *paused.lock(); + *data_producer_paused = false; - let payload_subscription_handler = { - let handlers = Arc::clone(&handlers); + handlers.data_producer_resume.call_simple(); - payload_channel.subscribe_to_notifications(id.into(), move |message, payload| { - match serde_json::from_slice::(message) { - Ok(notification) => match notification { - PayloadNotification::Message { ppid } => { - match WebRtcMessage::new(ppid, Cow::from(payload)) { + if !paused { + handlers.resume.call_simple(); + } + } + Notification::SctpSendBufferFull => { + handlers.sctp_send_buffer_full.call_simple(); + } + Notification::Message { ppid, data } => { + match WebRtcMessage::new(ppid, Cow::from(data)) { Ok(message) => { handlers.message.call(|callback| { callback(&message); @@ -417,9 +548,14 @@ impl DataConsumer { } } } + Notification::BufferedAmountLow { buffered_amount } => { + handlers.buffered_amount_low.call(|callback| { + callback(buffered_amount); + }); + } }, Err(error) => { - error!("Failed to parse payload notification: {}", error); + error!("Failed to parse notification: {}", error); } } }) @@ -443,19 +579,18 @@ impl DataConsumer { label, protocol, data_producer_id: data_producer.id(), + paused, + data_producer_paused, direct, executor, channel, - payload_channel, handlers, + subchannels, app_data, transport, weak_data_producer: data_producer.downgrade(), closed, - _subscription_handlers: Mutex::new(vec![ - subscription_handler, - payload_subscription_handler, - ]), + _subscription_handlers: Mutex::new(vec![subscription_handler]), _on_transport_close_handler: Mutex::new(on_transport_close_handler), }); @@ -491,6 +626,19 @@ impl DataConsumer { self.inner().r#type } + /// Whether the data consumer is paused. It does not take into account whether the + /// associated data producer is paused. + #[must_use] + pub fn paused(&self) -> bool { + *self.inner().paused.lock() + } + + /// Whether the associate data producer is paused. + #[must_use] + pub fn producer_paused(&self) -> bool { + *self.inner().data_producer_paused.lock() + } + /// The SCTP stream parameters (just if the data consumer type is `Sctp`). #[must_use] pub fn sctp_stream_parameters(&self) -> Option { @@ -509,6 +657,12 @@ impl DataConsumer { &self.inner().protocol } + /// The data consumer subchannels. + #[must_use] + pub fn subchannels(&self) -> Vec { + self.inner().subchannels.lock().clone() + } + /// Custom application data. #[must_use] pub fn app_data(&self) -> &AppData { @@ -526,10 +680,17 @@ impl DataConsumer { pub async fn dump(&self) -> Result { debug!("dump()"); - self.inner() + let response = self + .inner() .channel .request(self.id(), DataConsumerDumpRequest {}) - .await + .await?; + + if let response::Body::DataConsumerDumpResponse(data) = response { + Ok(DataConsumerDump::from_fbs(*data).expect("Error parsing dump response")) + } else { + panic!("Wrong message from worker"); + } } /// Returns current statistics of the data consumer. @@ -539,10 +700,57 @@ impl DataConsumer { pub async fn get_stats(&self) -> Result, RequestError> { debug!("get_stats()"); - self.inner() + let response = self + .inner() .channel .request(self.id(), DataConsumerGetStatsRequest {}) - .await + .await?; + + if let response::Body::DataConsumerGetStatsResponse(data) = response { + Ok(vec![DataConsumerStat::from_fbs(&data)]) + } else { + panic!("Wrong message from worker"); + } + } + + /// Pauses the data consumer (no mossage is sent to the consuming endpoint). + pub async fn pause(&self) -> Result<(), RequestError> { + debug!("pause()"); + + self.inner() + .channel + .request(self.id(), DataConsumerPauseRequest {}) + .await?; + + let mut paused = self.inner().paused.lock(); + let was_paused = *paused || *self.inner().data_producer_paused.lock(); + *paused = true; + + if !was_paused { + self.inner().handlers.pause.call_simple(); + } + + Ok(()) + } + + /// Resumes the data consumer (messages are sent again to the consuming endpoint). + pub async fn resume(&self) -> Result<(), RequestError> { + debug!("resume()"); + + self.inner() + .channel + .request(self.id(), DataConsumerResumeRequest {}) + .await?; + + let mut paused = self.inner().paused.lock(); + let was_paused = *paused || *self.inner().data_producer_paused.lock(); + *paused = false; + + if was_paused { + self.inner().handlers.resume.call_simple(); + } + + Ok(()) } /// Returns the number of bytes of data currently buffered to be sent over the underlying SCTP @@ -584,6 +792,48 @@ impl DataConsumer { .await } + /// Sets subchannels to the worker DataConsumer. + pub async fn set_subchannels(&self, subchannels: Vec) -> Result<(), RequestError> { + let response = self + .inner() + .channel + .request(self.id(), DataConsumerSetSubchannelsRequest { subchannels }) + .await?; + + *self.inner().subchannels.lock() = response.subchannels; + + Ok(()) + } + + /// Adds a subchannel to the worker DataConsumer. + pub async fn add_subchannel(&self, subchannel: u16) -> Result<(), RequestError> { + let response = self + .inner() + .channel + .request(self.id(), DataConsumerAddSubchannelRequest { subchannel }) + .await?; + + *self.inner().subchannels.lock() = response.subchannels; + + Ok(()) + } + + /// Removes a subchannel to the worker DataConsumer. + pub async fn remove_subchannel(&self, subchannel: u16) -> Result<(), RequestError> { + let response = self + .inner() + .channel + .request( + self.id(), + DataConsumerRemoveSubchannelRequest { subchannel }, + ) + .await?; + + *self.inner().subchannels.lock() = response.subchannels; + + Ok(()) + } + /// Callback is called when a message has been received from the corresponding data producer. /// /// # Notes on usage @@ -631,6 +881,40 @@ impl DataConsumer { .add(Box::new(callback)) } + /// Callback is called when the data consumer or its associated data producer is + /// paused and, as result, the data consumer becomes paused. + pub fn on_pause(&self, callback: F) -> HandlerId { + self.inner().handlers.pause.add(Arc::new(callback)) + } + + /// Callback is called when the data consumer or its associated data producer is + /// resumed and, as result, the data consumer is no longer paused. + pub fn on_resume(&self, callback: F) -> HandlerId { + self.inner().handlers.resume.add(Arc::new(callback)) + } + + /// Callback is called when the associated data producer is paused. + pub fn on_data_producer_pause( + &self, + callback: F, + ) -> HandlerId { + self.inner() + .handlers + .data_producer_pause + .add(Arc::new(callback)) + } + + /// Callback is called when the associated data producer is resumed. + pub fn on_data_producer_resume( + &self, + callback: F, + ) -> HandlerId { + self.inner() + .handlers + .data_producer_resume + .add(Arc::new(callback)) + } + /// Callback is called when the transport this data consumer belongs to is closed for whatever /// reason. The data consumer itself is also closed. pub fn on_transport_close(&self, callback: F) -> HandlerId { @@ -673,11 +957,13 @@ impl DirectDataConsumer { let (ppid, payload) = message.into_ppid_and_payload(); self.inner - .payload_channel + .channel .request( self.inner.id, - DataConsumerSendRequest { ppid }, - payload.into_owned(), + DataConsumerSendRequest { + ppid, + payload: payload.into_owned(), + }, ) .await } diff --git a/rust/src/router/data_consumer/tests.rs b/rust/src/router/data_consumer/tests.rs index d12a876eb7..5edcb94537 100644 --- a/rust/src/router/data_consumer/tests.rs +++ b/rust/src/router/data_consumer/tests.rs @@ -1,11 +1,11 @@ use crate::data_consumer::DataConsumerOptions; use crate::data_producer::{DataProducer, DataProducerOptions}; -use crate::data_structures::ListenIp; +use crate::data_structures::{ListenInfo, Protocol}; use crate::plain_transport::PlainTransportOptions; use crate::router::{Router, RouterOptions}; use crate::sctp_parameters::SctpStreamParameters; use crate::transport::Transport; -use crate::webrtc_transport::{TransportListenIps, WebRtcTransportOptions}; +use crate::webrtc_transport::{WebRtcTransportListenInfos, WebRtcTransportOptions}; use crate::worker::WorkerSettings; use crate::worker_manager::WorkerManager; use futures_lite::future; @@ -36,9 +36,15 @@ async fn init() -> (Router, DataProducer) { let transport = router .create_webrtc_transport({ let mut transport_options = - WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, })); transport_options.enable_sctp = true; @@ -65,9 +71,15 @@ fn data_producer_close_event() { let transport2 = router .create_plain_transport({ - let mut transport_options = PlainTransportOptions::new(ListenIp { + let mut transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); transport_options.enable_sctp = true; @@ -114,9 +126,15 @@ fn transport_close_event() { let transport2 = router .create_plain_transport({ - let mut transport_options = PlainTransportOptions::new(ListenIp { + let mut transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); transport_options.enable_sctp = true; diff --git a/rust/src/router/data_producer.rs b/rust/src/router/data_producer.rs index 64e9f2ad55..590c4c8a6a 100644 --- a/rust/src/router/data_producer.rs +++ b/rust/src/router/data_producer.rs @@ -4,17 +4,19 @@ mod tests; use crate::data_structures::{AppData, WebRtcMessage}; use crate::messages::{ DataProducerCloseRequest, DataProducerDumpRequest, DataProducerGetStatsRequest, - DataProducerSendNotification, + DataProducerPauseRequest, DataProducerResumeRequest, DataProducerSendNotification, }; use crate::sctp_parameters::SctpStreamParameters; use crate::transport::Transport; use crate::uuid_based_wrapper_type; -use crate::worker::{Channel, NotificationError, PayloadChannel, RequestError}; +use crate::worker::{Channel, NotificationError, RequestError}; use async_executor::Executor; -use event_listener_primitives::{BagOnce, HandlerId}; +use event_listener_primitives::{Bag, BagOnce, HandlerId}; use log::{debug, error}; +use mediasoup_sys::fbs::{data_producer, response}; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; +use std::error::Error; use std::fmt; use std::fmt::Debug; use std::sync::atomic::{AtomicBool, Ordering}; @@ -41,6 +43,8 @@ pub struct DataProducerOptions { pub label: String, /// Name of the sub-protocol used by this DataChannel. pub protocol: String, + /// Whether the data producer must start in paused mode. Default false. + pub paused: bool, /// Custom application data. pub app_data: AppData, } @@ -56,6 +60,7 @@ impl DataProducerOptions { sctp_stream_parameters: Some(sctp_stream_parameters), label: "".to_string(), protocol: "".to_string(), + paused: false, app_data: AppData::default(), } } @@ -68,6 +73,7 @@ impl DataProducerOptions { sctp_stream_parameters: Some(sctp_stream_parameters), label: "".to_string(), protocol: "".to_string(), + paused: false, app_data: AppData::default(), } } @@ -80,6 +86,7 @@ impl DataProducerOptions { sctp_stream_parameters: None, label: "".to_string(), protocol: "".to_string(), + paused: false, app_data: AppData::default(), } } @@ -105,6 +112,28 @@ pub struct DataProducerDump { pub label: String, pub protocol: String, pub sctp_stream_parameters: Option, + pub paused: bool, +} + +impl DataProducerDump { + pub(crate) fn from_fbs( + dump: data_producer::DumpResponse, + ) -> Result> { + Ok(Self { + id: dump.id.parse()?, + r#type: if dump.type_ == data_producer::Type::Sctp { + DataProducerType::Sctp + } else { + DataProducerType::Direct + }, + label: dump.label, + protocol: dump.protocol, + sctp_stream_parameters: dump + .sctp_stream_parameters + .map(|parameters| SctpStreamParameters::from_fbs(*parameters)), + paused: dump.paused, + }) + } } /// RTC statistics of the data producer. @@ -117,13 +146,27 @@ pub struct DataProducerStat { pub timestamp: u64, pub label: String, pub protocol: String, - pub messages_received: usize, - pub bytes_received: usize, + pub messages_received: u64, + pub bytes_received: u64, +} + +impl DataProducerStat { + pub(crate) fn from_fbs(stats: &data_producer::GetStatsResponse) -> Self { + Self { + timestamp: stats.timestamp, + label: stats.label.to_string(), + protocol: stats.protocol.to_string(), + messages_received: stats.messages_received, + bytes_received: stats.bytes_received, + } + } } #[derive(Default)] #[allow(clippy::type_complexity)] struct Handlers { + pause: Bag>, + resume: Bag>, transport_close: BagOnce>, close: BagOnce>, } @@ -134,10 +177,10 @@ struct Inner { sctp_stream_parameters: Option, label: String, protocol: String, + paused: AtomicBool, direct: bool, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, handlers: Arc, app_data: AppData, transport: Arc, @@ -194,6 +237,7 @@ impl fmt::Debug for RegularDataProducer { .field("sctp_stream_parameters", &self.inner.sctp_stream_parameters) .field("label", &self.inner.label) .field("protocol", &self.inner.protocol) + .field("paused", &self.inner.paused) .field("transport", &self.inner.transport) .field("closed", &self.inner.closed) .finish() @@ -221,6 +265,7 @@ impl fmt::Debug for DirectDataProducer { .field("sctp_stream_parameters", &self.inner.sctp_stream_parameters) .field("label", &self.inner.label) .field("protocol", &self.inner.protocol) + .field("paused", &self.inner.paused) .field("transport", &self.inner.transport) .field("closed", &self.inner.closed) .finish() @@ -267,9 +312,9 @@ impl DataProducer { sctp_stream_parameters: Option, label: String, protocol: String, + paused: bool, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, app_data: AppData, transport: Arc, direct: bool, @@ -296,10 +341,10 @@ impl DataProducer { sctp_stream_parameters, label, protocol, + paused: AtomicBool::new(paused), direct, executor, channel, - payload_channel, handlers, app_data, transport, @@ -339,6 +384,12 @@ impl DataProducer { self.inner().sctp_stream_parameters } + /// Whether the DataProducer is paused. + #[must_use] + pub fn paused(&self) -> bool { + self.inner().paused.load(Ordering::SeqCst) + } + /// The data producer label. #[must_use] pub fn label(&self) -> &String { @@ -368,10 +419,17 @@ impl DataProducer { pub async fn dump(&self) -> Result { debug!("dump()"); - self.inner() + let response = self + .inner() .channel .request(self.id(), DataProducerDumpRequest {}) - .await + .await?; + + if let response::Body::DataProducerDumpResponse(data) = response { + Ok(DataProducerDump::from_fbs(*data).expect("Error parsing dump response")) + } else { + panic!("Wrong message from worker"); + } } /// Returns current statistics of the data producer. @@ -381,10 +439,57 @@ impl DataProducer { pub async fn get_stats(&self) -> Result, RequestError> { debug!("get_stats()"); - self.inner() + let response = self + .inner() .channel .request(self.id(), DataProducerGetStatsRequest {}) - .await + .await?; + + if let response::Body::DataProducerGetStatsResponse(data) = response { + Ok(vec![DataProducerStat::from_fbs(&data)]) + } else { + panic!("Wrong message from worker"); + } + } + + /// Pauses the data producer (no message is sent to its associated data consumers). + /// Calls [`DataConsumer::on_data_producer_pause`](crate::data_consumer::DataConsumer::on_data_producer_pause) + /// callback on all its associated data consumers. + pub async fn pause(&self) -> Result<(), RequestError> { + debug!("pause()"); + + self.inner() + .channel + .request(self.id(), DataProducerPauseRequest {}) + .await?; + + let was_paused = self.inner().paused.swap(true, Ordering::SeqCst); + + if !was_paused { + self.inner().handlers.pause.call_simple(); + } + + Ok(()) + } + + /// Resumes the data producer (messages are sent to its associated data consumers). + /// Calls [`DataConsumer::on_data_producer_resume`](crate::data_consumer::DataConsumer::on_data_producer_resume) + /// callback on all its associated data consumers. + pub async fn resume(&self) -> Result<(), RequestError> { + debug!("resume()"); + + self.inner() + .channel + .request(self.id(), DataProducerResumeRequest {}) + .await?; + + let was_paused = self.inner().paused.swap(false, Ordering::SeqCst); + + if was_paused { + self.inner().handlers.resume.call_simple(); + } + + Ok(()) } /// Callback is called when the transport this data producer belongs to is closed for whatever @@ -397,6 +502,16 @@ impl DataProducer { .add(Box::new(callback)) } + /// Callback is called when the data producer is paused. + pub fn on_pause(&self, callback: F) -> HandlerId { + self.inner().handlers.pause.add(Arc::new(callback)) + } + + /// Callback is called when the data producer is resumed. + pub fn on_resume(&self, callback: F) -> HandlerId { + self.inner().handlers.resume.add(Arc::new(callback)) + } + /// Callback is called when the producer is closed for whatever reason. /// /// NOTE: Callback will be called in place if data producer is already closed. @@ -430,13 +545,22 @@ impl DataProducer { impl DirectDataProducer { /// Sends direct messages from the Rust to the worker. - pub fn send(&self, message: WebRtcMessage<'_>) -> Result<(), NotificationError> { + pub fn send( + &self, + message: WebRtcMessage<'_>, + subchannels: Option>, + required_subchannel: Option, + ) -> Result<(), NotificationError> { let (ppid, payload) = message.into_ppid_and_payload(); - self.inner.payload_channel.notify( + self.inner.channel.notify( self.inner.id, - DataProducerSendNotification { ppid }, - payload.into_owned(), + DataProducerSendNotification { + ppid, + payload: payload.into_owned(), + subchannels, + required_subchannel, + }, ) } } diff --git a/rust/src/router/data_producer/tests.rs b/rust/src/router/data_producer/tests.rs index a98116529d..5b910f570e 100644 --- a/rust/src/router/data_producer/tests.rs +++ b/rust/src/router/data_producer/tests.rs @@ -1,9 +1,11 @@ use crate::data_producer::DataProducerOptions; -use crate::data_structures::ListenIp; +use crate::data_structures::{ListenInfo, Protocol}; use crate::router::{Router, RouterOptions}; use crate::sctp_parameters::SctpStreamParameters; use crate::transport::Transport; -use crate::webrtc_transport::{TransportListenIps, WebRtcTransport, WebRtcTransportOptions}; +use crate::webrtc_transport::{ + WebRtcTransport, WebRtcTransportListenInfos, WebRtcTransportOptions, +}; use crate::worker::WorkerSettings; use crate::worker_manager::WorkerManager; use futures_lite::future; @@ -34,9 +36,15 @@ async fn init() -> (Router, WebRtcTransport) { let transport1 = router .create_webrtc_transport({ let mut transport_options = - WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, })); transport_options.enable_sctp = true; diff --git a/rust/src/router/direct_transport.rs b/rust/src/router/direct_transport.rs index 4658bb0564..8222500022 100644 --- a/rust/src/router/direct_transport.rs +++ b/rust/src/router/direct_transport.rs @@ -16,15 +16,17 @@ use crate::transport::{ TransportTraceEventType, }; use crate::worker::{ - Channel, NotificationError, PayloadChannel, RequestError, SubscriptionHandler, + Channel, NotificationError, NotificationParseError, RequestError, SubscriptionHandler, }; use async_executor::Executor; use async_trait::async_trait; use event_listener_primitives::{Bag, BagOnce, HandlerId}; use log::{debug, error}; +use mediasoup_sys::fbs::{direct_transport, notification, response, transport}; use nohash_hasher::IntMap; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; +use std::error::Error; use std::fmt; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Weak}; @@ -35,7 +37,7 @@ use std::sync::{Arc, Weak}; pub struct DirectTransportOptions { /// Maximum allowed size for direct messages sent from DataProducers. /// Default 262_144. - pub max_message_size: usize, + pub max_message_size: u32, /// Custom application data. pub app_data: AppData, } @@ -59,17 +61,87 @@ pub struct DirectTransportDump { pub direct: bool, pub producer_ids: Vec, pub consumer_ids: Vec, - pub map_ssrc_consumer_id: IntMap, - pub map_rtx_ssrc_consumer_id: IntMap, + pub map_ssrc_consumer_id: Vec<(u32, ConsumerId)>, + pub map_rtx_ssrc_consumer_id: Vec<(u32, ConsumerId)>, pub data_producer_ids: Vec, pub data_consumer_ids: Vec, pub recv_rtp_header_extensions: RecvRtpHeaderExtensions, pub rtp_listener: RtpListener, - pub max_message_size: usize, + pub max_message_size: u32, pub sctp_parameters: Option, pub sctp_state: Option, pub sctp_listener: Option, - pub trace_event_types: String, + pub trace_event_types: Vec, +} + +impl DirectTransportDump { + pub(crate) fn from_fbs( + dump: direct_transport::DumpResponse, + ) -> Result> { + Ok(Self { + id: dump.base.id.parse()?, + direct: true, + producer_ids: dump + .base + .producer_ids + .iter() + .map(|producer_id| Ok(producer_id.parse()?)) + .collect::>>()?, + consumer_ids: dump + .base + .consumer_ids + .iter() + .map(|consumer_id| Ok(consumer_id.parse()?)) + .collect::>>()?, + map_ssrc_consumer_id: dump + .base + .map_ssrc_consumer_id + .iter() + .map(|key_value| Ok((key_value.key, key_value.value.parse()?))) + .collect::>>()?, + map_rtx_ssrc_consumer_id: dump + .base + .map_rtx_ssrc_consumer_id + .iter() + .map(|key_value| Ok((key_value.key, key_value.value.parse()?))) + .collect::>>()?, + data_producer_ids: dump + .base + .data_producer_ids + .iter() + .map(|data_producer_id| Ok(data_producer_id.parse()?)) + .collect::>>()?, + data_consumer_ids: dump + .base + .data_consumer_ids + .iter() + .map(|data_consumer_id| Ok(data_consumer_id.parse()?)) + .collect::>>()?, + recv_rtp_header_extensions: RecvRtpHeaderExtensions::from_fbs( + dump.base.recv_rtp_header_extensions.as_ref(), + ), + rtp_listener: RtpListener::from_fbs(dump.base.rtp_listener.as_ref())?, + max_message_size: dump.base.max_message_size, + sctp_parameters: dump + .base + .sctp_parameters + .as_ref() + .map(|parameters| SctpParameters::from_fbs(parameters.as_ref())), + sctp_state: dump + .base + .sctp_state + .map(|state| SctpState::from_fbs(&state)), + sctp_listener: dump.base.sctp_listener.as_ref().map(|listener| { + SctpListener::from_fbs(listener.as_ref()).expect("Error parsing SctpListner") + }), + trace_event_types: dump + .base + .trace_event_types + .iter() + .map(TransportTraceEventType::from_fbs) + .collect(), + }) + } } /// RTC statistics of the direct transport. @@ -79,36 +151,69 @@ pub struct DirectTransportDump { #[allow(missing_docs)] pub struct DirectTransportStat { // Common to all Transports. - // `type` field is present in worker, but ignored here pub transport_id: TransportId, pub timestamp: u64, pub sctp_state: Option, - pub bytes_received: usize, + pub bytes_received: u64, pub recv_bitrate: u32, - pub bytes_sent: usize, + pub bytes_sent: u64, pub send_bitrate: u32, - pub rtp_bytes_received: usize, + pub rtp_bytes_received: u64, pub rtp_recv_bitrate: u32, - pub rtp_bytes_sent: usize, + pub rtp_bytes_sent: u64, pub rtp_send_bitrate: u32, - pub rtx_bytes_received: usize, + pub rtx_bytes_received: u64, pub rtx_recv_bitrate: u32, - pub rtx_bytes_sent: usize, + pub rtx_bytes_sent: u64, pub rtx_send_bitrate: u32, - pub probation_bytes_sent: usize, + pub probation_bytes_sent: u64, pub probation_send_bitrate: u32, #[serde(skip_serializing_if = "Option::is_none")] pub available_outgoing_bitrate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub available_incoming_bitrate: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub max_incoming_bitrate: Option, + pub max_outgoing_bitrate: Option, + pub min_outgoing_bitrate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rtp_packet_loss_received: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rtp_packet_loss_sent: Option, } +impl DirectTransportStat { + pub(crate) fn from_fbs( + stats: direct_transport::GetStatsResponse, + ) -> Result> { + Ok(Self { + transport_id: stats.base.transport_id.parse()?, + timestamp: stats.base.timestamp, + sctp_state: stats.base.sctp_state.as_ref().map(SctpState::from_fbs), + bytes_received: stats.base.bytes_received, + recv_bitrate: stats.base.recv_bitrate, + bytes_sent: stats.base.bytes_sent, + send_bitrate: stats.base.send_bitrate, + rtp_bytes_received: stats.base.rtp_bytes_received, + rtp_recv_bitrate: stats.base.rtp_recv_bitrate, + rtp_bytes_sent: stats.base.rtp_bytes_sent, + rtp_send_bitrate: stats.base.rtp_send_bitrate, + rtx_bytes_received: stats.base.rtx_bytes_received, + rtx_recv_bitrate: stats.base.rtx_recv_bitrate, + rtx_bytes_sent: stats.base.rtx_bytes_sent, + rtx_send_bitrate: stats.base.rtx_send_bitrate, + probation_bytes_sent: stats.base.probation_bytes_sent, + probation_send_bitrate: stats.base.probation_send_bitrate, + available_outgoing_bitrate: stats.base.available_outgoing_bitrate, + available_incoming_bitrate: stats.base.available_incoming_bitrate, + max_incoming_bitrate: stats.base.max_incoming_bitrate, + max_outgoing_bitrate: stats.base.max_outgoing_bitrate, + min_outgoing_bitrate: stats.base.min_outgoing_bitrate, + rtp_packet_loss_received: stats.base.rtp_packet_loss_received, + rtp_packet_loss_sent: stats.base.rtp_packet_loss_sent, + }) + } +} + #[derive(Default)] #[allow(clippy::type_complexity)] struct Handlers { @@ -126,12 +231,41 @@ struct Handlers { #[serde(tag = "event", rename_all = "lowercase", content = "data")] enum Notification { Trace(TransportTraceEventData), + Rtcp(Vec), } -#[derive(Debug, Deserialize)] -#[serde(tag = "event", rename_all = "lowercase", content = "data")] -enum PayloadNotification { - Rtcp, +impl Notification { + pub(crate) fn from_fbs( + notification: notification::NotificationRef<'_>, + ) -> Result { + match notification.event().unwrap() { + notification::Event::TransportTrace => { + let Ok(Some(notification::BodyRef::TransportTraceNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let trace_notification_fbs = transport::TraceNotification::try_from(body).unwrap(); + let trace_notification = TransportTraceEventData::from_fbs(trace_notification_fbs); + + Ok(Notification::Trace(trace_notification)) + } + notification::Event::DirecttransportRtcp => { + let Ok(Some(notification::BodyRef::DirectTransportRtcpNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let rtcp_notification_fbs = + direct_transport::RtcpNotification::try_from(body).unwrap(); + + Ok(Notification::Rtcp(rtcp_notification_fbs.data)) + } + _ => Err(NotificationParseError::InvalidEvent), + } + } } struct Inner { @@ -141,7 +275,6 @@ struct Inner { cname_for_producers: Mutex>, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, handlers: Arc, app_data: AppData, // Make sure router is not dropped until this transport is not dropped @@ -374,11 +507,11 @@ impl TransportGeneric for DirectTransport { async fn dump(&self) -> Result { debug!("dump()"); - serde_json::from_value(self.dump_impl().await?).map_err(|error| { - RequestError::FailedToParse { - error: error.to_string(), - } - }) + if let response::Body::DirectTransportDumpResponse(data) = self.dump_impl().await? { + Ok(DirectTransportDump::from_fbs(*data).expect("Error parsing dump response")) + } else { + panic!("Wrong message from worker"); + } } /// Returns current RTC statistics of the transport. @@ -388,11 +521,14 @@ impl TransportGeneric for DirectTransport { async fn get_stats(&self) -> Result, RequestError> { debug!("get_stats()"); - serde_json::from_value(self.get_stats_impl().await?).map_err(|error| { - RequestError::FailedToParse { - error: error.to_string(), - } - }) + if let response::Body::DirectTransportGetStatsResponse(data) = self.get_stats_impl().await? + { + Ok(vec![ + DirectTransportStat::from_fbs(*data).expect("Error parsing dump response") + ]) + } else { + panic!("Wrong message from worker"); + } } } @@ -401,10 +537,6 @@ impl TransportImpl for DirectTransport { &self.inner.channel } - fn payload_channel(&self) -> &PayloadChannel { - &self.inner.payload_channel - } - fn executor(&self) -> &Arc> { &self.inner.executor } @@ -427,7 +559,6 @@ impl DirectTransport { id: TransportId, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, app_data: AppData, router: Router, ) -> Self { @@ -439,33 +570,19 @@ impl DirectTransport { let handlers = Arc::clone(&handlers); channel.subscribe_to_notifications(id.into(), move |notification| { - match serde_json::from_slice::(notification) { + match Notification::from_fbs(notification) { Ok(notification) => match notification { Notification::Trace(trace_event_data) => { handlers.trace.call_simple(&trace_event_data); } - }, - Err(error) => { - error!("Failed to parse notification: {}", error); - } - } - }) - }; - - let payload_subscription_handler = { - let handlers = Arc::clone(&handlers); - - payload_channel.subscribe_to_notifications(id.into(), move |message, payload| { - match serde_json::from_slice::(message) { - Ok(notification) => match notification { - PayloadNotification::Rtcp => { + Notification::Rtcp(data) => { handlers.rtcp.call(|callback| { - callback(payload); + callback(&data); }); } }, Err(error) => { - error!("Failed to parse payload notification: {}", error); + error!("Failed to parse notification: {}", error); } } }) @@ -493,15 +610,11 @@ impl DirectTransport { cname_for_producers, executor, channel, - payload_channel, handlers, app_data, router, closed: AtomicBool::new(false), - _subscription_handlers: Mutex::new(vec![ - subscription_handler, - payload_subscription_handler, - ]), + _subscription_handlers: Mutex::new(vec![subscription_handler]), _on_router_close_handler: Mutex::new(on_router_close_handler), }); @@ -515,8 +628,8 @@ impl DirectTransport { /// * `rtcp_packet` - Bytes containing a valid RTCP packet (can be a compound packet). pub fn send_rtcp(&self, rtcp_packet: Vec) -> Result<(), NotificationError> { self.inner - .payload_channel - .notify(self.id(), TransportSendRtcpNotification {}, rtcp_packet) + .channel + .notify(self.id(), TransportSendRtcpNotification { rtcp_packet }) } /// Callback is called when the direct transport receives a RTCP packet from its router. diff --git a/rust/src/router/pipe_transport.rs b/rust/src/router/pipe_transport.rs index 187811d494..613e12b465 100644 --- a/rust/src/router/pipe_transport.rs +++ b/rust/src/router/pipe_transport.rs @@ -4,8 +4,8 @@ mod tests; use crate::consumer::{Consumer, ConsumerId, ConsumerOptions}; use crate::data_consumer::{DataConsumer, DataConsumerId, DataConsumerOptions, DataConsumerType}; use crate::data_producer::{DataProducer, DataProducerId, DataProducerOptions, DataProducerType}; -use crate::data_structures::{AppData, ListenIp, SctpState, TransportTuple}; -use crate::messages::{PipeTransportData, TransportCloseRequest, TransportConnectPipeRequest}; +use crate::data_structures::{AppData, ListenInfo, SctpState, TransportTuple}; +use crate::messages::{PipeTransportConnectRequest, PipeTransportData, TransportCloseRequest}; use crate::producer::{Producer, ProducerId, ProducerOptions}; use crate::router::transport::{TransportImpl, TransportType}; use crate::router::Router; @@ -16,14 +16,16 @@ use crate::transport::{ RtpListener, SctpListener, Transport, TransportGeneric, TransportId, TransportTraceEventData, TransportTraceEventType, }; -use crate::worker::{Channel, PayloadChannel, RequestError, SubscriptionHandler}; +use crate::worker::{Channel, NotificationParseError, RequestError, SubscriptionHandler}; use async_executor::Executor; use async_trait::async_trait; use event_listener_primitives::{Bag, BagOnce, HandlerId}; use log::{debug, error}; +use mediasoup_sys::fbs::{notification, pipe_transport, response, transport}; use nohash_hasher::IntMap; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; +use std::error::Error; use std::fmt; use std::net::IpAddr; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; @@ -33,10 +35,8 @@ use std::sync::{Arc, Weak}; #[derive(Debug, Clone)] #[non_exhaustive] pub struct PipeTransportOptions { - /// Listening IP address. - pub listen_ip: ListenIp, - /// Fixed port to listen on instead of selecting automatically from Worker's port range. - pub port: Option, + /// Listening info. + pub listen_info: ListenInfo, /// Create a SCTP association. /// Default false. pub enable_sctp: bool, @@ -64,10 +64,9 @@ pub struct PipeTransportOptions { impl PipeTransportOptions { /// Create Pipe transport options with given listen IP. #[must_use] - pub fn new(listen_ip: ListenIp) -> Self { + pub fn new(listen_info: ListenInfo) -> Self { Self { - listen_ip, - port: None, + listen_info, enable_sctp: false, num_sctp_streams: NumSctpStreams::default(), max_sctp_message_size: 268_435_456, @@ -95,17 +94,94 @@ pub struct PipeTransportDump { pub data_consumer_ids: Vec, pub recv_rtp_header_extensions: RecvRtpHeaderExtensions, pub rtp_listener: RtpListener, - pub max_message_size: usize, + pub max_message_size: u32, pub sctp_parameters: Option, pub sctp_state: Option, pub sctp_listener: Option, - pub trace_event_types: String, + pub trace_event_types: Vec, // PipeTransport specific. - pub tuple: Option, + pub tuple: TransportTuple, pub rtx: bool, pub srtp_parameters: Option, } +impl PipeTransportDump { + pub(crate) fn from_fbs( + dump: pipe_transport::DumpResponse, + ) -> Result> { + Ok(Self { + // Common to all Transports. + id: dump.base.id.parse()?, + direct: false, + producer_ids: dump + .base + .producer_ids + .iter() + .map(|producer_id| Ok(producer_id.parse()?)) + .collect::>>()?, + consumer_ids: dump + .base + .consumer_ids + .iter() + .map(|consumer_id| Ok(consumer_id.parse()?)) + .collect::>>()?, + map_ssrc_consumer_id: dump + .base + .map_ssrc_consumer_id + .iter() + .map(|key_value| Ok((key_value.key, key_value.value.parse()?))) + .collect::>>()?, + map_rtx_ssrc_consumer_id: dump + .base + .map_rtx_ssrc_consumer_id + .iter() + .map(|key_value| Ok((key_value.key, key_value.value.parse()?))) + .collect::>>()?, + data_producer_ids: dump + .base + .data_producer_ids + .iter() + .map(|data_producer_id| Ok(data_producer_id.parse()?)) + .collect::>>()?, + data_consumer_ids: dump + .base + .data_consumer_ids + .iter() + .map(|data_consumer_id| Ok(data_consumer_id.parse()?)) + .collect::>>()?, + recv_rtp_header_extensions: RecvRtpHeaderExtensions::from_fbs( + dump.base.recv_rtp_header_extensions.as_ref(), + ), + rtp_listener: RtpListener::from_fbs(dump.base.rtp_listener.as_ref())?, + max_message_size: dump.base.max_message_size, + sctp_parameters: dump + .base + .sctp_parameters + .as_ref() + .map(|parameters| SctpParameters::from_fbs(parameters.as_ref())), + sctp_state: dump + .base + .sctp_state + .map(|state| SctpState::from_fbs(&state)), + sctp_listener: dump.base.sctp_listener.as_ref().map(|listener| { + SctpListener::from_fbs(listener.as_ref()).expect("Error parsing SctpListner") + }), + trace_event_types: dump + .base + .trace_event_types + .iter() + .map(TransportTraceEventType::from_fbs) + .collect(), + // PipeTransport specific. + tuple: TransportTuple::from_fbs(dump.tuple.as_ref()), + rtx: dump.rtx, + srtp_parameters: dump + .srtp_parameters + .map(|parameters| SrtpParameters::from_fbs(parameters.as_ref())), + }) + } +} + /// RTC statistics of the pipe transport. #[derive(Debug, Clone, PartialOrd, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -117,32 +193,68 @@ pub struct PipeTransportStat { pub transport_id: TransportId, pub timestamp: u64, pub sctp_state: Option, - pub bytes_received: usize, + pub bytes_received: u64, pub recv_bitrate: u32, - pub bytes_sent: usize, + pub bytes_sent: u64, pub send_bitrate: u32, - pub rtp_bytes_received: usize, + pub rtp_bytes_received: u64, pub rtp_recv_bitrate: u32, - pub rtp_bytes_sent: usize, + pub rtp_bytes_sent: u64, pub rtp_send_bitrate: u32, - pub rtx_bytes_received: usize, + pub rtx_bytes_received: u64, pub rtx_recv_bitrate: u32, - pub rtx_bytes_sent: usize, + pub rtx_bytes_sent: u64, pub rtx_send_bitrate: u32, - pub probation_bytes_sent: usize, + pub probation_bytes_sent: u64, pub probation_send_bitrate: u32, #[serde(skip_serializing_if = "Option::is_none")] pub available_outgoing_bitrate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub available_incoming_bitrate: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub max_incoming_bitrate: Option, + pub max_outgoing_bitrate: Option, + pub min_outgoing_bitrate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rtp_packet_loss_received: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rtp_packet_loss_sent: Option, // PipeTransport specific. - pub tuple: Option, + pub tuple: TransportTuple, +} + +impl PipeTransportStat { + pub(crate) fn from_fbs( + stats: pipe_transport::GetStatsResponse, + ) -> Result> { + Ok(Self { + transport_id: stats.base.transport_id.parse()?, + timestamp: stats.base.timestamp, + sctp_state: stats.base.sctp_state.as_ref().map(SctpState::from_fbs), + bytes_received: stats.base.bytes_received, + recv_bitrate: stats.base.recv_bitrate, + bytes_sent: stats.base.bytes_sent, + send_bitrate: stats.base.send_bitrate, + rtp_bytes_received: stats.base.rtp_bytes_received, + rtp_recv_bitrate: stats.base.rtp_recv_bitrate, + rtp_bytes_sent: stats.base.rtp_bytes_sent, + rtp_send_bitrate: stats.base.rtp_send_bitrate, + rtx_bytes_received: stats.base.rtx_bytes_received, + rtx_recv_bitrate: stats.base.rtx_recv_bitrate, + rtx_bytes_sent: stats.base.rtx_bytes_sent, + rtx_send_bitrate: stats.base.rtx_send_bitrate, + probation_bytes_sent: stats.base.probation_bytes_sent, + probation_send_bitrate: stats.base.probation_send_bitrate, + available_outgoing_bitrate: stats.base.available_outgoing_bitrate, + available_incoming_bitrate: stats.base.available_incoming_bitrate, + max_incoming_bitrate: stats.base.max_incoming_bitrate, + max_outgoing_bitrate: stats.base.max_outgoing_bitrate, + min_outgoing_bitrate: stats.base.min_outgoing_bitrate, + rtp_packet_loss_received: stats.base.rtp_packet_loss_received, + rtp_packet_loss_sent: stats.base.rtp_packet_loss_sent, + // PlainTransport specific. + tuple: TransportTuple::from_fbs(stats.tuple.as_ref()), + }) + } } /// Remote parameters for pipe transport. @@ -181,6 +293,39 @@ enum Notification { Trace(TransportTraceEventData), } +impl Notification { + pub(crate) fn from_fbs( + notification: notification::NotificationRef<'_>, + ) -> Result { + match notification.event().unwrap() { + notification::Event::TransportSctpStateChange => { + let Ok(Some(notification::BodyRef::TransportSctpStateChangeNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let sctp_state = SctpState::from_fbs(&body.sctp_state().unwrap()); + + Ok(Notification::SctpStateChange { sctp_state }) + } + notification::Event::TransportTrace => { + let Ok(Some(notification::BodyRef::TransportTraceNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let trace_notification_fbs = transport::TraceNotification::try_from(body).unwrap(); + let trace_notification = TransportTraceEventData::from_fbs(trace_notification_fbs); + + Ok(Notification::Trace(trace_notification)) + } + _ => Err(NotificationParseError::InvalidEvent), + } + } +} + struct Inner { id: TransportId, next_mid_for_consumers: AtomicUsize, @@ -188,7 +333,6 @@ struct Inner { cname_for_producers: Mutex>, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, handlers: Arc, data: Arc, app_data: AppData, @@ -414,21 +558,23 @@ impl TransportGeneric for PipeTransport { async fn dump(&self) -> Result { debug!("dump()"); - serde_json::from_value(self.dump_impl().await?).map_err(|error| { - RequestError::FailedToParse { - error: error.to_string(), - } - }) + if let response::Body::PipeTransportDumpResponse(data) = self.dump_impl().await? { + Ok(PipeTransportDump::from_fbs(*data).expect("Error parsing dump response")) + } else { + panic!("Wrong message from worker"); + } } async fn get_stats(&self) -> Result, RequestError> { debug!("get_stats()"); - serde_json::from_value(self.get_stats_impl().await?).map_err(|error| { - RequestError::FailedToParse { - error: error.to_string(), - } - }) + if let response::Body::PipeTransportGetStatsResponse(data) = self.get_stats_impl().await? { + Ok(vec![ + PipeTransportStat::from_fbs(*data).expect("Error parsing dump response") + ]) + } else { + panic!("Wrong message from worker"); + } } } @@ -437,10 +583,6 @@ impl TransportImpl for PipeTransport { &self.inner.channel } - fn payload_channel(&self) -> &PayloadChannel { - &self.inner.payload_channel - } - fn executor(&self) -> &Arc> { &self.inner.executor } @@ -463,7 +605,6 @@ impl PipeTransport { id: TransportId, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, data: PipeTransportData, app_data: AppData, router: Router, @@ -478,7 +619,7 @@ impl PipeTransport { let data = Arc::clone(&data); channel.subscribe_to_notifications(id.into(), move |notification| { - match serde_json::from_slice::(notification) { + match Notification::from_fbs(notification) { Ok(notification) => match notification { Notification::SctpStateChange { sctp_state } => { data.sctp_state.lock().replace(sctp_state); @@ -528,7 +669,6 @@ impl PipeTransport { cname_for_producers, executor, channel, - payload_channel, handlers, data, app_data, @@ -555,7 +695,7 @@ impl PipeTransport { .channel .request( self.id(), - TransportConnectPipeRequest { + PipeTransportConnectRequest { ip: remote_parameters.ip, port: remote_parameters.port, srtp_parameters: remote_parameters.srtp_parameters, @@ -581,12 +721,12 @@ impl PipeTransport { /// /// # Notes on usage /// * Once the pipe transport is created, `transport.tuple()` will contain information about - /// its `local_ip`, `local_port` and `protocol`. + /// its `local_address`, `local_port` and `protocol`. /// * Information about `remote_ip` and `remote_port` will be set after calling `connect()` /// method. #[must_use] pub fn tuple(&self) -> TransportTuple { - *self.inner.data.tuple.lock() + self.inner.data.tuple.lock().clone() } /// Local SCTP parameters. Or `None` if SCTP is not enabled. diff --git a/rust/src/router/pipe_transport/tests.rs b/rust/src/router/pipe_transport/tests.rs index 67862e5bc2..ef09da4a40 100644 --- a/rust/src/router/pipe_transport/tests.rs +++ b/rust/src/router/pipe_transport/tests.rs @@ -1,7 +1,7 @@ use crate::consumer::ConsumerOptions; use crate::data_consumer::DataConsumerOptions; use crate::data_producer::DataProducerOptions; -use crate::data_structures::ListenIp; +use crate::data_structures::{ListenInfo, Protocol}; use crate::producer::ProducerOptions; use crate::router::{PipeToRouterOptions, Router, RouterOptions}; use crate::rtp_parameters::{ @@ -10,7 +10,9 @@ use crate::rtp_parameters::{ }; use crate::sctp_parameters::SctpStreamParameters; use crate::transport::Transport; -use crate::webrtc_transport::{TransportListenIps, WebRtcTransport, WebRtcTransportOptions}; +use crate::webrtc_transport::{ + WebRtcTransport, WebRtcTransportListenInfos, WebRtcTransportOptions, +}; use crate::worker::WorkerSettings; use crate::worker_manager::WorkerManager; use futures_lite::future; @@ -95,10 +97,17 @@ async fn init() -> (Router, Router, WebRtcTransport, WebRtcTransport) { .await .expect("Failed to create router"); - let mut transport_options = WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - })); + let mut transport_options = + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })); transport_options.enable_sctp = true; let transport_1 = router1 diff --git a/rust/src/router/plain_transport.rs b/rust/src/router/plain_transport.rs index ee15cb195d..5c6aaa3206 100644 --- a/rust/src/router/plain_transport.rs +++ b/rust/src/router/plain_transport.rs @@ -4,7 +4,7 @@ mod tests; use crate::consumer::{Consumer, ConsumerId, ConsumerOptions}; use crate::data_consumer::{DataConsumer, DataConsumerId, DataConsumerOptions, DataConsumerType}; use crate::data_producer::{DataProducer, DataProducerId, DataProducerOptions, DataProducerType}; -use crate::data_structures::{AppData, ListenIp, SctpState, TransportTuple}; +use crate::data_structures::{AppData, ListenInfo, SctpState, TransportTuple}; use crate::messages::{PlainTransportData, TransportCloseRequest, TransportConnectPlainRequest}; use crate::producer::{Producer, ProducerId, ProducerOptions}; use crate::router::transport::{TransportImpl, TransportType}; @@ -16,14 +16,16 @@ use crate::transport::{ RtpListener, SctpListener, Transport, TransportGeneric, TransportId, TransportTraceEventData, TransportTraceEventType, }; -use crate::worker::{Channel, PayloadChannel, RequestError, SubscriptionHandler}; +use crate::worker::{Channel, NotificationParseError, RequestError, SubscriptionHandler}; use async_executor::Executor; use async_trait::async_trait; use event_listener_primitives::{Bag, BagOnce, HandlerId}; use log::{debug, error}; +use mediasoup_sys::fbs::{notification, plain_transport, response, transport}; use nohash_hasher::IntMap; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; +use std::error::Error; use std::fmt; use std::net::IpAddr; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; @@ -42,10 +44,10 @@ use std::sync::{Arc, Weak}; #[derive(Debug, Clone)] #[non_exhaustive] pub struct PlainTransportOptions { - /// Listening IP address. - pub listen_ip: ListenIp, - /// Fixed port to listen on instead of selecting automatically from Worker's port range. - pub port: Option, + /// Listening info. + pub listen_info: ListenInfo, + /// Optional listening info for RTCP. + pub rtcp_listen_info: Option, /// Use RTCP-mux (RTP and RTCP in the same port). /// Default true. pub rtcp_mux: bool, @@ -78,10 +80,10 @@ pub struct PlainTransportOptions { impl PlainTransportOptions { /// Create Plain transport options with given listen IP. #[must_use] - pub fn new(listen_ip: ListenIp) -> Self { + pub fn new(listen_info: ListenInfo) -> Self { Self { - listen_ip, - port: None, + listen_info, + rtcp_listen_info: None, rtcp_mux: true, comedia: false, enable_sctp: false, @@ -111,11 +113,11 @@ pub struct PlainTransportDump { pub data_consumer_ids: Vec, pub recv_rtp_header_extensions: RecvRtpHeaderExtensions, pub rtp_listener: RtpListener, - pub max_message_size: usize, + pub max_message_size: u32, pub sctp_parameters: Option, pub sctp_state: Option, pub sctp_listener: Option, - pub trace_event_types: String, + pub trace_event_types: Vec, // PlainTransport specific. pub rtcp_mux: bool, pub comedia: bool, @@ -124,6 +126,87 @@ pub struct PlainTransportDump { pub srtp_parameters: Option, } +impl PlainTransportDump { + pub(crate) fn from_fbs( + dump: plain_transport::DumpResponse, + ) -> Result> { + Ok(Self { + // Common to all Transports. + id: dump.base.id.parse()?, + direct: false, + producer_ids: dump + .base + .producer_ids + .iter() + .map(|producer_id| Ok(producer_id.parse()?)) + .collect::>>()?, + consumer_ids: dump + .base + .consumer_ids + .iter() + .map(|consumer_id| Ok(consumer_id.parse()?)) + .collect::>>()?, + map_ssrc_consumer_id: dump + .base + .map_ssrc_consumer_id + .iter() + .map(|key_value| Ok((key_value.key, key_value.value.parse()?))) + .collect::>>()?, + map_rtx_ssrc_consumer_id: dump + .base + .map_rtx_ssrc_consumer_id + .iter() + .map(|key_value| Ok((key_value.key, key_value.value.parse()?))) + .collect::>>()?, + data_producer_ids: dump + .base + .data_producer_ids + .iter() + .map(|data_producer_id| Ok(data_producer_id.parse()?)) + .collect::>>()?, + data_consumer_ids: dump + .base + .data_consumer_ids + .iter() + .map(|data_consumer_id| Ok(data_consumer_id.parse()?)) + .collect::>>()?, + recv_rtp_header_extensions: RecvRtpHeaderExtensions::from_fbs( + dump.base.recv_rtp_header_extensions.as_ref(), + ), + rtp_listener: RtpListener::from_fbs(dump.base.rtp_listener.as_ref())?, + max_message_size: dump.base.max_message_size, + sctp_parameters: dump + .base + .sctp_parameters + .as_ref() + .map(|parameters| SctpParameters::from_fbs(parameters.as_ref())), + sctp_state: dump + .base + .sctp_state + .map(|state| SctpState::from_fbs(&state)), + sctp_listener: dump.base.sctp_listener.as_ref().map(|listener| { + SctpListener::from_fbs(listener.as_ref()).expect("Error parsing SctpListner") + }), + trace_event_types: dump + .base + .trace_event_types + .iter() + .map(TransportTraceEventType::from_fbs) + .collect(), + // PlainTransport specific. + rtcp_mux: dump.rtcp_mux, + comedia: dump.comedia, + tuple: TransportTuple::from_fbs(dump.tuple.as_ref()), + rtcp_tuple: dump + .rtcp_tuple + .map(|tuple| TransportTuple::from_fbs(tuple.as_ref())), + srtp_parameters: dump + .srtp_parameters + .map(|parameters| SrtpParameters::from_fbs(parameters.as_ref())), + }) + } +} + /// RTC statistics of the plain transport. #[derive(Debug, Clone, PartialOrd, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -135,26 +218,27 @@ pub struct PlainTransportStat { pub transport_id: TransportId, pub timestamp: u64, pub sctp_state: Option, - pub bytes_received: usize, + pub bytes_received: u64, pub recv_bitrate: u32, - pub bytes_sent: usize, + pub bytes_sent: u64, pub send_bitrate: u32, - pub rtp_bytes_received: usize, + pub rtp_bytes_received: u64, pub rtp_recv_bitrate: u32, - pub rtp_bytes_sent: usize, + pub rtp_bytes_sent: u64, pub rtp_send_bitrate: u32, - pub rtx_bytes_received: usize, + pub rtx_bytes_received: u64, pub rtx_recv_bitrate: u32, - pub rtx_bytes_sent: usize, + pub rtx_bytes_sent: u64, pub rtx_send_bitrate: u32, - pub probation_bytes_sent: usize, + pub probation_bytes_sent: u64, pub probation_send_bitrate: u32, #[serde(skip_serializing_if = "Option::is_none")] pub available_outgoing_bitrate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub available_incoming_bitrate: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub max_incoming_bitrate: Option, + pub max_outgoing_bitrate: Option, + pub min_outgoing_bitrate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rtp_packet_loss_received: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -162,10 +246,50 @@ pub struct PlainTransportStat { // PlainTransport specific. pub rtcp_mux: bool, pub comedia: bool, - pub tuple: Option, + pub tuple: TransportTuple, pub rtcp_tuple: Option, } +impl PlainTransportStat { + pub(crate) fn from_fbs( + stats: plain_transport::GetStatsResponse, + ) -> Result> { + Ok(Self { + transport_id: stats.base.transport_id.parse()?, + timestamp: stats.base.timestamp, + sctp_state: stats.base.sctp_state.as_ref().map(SctpState::from_fbs), + bytes_received: stats.base.bytes_received, + recv_bitrate: stats.base.recv_bitrate, + bytes_sent: stats.base.bytes_sent, + send_bitrate: stats.base.send_bitrate, + rtp_bytes_received: stats.base.rtp_bytes_received, + rtp_recv_bitrate: stats.base.rtp_recv_bitrate, + rtp_bytes_sent: stats.base.rtp_bytes_sent, + rtp_send_bitrate: stats.base.rtp_send_bitrate, + rtx_bytes_received: stats.base.rtx_bytes_received, + rtx_recv_bitrate: stats.base.rtx_recv_bitrate, + rtx_bytes_sent: stats.base.rtx_bytes_sent, + rtx_send_bitrate: stats.base.rtx_send_bitrate, + probation_bytes_sent: stats.base.probation_bytes_sent, + probation_send_bitrate: stats.base.probation_send_bitrate, + available_outgoing_bitrate: stats.base.available_outgoing_bitrate, + available_incoming_bitrate: stats.base.available_incoming_bitrate, + max_incoming_bitrate: stats.base.max_incoming_bitrate, + max_outgoing_bitrate: stats.base.max_outgoing_bitrate, + min_outgoing_bitrate: stats.base.min_outgoing_bitrate, + rtp_packet_loss_received: stats.base.rtp_packet_loss_received, + rtp_packet_loss_sent: stats.base.rtp_packet_loss_sent, + // PlainTransport specific. + rtcp_mux: stats.rtcp_mux, + comedia: stats.comedia, + tuple: TransportTuple::from_fbs(stats.tuple.as_ref()), + rtcp_tuple: stats + .rtcp_tuple + .map(|tuple| TransportTuple::from_fbs(tuple.as_ref())), + }) + } +} + /// Remote parameters for plain transport. #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -218,6 +342,63 @@ enum Notification { Trace(TransportTraceEventData), } +impl Notification { + pub(crate) fn from_fbs( + notification: notification::NotificationRef<'_>, + ) -> Result { + match notification.event().unwrap() { + notification::Event::PlaintransportTuple => { + let Ok(Some(notification::BodyRef::PlainTransportTupleNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let tuple_fbs = transport::Tuple::try_from(body.tuple().unwrap()).unwrap(); + let tuple = TransportTuple::from_fbs(&tuple_fbs); + + Ok(Notification::Tuple { tuple }) + } + notification::Event::PlaintransportRtcpTuple => { + let Ok(Some(notification::BodyRef::PlainTransportRtcpTupleNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let rtcp_tuple_fbs = transport::Tuple::try_from(body.tuple().unwrap()).unwrap(); + let rtcp_tuple = TransportTuple::from_fbs(&rtcp_tuple_fbs); + + Ok(Notification::RtcpTuple { rtcp_tuple }) + } + notification::Event::TransportSctpStateChange => { + let Ok(Some(notification::BodyRef::TransportSctpStateChangeNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let sctp_state = SctpState::from_fbs(&body.sctp_state().unwrap()); + + Ok(Notification::SctpStateChange { sctp_state }) + } + notification::Event::TransportTrace => { + let Ok(Some(notification::BodyRef::TransportTraceNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let trace_notification_fbs = transport::TraceNotification::try_from(body).unwrap(); + let trace_notification = TransportTraceEventData::from_fbs(trace_notification_fbs); + + Ok(Notification::Trace(trace_notification)) + } + _ => Err(NotificationParseError::InvalidEvent), + } + } +} + struct Inner { id: TransportId, next_mid_for_consumers: AtomicUsize, @@ -225,7 +406,6 @@ struct Inner { cname_for_producers: Mutex>, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, handlers: Arc, data: Arc, app_data: AppData, @@ -444,21 +624,23 @@ impl TransportGeneric for PlainTransport { async fn dump(&self) -> Result { debug!("dump()"); - serde_json::from_value(self.dump_impl().await?).map_err(|error| { - RequestError::FailedToParse { - error: error.to_string(), - } - }) + if let response::Body::PlainTransportDumpResponse(data) = self.dump_impl().await? { + Ok(PlainTransportDump::from_fbs(*data).expect("Error parsing dump response")) + } else { + panic!("Wrong message from worker"); + } } async fn get_stats(&self) -> Result, RequestError> { debug!("get_stats()"); - serde_json::from_value(self.get_stats_impl().await?).map_err(|error| { - RequestError::FailedToParse { - error: error.to_string(), - } - }) + if let response::Body::PlainTransportGetStatsResponse(data) = self.get_stats_impl().await? { + Ok(vec![ + PlainTransportStat::from_fbs(*data).expect("Error parsing dump response") + ]) + } else { + panic!("Wrong message from worker"); + } } } @@ -467,10 +649,6 @@ impl TransportImpl for PlainTransport { &self.inner.channel } - fn payload_channel(&self) -> &PayloadChannel { - &self.inner.payload_channel - } - fn executor(&self) -> &Arc> { &self.inner.executor } @@ -493,7 +671,6 @@ impl PlainTransport { id: TransportId, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, data: PlainTransportData, app_data: AppData, router: Router, @@ -508,15 +685,15 @@ impl PlainTransport { let data = Arc::clone(&data); channel.subscribe_to_notifications(id.into(), move |notification| { - match serde_json::from_slice::(notification) { + match Notification::from_fbs(notification) { Ok(notification) => match notification { Notification::Tuple { tuple } => { - *data.tuple.lock() = tuple; + *data.tuple.lock() = tuple.clone(); handlers.tuple.call_simple(&tuple); } Notification::RtcpTuple { rtcp_tuple } => { - data.rtcp_tuple.lock().replace(rtcp_tuple); + data.rtcp_tuple.lock().replace(rtcp_tuple.clone()); handlers.rtcp_tuple.call_simple(&rtcp_tuple); } @@ -568,7 +745,6 @@ impl PlainTransport { cname_for_producers, executor, channel, - payload_channel, handlers, data, app_data, @@ -663,8 +839,8 @@ impl PlainTransport { /// # async fn f( /// # plain_transport: mediasoup::plain_transport::PlainTransport, /// # ) -> Result<(), Box> { - /// // Calling connect() on a PlainTransport created with comedia unset, rtcpMux - /// // set and enableSrtp enabled. + /// // Calling connect() on a PlainTransport created with comedia unset, + /// // rtcp_mux set and enableSrtp enabled. /// plain_transport /// .connect(PlainTransportRemoteParameters { /// ip: Some("1.2.3.4".parse().unwrap()), @@ -699,9 +875,7 @@ impl PlainTransport { ) .await?; - if let Some(tuple) = response.tuple { - *self.inner.data.tuple.lock() = tuple; - } + *self.inner.data.tuple.lock() = response.tuple; if let Some(rtcp_tuple) = response.rtcp_tuple { self.inner.data.rtcp_tuple.lock().replace(rtcp_tuple); @@ -731,13 +905,13 @@ impl PlainTransport { /// /// # Notes on usage /// * Once the plain transport is created, `transport.tuple()` will contain information about - /// its `local_ip`, `local_port` and `protocol`. + /// its `local_address`, `local_port` and `protocol`. /// * Information about `remote_ip` and `remote_port` will be set: /// * after calling `connect()` method, or /// * via dynamic remote address detection when using `comedia` mode. #[must_use] pub fn tuple(&self) -> TransportTuple { - *self.inner.data.tuple.lock() + self.inner.data.tuple.lock().clone() } /// The transport tuple for RTCP. If RTCP-mux is enabled (`rtcp_mux` is set), its value is @@ -745,13 +919,13 @@ impl PlainTransport { /// /// # Notes on usage /// * Once the plain transport is created (with RTCP-mux disabled), `transport.rtcp_tuple()` - /// will contain information about its `local_ip`, `local_port` and `protocol`. + /// will contain information about its `local_address`, `local_port` and `protocol`. /// * Information about `remote_ip` and `remote_port` will be set: /// * after calling `connect()` method, or /// * via dynamic remote address detection when using `comedia` mode. #[must_use] pub fn rtcp_tuple(&self) -> Option { - *self.inner.data.rtcp_tuple.lock() + self.inner.data.rtcp_tuple.lock().clone() } /// Current SCTP state. Or `None` if SCTP is not enabled. diff --git a/rust/src/router/plain_transport/tests.rs b/rust/src/router/plain_transport/tests.rs index 5fd65b1a40..496d25a9bd 100644 --- a/rust/src/router/plain_transport/tests.rs +++ b/rust/src/router/plain_transport/tests.rs @@ -1,4 +1,4 @@ -use crate::data_structures::ListenIp; +use crate::data_structures::{ListenInfo, Protocol}; use crate::plain_transport::PlainTransportOptions; use crate::router::{Router, RouterOptions}; use crate::transport::Transport; @@ -37,9 +37,15 @@ fn router_close_event() { let transport = router .create_plain_transport({ - let mut plain_transport_options = PlainTransportOptions::new(ListenIp { + let mut plain_transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("4.4.4.4".parse().unwrap()), + announced_address: Some("4.4.4.4".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); plain_transport_options.rtcp_mux = false; diff --git a/rust/src/router/producer.rs b/rust/src/router/producer.rs index 17549fd822..3b64975e35 100644 --- a/rust/src/router/producer.rs +++ b/rust/src/router/producer.rs @@ -1,8 +1,10 @@ #[cfg(test)] mod tests; -use crate::consumer::RtpStreamParams; -use crate::data_structures::{AppData, RtpPacketTraceInfo, SsrcTraceInfo, TraceEventDirection}; +use crate::consumer::{RtpStreamParams, RtxStreamParams}; +use crate::data_structures::{ + AppData, RtpPacketTraceInfo, SrTraceInfo, SsrcTraceInfo, TraceEventDirection, +}; use crate::messages::{ ProducerCloseRequest, ProducerDumpRequest, ProducerEnableTraceEventRequest, ProducerGetStatsRequest, ProducerPauseRequest, ProducerResumeRequest, ProducerSendNotification, @@ -12,15 +14,16 @@ use crate::rtp_parameters::{MediaKind, MimeType, RtpParameters}; use crate::transport::Transport; use crate::uuid_based_wrapper_type; use crate::worker::{ - Channel, NotificationError, PayloadChannel, RequestError, SubscriptionHandler, + Channel, NotificationError, NotificationParseError, RequestError, SubscriptionHandler, }; use async_executor::Executor; use event_listener_primitives::{Bag, BagOnce, HandlerId}; -use hash_hasher::HashedMap; use log::{debug, error}; +use mediasoup_sys::fbs::{notification, producer, response, rtp_parameters, rtp_stream}; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::error::Error; use std::fmt; use std::fmt::Debug; use std::sync::atomic::{AtomicBool, Ordering}; @@ -88,18 +91,38 @@ impl ProducerOptions { } } +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[doc(hidden)] +pub struct RtxStream { + pub params: RtxStreamParams, +} + #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[doc(hidden)] pub struct RtpStreamRecv { pub params: RtpStreamParams, pub score: u8, - // `type` field is present in worker, but ignored here - pub jitter: u32, - pub packet_count: usize, - pub byte_count: usize, - pub bitrate: u32, - pub bitrate_by_layer: Option>, + pub rtx_stream: Option, +} + +impl RtpStreamRecv { + pub(crate) fn from_fbs_ref( + dump: rtp_stream::DumpRef<'_>, + ) -> Result> { + Ok(Self { + params: RtpStreamParams::from_fbs_ref(dump.params()?)?, + score: dump.score()?, + rtx_stream: if let Some(rtx_stream) = dump.rtx_stream()? { + Some(RtxStream { + params: RtxStreamParams::from_fbs_ref(rtx_stream.params()?)?, + }) + } else { + None + }, + }) + } } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] @@ -113,10 +136,38 @@ pub struct ProducerDump { pub rtp_mapping: RtpMapping, pub rtp_parameters: RtpParameters, pub rtp_streams: Vec, - pub trace_event_types: String, + pub trace_event_types: Vec, pub r#type: ProducerType, } +impl ProducerDump { + pub(crate) fn from_fbs_ref( + dump: producer::DumpResponseRef<'_>, + ) -> Result> { + Ok(Self { + id: dump.id()?.parse()?, + kind: MediaKind::from_fbs(dump.kind()?), + paused: dump.paused()?, + rtp_mapping: RtpMapping::from_fbs_ref(dump.rtp_mapping()?)?, + rtp_parameters: RtpParameters::from_fbs_ref(dump.rtp_parameters()?)?, + rtp_streams: dump + .rtp_streams()? + .iter() + .map(|rtp_stream| RtpStreamRecv::from_fbs_ref(rtp_stream?)) + .collect::>>()?, + trace_event_types: dump + .trace_event_types()? + .iter() + .map(|trace_event_type| { + ProducerTraceEventType::from_fbs(&trace_event_type.unwrap()) + }) + .collect(), + + r#type: ProducerType::from_fbs(dump.type_()?), + }) + } +} + /// Producer type. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] @@ -129,6 +180,19 @@ pub enum ProducerType { Svc, } +impl ProducerType { + pub(crate) fn from_fbs(producer_type: rtp_parameters::Type) -> Self { + match producer_type { + rtp_parameters::Type::Simple => ProducerType::Simple, + rtp_parameters::Type::Simulcast => ProducerType::Simulcast, + rtp_parameters::Type::Svc => ProducerType::Svc, + // TODO: Create a new FBS type ProducerType with just Simple, + // Simulcast and Svc. + rtp_parameters::Type::Pipe => unimplemented!(), + } + } +} + /// Score of the RTP stream in the producer representing its transmission quality. #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -143,6 +207,17 @@ pub struct ProducerScore { pub score: u8, } +impl ProducerScore { + pub(crate) fn from_fbs(producer_score: &producer::Score) -> Self { + Self { + encoding_idx: producer_score.encoding_idx, + ssrc: producer_score.ssrc, + rid: producer_score.rid.clone(), + score: producer_score.score, + } + } +} + /// Rotation angle #[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize_repr, Serialize_repr)] #[repr(u16)] @@ -169,6 +244,34 @@ pub struct ProducerVideoOrientation { pub rotation: Rotation, } +impl ProducerVideoOrientation { + pub(crate) fn from_fbs( + video_orientation: producer::VideoOrientationChangeNotification, + ) -> Self { + Self { + camera: video_orientation.camera, + flip: video_orientation.flip, + rotation: match video_orientation.rotation { + 0 => Rotation::None, + 90 => Rotation::Clockwise, + 180 => Rotation::Rotate180, + 270 => Rotation::CounterClockwise, + _ => Rotation::None, + }, + } + } +} + +/// Bitrate by layer. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +#[allow(missing_docs)] +pub struct BitrateByLayer { + layer: String, + bitrate: u32, +} + /// RTC statistics of the producer. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -181,26 +284,71 @@ pub struct ProducerStat { pub ssrc: u32, pub rtx_ssrc: Option, pub rid: Option, - pub kind: String, + pub kind: MediaKind, pub mime_type: MimeType, - pub packets_lost: u32, + pub packets_lost: u64, pub fraction_lost: u8, - pub packets_discarded: usize, - pub packets_retransmitted: usize, - pub packets_repaired: usize, - pub nack_count: usize, - pub nack_packet_count: usize, - pub pli_count: usize, - pub fir_count: usize, + pub packets_discarded: u64, + pub packets_retransmitted: u64, + pub packets_repaired: u64, + pub nack_count: u64, + pub nack_packet_count: u64, + pub pli_count: u64, + pub fir_count: u64, pub score: u8, - pub packet_count: usize, - pub byte_count: usize, + pub packet_count: u64, + pub byte_count: u64, pub bitrate: u32, pub round_trip_time: Option, - pub rtx_packets_discarded: Option, + pub rtx_packets_discarded: Option, // RtpStreamRecv specific. pub jitter: u32, - pub bitrate_by_layer: Option>, + pub bitrate_by_layer: Vec, +} + +impl ProducerStat { + pub(crate) fn from_fbs(stats: &rtp_stream::Stats) -> Self { + let rtp_stream::StatsData::RecvStats(ref stats) = stats.data else { + panic!("Wrong message from worker"); + }; + + let rtp_stream::StatsData::BaseStats(ref base) = stats.base.data else { + panic!("Wrong message from worker"); + }; + + Self { + timestamp: base.timestamp, + ssrc: base.ssrc, + rtx_ssrc: base.rtx_ssrc, + rid: base.rid.clone(), + kind: MediaKind::from_fbs(base.kind), + mime_type: base.mime_type.to_string().parse().unwrap(), + packets_lost: base.packets_lost, + fraction_lost: base.fraction_lost, + packets_discarded: base.packets_discarded, + packets_retransmitted: base.packets_retransmitted, + packets_repaired: base.packets_repaired, + nack_count: base.nack_count, + nack_packet_count: base.nack_packet_count, + pli_count: base.pli_count, + fir_count: base.fir_count, + score: base.score, + packet_count: stats.packet_count, + byte_count: stats.byte_count, + bitrate: stats.bitrate, + round_trip_time: Some(base.round_trip_time), + rtx_packets_discarded: Some(base.rtx_packets_discarded), + jitter: stats.jitter, + bitrate_by_layer: stats + .bitrate_by_layer + .iter() + .map(|bitrate_by_layer| BitrateByLayer { + layer: bitrate_by_layer.layer.to_string(), + bitrate: bitrate_by_layer.bitrate, + }) + .collect(), + } + } } /// 'trace' event data. @@ -250,6 +398,81 @@ pub enum ProducerTraceEventData { /// SSRC info. info: SsrcTraceInfo, }, + /// RTCP Sender Report. + Sr { + /// Event timestamp. + timestamp: u64, + /// Event direction. + direction: TraceEventDirection, + /// SSRC info. + info: SrTraceInfo, + }, +} + +impl ProducerTraceEventData { + pub(crate) fn from_fbs(data: producer::TraceNotification) -> Self { + match data.type_ { + producer::TraceEventType::Rtp => ProducerTraceEventData::Rtp { + timestamp: data.timestamp, + direction: TraceEventDirection::from_fbs(data.direction), + info: { + let Some(producer::TraceInfo::RtpTraceInfo(info)) = data.info else { + panic!("Wrong message from worker: {data:?}"); + }; + + RtpPacketTraceInfo::from_fbs(*info.rtp_packet, info.is_rtx) + }, + }, + producer::TraceEventType::Keyframe => ProducerTraceEventData::KeyFrame { + timestamp: data.timestamp, + direction: TraceEventDirection::from_fbs(data.direction), + info: { + let Some(producer::TraceInfo::KeyFrameTraceInfo(info)) = data.info else { + panic!("Wrong message from worker: {data:?}"); + }; + + RtpPacketTraceInfo::from_fbs(*info.rtp_packet, info.is_rtx) + }, + }, + producer::TraceEventType::Nack => ProducerTraceEventData::Nack { + timestamp: data.timestamp, + direction: TraceEventDirection::from_fbs(data.direction), + }, + producer::TraceEventType::Pli => ProducerTraceEventData::Pli { + timestamp: data.timestamp, + direction: TraceEventDirection::from_fbs(data.direction), + info: { + let Some(producer::TraceInfo::PliTraceInfo(info)) = data.info else { + panic!("Wrong message from worker: {data:?}"); + }; + + SsrcTraceInfo { ssrc: info.ssrc } + }, + }, + producer::TraceEventType::Fir => ProducerTraceEventData::Fir { + timestamp: data.timestamp, + direction: TraceEventDirection::from_fbs(data.direction), + info: { + let Some(producer::TraceInfo::FirTraceInfo(info)) = data.info else { + panic!("Wrong message from worker: {data:?}"); + }; + + SsrcTraceInfo { ssrc: info.ssrc } + }, + }, + producer::TraceEventType::Sr => ProducerTraceEventData::Sr { + timestamp: data.timestamp, + direction: TraceEventDirection::from_fbs(data.direction), + info: { + let Some(producer::TraceInfo::SrTraceInfo(info)) = data.info else { + panic!("Wrong message from worker: {data:?}"); + }; + + SrTraceInfo::from_fbs(*info) + }, + }, + } + } } /// Types of consumer trace events. @@ -266,6 +489,32 @@ pub enum ProducerTraceEventType { Pli, /// RTCP FIR packet. Fir, + /// RTCP Sender Report. + SR, +} + +impl ProducerTraceEventType { + pub(crate) fn to_fbs(self) -> producer::TraceEventType { + match self { + ProducerTraceEventType::Rtp => producer::TraceEventType::Rtp, + ProducerTraceEventType::KeyFrame => producer::TraceEventType::Keyframe, + ProducerTraceEventType::Nack => producer::TraceEventType::Nack, + ProducerTraceEventType::Pli => producer::TraceEventType::Pli, + ProducerTraceEventType::Fir => producer::TraceEventType::Fir, + ProducerTraceEventType::SR => producer::TraceEventType::Sr, + } + } + + pub(crate) fn from_fbs(event_type: &producer::TraceEventType) -> Self { + match event_type { + producer::TraceEventType::Rtp => ProducerTraceEventType::Rtp, + producer::TraceEventType::Keyframe => ProducerTraceEventType::KeyFrame, + producer::TraceEventType::Nack => ProducerTraceEventType::Nack, + producer::TraceEventType::Pli => ProducerTraceEventType::Pli, + producer::TraceEventType::Fir => ProducerTraceEventType::Fir, + producer::TraceEventType::Sr => ProducerTraceEventType::SR, + } + } } #[derive(Debug, Deserialize)] @@ -276,6 +525,59 @@ enum Notification { Trace(ProducerTraceEventData), } +impl Notification { + pub(crate) fn from_fbs( + notification: notification::NotificationRef<'_>, + ) -> Result { + match notification.event().unwrap() { + notification::Event::ProducerScore => { + let Ok(Some(notification::BodyRef::ProducerScoreNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let scores_fbs: Vec<_> = body + .scores() + .unwrap() + .iter() + .map(|score| producer::Score::try_from(score.unwrap()).unwrap()) + .collect(); + let scores = scores_fbs.iter().map(ProducerScore::from_fbs).collect(); + + Ok(Notification::Score(scores)) + } + notification::Event::ProducerVideoOrientationChange => { + let Ok(Some(notification::BodyRef::ProducerVideoOrientationChangeNotification( + body, + ))) = notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let video_orientation_fbs = + producer::VideoOrientationChangeNotification::try_from(body).unwrap(); + let video_orientation = ProducerVideoOrientation::from_fbs(video_orientation_fbs); + + Ok(Notification::VideoOrientationChange(video_orientation)) + } + notification::Event::ProducerTrace => { + let Ok(Some(notification::BodyRef::ProducerTraceNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let trace_notification_fbs = producer::TraceNotification::try_from(body).unwrap(); + let trace_notification = ProducerTraceEventData::from_fbs(trace_notification_fbs); + + Ok(Notification::Trace(trace_notification)) + } + _ => Err(NotificationParseError::InvalidEvent), + } + } +} + #[derive(Default)] #[allow(clippy::type_complexity)] struct Handlers { @@ -299,7 +601,6 @@ struct Inner { score: Arc>>, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, handlers: Arc, app_data: AppData, transport: Arc, @@ -441,7 +742,6 @@ impl Producer { paused: bool, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, app_data: AppData, transport: Arc, direct: bool, @@ -456,10 +756,10 @@ impl Producer { let score = Arc::clone(&score); channel.subscribe_to_notifications(id.into(), move |notification| { - match serde_json::from_slice::(notification) { + match Notification::from_fbs(notification) { Ok(notification) => match notification { Notification::Score(scores) => { - *score.lock() = scores.clone(); + score.lock().clone_from(&scores); handlers.score.call(|callback| { callback(&scores); }); @@ -503,7 +803,6 @@ impl Producer { score, executor, channel, - payload_channel, handlers, app_data, transport, @@ -596,10 +895,17 @@ impl Producer { pub async fn get_stats(&self) -> Result, RequestError> { debug!("get_stats()"); - self.inner() + let response = self + .inner() .channel .request(self.id(), ProducerGetStatsRequest {}) - .await + .await?; + + if let response::Body::ProducerGetStatsResponse(data) = response { + Ok(data.stats.iter().map(ProducerStat::from_fbs).collect()) + } else { + panic!("Wrong message from worker"); + } } /// Pauses the producer (no RTP is sent to its associated consumers). Calls @@ -622,7 +928,7 @@ impl Producer { Ok(()) } - /// Resumes the producer (no RTP is sent to its associated consumers). Calls + /// Resumes the producer (RTP is sent to its associated consumers). Calls /// [`Consumer::on_producer_resume`](crate::consumer::Consumer::on_producer_resume) callback on /// all its associated consumers. pub async fn resume(&self) -> Result<(), RequestError> { @@ -747,8 +1053,8 @@ impl DirectProducer { /// Sends a RTP packet from the Rust process. pub fn send(&self, rtp_packet: Vec) -> Result<(), NotificationError> { self.inner - .payload_channel - .notify(self.inner.id, ProducerSendNotification {}, rtp_packet) + .channel + .notify(self.inner.id, ProducerSendNotification { rtp_packet }) } } diff --git a/rust/src/router/producer/tests.rs b/rust/src/router/producer/tests.rs index f5fd9d9e12..c4941e4f58 100644 --- a/rust/src/router/producer/tests.rs +++ b/rust/src/router/producer/tests.rs @@ -1,4 +1,4 @@ -use crate::data_structures::ListenIp; +use crate::data_structures::{ListenInfo, Protocol}; use crate::producer::ProducerOptions; use crate::router::{Router, RouterOptions}; use crate::rtp_parameters::{ @@ -6,7 +6,9 @@ use crate::rtp_parameters::{ RtpParameters, }; use crate::transport::Transport; -use crate::webrtc_transport::{TransportListenIps, WebRtcTransport, WebRtcTransportOptions}; +use crate::webrtc_transport::{ + WebRtcTransport, WebRtcTransportListenInfos, WebRtcTransportOptions, +}; use crate::worker::WorkerSettings; use crate::worker_manager::WorkerManager; use futures_lite::future; @@ -64,10 +66,17 @@ async fn init() -> (Router, WebRtcTransport) { .await .expect("Failed to create router"); - let transport_options = WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - })); + let transport_options = + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })); let transport_1 = router .create_webrtc_transport(transport_options.clone()) diff --git a/rust/src/router/tests.rs b/rust/src/router/tests.rs index e36d1c46c5..4ebc78cc75 100644 --- a/rust/src/router/tests.rs +++ b/rust/src/router/tests.rs @@ -16,7 +16,10 @@ async fn init() -> Worker { let worker_manager = WorkerManager::new(); worker_manager - .create_worker(WorkerSettings::default()) + .create_worker(WorkerSettings { + enable_liburing: false, + ..WorkerSettings::default() + }) .await .expect("Failed to create worker") } diff --git a/rust/src/router/transport.rs b/rust/src/router/transport.rs index e71616ee4b..9e073d026c 100644 --- a/rust/src/router/transport.rs +++ b/rust/src/router/transport.rs @@ -6,26 +6,26 @@ use crate::messages::{ TransportConsumeDataRequest, TransportConsumeRequest, TransportDumpRequest, TransportEnableTraceEventRequest, TransportGetStatsRequest, TransportProduceDataRequest, TransportProduceRequest, TransportSetMaxIncomingBitrateRequest, - TransportSetMaxOutgoingBitrateRequest, + TransportSetMaxOutgoingBitrateRequest, TransportSetMinOutgoingBitrateRequest, }; pub use crate::ortc::{ ConsumerRtpParametersError, RtpCapabilitiesError, RtpParametersError, RtpParametersMappingError, }; use crate::producer::{Producer, ProducerId, ProducerOptions}; use crate::router::Router; -use crate::rtp_parameters::RtpEncodingParameters; -use crate::worker::{Channel, PayloadChannel, RequestError}; +use crate::rtp_parameters::{MediaKind, RtpEncodingParameters}; +use crate::sctp_parameters::SctpStreamParameters; +use crate::worker::{Channel, RequestError}; use crate::{ortc, uuid_based_wrapper_type}; use async_executor::Executor; use async_trait::async_trait; use event_listener_primitives::HandlerId; -use hash_hasher::HashedMap; use log::{error, warn}; +use mediasoup_sys::fbs::{response, transport}; use nohash_hasher::IntMap; use parking_lot::Mutex; -use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use std::error::Error; use std::fmt::Debug; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -64,6 +64,25 @@ pub enum TransportTraceEventData { }, } +impl TransportTraceEventData { + pub(crate) fn from_fbs(data: transport::TraceNotification) -> Self { + match data.type_ { + transport::TraceEventType::Probation => unimplemented!(), + transport::TraceEventType::Bwe => TransportTraceEventData::Bwe { + timestamp: data.timestamp, + direction: TraceEventDirection::from_fbs(data.direction), + info: { + let Some(transport::TraceInfo::BweTraceInfo(info)) = data.info else { + panic!("Wrong message from worker: {data:?}"); + }; + + BweTraceInfo::from_fbs(*info) + }, + }, + } + } +} + /// Valid types for "trace" event. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] @@ -74,16 +93,56 @@ pub enum TransportTraceEventType { Bwe, } +impl TransportTraceEventType { + pub(crate) fn to_fbs(self) -> transport::TraceEventType { + match self { + TransportTraceEventType::Probation => transport::TraceEventType::Probation, + TransportTraceEventType::Bwe => transport::TraceEventType::Bwe, + } + } + + pub(crate) fn from_fbs(event_type: &transport::TraceEventType) -> Self { + match event_type { + transport::TraceEventType::Probation => TransportTraceEventType::Probation, + transport::TraceEventType::Bwe => TransportTraceEventType::Bwe, + } + } +} + #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[doc(hidden)] pub struct RtpListener { - /// Map from Ssrc (as string) to producer ID - pub mid_table: HashedMap, - /// Map from Ssrc (as string) to producer ID - pub rid_table: HashedMap, - /// Map from Ssrc (as string) to producer ID - pub ssrc_table: HashedMap, + /// Vector of mid and producer ID + pub mid_table: Vec<(String, ProducerId)>, + /// Vector of rid and producer ID + pub rid_table: Vec<(String, ProducerId)>, + /// Vector of Ssrc and producer ID + pub ssrc_table: Vec<(u32, ProducerId)>, +} + +impl RtpListener { + pub(crate) fn from_fbs( + rtp_listener: &transport::RtpListener, + ) -> Result> { + Ok(Self { + mid_table: rtp_listener + .mid_table + .iter() + .map(|key_value| Ok((key_value.key.to_string(), key_value.value.parse()?))) + .collect::>>()?, + rid_table: rtp_listener + .rid_table + .iter() + .map(|key_value| Ok((key_value.key.to_string(), key_value.value.parse()?))) + .collect::>>()?, + ssrc_table: rtp_listener + .ssrc_table + .iter() + .map(|key_value| Ok((key_value.key, key_value.value.parse()?))) + .collect::>>()?, + }) + } } #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)] @@ -97,12 +156,38 @@ pub struct RecvRtpHeaderExtensions { transport_wide_cc01: Option, } +impl RecvRtpHeaderExtensions { + pub(crate) fn from_fbs(extensions: &transport::RecvRtpHeaderExtensions) -> Self { + Self { + mid: extensions.mid, + rid: extensions.rid, + rrid: extensions.rrid, + abs_send_time: extensions.abs_send_time, + transport_wide_cc01: extensions.transport_wide_cc01, + } + } +} + #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[doc(hidden)] pub struct SctpListener { - /// Map from stream ID (as string) to data producer ID - stream_id_table: HashedMap, + /// Vector of stream ID (as string) to data producer ID + stream_id_table: Vec<(u16, DataProducerId)>, +} + +impl SctpListener { + pub(crate) fn from_fbs( + listener: &transport::SctpListener, + ) -> Result> { + Ok(Self { + stream_id_table: listener + .stream_id_table + .iter() + .map(|key_value| Ok((key_value.key, key_value.value.parse()?))) + .collect::>>()?, + }) + } } #[derive(Debug, Copy, Clone, PartialEq)] @@ -254,9 +339,9 @@ pub trait Transport: Debug + Send + Sync { pub trait TransportGeneric: Transport + Clone + 'static { /// Dump data structure specific to each transport. #[doc(hidden)] - type Dump: Debug + DeserializeOwned + 'static; + type Dump: Debug + 'static; /// Stats data structure specific to each transport. - type Stat: Debug + DeserializeOwned + 'static; + type Stat: Debug + 'static; /// Dump Transport. async fn dump(&self) -> Result; @@ -267,7 +352,7 @@ pub trait TransportGeneric: Transport + Clone + 'static { } /// Error that caused [`Transport::produce`] to fail. -#[derive(Debug, Error, Eq, PartialEq)] +#[derive(Debug, Error)] pub enum ProduceError { /// Producer with the same id already exists. #[error("Producer with the same id \"{0}\" already exists")] @@ -284,7 +369,7 @@ pub enum ProduceError { } /// Error that caused [`Transport::consume`] to fail. -#[derive(Debug, Error, Eq, PartialEq)] +#[derive(Debug, Error)] pub enum ConsumeError { /// Producer with specified id not found. #[error("Producer with id \"{0}\" not found")] @@ -301,7 +386,7 @@ pub enum ConsumeError { } /// Error that caused [`Transport::produce_data`] to fail. -#[derive(Debug, Error, Eq, PartialEq)] +#[derive(Debug, Error)] pub enum ProduceDataError { /// Data producer with the same id already exists. #[error("Data producer with the same id \"{0}\" already exists")] @@ -315,7 +400,7 @@ pub enum ProduceDataError { } /// Error that caused [`Transport::consume_data`] to fail. -#[derive(Debug, Error, Eq, PartialEq)] +#[derive(Debug, Error)] pub enum ConsumeDataError { /// Data producer with specified id not found #[error("Data producer with id \"{0}\" not found")] @@ -332,8 +417,6 @@ pub enum ConsumeDataError { pub(super) trait TransportImpl: TransportGeneric { fn channel(&self) -> &Channel; - fn payload_channel(&self) -> &PayloadChannel; - fn executor(&self) -> &Arc>; fn next_mid_for_consumers(&self) -> &AtomicUsize; @@ -362,13 +445,13 @@ pub(super) trait TransportImpl: TransportGeneric { } } - async fn dump_impl(&self) -> Result { + async fn dump_impl(&self) -> Result { self.channel() .request(self.id(), TransportDumpRequest {}) .await } - async fn get_stats_impl(&self) -> Result { + async fn get_stats_impl(&self) -> Result { self.channel() .request(self.id(), TransportGetStatsRequest {}) .await @@ -395,6 +478,12 @@ pub(super) trait TransportImpl: TransportGeneric { .await } + async fn set_min_outgoing_bitrate_impl(&self, bitrate: u32) -> Result<(), RequestError> { + self.channel() + .request(self.id(), TransportSetMinOutgoingBitrateRequest { bitrate }) + .await + } + async fn produce_impl( &self, producer_options: ProducerOptions, @@ -486,7 +575,6 @@ pub(super) trait TransportImpl: TransportGeneric { paused, Arc::clone(self.executor()), self.channel().clone(), - self.payload_channel().clone(), app_data, Arc::new(self.clone()), transport_type == TransportType::Direct, @@ -507,6 +595,7 @@ pub(super) trait TransportImpl: TransportGeneric { paused, mid, preferred_layers, + enable_rtx, ignore_dtx, pipe, app_data, @@ -521,6 +610,8 @@ pub(super) trait TransportImpl: TransportGeneric { } }; + let enable_rtx = enable_rtx.unwrap_or(producer.kind() == MediaKind::Video); + let rtp_parameters = if transport_type == TransportType::Pipe { ortc::get_pipe_consumer_rtp_parameters(producer.consumable_rtp_parameters(), rtx) } else { @@ -528,6 +619,7 @@ pub(super) trait TransportImpl: TransportGeneric { producer.consumable_rtp_parameters(), &rtp_capabilities, pipe, + enable_rtx, ) .map_err(ConsumeError::BadConsumerRtpParameters)?; @@ -586,7 +678,6 @@ pub(super) trait TransportImpl: TransportGeneric { response.paused, Arc::clone(self.executor()), self.channel().clone(), - self.payload_channel(), response.producer_paused, response.score, response.preferred_layers, @@ -627,6 +718,7 @@ pub(super) trait TransportImpl: TransportGeneric { sctp_stream_parameters, label, protocol, + paused, app_data, } = data_producer_options; @@ -644,6 +736,7 @@ pub(super) trait TransportImpl: TransportGeneric { sctp_stream_parameters, label, protocol, + paused, }, ) .await @@ -655,9 +748,9 @@ pub(super) trait TransportImpl: TransportGeneric { response.sctp_stream_parameters, response.label, response.protocol, + response.paused, Arc::clone(self.executor()), self.channel().clone(), - self.payload_channel().clone(), app_data, Arc::new(self.clone()), transport_type == TransportType::Direct, @@ -675,6 +768,8 @@ pub(super) trait TransportImpl: TransportGeneric { ordered, max_packet_life_time, max_retransmits, + paused, + subchannels, app_data, } = data_consumer_options; @@ -687,24 +782,33 @@ pub(super) trait TransportImpl: TransportGeneric { let sctp_stream_parameters = match r#type { DataConsumerType::Sctp => { - let mut sctp_stream_parameters = data_producer.sctp_stream_parameters(); - if let Some(sctp_stream_parameters) = &mut sctp_stream_parameters { - if let Some(stream_id) = self.allocate_sctp_stream_id() { - sctp_stream_parameters.stream_id = stream_id; - } else { - return Err(ConsumeDataError::NoSctpStreamId); - } - if let Some(ordered) = ordered { - sctp_stream_parameters.ordered = ordered; - } - if let Some(max_packet_life_time) = max_packet_life_time { - sctp_stream_parameters.max_packet_life_time = Some(max_packet_life_time); - } - if let Some(max_retransmits) = max_retransmits { - sctp_stream_parameters.max_retransmits = Some(max_retransmits); - } + let stream_id = self + .allocate_sctp_stream_id() + .ok_or(ConsumeDataError::NoSctpStreamId)?; + let mut sctp_stream_parameters = data_producer.sctp_stream_parameters().map_or( + SctpStreamParameters { + stream_id, + ordered: true, + max_packet_life_time, + max_retransmits, + }, + |mut sctp_parameters| { + sctp_parameters.stream_id = stream_id; + + sctp_parameters + }, + ); + if let Some(ordered) = ordered { + sctp_stream_parameters.ordered = ordered; + } + if let Some(max_packet_life_time) = max_packet_life_time { + sctp_stream_parameters.max_packet_life_time = Some(max_packet_life_time); } - sctp_stream_parameters + if let Some(max_retransmits) = max_retransmits { + sctp_stream_parameters.max_retransmits = Some(max_retransmits); + } + + Some(sctp_stream_parameters) } DataConsumerType::Direct => { if ordered.is_some() || max_packet_life_time.is_some() || max_retransmits.is_some() @@ -730,6 +834,8 @@ pub(super) trait TransportImpl: TransportGeneric { sctp_stream_parameters, label: data_producer.label().clone(), protocol: data_producer.protocol().clone(), + subchannels, + paused, }, ) .await @@ -741,10 +847,12 @@ pub(super) trait TransportImpl: TransportGeneric { response.sctp_stream_parameters, response.label, response.protocol, + response.paused, data_producer, Arc::clone(self.executor()), self.channel().clone(), - self.payload_channel().clone(), + response.data_producer_paused, + response.subchannels, app_data, Arc::new(self.clone()), transport_type == TransportType::Direct, diff --git a/rust/src/router/webrtc_transport.rs b/rust/src/router/webrtc_transport.rs index 0ea5f47cdd..e3f8c3babe 100644 --- a/rust/src/router/webrtc_transport.rs +++ b/rust/src/router/webrtc_transport.rs @@ -5,11 +5,11 @@ use crate::consumer::{Consumer, ConsumerId, ConsumerOptions}; use crate::data_consumer::{DataConsumer, DataConsumerId, DataConsumerOptions, DataConsumerType}; use crate::data_producer::{DataProducer, DataProducerId, DataProducerOptions, DataProducerType}; use crate::data_structures::{ - AppData, DtlsParameters, DtlsState, IceCandidate, IceParameters, IceRole, IceState, ListenIp, + AppData, DtlsParameters, DtlsState, IceCandidate, IceParameters, IceRole, IceState, ListenInfo, SctpState, TransportTuple, }; use crate::messages::{ - TransportCloseRequest, TransportConnectWebRtcRequest, TransportRestartIceRequest, + TransportCloseRequest, TransportRestartIceRequest, WebRtcTransportConnectRequest, WebRtcTransportData, }; use crate::producer::{Producer, ProducerId, ProducerOptions}; @@ -22,15 +22,17 @@ use crate::transport::{ TransportTraceEventType, }; use crate::webrtc_server::WebRtcServer; -use crate::worker::{Channel, PayloadChannel, RequestError, SubscriptionHandler}; +use crate::worker::{Channel, NotificationParseError, RequestError, SubscriptionHandler}; use async_executor::Executor; use async_trait::async_trait; use event_listener_primitives::{Bag, BagOnce, HandlerId}; use log::{debug, error}; +use mediasoup_sys::fbs::{notification, response, transport, web_rtc_transport}; use nohash_hasher::IntMap; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; +use std::error::Error; use std::fmt; use std::ops::Deref; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; @@ -39,25 +41,25 @@ use thiserror::Error; /// Struct that protects an invariant of having non-empty list of listen IPs #[derive(Debug, Clone, Eq, PartialEq, Serialize)] -pub struct TransportListenIps(Vec); +pub struct WebRtcTransportListenInfos(Vec); -impl TransportListenIps { +impl WebRtcTransportListenInfos { /// Create transport listen IPs with given IP populated initially. #[must_use] - pub fn new(listen_ip: ListenIp) -> Self { - Self(vec![listen_ip]) + pub fn new(listen_info: ListenInfo) -> Self { + Self(vec![listen_info]) } /// Insert another listen IP. #[must_use] - pub fn insert(mut self, listen_ip: ListenIp) -> Self { - self.0.push(listen_ip); + pub fn insert(mut self, listen_info: ListenInfo) -> Self { + self.0.push(listen_info); self } } -impl Deref for TransportListenIps { - type Target = Vec; +impl Deref for WebRtcTransportListenInfos { + type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 @@ -69,14 +71,14 @@ impl Deref for TransportListenIps { #[error("Empty list of listen IPs provided, should have at least one element")] pub struct EmptyListError; -impl TryFrom> for TransportListenIps { +impl TryFrom> for WebRtcTransportListenInfos { type Error = EmptyListError; - fn try_from(listen_ips: Vec) -> Result { - if listen_ips.is_empty() { + fn try_from(listen_infos: Vec) -> Result { + if listen_infos.is_empty() { Err(EmptyListError) } else { - Ok(Self(listen_ips)) + Ok(Self(listen_infos)) } } } @@ -84,18 +86,16 @@ impl TryFrom> for TransportListenIps { /// How [`WebRtcTransport`] should listen on interfaces. /// /// # Notes on usage -/// * Do not use "0.0.0.0" into `listen_ips`. Values in `listen_ips` must be specific bindable IPs +/// * Do not use "0.0.0.0" into `listen_infos`. Values in `listen_infos` must be specific bindable IPs /// on the host. -/// * If you use "0.0.0.0" or "::" into `listen_ips`, then you need to also provide `announced_ip` -/// in the corresponding entry in `listen_ips`. +/// * If you use "0.0.0.0" or "::" into `listen_infos`, then you need to also provide +/// `announced_address` in the corresponding entry in `listen_infos`. #[derive(Debug, Clone)] pub enum WebRtcTransportListen { - /// Listen on individual IP/port combinations specific to this transport. + /// Listen on individual protocol/IP/port combinations specific to this transport. Individual { - /// Listening IP address or addresses in order of preference (first one is the preferred one). - listen_ips: TransportListenIps, - /// Fixed port to listen on instead of selecting automatically from Worker's port range. - port: Option, + /// Listening infos in order of preference (first one is the preferred one). + listen_infos: WebRtcTransportListenInfos, }, /// Share [`WebRtcServer`] with other transports withing the same worker. Server { @@ -114,10 +114,14 @@ pub enum WebRtcTransportListen { pub struct WebRtcTransportOptions { /// How [`WebRtcTransport`] should listen on interfaces. pub listen: WebRtcTransportListen, - /// Listen in UDP. Default true. + /// Initial available outgoing bitrate (in bps). + /// Default 600000. + pub initial_available_outgoing_bitrate: u32, + /// Enable UDP. + /// Default true. pub enable_udp: bool, - /// Listen in TCP. - /// Default false. + /// Enable TCP. + /// Default true if webrtc_server is given, false otherwise. pub enable_tcp: bool, /// Prefer UDP. /// Default false. @@ -125,9 +129,9 @@ pub struct WebRtcTransportOptions { /// Prefer TCP. /// Default false. pub prefer_tcp: bool, - /// Initial available outgoing bitrate (in bps). - /// Default 600000. - pub initial_available_outgoing_bitrate: u32, + /// ICE consent timeout (in seconds). If 0 it is disabled. + /// Default 30. + pub ice_consent_timeout: u8, /// Create a SCTP association. /// Default false. pub enable_sctp: bool, @@ -144,19 +148,17 @@ pub struct WebRtcTransportOptions { } impl WebRtcTransportOptions { - /// Create [`WebRtcTransport`] options with given listen IPs. + /// Create [`WebRtcTransport`] options with given listen infos. #[must_use] - pub fn new(listen_ips: TransportListenIps) -> Self { + pub fn new(listen_infos: WebRtcTransportListenInfos) -> Self { Self { - listen: WebRtcTransportListen::Individual { - listen_ips, - port: None, - }, + listen: WebRtcTransportListen::Individual { listen_infos }, + initial_available_outgoing_bitrate: 600_000, enable_udp: true, enable_tcp: false, prefer_udp: false, prefer_tcp: false, - initial_available_outgoing_bitrate: 600_000, + ice_consent_timeout: 30, enable_sctp: false, num_sctp_streams: NumSctpStreams::default(), max_sctp_message_size: 262_144, @@ -169,11 +171,12 @@ impl WebRtcTransportOptions { pub fn new_with_server(webrtc_server: WebRtcServer) -> Self { Self { listen: WebRtcTransportListen::Server { webrtc_server }, + initial_available_outgoing_bitrate: 600_000, enable_udp: true, - enable_tcp: false, + enable_tcp: true, prefer_udp: false, prefer_tcp: false, - initial_available_outgoing_bitrate: 600_000, + ice_consent_timeout: 30, enable_sctp: false, num_sctp_streams: NumSctpStreams::default(), max_sctp_message_size: 262_144, @@ -199,11 +202,11 @@ pub struct WebRtcTransportDump { pub data_consumer_ids: Vec, pub recv_rtp_header_extensions: RecvRtpHeaderExtensions, pub rtp_listener: RtpListener, - pub max_message_size: usize, + pub max_message_size: u32, pub sctp_parameters: Option, pub sctp_state: Option, pub sctp_listener: Option, - pub trace_event_types: String, + pub trace_event_types: Vec, // WebRtcTransport specific. pub dtls_parameters: DtlsParameters, pub dtls_state: DtlsState, @@ -214,6 +217,91 @@ pub struct WebRtcTransportDump { pub ice_selected_tuple: Option, } +impl WebRtcTransportDump { + pub(crate) fn from_fbs( + dump: web_rtc_transport::DumpResponse, + ) -> Result> { + Ok(Self { + // Common to all Transports. + id: dump.base.id.parse()?, + direct: false, + producer_ids: dump + .base + .producer_ids + .iter() + .map(|producer_id| Ok(producer_id.parse()?)) + .collect::>>()?, + consumer_ids: dump + .base + .consumer_ids + .iter() + .map(|consumer_id| Ok(consumer_id.parse()?)) + .collect::>>()?, + map_ssrc_consumer_id: dump + .base + .map_ssrc_consumer_id + .iter() + .map(|key_value| Ok((key_value.key, key_value.value.parse()?))) + .collect::>>()?, + map_rtx_ssrc_consumer_id: dump + .base + .map_rtx_ssrc_consumer_id + .iter() + .map(|key_value| Ok((key_value.key, key_value.value.parse()?))) + .collect::>>()?, + data_producer_ids: dump + .base + .data_producer_ids + .iter() + .map(|data_producer_id| Ok(data_producer_id.parse()?)) + .collect::>>()?, + data_consumer_ids: dump + .base + .data_consumer_ids + .iter() + .map(|data_consumer_id| Ok(data_consumer_id.parse()?)) + .collect::>>()?, + recv_rtp_header_extensions: RecvRtpHeaderExtensions::from_fbs( + dump.base.recv_rtp_header_extensions.as_ref(), + ), + rtp_listener: RtpListener::from_fbs(dump.base.rtp_listener.as_ref())?, + max_message_size: dump.base.max_message_size, + sctp_parameters: dump + .base + .sctp_parameters + .as_ref() + .map(|parameters| SctpParameters::from_fbs(parameters.as_ref())), + sctp_state: dump + .base + .sctp_state + .map(|state| SctpState::from_fbs(&state)), + sctp_listener: dump.base.sctp_listener.as_ref().map(|listener| { + SctpListener::from_fbs(listener.as_ref()).expect("Error parsing SctpListner") + }), + trace_event_types: dump + .base + .trace_event_types + .iter() + .map(TransportTraceEventType::from_fbs) + .collect(), + // WebRtcTransport specific. + dtls_parameters: DtlsParameters::from_fbs(*dump.dtls_parameters), + dtls_state: DtlsState::from_fbs(dump.dtls_state), + ice_candidates: dump + .ice_candidates + .iter() + .map(IceCandidate::from_fbs) + .collect(), + ice_parameters: IceParameters::from_fbs(*dump.ice_parameters), + ice_role: IceRole::from_fbs(dump.ice_role), + ice_state: IceState::from_fbs(dump.ice_state), + ice_selected_tuple: dump + .ice_selected_tuple + .map(|tuple| TransportTuple::from_fbs(tuple.as_ref())), + }) + } +} + /// RTC statistics of the [`WebRtcTransport`]. #[derive(Debug, Clone, PartialOrd, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -225,26 +313,27 @@ pub struct WebRtcTransportStat { pub transport_id: TransportId, pub timestamp: u64, pub sctp_state: Option, - pub bytes_received: usize, + pub bytes_received: u64, pub recv_bitrate: u32, - pub bytes_sent: usize, + pub bytes_sent: u64, pub send_bitrate: u32, - pub rtp_bytes_received: usize, + pub rtp_bytes_received: u64, pub rtp_recv_bitrate: u32, - pub rtp_bytes_sent: usize, + pub rtp_bytes_sent: u64, pub rtp_send_bitrate: u32, - pub rtx_bytes_received: usize, + pub rtx_bytes_received: u64, pub rtx_recv_bitrate: u32, - pub rtx_bytes_sent: usize, + pub rtx_bytes_sent: u64, pub rtx_send_bitrate: u32, - pub probation_bytes_sent: usize, + pub probation_bytes_sent: u64, pub probation_send_bitrate: u32, #[serde(skip_serializing_if = "Option::is_none")] pub available_outgoing_bitrate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub available_incoming_bitrate: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub max_incoming_bitrate: Option, + pub max_outgoing_bitrate: Option, + pub min_outgoing_bitrate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rtp_packet_loss_received: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -257,6 +346,45 @@ pub struct WebRtcTransportStat { pub dtls_state: DtlsState, } +impl WebRtcTransportStat { + pub(crate) fn from_fbs( + stats: web_rtc_transport::GetStatsResponse, + ) -> Result> { + Ok(Self { + transport_id: stats.base.transport_id.parse()?, + timestamp: stats.base.timestamp, + sctp_state: stats.base.sctp_state.as_ref().map(SctpState::from_fbs), + bytes_received: stats.base.bytes_received, + recv_bitrate: stats.base.recv_bitrate, + bytes_sent: stats.base.bytes_sent, + send_bitrate: stats.base.send_bitrate, + rtp_bytes_received: stats.base.rtp_bytes_received, + rtp_recv_bitrate: stats.base.rtp_recv_bitrate, + rtp_bytes_sent: stats.base.rtp_bytes_sent, + rtp_send_bitrate: stats.base.rtp_send_bitrate, + rtx_bytes_received: stats.base.rtx_bytes_received, + rtx_recv_bitrate: stats.base.rtx_recv_bitrate, + rtx_bytes_sent: stats.base.rtx_bytes_sent, + rtx_send_bitrate: stats.base.rtx_send_bitrate, + probation_bytes_sent: stats.base.probation_bytes_sent, + probation_send_bitrate: stats.base.probation_send_bitrate, + available_outgoing_bitrate: stats.base.available_outgoing_bitrate, + available_incoming_bitrate: stats.base.available_incoming_bitrate, + max_incoming_bitrate: stats.base.max_incoming_bitrate, + max_outgoing_bitrate: stats.base.max_outgoing_bitrate, + min_outgoing_bitrate: stats.base.min_outgoing_bitrate, + rtp_packet_loss_received: stats.base.rtp_packet_loss_received, + rtp_packet_loss_sent: stats.base.rtp_packet_loss_sent, + // WebRtcTransport specific. + ice_role: IceRole::from_fbs(stats.ice_role), + ice_state: IceState::from_fbs(stats.ice_state), + ice_selected_tuple: stats + .ice_selected_tuple + .map(|tuple| TransportTuple::from_fbs(tuple.as_ref())), + dtls_state: DtlsState::from_fbs(stats.dtls_state), + }) + } +} /// Remote parameters for [`WebRtcTransport`]. #[derive(Debug, Clone, PartialOrd, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -305,6 +433,80 @@ enum Notification { Trace(TransportTraceEventData), } +impl Notification { + pub(crate) fn from_fbs( + notification: notification::NotificationRef<'_>, + ) -> Result { + match notification.event().unwrap() { + notification::Event::WebrtctransportIceStateChange => { + let Ok(Some(notification::BodyRef::WebRtcTransportIceStateChangeNotification( + body, + ))) = notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let ice_state = IceState::from_fbs(body.ice_state().unwrap()); + + Ok(Notification::IceStateChange { ice_state }) + } + notification::Event::WebrtctransportIceSelectedTupleChange => { + let Ok(Some( + notification::BodyRef::WebRtcTransportIceSelectedTupleChangeNotification(body), + )) = notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let ice_selected_tuple_fbs = + transport::Tuple::try_from(body.tuple().unwrap()).unwrap(); + let ice_selected_tuple = TransportTuple::from_fbs(&ice_selected_tuple_fbs); + + Ok(Notification::IceSelectedTupleChange { ice_selected_tuple }) + } + notification::Event::WebrtctransportDtlsStateChange => { + let Ok(Some(notification::BodyRef::WebRtcTransportDtlsStateChangeNotification( + body, + ))) = notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let dtls_state = DtlsState::from_fbs(body.dtls_state().unwrap()); + + Ok(Notification::DtlsStateChange { + dtls_state, + dtls_remote_cert: None, + }) + } + notification::Event::TransportSctpStateChange => { + let Ok(Some(notification::BodyRef::TransportSctpStateChangeNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let sctp_state = SctpState::from_fbs(&body.sctp_state().unwrap()); + + Ok(Notification::SctpStateChange { sctp_state }) + } + notification::Event::TransportTrace => { + let Ok(Some(notification::BodyRef::TransportTraceNotification(body))) = + notification.body() + else { + panic!("Wrong message from worker: {notification:?}"); + }; + + let trace_notification_fbs = transport::TraceNotification::try_from(body).unwrap(); + let trace_notification = TransportTraceEventData::from_fbs(trace_notification_fbs); + + Ok(Notification::Trace(trace_notification)) + } + _ => Err(NotificationParseError::InvalidEvent), + } + } +} + struct Inner { id: TransportId, next_mid_for_consumers: AtomicUsize, @@ -312,7 +514,6 @@ struct Inner { cname_for_producers: Mutex>, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, handlers: Arc, data: Arc, app_data: AppData, @@ -542,21 +743,24 @@ impl TransportGeneric for WebRtcTransport { async fn dump(&self) -> Result { debug!("dump()"); - serde_json::from_value(self.dump_impl().await?).map_err(|error| { - RequestError::FailedToParse { - error: error.to_string(), - } - }) + if let response::Body::WebRtcTransportDumpResponse(data) = self.dump_impl().await? { + Ok(WebRtcTransportDump::from_fbs(*data).expect("Error parsing dump response")) + } else { + panic!("Wrong message from worker"); + } } async fn get_stats(&self) -> Result, RequestError> { debug!("get_stats()"); - serde_json::from_value(self.get_stats_impl().await?).map_err(|error| { - RequestError::FailedToParse { - error: error.to_string(), - } - }) + if let response::Body::WebRtcTransportGetStatsResponse(data) = self.get_stats_impl().await? + { + Ok(vec![ + WebRtcTransportStat::from_fbs(*data).expect("Error parsing dump response") + ]) + } else { + panic!("Wrong message from worker"); + } } } @@ -565,10 +769,6 @@ impl TransportImpl for WebRtcTransport { &self.inner.channel } - fn payload_channel(&self) -> &PayloadChannel { - &self.inner.payload_channel - } - fn executor(&self) -> &Arc> { &self.inner.executor } @@ -592,7 +792,6 @@ impl WebRtcTransport { id: TransportId, executor: Arc>, channel: Channel, - payload_channel: PayloadChannel, data: WebRtcTransportData, app_data: AppData, router: Router, @@ -608,7 +807,7 @@ impl WebRtcTransport { let data = Arc::clone(&data); channel.subscribe_to_notifications(id.into(), move |notification| { - match serde_json::from_slice::(notification) { + match Notification::from_fbs(notification) { Ok(notification) => match notification { Notification::IceStateChange { ice_state } => { *data.ice_state.lock() = ice_state; @@ -617,7 +816,9 @@ impl WebRtcTransport { }); } Notification::IceSelectedTupleChange { ice_selected_tuple } => { - data.ice_selected_tuple.lock().replace(ice_selected_tuple); + data.ice_selected_tuple + .lock() + .replace(ice_selected_tuple.clone()); handlers .ice_selected_tuple_change .call_simple(&ice_selected_tuple); @@ -697,7 +898,6 @@ impl WebRtcTransport { cname_for_producers, executor, channel, - payload_channel, handlers, data, app_data, @@ -762,7 +962,7 @@ impl WebRtcTransport { .channel .request( self.id(), - TransportConnectWebRtcRequest { + WebRtcTransportConnectRequest { dtls_parameters: remote_parameters.dtls_parameters, }, ) @@ -794,6 +994,14 @@ impl WebRtcTransport { self.set_max_outgoing_bitrate_impl(bitrate).await } + /// Set minimum outgoing bitrate for media streams sent by the remote endpoint over this + /// transport. + pub async fn set_min_outgoing_bitrate(&self, bitrate: u32) -> Result<(), RequestError> { + debug!("set_min_outgoing_bitrate() [bitrate:{}]", bitrate); + + self.set_min_outgoing_bitrate_impl(bitrate).await + } + /// Local ICE role. Due to the mediasoup ICE Lite design, this is always `Controlled`. #[must_use] pub fn ice_role(&self) -> IceRole { @@ -822,7 +1030,7 @@ impl WebRtcTransport { /// ICE is not established (no working candidate pair was found). #[must_use] pub fn ice_selected_tuple(&self) -> Option { - *self.inner.data.ice_selected_tuple.lock() + self.inner.data.ice_selected_tuple.lock().clone() } /// Local DTLS parameters. @@ -864,13 +1072,10 @@ impl WebRtcTransport { pub async fn restart_ice(&self) -> Result { debug!("restart_ice()"); - let response = self - .inner + self.inner .channel .request(self.id(), TransportRestartIceRequest {}) - .await?; - - Ok(response.ice_parameters) + .await } /// Callback is called when the WebRTC server used during creation of this transport is closed diff --git a/rust/src/router/webrtc_transport/tests.rs b/rust/src/router/webrtc_transport/tests.rs index 9ddf7f66f8..86a7518797 100644 --- a/rust/src/router/webrtc_transport/tests.rs +++ b/rust/src/router/webrtc_transport/tests.rs @@ -1,11 +1,11 @@ -use crate::data_structures::{IceCandidateType, IceState, ListenIp, Protocol}; +use crate::data_structures::{ + IceCandidateTcpType, IceCandidateType, IceState, ListenInfo, Protocol, +}; use crate::prelude::WebRtcTransport; use crate::router::{NewTransport, Router, RouterOptions}; use crate::transport::Transport; -use crate::webrtc_server::{ - WebRtcServerIpPort, WebRtcServerListenInfo, WebRtcServerListenInfos, WebRtcServerOptions, -}; -use crate::webrtc_transport::{TransportListenIps, WebRtcTransportOptions}; +use crate::webrtc_server::{WebRtcServerIpPort, WebRtcServerListenInfos, WebRtcServerOptions}; +use crate::webrtc_transport::{WebRtcTransportListenInfos, WebRtcTransportOptions}; use crate::worker::{Worker, WorkerSettings}; use crate::worker_manager::WorkerManager; use async_io::Timer; @@ -52,21 +52,25 @@ fn create_with_webrtc_server_succeeds() { let webrtc_server = worker .create_webrtc_server({ - let listen_infos = WebRtcServerListenInfos::new(WebRtcServerListenInfo { + let listen_infos = WebRtcServerListenInfos::new(ListenInfo { protocol: Protocol::Udp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - }, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, port: Some(port1), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); - let listen_infos = listen_infos.insert(WebRtcServerListenInfo { + let listen_infos = listen_infos.insert(ListenInfo { protocol: Protocol::Tcp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - }, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, port: Some(port2), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); WebRtcServerOptions::new(listen_infos) }) @@ -104,9 +108,14 @@ fn create_with_webrtc_server_succeeds() { }); let transport = router - .create_webrtc_transport(WebRtcTransportOptions::new_with_server( - webrtc_server.clone(), - )) + .create_webrtc_transport({ + let mut webrtc_transport_options = + WebRtcTransportOptions::new_with_server(webrtc_server.clone()); + // Let's disable UDP so resulting ICE candidates should only contain TCP. + webrtc_transport_options.enable_udp = false; + + webrtc_transport_options + }) .await .expect("Failed to create WebRTC transport"); @@ -125,11 +134,14 @@ fn create_with_webrtc_server_succeeds() { { let ice_candidates = transport.ice_candidates(); assert_eq!(ice_candidates.len(), 1); - assert_eq!(ice_candidates[0].ip, IpAddr::V4(Ipv4Addr::LOCALHOST)); - assert_eq!(ice_candidates[0].protocol, Protocol::Udp); - assert_eq!(ice_candidates[0].port, port1); + assert_eq!(ice_candidates[0].address, "127.0.0.1"); + assert_eq!(ice_candidates[0].protocol, Protocol::Tcp); + assert_eq!(ice_candidates[0].port, port2); assert_eq!(ice_candidates[0].r#type, IceCandidateType::Host); - assert_eq!(ice_candidates[0].tcp_type, None); + assert_eq!( + ice_candidates[0].tcp_type, + Some(IceCandidateTcpType::Passive) + ); } assert_eq!(transport.ice_state(), IceState::New); @@ -219,12 +231,18 @@ fn router_close_event() { let (_worker, router) = init().await; let transport = router - .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - ListenIp { + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("9.9.9.1".parse().unwrap()), - }, - ))) + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) .await .expect("Failed to create WebRTC transport"); @@ -257,21 +275,25 @@ fn webrtc_server_close_event() { let webrtc_server = worker .create_webrtc_server({ - let listen_infos = WebRtcServerListenInfos::new(WebRtcServerListenInfo { + let listen_infos = WebRtcServerListenInfos::new(ListenInfo { protocol: Protocol::Udp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - }, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, port: Some(port1), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); - let listen_infos = listen_infos.insert(WebRtcServerListenInfo { + let listen_infos = listen_infos.insert(ListenInfo { protocol: Protocol::Tcp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - }, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, port: Some(port2), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); WebRtcServerOptions::new(listen_infos) }) diff --git a/rust/src/rtp_parameters.rs b/rust/src/rtp_parameters.rs index d3563f5724..ce9fcb422c 100644 --- a/rust/src/rtp_parameters.rs +++ b/rust/src/rtp_parameters.rs @@ -5,14 +5,18 @@ mod tests; use crate::scalability_modes::ScalabilityMode; +use mediasoup_sys::fbs::rtp_parameters; use serde::de::{MapAccess, Visitor}; use serde::ser::SerializeStruct; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::borrow::Cow; use std::collections::BTreeMap; +use std::error::Error; use std::fmt; use std::iter::FromIterator; use std::num::{NonZeroU32, NonZeroU8}; +use std::str::FromStr; +use thiserror::Error; /// Codec specific parameters. Some parameters (such as `packetization-mode` and `profile-level-id` /// in H264 or `profile-id` in VP9) are critical for codec matching. @@ -200,6 +204,33 @@ pub enum MediaKind { Video, } +impl MediaKind { + pub(crate) fn to_fbs(self) -> rtp_parameters::MediaKind { + match self { + MediaKind::Audio => rtp_parameters::MediaKind::Audio, + MediaKind::Video => rtp_parameters::MediaKind::Video, + } + } + + pub(crate) fn from_fbs(kind: rtp_parameters::MediaKind) -> Self { + match kind { + rtp_parameters::MediaKind::Audio => MediaKind::Audio, + rtp_parameters::MediaKind::Video => MediaKind::Video, + } + } +} + +/// Error that caused [`MimeType`] parsing error. +#[derive(Debug, Error, Eq, PartialEq)] +pub enum ParseMimeTypeError { + /// Invalid MIME type input string + #[error("Invalid MIME type input string")] + InvalidInput, + /// Unknown MIME type + #[error("Unknown MIME type")] + UnknownMimeType, +} + /// Known Audio or Video MIME type. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] #[serde(untagged)] @@ -210,6 +241,30 @@ pub enum MimeType { Video(MimeTypeVideo), } +impl FromStr for MimeType { + type Err = ParseMimeTypeError; + + fn from_str(s: &str) -> Result { + if s.starts_with("audio/") { + MimeTypeAudio::from_str(s).map(Self::Audio) + } else if s.starts_with("video/") { + MimeTypeVideo::from_str(s).map(Self::Video) + } else { + Err(ParseMimeTypeError::InvalidInput) + } + } +} + +impl MimeType { + /// String representation of MIME type. + pub fn as_str(&self) -> &'static str { + match self { + Self::Audio(mime_type) => mime_type.as_str(), + Self::Video(mime_type) => mime_type.as_str(), + } + } +} + /// Known Audio MIME types. #[allow(non_camel_case_types)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] @@ -252,6 +307,52 @@ pub enum MimeTypeAudio { Red, } +impl FromStr for MimeTypeAudio { + type Err = ParseMimeTypeError; + + fn from_str(s: &str) -> Result { + match s { + "audio/opus" => Ok(Self::Opus), + "audio/multiopus" => Ok(Self::MultiChannelOpus), + "audio/PCMU" => Ok(Self::Pcmu), + "audio/PCMA" => Ok(Self::Pcma), + "audio/ISAC" => Ok(Self::Isac), + "audio/G722" => Ok(Self::G722), + "audio/iLBC" => Ok(Self::Ilbc), + "audio/SILK" => Ok(Self::Silk), + "audio/CN" => Ok(Self::Cn), + "audio/telephone-event" => Ok(Self::TelephoneEvent), + "audio/rtx" => Ok(Self::Rtx), + "audio/red" => Ok(Self::Red), + s => Err(if s.starts_with("audio/") { + ParseMimeTypeError::UnknownMimeType + } else { + ParseMimeTypeError::InvalidInput + }), + } + } +} + +impl MimeTypeAudio { + /// String representation of MIME type. + pub fn as_str(&self) -> &'static str { + match self { + Self::Opus => "audio/opus", + Self::MultiChannelOpus => "audio/multiopus", + Self::Pcmu => "audio/PCMU", + Self::Pcma => "audio/PCMA", + Self::Isac => "audio/ISAC", + Self::G722 => "audio/G722", + Self::Ilbc => "audio/iLBC", + Self::Silk => "audio/SILK", + Self::Cn => "audio/CN", + Self::TelephoneEvent => "audio/telephone-event", + Self::Rtx => "audio/rtx", + Self::Red => "audio/red", + } + } +} + /// Known Video MIME types. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] pub enum MimeTypeVideo { @@ -281,6 +382,44 @@ pub enum MimeTypeVideo { Ulpfec, } +impl FromStr for MimeTypeVideo { + type Err = ParseMimeTypeError; + + fn from_str(s: &str) -> Result { + match s { + "video/VP8" => Ok(Self::Vp8), + "video/VP9" => Ok(Self::Vp9), + "video/H264" => Ok(Self::H264), + "video/H264-SVC" => Ok(Self::H264Svc), + "video/H265" => Ok(Self::H265), + "video/rtx" => Ok(Self::Rtx), + "video/red" => Ok(Self::Red), + "video/ulpfec" => Ok(Self::Ulpfec), + s => Err(if s.starts_with("video/") { + ParseMimeTypeError::UnknownMimeType + } else { + ParseMimeTypeError::InvalidInput + }), + } + } +} + +impl MimeTypeVideo { + /// String representation of MIME type. + pub fn as_str(&self) -> &'static str { + match self { + Self::Vp8 => "video/VP8", + Self::Vp9 => "video/VP9", + Self::H264 => "video/H264", + Self::H264Svc => "video/H264-SVC", + Self::H265 => "video/H265", + Self::Rtx => "video/rtx", + Self::Red => "video/red", + Self::Ulpfec => "video/ulpfec", + } + } +} + /// Provides information on the capabilities of a codec within the RTP capabilities. The list of /// media codecs supported by mediasoup and their settings is defined in the /// `supported_rtp_capabilities.rs` file. @@ -402,6 +541,14 @@ impl Default for RtpHeaderExtensionDirection { } } +/// Error that caused [`RtpHeaderExtensionUri`] parsing error. +#[derive(Debug, Error, Eq, PartialEq)] +pub enum RtpHeaderExtensionUriParseError { + /// Unsupported + #[error("Unsupported")] + Unsupported, +} + /// URI for supported RTP header extension #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] pub enum RtpHeaderExtensionUri { @@ -438,11 +585,116 @@ pub enum RtpHeaderExtensionUri { /// #[serde(rename = "http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time")] AbsCaptureTime, + /// + #[serde(rename = "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay")] + PlayoutDelay, + #[doc(hidden)] #[serde(other, rename = "unsupported")] Unsupported, } +impl RtpHeaderExtensionUri { + pub(crate) fn to_fbs(self) -> rtp_parameters::RtpHeaderExtensionUri { + match self { + RtpHeaderExtensionUri::Mid => rtp_parameters::RtpHeaderExtensionUri::Mid, + RtpHeaderExtensionUri::RtpStreamId => { + rtp_parameters::RtpHeaderExtensionUri::RtpStreamId + } + RtpHeaderExtensionUri::RepairRtpStreamId => { + rtp_parameters::RtpHeaderExtensionUri::RepairRtpStreamId + } + RtpHeaderExtensionUri::FrameMarkingDraft07 => { + rtp_parameters::RtpHeaderExtensionUri::FrameMarkingDraft07 + } + RtpHeaderExtensionUri::FrameMarking => { + rtp_parameters::RtpHeaderExtensionUri::FrameMarking + } + RtpHeaderExtensionUri::AudioLevel => rtp_parameters::RtpHeaderExtensionUri::AudioLevel, + RtpHeaderExtensionUri::VideoOrientation => { + rtp_parameters::RtpHeaderExtensionUri::VideoOrientation + } + RtpHeaderExtensionUri::TimeOffset => rtp_parameters::RtpHeaderExtensionUri::TimeOffset, + RtpHeaderExtensionUri::TransportWideCcDraft01 => { + rtp_parameters::RtpHeaderExtensionUri::TransportWideCcDraft01 + } + RtpHeaderExtensionUri::AbsSendTime => { + rtp_parameters::RtpHeaderExtensionUri::AbsSendTime + } + RtpHeaderExtensionUri::AbsCaptureTime => { + rtp_parameters::RtpHeaderExtensionUri::AbsCaptureTime + } + RtpHeaderExtensionUri::PlayoutDelay => { + rtp_parameters::RtpHeaderExtensionUri::PlayoutDelay + } + RtpHeaderExtensionUri::Unsupported => panic!("Invalid RTP extension header URI"), + } + } + + pub(crate) fn from_fbs(uri: rtp_parameters::RtpHeaderExtensionUri) -> Self { + match uri { + rtp_parameters::RtpHeaderExtensionUri::Mid => RtpHeaderExtensionUri::Mid, + rtp_parameters::RtpHeaderExtensionUri::RtpStreamId => { + RtpHeaderExtensionUri::RtpStreamId + } + rtp_parameters::RtpHeaderExtensionUri::RepairRtpStreamId => { + RtpHeaderExtensionUri::RepairRtpStreamId + } + rtp_parameters::RtpHeaderExtensionUri::FrameMarkingDraft07 => { + RtpHeaderExtensionUri::FrameMarkingDraft07 + } + rtp_parameters::RtpHeaderExtensionUri::FrameMarking => { + RtpHeaderExtensionUri::FrameMarking + } + rtp_parameters::RtpHeaderExtensionUri::AudioLevel => RtpHeaderExtensionUri::AudioLevel, + rtp_parameters::RtpHeaderExtensionUri::VideoOrientation => { + RtpHeaderExtensionUri::VideoOrientation + } + rtp_parameters::RtpHeaderExtensionUri::TimeOffset => RtpHeaderExtensionUri::TimeOffset, + rtp_parameters::RtpHeaderExtensionUri::TransportWideCcDraft01 => { + RtpHeaderExtensionUri::TransportWideCcDraft01 + } + rtp_parameters::RtpHeaderExtensionUri::AbsSendTime => { + RtpHeaderExtensionUri::AbsSendTime + } + rtp_parameters::RtpHeaderExtensionUri::AbsCaptureTime => { + RtpHeaderExtensionUri::AbsCaptureTime + } + rtp_parameters::RtpHeaderExtensionUri::PlayoutDelay => { + RtpHeaderExtensionUri::PlayoutDelay + } + } + } +} + +impl FromStr for RtpHeaderExtensionUri { + type Err = RtpHeaderExtensionUriParseError; + + fn from_str(s: &str) -> Result { + match s { + "urn:ietf:params:rtp-hdrext:sdes:mid" => Ok(Self::Mid), + "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id" => Ok(Self::RtpStreamId), + "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id" => Ok(Self::RepairRtpStreamId), + "http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07" => { + Ok(Self::FrameMarkingDraft07) + } + "urn:ietf:params:rtp-hdrext:framemarking" => Ok(Self::FrameMarking), + "urn:ietf:params:rtp-hdrext:ssrc-audio-level" => Ok(Self::AudioLevel), + "urn:3gpp:video-orientation" => Ok(Self::VideoOrientation), + "urn:ietf:params:rtp-hdrext:toffset" => Ok(Self::TimeOffset), + "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01" => { + Ok(Self::TransportWideCcDraft01) + } + "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time" => Ok(Self::AbsSendTime), + "http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time" => { + Ok(Self::AbsCaptureTime) + } + "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" => Ok(Self::PlayoutDelay), + _ => Err(RtpHeaderExtensionUriParseError::Unsupported), + } + } +} + impl RtpHeaderExtensionUri { /// RTP header extension as a string #[must_use] @@ -469,6 +721,9 @@ impl RtpHeaderExtensionUri { RtpHeaderExtensionUri::AbsCaptureTime => { "http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time" } + RtpHeaderExtensionUri::PlayoutDelay => { + "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" + } RtpHeaderExtensionUri::Unsupported => "unsupported", } } @@ -545,6 +800,216 @@ pub struct RtpParameters { pub rtcp: RtcpParameters, } +impl RtpParameters { + pub(crate) fn from_fbs_ref( + rtp_parameters: rtp_parameters::RtpParametersRef<'_>, + ) -> Result> { + Ok(Self { + mid: rtp_parameters.mid()?.map(|mid| mid.to_string()), + codecs: rtp_parameters + .codecs()? + .into_iter() + .map(|codec| { + let parameters = codec? + .parameters()? + .unwrap_or(planus::Vector::new_empty()) + .into_iter() + .map(|parameters| { + Ok(( + Cow::Owned(parameters?.name()?.to_string()), + match parameters?.value()? { + rtp_parameters::ValueRef::Boolean(_) + | rtp_parameters::ValueRef::Double(_) + | rtp_parameters::ValueRef::Integer32Array(_) => { + // TODO: Above value variant should not exist in the + // first place + panic!("Invalid parameter") + } + rtp_parameters::ValueRef::Integer32(n) => { + RtpCodecParametersParametersValue::Number( + n.value()?.try_into()?, + ) + } + rtp_parameters::ValueRef::String(s) => { + RtpCodecParametersParametersValue::String( + s.value()?.to_string().into(), + ) + } + }, + )) + }) + .collect::>>()?; + let rtcp_feedback = codec? + .rtcp_feedback()? + .unwrap_or(planus::Vector::new_empty()) + .into_iter() + .map(|rtcp_feedback| { + Ok(RtcpFeedback::from_type_parameter( + rtcp_feedback?.type_()?, + rtcp_feedback?.parameter()?.unwrap_or_default(), + )?) + }) + .collect::>>()?; + + Ok(match MimeType::from_str(codec?.mime_type()?)? { + MimeType::Audio(mime_type) => RtpCodecParameters::Audio { + mime_type, + payload_type: codec?.payload_type()?, + clock_rate: codec?.clock_rate()?.try_into()?, + channels: codec? + .channels()? + .ok_or("Audio must have channels specified")? + .try_into()?, + parameters, + rtcp_feedback: vec![], + }, + MimeType::Video(mime_type) => RtpCodecParameters::Video { + mime_type, + payload_type: codec?.payload_type()?, + clock_rate: codec?.clock_rate()?.try_into()?, + parameters, + rtcp_feedback, + }, + }) + }) + .collect::>>()?, + header_extensions: rtp_parameters + .header_extensions()? + .into_iter() + .map(|header_extension_parameters| { + Ok(RtpHeaderExtensionParameters { + uri: RtpHeaderExtensionUri::from_fbs(header_extension_parameters?.uri()?), + id: u16::from(header_extension_parameters?.id()?), + encrypt: header_extension_parameters?.encrypt()?, + }) + }) + .collect::>>()?, + encodings: rtp_parameters + .encodings()? + .into_iter() + .map(|encoding| { + Ok(RtpEncodingParameters { + ssrc: encoding?.ssrc()?, + rid: encoding?.rid()?.map(|rid| rid.to_string()), + codec_payload_type: encoding?.codec_payload_type()?, + rtx: encoding?.rtx()?.map(|rtx| RtpEncodingParametersRtx { + ssrc: rtx.ssrc().unwrap(), + }), + dtx: { + match encoding?.dtx()? { + true => Some(true), + false => None, + } + }, + scalability_mode: encoding? + .scalability_mode()? + .unwrap_or(String::from("S1T1").as_str()) + .parse()?, + max_bitrate: encoding?.max_bitrate()?, + }) + }) + .collect::>>()?, + rtcp: RtcpParameters { + cname: rtp_parameters + .rtcp()? + .cname()? + .map(|cname| cname.to_string()), + reduced_size: rtp_parameters.rtcp()?.reduced_size()?, + }, + }) + } + + pub(crate) fn into_fbs(self) -> rtp_parameters::RtpParameters { + rtp_parameters::RtpParameters { + mid: self.mid, + codecs: self + .codecs + .into_iter() + .map(|codec| rtp_parameters::RtpCodecParameters { + mime_type: codec.mime_type().as_str().to_string(), + payload_type: codec.payload_type(), + clock_rate: codec.clock_rate().get(), + channels: match &codec { + RtpCodecParameters::Audio { channels, .. } => Some(channels.get()), + RtpCodecParameters::Video { .. } => None, + }, + parameters: Some( + codec + .parameters() + .iter() + .map(|(name, value)| rtp_parameters::Parameter { + name: name.to_string(), + value: match value { + RtpCodecParametersParametersValue::String(s) => { + rtp_parameters::Value::String(Box::new( + rtp_parameters::String { + value: s.to_string(), + }, + )) + } + RtpCodecParametersParametersValue::Number(n) => { + rtp_parameters::Value::Integer32(Box::new( + rtp_parameters::Integer32 { value: *n as i32 }, + )) + } + }, + }) + .collect(), + ), + rtcp_feedback: Some( + codec + .rtcp_feedback() + .iter() + .map(|rtcp_feedback| { + let (r#type, parameter) = rtcp_feedback.as_type_parameter(); + rtp_parameters::RtcpFeedback { + type_: r#type.to_string(), + parameter: Some(parameter.to_string()), + } + }) + .collect(), + ), + }) + .collect(), + header_extensions: self + .header_extensions + .into_iter() + .map( + |header_extension_parameters| rtp_parameters::RtpHeaderExtensionParameters { + uri: header_extension_parameters.uri.to_fbs(), + id: header_extension_parameters.id as u8, + encrypt: header_extension_parameters.encrypt, + parameters: None, + }, + ) + .collect(), + encodings: self + .encodings + .into_iter() + .map(|encoding| rtp_parameters::RtpEncodingParameters { + ssrc: encoding.ssrc, + rid: encoding.rid, + codec_payload_type: encoding.codec_payload_type, + rtx: encoding + .rtx + .map(|rtx| Box::new(rtp_parameters::Rtx { ssrc: rtx.ssrc })), + dtx: encoding.dtx.unwrap_or_default(), + scalability_mode: if encoding.scalability_mode.is_none() { + None + } else { + Some(encoding.scalability_mode.as_str().to_string()) + }, + max_bitrate: encoding.max_bitrate, + }) + .collect(), + rtcp: Box::new(rtp_parameters::RtcpParameters { + cname: self.rtcp.cname, + reduced_size: self.rtcp.reduced_size, + }), + } + } +} + /// Single value used in RTP codec parameters. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] #[serde(untagged)] @@ -654,11 +1119,21 @@ impl RtpCodecParameters { *payload_type } + pub(crate) fn clock_rate(&self) -> NonZeroU32 { + let (Self::Audio { clock_rate, .. } | Self::Video { clock_rate, .. }) = self; + *clock_rate + } + pub(crate) fn parameters(&self) -> &RtpCodecParametersParameters { let (Self::Audio { parameters, .. } | Self::Video { parameters, .. }) = self; parameters } + pub(crate) fn rtcp_feedback(&self) -> &[RtcpFeedback] { + let (Self::Audio { rtcp_feedback, .. } | Self::Video { rtcp_feedback, .. }) = self; + rtcp_feedback + } + pub(crate) fn rtcp_feedback_mut(&mut self) -> &mut Vec { let (Self::Audio { rtcp_feedback, .. } | Self::Video { rtcp_feedback, .. }) = self; rtcp_feedback @@ -690,32 +1165,9 @@ impl Serialize for RtcpFeedback { S: Serializer, { let mut rtcp_feedback = serializer.serialize_struct("RtcpFeedback", 2)?; - match self { - RtcpFeedback::Nack => { - rtcp_feedback.serialize_field("type", "nack")?; - rtcp_feedback.serialize_field("parameter", "")?; - } - RtcpFeedback::NackPli => { - rtcp_feedback.serialize_field("type", "nack")?; - rtcp_feedback.serialize_field("parameter", "pli")?; - } - RtcpFeedback::CcmFir => { - rtcp_feedback.serialize_field("type", "ccm")?; - rtcp_feedback.serialize_field("parameter", "fir")?; - } - RtcpFeedback::GoogRemb => { - rtcp_feedback.serialize_field("type", "goog-remb")?; - rtcp_feedback.serialize_field("parameter", "")?; - } - RtcpFeedback::TransportCc => { - rtcp_feedback.serialize_field("type", "transport-cc")?; - rtcp_feedback.serialize_field("parameter", "")?; - } - RtcpFeedback::Unsupported => { - rtcp_feedback.serialize_field("type", "unknown")?; - rtcp_feedback.serialize_field("parameter", "")?; - } - } + let (r#type, parameter) = self.as_type_parameter(); + rtcp_feedback.serialize_field("type", r#type)?; + rtcp_feedback.serialize_field("parameter", parameter)?; rtcp_feedback.end() } } @@ -767,14 +1219,10 @@ impl<'de> Deserialize<'de> for RtcpFeedback { } let r#type = r#type.ok_or_else(|| de::Error::missing_field("type"))?; - Ok(match (r#type.as_ref(), parameter.as_ref()) { - ("nack", "") => RtcpFeedback::Nack, - ("nack", "pli") => RtcpFeedback::NackPli, - ("ccm", "fir") => RtcpFeedback::CcmFir, - ("goog-remb", "") => RtcpFeedback::GoogRemb, - ("transport-cc", "") => RtcpFeedback::TransportCc, - _ => RtcpFeedback::Unsupported, - }) + Ok( + RtcpFeedback::from_type_parameter(r#type.as_ref(), parameter.as_ref()) + .unwrap_or(RtcpFeedback::Unsupported), + ) } } @@ -783,6 +1231,42 @@ impl<'de> Deserialize<'de> for RtcpFeedback { } } +/// Error of failure to create [`RtcpFeedback`] from type and parameter. +#[derive(Debug, Error, Eq, PartialEq)] +pub enum RtcpFeedbackFromTypeParameterError { + /// Unsupported + #[error("Unsupported")] + Unsupported, +} + +impl RtcpFeedback { + pub(crate) fn from_type_parameter( + r#type: &str, + parameter: &str, + ) -> Result { + match (r#type, parameter) { + ("nack", "") => Ok(RtcpFeedback::Nack), + ("nack", "pli") => Ok(RtcpFeedback::NackPli), + ("ccm", "fir") => Ok(RtcpFeedback::CcmFir), + ("goog-remb", "") => Ok(RtcpFeedback::GoogRemb), + ("transport-cc", "") => Ok(RtcpFeedback::TransportCc), + ("unknown", "") => Ok(RtcpFeedback::Unsupported), + _ => Err(RtcpFeedbackFromTypeParameterError::Unsupported), + } + } + + pub(crate) fn as_type_parameter(&self) -> (&'static str, &'static str) { + match self { + RtcpFeedback::Nack => ("nack", ""), + RtcpFeedback::NackPli => ("nack", "pli"), + RtcpFeedback::CcmFir => ("ccm", "fir"), + RtcpFeedback::GoogRemb => ("goog-remb", ""), + RtcpFeedback::TransportCc => ("transport-cc", ""), + RtcpFeedback::Unsupported => ("unknown", ""), + } + } +} + /// RTX stream information. It must contain a numeric ssrc field indicating the RTX SSRC. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)] pub struct RtpEncodingParametersRtx { @@ -816,14 +1300,58 @@ pub struct RtpEncodingParameters { /// Number of spatial and temporal layers in the RTP stream. #[serde(default, skip_serializing_if = "ScalabilityMode::is_none")] pub scalability_mode: ScalabilityMode, - /// Factor by which to reduce the size of a video track during encoding. - #[serde(skip_serializing_if = "Option::is_none")] - pub scale_resolution_down_by: Option, /// Maximum number of bits per second to allow a track encoded with this encoding to use. #[serde(skip_serializing_if = "Option::is_none")] pub max_bitrate: Option, } +impl RtpEncodingParameters { + pub(crate) fn to_fbs(&self) -> rtp_parameters::RtpEncodingParameters { + rtp_parameters::RtpEncodingParameters { + ssrc: self.ssrc, + rid: self.rid.clone(), + codec_payload_type: self.codec_payload_type, + rtx: self + .rtx + .map(|rtx| Box::new(rtp_parameters::Rtx { ssrc: rtx.ssrc })), + dtx: self.dtx.unwrap_or_default(), + scalability_mode: if self.scalability_mode.is_none() { + None + } else { + Some(self.scalability_mode.as_str().to_string()) + }, + max_bitrate: self.max_bitrate, + } + } + + pub(crate) fn from_fbs_ref( + encoding_parameters: rtp_parameters::RtpEncodingParametersRef<'_>, + ) -> Result> { + Ok(Self { + ssrc: encoding_parameters.ssrc()?, + rid: encoding_parameters.rid()?.map(|rid| rid.to_string()), + codec_payload_type: encoding_parameters.codec_payload_type()?, + rtx: if let Some(rtx) = encoding_parameters.rtx()? { + Some(RtpEncodingParametersRtx { ssrc: rtx.ssrc()? }) + } else { + None + }, + dtx: { + match encoding_parameters.dtx()? { + true => Some(true), + false => None, + } + }, + scalability_mode: encoding_parameters + .scalability_mode()? + .map(|maybe_scalability_mode| maybe_scalability_mode.parse()) + .transpose()? + .unwrap_or_default(), + max_bitrate: encoding_parameters.max_bitrate()?, + }) + } +} + /// Defines a RTP header extension within the RTP parameters. The list of RTP /// header extensions supported by mediasoup is defined in the `supported_rtp_capabilities.rs` file. /// @@ -858,9 +1386,6 @@ pub struct RtcpParameters { /// Whether reduced size RTCP RFC 5506 is configured (if true) or compound RTCP /// as specified in RFC 3550 (if false). Default true. pub reduced_size: bool, - /// Whether RTCP-mux is used. Default true. - #[serde(skip_serializing_if = "Option::is_none")] - pub mux: Option, } impl Default for RtcpParameters { @@ -868,7 +1393,6 @@ impl Default for RtcpParameters { Self { cname: None, reduced_size: true, - mux: None, } } } diff --git a/rust/src/scalability_modes.rs b/rust/src/scalability_modes.rs index 429802f8cb..f072cf026c 100644 --- a/rust/src/scalability_modes.rs +++ b/rust/src/scalability_modes.rs @@ -118,7 +118,7 @@ impl Default for ScalabilityMode { #[derive(Debug, Error, Eq, PartialEq)] pub enum ParseScalabilityModeError { /// Invalid input string - #[error("Invalid input string")] + #[error("Invalid Scalability Mode input string")] InvalidInput, } @@ -184,9 +184,9 @@ impl FromStr for ScalabilityMode { } } -impl ToString for ScalabilityMode { - fn to_string(&self) -> String { - self.as_str().to_string() +impl std::fmt::Display for ScalabilityMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) } } diff --git a/rust/src/sctp_parameters.rs b/rust/src/sctp_parameters.rs index 51ac9b797a..807dafbea8 100644 --- a/rust/src/sctp_parameters.rs +++ b/rust/src/sctp_parameters.rs @@ -1,5 +1,6 @@ //! Collection of SCTP-related data structures that are used to specify SCTP association parameters. +use mediasoup_sys::fbs::sctp_parameters; use serde::{Deserialize, Serialize}; /// Number of SCTP streams. @@ -42,6 +43,15 @@ impl Default for NumSctpStreams { } } +impl NumSctpStreams { + pub(crate) fn to_fbs(self) -> sctp_parameters::NumSctpStreams { + sctp_parameters::NumSctpStreams { + os: self.os, + mis: self.mis, + } + } +} + /// Parameters of the SCTP association. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -55,7 +65,18 @@ pub struct SctpParameters { #[serde(rename = "MIS")] pub mis: u16, /// Maximum allowed size for SCTP messages. - pub max_message_size: usize, + pub max_message_size: u32, +} + +impl SctpParameters { + pub(crate) fn from_fbs(parameters: &sctp_parameters::SctpParameters) -> Self { + Self { + port: parameters.port, + os: parameters.os, + mis: parameters.mis, + max_message_size: parameters.max_message_size, + } + } } /// SCTP stream parameters describe the reliability of a certain SCTP stream. @@ -110,6 +131,26 @@ impl SctpStreamParameters { } } +impl SctpStreamParameters { + pub(crate) fn to_fbs(self) -> sctp_parameters::SctpStreamParameters { + sctp_parameters::SctpStreamParameters { + stream_id: self.stream_id, + ordered: Some(self.ordered), + max_packet_life_time: self.max_packet_life_time, + max_retransmits: self.max_retransmits, + } + } + + pub(crate) fn from_fbs(stream_parameters: sctp_parameters::SctpStreamParameters) -> Self { + Self { + stream_id: stream_parameters.stream_id, + ordered: stream_parameters.ordered.unwrap_or(false), + max_packet_life_time: stream_parameters.max_packet_life_time, + max_retransmits: stream_parameters.max_retransmits, + } + } +} + impl SctpStreamParameters { /// Messages will be sent reliably in order. #[must_use] diff --git a/rust/src/srtp_parameters.rs b/rust/src/srtp_parameters.rs index 3d5d48389c..99bb898cf8 100644 --- a/rust/src/srtp_parameters.rs +++ b/rust/src/srtp_parameters.rs @@ -1,6 +1,7 @@ //! Collection of SRTP-related data structures that are used to specify SRTP encryption/decryption //! parameters. +use mediasoup_sys::fbs::srtp_parameters; use serde::{Deserialize, Serialize}; /// SRTP parameters. @@ -13,6 +14,22 @@ pub struct SrtpParameters { pub key_base64: String, } +impl SrtpParameters { + pub(crate) fn from_fbs(tuple: &srtp_parameters::SrtpParameters) -> Self { + Self { + crypto_suite: SrtpCryptoSuite::from_fbs(tuple.crypto_suite), + key_base64: String::from(tuple.key_base64.as_str()), + } + } + + pub(crate) fn to_fbs(&self) -> srtp_parameters::SrtpParameters { + srtp_parameters::SrtpParameters { + crypto_suite: SrtpCryptoSuite::to_fbs(self.crypto_suite), + key_base64: String::from(self.key_base64.as_str()), + } + } +} + /// SRTP crypto suite. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Deserialize, Serialize)] pub enum SrtpCryptoSuite { @@ -30,6 +47,26 @@ pub enum SrtpCryptoSuite { AesCm128HmacSha132, } +impl SrtpCryptoSuite { + pub(crate) fn from_fbs(crypto_suite: srtp_parameters::SrtpCryptoSuite) -> Self { + match crypto_suite { + srtp_parameters::SrtpCryptoSuite::AeadAes256Gcm => Self::AeadAes256Gcm, + srtp_parameters::SrtpCryptoSuite::AeadAes128Gcm => Self::AeadAes128Gcm, + srtp_parameters::SrtpCryptoSuite::AesCm128HmacSha180 => Self::AesCm128HmacSha180, + srtp_parameters::SrtpCryptoSuite::AesCm128HmacSha132 => Self::AesCm128HmacSha132, + } + } + + pub(crate) fn to_fbs(self) -> srtp_parameters::SrtpCryptoSuite { + match self { + Self::AeadAes256Gcm => srtp_parameters::SrtpCryptoSuite::AeadAes256Gcm, + Self::AeadAes128Gcm => srtp_parameters::SrtpCryptoSuite::AeadAes128Gcm, + Self::AesCm128HmacSha180 => srtp_parameters::SrtpCryptoSuite::AesCm128HmacSha180, + Self::AesCm128HmacSha132 => srtp_parameters::SrtpCryptoSuite::AesCm128HmacSha132, + } + } +} + impl Default for SrtpCryptoSuite { fn default() -> Self { Self::AesCm128HmacSha180 diff --git a/rust/src/supported_rtp_capabilities.rs b/rust/src/supported_rtp_capabilities.rs index 38ea0e2910..a7a9369520 100644 --- a/rust/src/supported_rtp_capabilities.rs +++ b/rust/src/supported_rtp_capabilities.rs @@ -26,7 +26,7 @@ pub fn get_supported_rtp_capabilities() -> RtpCapabilities { clock_rate: NonZeroU32::new(48000).unwrap(), channels: NonZeroU8::new(2).unwrap(), parameters: RtpCodecParametersParameters::default(), - rtcp_feedback: vec![RtcpFeedback::TransportCc], + rtcp_feedback: vec![RtcpFeedback::Nack, RtcpFeedback::TransportCc], }, RtpCodecCapability::Audio { mime_type: MimeTypeAudio::MultiChannelOpus, @@ -39,7 +39,7 @@ pub fn get_supported_rtp_capabilities() -> RtpCapabilities { ("num_streams", 2_u32.into()), ("coupled_streams", 2_u32.into()), ]), - rtcp_feedback: vec![RtcpFeedback::TransportCc], + rtcp_feedback: vec![RtcpFeedback::Nack, RtcpFeedback::TransportCc], }, RtpCodecCapability::Audio { mime_type: MimeTypeAudio::MultiChannelOpus, @@ -52,7 +52,7 @@ pub fn get_supported_rtp_capabilities() -> RtpCapabilities { ("num_streams", 4_u32.into()), ("coupled_streams", 2_u32.into()), ]), - rtcp_feedback: vec![RtcpFeedback::TransportCc], + rtcp_feedback: vec![RtcpFeedback::Nack, RtcpFeedback::TransportCc], }, RtpCodecCapability::Audio { mime_type: MimeTypeAudio::MultiChannelOpus, @@ -65,7 +65,7 @@ pub fn get_supported_rtp_capabilities() -> RtpCapabilities { ("num_streams", 5_u32.into()), ("coupled_streams", 3_u32.into()), ]), - rtcp_feedback: vec![RtcpFeedback::TransportCc], + rtcp_feedback: vec![RtcpFeedback::Nack, RtcpFeedback::TransportCc], }, RtpCodecCapability::Audio { mime_type: MimeTypeAudio::Pcmu, @@ -386,6 +386,20 @@ pub fn get_supported_rtp_capabilities() -> RtpCapabilities { preferred_encrypt: false, direction: RtpHeaderExtensionDirection::SendRecv, }, + RtpHeaderExtension { + kind: MediaKind::Audio, + uri: RtpHeaderExtensionUri::PlayoutDelay, + preferred_id: 14, + preferred_encrypt: false, + direction: RtpHeaderExtensionDirection::SendRecv, + }, + RtpHeaderExtension { + kind: MediaKind::Video, + uri: RtpHeaderExtensionUri::PlayoutDelay, + preferred_id: 14, + preferred_encrypt: false, + direction: RtpHeaderExtensionDirection::SendRecv, + }, ], } } diff --git a/rust/src/webrtc_server.rs b/rust/src/webrtc_server.rs index 7d70430e0d..4750323c56 100644 --- a/rust/src/webrtc_server.rs +++ b/rust/src/webrtc_server.rs @@ -10,7 +10,7 @@ #[cfg(test)] mod tests; -use crate::data_structures::{AppData, ListenIp, Protocol}; +use crate::data_structures::{AppData, ListenInfo}; use crate::messages::{WebRtcServerCloseRequest, WebRtcServerDumpRequest}; use crate::transport::TransportId; use crate::uuid_based_wrapper_type; @@ -20,6 +20,7 @@ use async_executor::Executor; use event_listener_primitives::{BagOnce, HandlerId}; use hash_hasher::HashedSet; use log::{debug, error}; +use mediasoup_sys::fbs::transport; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use std::fmt; @@ -73,41 +74,34 @@ pub struct WebRtcServerDump { pub tuple_hashes: Vec, } -/// Listening protocol, IP and port for [`WebRtcServer`] to listen on. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct WebRtcServerListenInfo { - /// Network protocol. - pub protocol: Protocol, - /// Listening IP address. - #[serde(flatten)] - pub listen_ip: ListenIp, - /// Listening port. - #[serde(skip_serializing_if = "Option::is_none")] - pub port: Option, -} - /// Struct that protects an invariant of having non-empty list of listen infos. #[derive(Debug, Clone, Eq, PartialEq, Serialize)] -pub struct WebRtcServerListenInfos(Vec); +pub struct WebRtcServerListenInfos(Vec); impl WebRtcServerListenInfos { /// Create WebRTC server listen infos with given info populated initially. #[must_use] - pub fn new(listen_info: WebRtcServerListenInfo) -> Self { + pub fn new(listen_info: ListenInfo) -> Self { Self(vec![listen_info]) } /// Insert another listen info. #[must_use] - pub fn insert(mut self, listen_info: WebRtcServerListenInfo) -> Self { + pub fn insert(mut self, listen_info: ListenInfo) -> Self { self.0.push(listen_info); self } + + pub(crate) fn to_fbs(&self) -> Vec { + self.0 + .iter() + .map(|listen_info| listen_info.to_fbs()) + .collect() + } } impl Deref for WebRtcServerListenInfos { - type Target = Vec; + type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 @@ -119,10 +113,10 @@ impl Deref for WebRtcServerListenInfos { #[error("Empty list of listen infos provided, should have at least one element")] pub struct EmptyListError; -impl TryFrom> for WebRtcServerListenInfos { +impl TryFrom> for WebRtcServerListenInfos { type Error = EmptyListError; - fn try_from(listen_infos: Vec) -> Result { + fn try_from(listen_infos: Vec) -> Result { if listen_infos.is_empty() { Err(EmptyListError) } else { diff --git a/rust/src/webrtc_server/tests.rs b/rust/src/webrtc_server/tests.rs index a87f17d653..63fb7d2c50 100644 --- a/rust/src/webrtc_server/tests.rs +++ b/rust/src/webrtc_server/tests.rs @@ -1,5 +1,5 @@ -use crate::data_structures::{ListenIp, Protocol}; -use crate::webrtc_server::{WebRtcServerListenInfo, WebRtcServerListenInfos, WebRtcServerOptions}; +use crate::data_structures::{ListenInfo, Protocol}; +use crate::webrtc_server::{WebRtcServerListenInfos, WebRtcServerOptions}; use crate::worker::{Worker, WorkerSettings}; use crate::worker_manager::WorkerManager; use futures_lite::future; @@ -33,13 +33,15 @@ fn worker_close_event() { let webrtc_server = worker .create_webrtc_server(WebRtcServerOptions::new(WebRtcServerListenInfos::new( - WebRtcServerListenInfo { + ListenInfo { protocol: Protocol::Udp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - }, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, port: Some(port), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }, ))) .await diff --git a/rust/src/worker.rs b/rust/src/worker.rs index 431b2a9222..61085f888c 100644 --- a/rust/src/worker.rs +++ b/rust/src/worker.rs @@ -3,7 +3,6 @@ mod channel; mod common; -mod payload_channel; mod utils; use crate::data_structures::AppData; @@ -19,14 +18,15 @@ pub use crate::worker::utils::ExitError; use crate::worker_manager::WorkerManager; use crate::{ortc, uuid_based_wrapper_type}; use async_executor::Executor; -pub(crate) use channel::Channel; +pub(crate) use channel::{Channel, NotificationError, NotificationParseError}; pub(crate) use common::{SubscriptionHandler, SubscriptionTarget}; use event_listener_primitives::{Bag, BagOnce, HandlerId}; use futures_lite::FutureExt; use log::{debug, error, warn}; +use mediasoup_sys::fbs; use parking_lot::Mutex; -pub(crate) use payload_channel::{NotificationError, PayloadChannel}; use serde::{Deserialize, Serialize}; +use std::error::Error; use std::ops::RangeInclusive; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; @@ -42,7 +42,7 @@ uuid_based_wrapper_type!( ); /// Error that caused request to mediasoup-worker request to fail. -#[derive(Debug, Error, Eq, PartialEq)] +#[derive(Debug, Error)] pub enum RequestError { /// Channel already closed. #[error("Channel already closed")] @@ -65,6 +65,9 @@ pub enum RequestError { /// Worker did not return any data in response. #[error("Worker did not return any data in response")] NoData, + /// Response conversion error. + #[error("Response conversion error: {0}")] + ResponseConversion(Box), } /// Logging level for logs generated by the media worker thread (check the @@ -93,7 +96,7 @@ impl Default for WorkerLogLevel { } impl WorkerLogLevel { - fn as_str(self) -> &'static str { + pub(crate) fn as_str(self) -> &'static str { match self { Self::Debug => "debug", Self::Warn => "warn", @@ -137,7 +140,7 @@ pub enum WorkerLogTag { } impl WorkerLogTag { - fn as_str(self) -> &'static str { + pub(crate) fn as_str(self) -> &'static str { match self { Self::Info => "info", Self::Ice => "ice", @@ -176,8 +179,8 @@ pub struct WorkerSettings { /// Log tags for debugging. Check the meaning of each available tag in the /// [Debugging](https://mediasoup.org/documentation/v3/mediasoup/debugging/) documentation. pub log_tags: Vec, - /// RTC ports range for ICE, DTLS, RTP, etc. Default 10000..=59999. - pub rtc_ports_range: RangeInclusive, + /// RTC port range for ICE, DTLS, RTP, etc. Default 10000..=59999. + pub rtc_port_range: RangeInclusive, /// DTLS certificate and private key. /// /// If `None`, a certificate is dynamically created. @@ -189,6 +192,11 @@ pub struct WorkerSettings { /// "WebRTC-Bwe-AlrLimitedBackoff/Enabled/". #[doc(hidden)] pub libwebrtc_field_trials: Option, + /// Enable liburing This option is ignored if io_uring is not supported by + /// current host. + /// + /// Default `true`. + pub enable_liburing: bool, /// Function that will be called under worker thread before worker starts, can be used for /// pinning worker threads to CPU cores. pub thread_initializer: Option>, @@ -215,9 +223,10 @@ impl Default for WorkerSettings { WorkerLogTag::Sctp, WorkerLogTag::Message, ], - rtc_ports_range: 10000..=59999, + rtc_port_range: 10000..=59999, dtls_files: None, libwebrtc_field_trials: None, + enable_liburing: true, thread_initializer: None, app_data: AppData::default(), } @@ -229,9 +238,10 @@ impl fmt::Debug for WorkerSettings { let WorkerSettings { log_level, log_tags, - rtc_ports_range, + rtc_port_range, dtls_files, libwebrtc_field_trials, + enable_liburing, thread_initializer, app_data, } = self; @@ -239,9 +249,10 @@ impl fmt::Debug for WorkerSettings { f.debug_struct("WorkerSettings") .field("log_level", &log_level) .field("log_tags", &log_tags) - .field("rtc_ports_range", &rtc_ports_range) + .field("rtc_port_range", &rtc_port_range) .field("dtls_files", &dtls_files) .field("libwebrtc_field_trials", &libwebrtc_field_trials) + .field("enable_liburing", &enable_liburing) .field( "thread_initializer", &thread_initializer.as_ref().map(|_| "ThreadInitializer"), @@ -272,8 +283,15 @@ pub struct WorkerUpdateSettings { #[doc(hidden)] pub struct ChannelMessageHandlers { pub channel_request_handlers: Vec, - pub payload_channel_request_handlers: Vec, - pub payload_channel_notification_handlers: Vec, + pub channel_notification_handlers: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] +#[doc(hidden)] +pub struct LibUringDump { + pub sqe_process_count: u64, + pub sqe_miss_count: u64, + pub user_data_miss_count: u64, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -286,10 +304,11 @@ pub struct WorkerDump { #[serde(rename = "webRtcServerIds")] pub webrtc_server_ids: Vec, pub channel_message_handlers: ChannelMessageHandlers, + pub liburing: Option, } /// Error that caused [`Worker::create_webrtc_server`] to fail. -#[derive(Debug, Error, Eq, PartialEq)] +#[derive(Debug, Error)] pub enum CreateWebRtcServerError { /// Request to worker failed #[error("Request to worker failed: {0}")] @@ -297,7 +316,7 @@ pub enum CreateWebRtcServerError { } /// Error that caused [`Worker::create_router`] to fail. -#[derive(Debug, Error, Eq, PartialEq)] +#[derive(Debug, Error)] pub enum CreateRouterError { /// RTP capabilities generation error #[error("RTP capabilities generation error: {0}")] @@ -320,7 +339,6 @@ struct Handlers { struct Inner { id: WorkerId, channel: Channel, - payload_channel: PayloadChannel, executor: Arc>, handlers: Handlers, app_data: AppData, @@ -343,9 +361,10 @@ impl Inner { WorkerSettings { log_level, log_tags, - rtc_ports_range, + rtc_port_range, dtls_files, libwebrtc_field_trials, + enable_liburing, thread_initializer, app_data, }: WorkerSettings, @@ -361,14 +380,14 @@ impl Inner { spawn_args.push(format!("--logTag={}", log_tag.as_str())); } - if rtc_ports_range.is_empty() { + if rtc_port_range.is_empty() { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Invalid RTC ports range", )); } - spawn_args.push(format!("--rtcMinPort={}", rtc_ports_range.start())); - spawn_args.push(format!("--rtcMaxPort={}", rtc_ports_range.end())); + spawn_args.push(format!("--rtcMinPort={}", rtc_port_range.start())); + spawn_args.push(format!("--rtcMaxPort={}", rtc_port_range.end())); if let Some(dtls_files) = dtls_files { spawn_args.push(format!( @@ -394,6 +413,10 @@ impl Inner { )); } + if !enable_liburing { + spawn_args.push("--disableLiburing=true".to_string()); + } + let id = WorkerId::new(); debug!( "spawning worker with arguments [id:{}]: {}", @@ -406,7 +429,6 @@ impl Inner { let (mut status_sender, status_receiver) = async_oneshot::oneshot(); let WorkerRunResult { channel, - payload_channel, buffer_worker_messages_guard, } = utils::run_worker_with_channels( id, @@ -424,7 +446,6 @@ impl Inner { let mut inner = Self { id, channel, - payload_channel, executor, handlers, app_data, @@ -490,20 +511,19 @@ impl Inner { let id = self.id; let sender = Mutex::new(Some(sender)); let _handler = self.channel.subscribe_to_notifications( - std::process::id().into(), + SubscriptionTarget::String(std::process::id().to_string()), move |notification| { - let result = match serde_json::from_slice(notification) { - Ok(Notification::Running) => { + let result = match notification.event().unwrap() { + fbs::notification::Event::WorkerRunning => { debug!("worker thread running [id:{}]", id); Ok(()) } - Err(error) => Err(io::Error::new( + _ => Err(io::Error::new( io::ErrorKind::Other, - format!( - "unexpected first notification from worker [id:{id}]: {notification:?}; error = {error}" - ), + format!("unexpected first notification from worker [id:{id}]"), )), }; + let _ = sender .lock() .take() @@ -522,7 +542,6 @@ impl Inner { fn setup_message_handling(&mut self) { let channel_receiver = self.channel.get_internal_message_receiver(); - let payload_channel_receiver = self.payload_channel.get_internal_message_receiver(); let id = self.id; let closed = Arc::clone(&self.closed); self.executor @@ -546,20 +565,6 @@ impl Inner { } }) .detach(); - - self.executor - .spawn(async move { - while let Ok(message) = payload_channel_receiver.recv().await { - match message { - payload_channel::InternalMessage::UnexpectedData(data) => error!( - "worker[id:{}] unexpected payload channel data: {}", - id, - String::from_utf8_lossy(&data) - ), - } - } - }) - .detach(); } fn close(&self) { @@ -567,7 +572,6 @@ impl Inner { if !already_closed { let channel = self.channel.clone(); - let payload_channel = self.payload_channel.clone(); self.executor .spawn(async move { @@ -575,7 +579,6 @@ impl Inner { // Drop channels in here after response from worker drop(channel); - drop(payload_channel); }) .detach(); @@ -648,10 +651,15 @@ impl Worker { pub async fn update_settings(&self, data: WorkerUpdateSettings) -> Result<(), RequestError> { debug!("update_settings()"); - self.inner + match self + .inner .channel .request("", WorkerUpdateSettingsRequest { data }) .await + { + Ok(_) => Ok(()), + Err(error) => Err(error), + } } /// Create a WebRtcServer. @@ -661,7 +669,7 @@ impl Worker { &self, webrtc_server_options: WebRtcServerOptions, ) -> Result { - debug!("create_router()"); + debug!("create_webrtc_server()"); let WebRtcServerOptions { listen_infos, @@ -734,7 +742,6 @@ impl Worker { router_id, Arc::clone(&self.inner.executor), self.inner.channel.clone(), - self.inner.payload_channel.clone(), rtp_capabilities, app_data, self.clone(), diff --git a/rust/src/worker/channel.rs b/rust/src/worker/channel.rs index 7219903074..8dc53c9a76 100644 --- a/rust/src/worker/channel.rs +++ b/rust/src/worker/channel.rs @@ -1,4 +1,4 @@ -use crate::messages::{Request, WorkerCloseRequest}; +use crate::messages::{Notification, Request}; use crate::worker::common::{EventHandlers, SubscriptionTarget, WeakEventHandlers}; use crate::worker::utils; use crate::worker::utils::{PreparedChannelRead, PreparedChannelWrite}; @@ -7,16 +7,18 @@ use atomic_take::AtomicTake; use hash_hasher::HashedMap; use log::{debug, error, trace, warn}; use lru::LruCache; +use mediasoup_sys::fbs::{message, notification, request, response}; use mediasoup_sys::UvAsyncT; use parking_lot::Mutex; +use planus::ReadAsRoot; use serde::Deserialize; -use serde_json::Value; -use std::any::TypeId; use std::collections::VecDeque; use std::fmt::{Debug, Display}; use std::num::NonZeroUsize; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; +use thiserror::Error; +use uuid::Uuid; #[derive(Debug, Deserialize)] #[serde(untagged)] @@ -38,11 +40,26 @@ pub(super) enum InternalMessage { Unexpected(Vec), } +#[derive(Debug, Error, Eq, PartialEq)] +pub enum NotificationError { + #[error("Channel already closed")] + ChannelClosed, +} + +/// Flabtuffers notification parse error. +#[derive(Debug, Error, Eq, PartialEq)] +pub enum NotificationParseError { + /// Invalid event + #[error("Invalid event")] + InvalidEvent, +} + #[allow(clippy::type_complexity)] pub(crate) struct BufferMessagesGuard { target_id: SubscriptionTarget, buffered_notifications_for: Arc>>>>, - event_handlers_weak: WeakEventHandlers>, + event_handlers_weak: + WeakEventHandlers) + Send + Sync + 'static>>, } impl Drop for BufferMessagesGuard { @@ -50,59 +67,63 @@ impl Drop for BufferMessagesGuard { let mut buffered_notifications_for = self.buffered_notifications_for.lock(); if let Some(notifications) = buffered_notifications_for.remove(&self.target_id) { if let Some(event_handlers) = self.event_handlers_weak.upgrade() { - for notification in notifications { - event_handlers.call_callbacks_with_single_value(&self.target_id, ¬ification); + for bytes in notifications { + let message_ref = message::MessageRef::read_as_root(&bytes).unwrap(); + + let message::BodyRef::Notification(notification_ref) = + message_ref.data().unwrap() + else { + panic!("Wrong notification stored: {message_ref:?}"); + }; + + event_handlers + .call_callbacks_with_single_value(&self.target_id, notification_ref); } } } } } -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum ChannelReceiveMessage { - #[serde(rename_all = "camelCase")] - Notification { - target_id: SubscriptionTarget, - }, - ResponseSuccess { - id: u32, - // The following field is present, unused, but needed for differentiating successful - // response from error case - #[allow(dead_code)] - accepted: bool, - data: Option, - }, - ResponseError { - id: u32, - // The following field is present, but unused - // error: Value, - reason: String, - }, +#[derive(Debug)] +enum ChannelReceiveMessage<'a> { + Notification(notification::NotificationRef<'a>), + Response(response::ResponseRef<'a>), Event(InternalMessage), } -fn deserialize_message(bytes: &[u8]) -> ChannelReceiveMessage { - match bytes[0] { - // JSON message - b'{' => serde_json::from_slice(bytes).unwrap(), - // Debug log - b'D' => ChannelReceiveMessage::Event(InternalMessage::Debug( - String::from_utf8(Vec::from(&bytes[1..])).unwrap(), - )), - // Warn log - b'W' => ChannelReceiveMessage::Event(InternalMessage::Warn( - String::from_utf8(Vec::from(&bytes[1..])).unwrap(), - )), - // Error log - b'E' => ChannelReceiveMessage::Event(InternalMessage::Error( - String::from_utf8(Vec::from(&bytes[1..])).unwrap(), - )), - // Dump log - b'X' => ChannelReceiveMessage::Event(InternalMessage::Dump( - String::from_utf8(Vec::from(&bytes[1..])).unwrap(), - )), - // Unknown +// Remove the first 4 bytes which represent the buffer size. +// NOTE: The prefix is only needed for NodeJS. +fn unprefix_message(bytes: &[u8]) -> &[u8] { + &bytes[4..] +} + +fn deserialize_message(bytes: &[u8]) -> ChannelReceiveMessage<'_> { + let message_ref = message::MessageRef::read_as_root(bytes).unwrap(); + + match message_ref.data().unwrap() { + message::BodyRef::Log(data) => match data.data().unwrap().chars().next() { + // Debug log + Some('D') => ChannelReceiveMessage::Event(InternalMessage::Debug( + String::from_utf8(Vec::from(&data.data().unwrap().as_bytes()[1..])).unwrap(), + )), + // Warn log + Some('W') => ChannelReceiveMessage::Event(InternalMessage::Warn( + String::from_utf8(Vec::from(&data.data().unwrap().as_bytes()[1..])).unwrap(), + )), + // Error log + Some('E') => ChannelReceiveMessage::Event(InternalMessage::Error( + String::from_utf8(Vec::from(&data.data().unwrap().as_bytes()[1..])).unwrap(), + )), + // Dump log + Some('X') => ChannelReceiveMessage::Event(InternalMessage::Dump( + String::from_utf8(Vec::from(&data.data().unwrap().as_bytes()[1..])).unwrap(), + )), + // This should never happen. + _ => ChannelReceiveMessage::Event(InternalMessage::Unexpected(Vec::from(bytes))), + }, + message::BodyRef::Notification(data) => ChannelReceiveMessage::Notification(data), + message::BodyRef::Response(data) => ChannelReceiveMessage::Response(data), + _ => ChannelReceiveMessage::Event(InternalMessage::Unexpected(Vec::from(bytes))), } } @@ -111,7 +132,7 @@ struct ResponseError { reason: String, } -type ResponseResult = Result, ResponseError>; +type FBSResponseResult = Result>, ResponseError>; struct RequestDropGuard<'a> { id: u32, @@ -142,9 +163,9 @@ impl<'a> RequestDropGuard<'a> { } #[derive(Default)] -struct RequestsContainer { +struct FBSRequestsContainer { next_id: u32, - handlers: HashedMap>>, + handlers: HashedMap>, } struct OutgoingMessageBuffer { @@ -152,13 +173,15 @@ struct OutgoingMessageBuffer { messages: VecDeque>>>, } -#[allow(clippy::type_complexity)] +// TODO: use 'close' in 'request' method. +#[allow(clippy::type_complexity, dead_code)] struct Inner { outgoing_message_buffer: Arc>, internal_message_receiver: async_channel::Receiver, - requests_container_weak: Weak>, + requests_container_weak: Weak>, buffered_notifications_for: Arc>>>>, - event_handlers_weak: WeakEventHandlers>, + event_handlers_weak: + WeakEventHandlers) + Send + Sync + 'static>>, worker_closed: Arc, closed: AtomicBool, } @@ -182,7 +205,7 @@ impl Channel { handle: None, messages: VecDeque::with_capacity(10), })); - let requests_container = Arc::>::default(); + let requests_container = Arc::>::default(); let requests_container_weak = Arc::downgrade(&requests_container); let buffered_notifications_for = Arc::>>>>::default(); @@ -222,41 +245,61 @@ impl Channel { move |message| { trace!("received raw message: {}", String::from_utf8_lossy(message)); + let message = unprefix_message(message); + match deserialize_message(message) { - ChannelReceiveMessage::Notification { target_id } => { + ChannelReceiveMessage::Notification(notification) => { + let target_id = notification.handler_id().unwrap(); + // Target id can be either the worker PID or a UUID. + let target_id = match target_id.parse::() { + Ok(_) => SubscriptionTarget::String(target_id.to_string()), + Err(_) => SubscriptionTarget::Uuid(Uuid::parse_str(target_id).unwrap()), + }; + if !non_buffered_notifications.contains(&target_id) { let mut buffer_notifications_for = buffered_notifications_for.lock(); // Check if we need to buffer notifications for this // target_id if let Some(list) = buffer_notifications_for.get_mut(&target_id) { + // Store the whole message removing the size prefix. list.push(Vec::from(message)); return; } // Remember we don't need to buffer these - non_buffered_notifications.put(target_id, ()); + non_buffered_notifications.put(target_id.clone(), ()); } - event_handlers.call_callbacks_with_single_value(&target_id, message); + event_handlers.call_callbacks_with_single_value(&target_id, notification); } - ChannelReceiveMessage::ResponseSuccess { id, data, .. } => { - let sender = requests_container.lock().handlers.remove(&id); + ChannelReceiveMessage::Response(response) => { + let sender = requests_container + .lock() + .handlers + .remove(&response.id().unwrap()); if let Some(mut sender) = sender { - let _ = sender.send(Ok(data)); + // Request did not succeed. + if let Ok(Some(reason)) = response.reason() { + let _ = sender.send(Err(ResponseError { + reason: reason.to_string(), + })); + } + // Request succeeded. + else { + match response.body().expect("failed accessing response body") { + // Response has body. + Some(_) => { + let _ = sender.send(Ok(Some(Vec::from(message)))); + } + // Response does not have body. + None => { + let _ = sender.send(Ok(None)); + } + } + } } else { warn!( "received success response does not match any sent request [id:{}]", - id, - ); - } - } - ChannelReceiveMessage::ResponseError { id, reason } => { - let sender = requests_container.lock().handlers.remove(&id); - if let Some(mut sender) = sender { - let _ = sender.send(Err(ResponseError { reason })); - } else { - warn!( - "received error response does not match any sent request [id:{}]", - id, + response.id().unwrap(), ); } } @@ -295,7 +338,7 @@ impl Channel { let event_handlers_weak = self.inner.event_handlers_weak.clone(); buffered_notifications_for .lock() - .entry(target_id) + .entry(target_id.clone()) .or_default(); BufferMessagesGuard { target_id, @@ -313,8 +356,6 @@ impl Channel { R: Request + 'static, HandlerId: Display, { - let method = request.as_method(); - let id; let (result_sender, result_receiver) = async_oneshot::oneshot(); @@ -322,10 +363,6 @@ impl Channel { let requests_container = match self.inner.requests_container_weak.upgrade() { Some(requests_container_lock) => requests_container_lock, None => { - if let Some(default_response) = R::default_for_soft_error() { - return Ok(default_response); - } - return Err(RequestError::ChannelClosed); } }; @@ -337,35 +374,22 @@ impl Channel { requests_container_lock.handlers.insert(id, result_sender); } - debug!("request() [method:{}, id:{}]: {:?}", method, id, request); + debug!("request() [method:{:?}, id:{}]", R::METHOD, id); - // TODO: Todo pre-allocate fixed size string sufficient for most cases by default - // TODO: Refactor to avoid extra allocation during JSON serialization if possible - let message = Arc::new(AtomicTake::new( - format!( - "{id}:{method}:{handler_id}:{}", - serde_json::to_string(&request).unwrap() - ) - .into_bytes(), - )); + let data = request.into_bytes(id, handler_id); + + let buffer = Arc::new(AtomicTake::new(data)); { let mut outgoing_message_buffer = self.inner.outgoing_message_buffer.lock(); outgoing_message_buffer .messages - .push_back(Arc::clone(&message)); + .push_back(Arc::clone(&buffer)); if let Some(handle) = outgoing_message_buffer.handle { if self.inner.worker_closed.load(Ordering::Acquire) { // Forbid all requests after worker closing except one worker closing request - let first_worker_closing = TypeId::of::() - == TypeId::of::() - && !self.inner.closed.swap(true, Ordering::Relaxed); - - if !first_worker_closing { - if let Some(default_response) = R::default_for_soft_error() { - return Ok(default_response); - } - + // TODO: We were checking before that inner.closed. + if R::METHOD != request::Method::WorkerClose { return Err(RequestError::ChannelClosed); } } @@ -374,9 +398,6 @@ impl Channel { let ret = mediasoup_sys::uv_async_send(handle); if ret != 0 { error!("uv_async_send call failed with code {}", ret); - if let Some(default_response) = R::default_for_soft_error() { - return Ok(default_response); - } return Err(RequestError::ChannelClosed); } @@ -387,7 +408,7 @@ impl Channel { // Drop guard to make sure to drop pending request when future is cancelled let request_drop_guard = RequestDropGuard { id, - message, + message: buffer, channel: self, removed: false, }; @@ -398,28 +419,39 @@ impl Channel { let response_result = match response_result_fut { Ok(response_result) => response_result, - Err(_closed) => { - return if let Some(default_response) = R::default_for_soft_error() { - Ok(default_response) - } else { - Err(RequestError::ChannelClosed) - }; - } + Err(_closed) => Err(ResponseError { + reason: String::from("ChannelClosed"), + }), }; match response_result { - Ok(data) => { - debug!("request succeeded [method:{}, id:{}]", method, id); + Ok(bytes) => { + debug!("request succeeded [method:{:?}, id:{}]", R::METHOD, id); + + match bytes { + Some(bytes) => { + let message_ref = message::MessageRef::read_as_root(&bytes).unwrap(); - // Default will work for `()` response - serde_json::from_value(data.unwrap_or_default()).map_err(|error| { - RequestError::FailedToParse { - error: error.to_string(), + let message::BodyRef::Response(response_ref) = message_ref.data().unwrap() + else { + panic!("Wrong response stored: {message_ref:?}"); + }; + + Ok(R::convert_response(response_ref.body().unwrap()) + .map_err(RequestError::ResponseConversion)?) + } + None => { + Ok(R::convert_response(None).map_err(RequestError::ResponseConversion)?) } - }) + } } Err(ResponseError { reason }) => { - debug!("request failed [method:{}, id:{}]: {}", method, id, reason); + debug!( + "request failed [method:{:?}, id:{}]: {}", + R::METHOD, + id, + reason + ); if reason.contains("not found") { if let Some(default_response) = R::default_for_soft_error() { Ok(default_response) @@ -433,13 +465,50 @@ impl Channel { } } + pub(crate) fn notify( + &self, + handler_id: HandlerId, + notification: N, + ) -> Result<(), NotificationError> + where + N: Notification, + HandlerId: Display, + { + debug!("notify() [{notification:?}]"); + + let data = notification.into_bytes(handler_id); + + let message = Arc::new(AtomicTake::new(data)); + + { + let mut outgoing_message_buffer = self.inner.outgoing_message_buffer.lock(); + outgoing_message_buffer + .messages + .push_back(Arc::clone(&message)); + if let Some(handle) = outgoing_message_buffer.handle { + if self.inner.worker_closed.load(Ordering::Acquire) { + return Err(NotificationError::ChannelClosed); + } + unsafe { + // Notify worker that there is something to read + let ret = mediasoup_sys::uv_async_send(handle); + if ret != 0 { + error!("uv_async_send call failed with code {}", ret); + return Err(NotificationError::ChannelClosed); + } + } + } + } + + Ok(()) + } pub(crate) fn subscribe_to_notifications( &self, target_id: SubscriptionTarget, callback: F, ) -> Option where - F: Fn(&[u8]) + Send + Sync + 'static, + F: Fn(notification::NotificationRef<'_>) + Send + Sync + 'static, { self.inner .event_handlers_weak diff --git a/rust/src/worker/common.rs b/rust/src/worker/common.rs index 6616b48c17..100560b1e3 100644 --- a/rust/src/worker/common.rs +++ b/rust/src/worker/common.rs @@ -1,4 +1,5 @@ use hash_hasher::HashedMap; +use mediasoup_sys::fbs::notification; use nohash_hasher::IntMap; use parking_lot::Mutex; use serde::Deserialize; @@ -34,9 +35,7 @@ impl EventHandlers { let index; { let mut event_handlers = self.handlers.lock(); - let list = event_handlers - .entry(target_id) - .or_insert_with(EventHandlersList::default); + let list = event_handlers.entry(target_id.clone()).or_default(); index = list.index; list.index += 1; list.callbacks.insert(index, callback); @@ -79,11 +78,11 @@ impl EventHandlers { } } -impl EventHandlers> { +impl EventHandlers) + Send + Sync + 'static>> { pub(super) fn call_callbacks_with_single_value( &self, target_id: &SubscriptionTarget, - value: &V, + value: notification::NotificationRef<'_>, ) { let handlers = self.handlers.lock(); if let Some(list) = handlers.get(target_id) { @@ -94,22 +93,6 @@ impl EventHandlers> { } } -impl EventHandlers> { - pub(super) fn call_callbacks_with_two_values( - &self, - target_id: &SubscriptionTarget, - value1: &V1, - value2: &V2, - ) { - let handlers = self.handlers.lock(); - if let Some(list) = handlers.get(target_id) { - for callback in list.callbacks.values() { - callback(value1, value2); - } - } - } -} - #[derive(Clone)] pub(super) struct WeakEventHandlers { handlers: Weak>>>, @@ -123,23 +106,11 @@ impl WeakEventHandlers { } } -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize)] #[serde(untagged)] pub(crate) enum SubscriptionTarget { Uuid(Uuid), - Number(u64), -} - -impl From for SubscriptionTarget { - fn from(number: u32) -> Self { - Self::Number(u64::from(number)) - } -} - -impl From for SubscriptionTarget { - fn from(number: u64) -> Self { - Self::Number(number) - } + String(String), } /// Subscription handler, will remove corresponding subscription when dropped diff --git a/rust/src/worker/payload_channel.rs b/rust/src/worker/payload_channel.rs deleted file mode 100644 index c4a7ed3172..0000000000 --- a/rust/src/worker/payload_channel.rs +++ /dev/null @@ -1,398 +0,0 @@ -use crate::messages::{Notification, Request}; -use crate::worker::common::{EventHandlers, SubscriptionTarget, WeakEventHandlers}; -use crate::worker::utils::{PreparedPayloadChannelRead, PreparedPayloadChannelWrite}; -use crate::worker::{utils, RequestError, SubscriptionHandler}; -use atomic_take::AtomicTake; -use log::{debug, error, trace, warn}; -use mediasoup_sys::UvAsyncT; -use nohash_hasher::IntMap; -use parking_lot::Mutex; -use serde::Deserialize; -use serde_json::Value; -use std::collections::VecDeque; -use std::fmt::{Debug, Display}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Weak}; -use thiserror::Error; - -#[derive(Debug)] -pub(super) enum InternalMessage { - /// Unknown data - UnexpectedData(Vec), -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -enum PayloadChannelReceiveMessage { - #[serde(rename_all = "camelCase")] - Notification { target_id: SubscriptionTarget }, - ResponseSuccess { - id: u32, - // The following field is present, unused, but needed for differentiating successful - // response from error case - #[allow(dead_code)] - accepted: bool, - data: Option, - }, - ResponseError { - id: u32, - // The following field is present, but unused - // error: Value, - reason: String, - }, - /// Unknown data - #[serde(skip)] - Internal(InternalMessage), -} - -fn deserialize_message(bytes: &[u8]) -> PayloadChannelReceiveMessage { - match serde_json::from_slice(bytes) { - Ok(message) => message, - Err(error) => { - warn!("Failed to deserialize message: {}", error); - - PayloadChannelReceiveMessage::Internal(InternalMessage::UnexpectedData(Vec::from( - bytes, - ))) - } - } -} - -struct ResponseError { - reason: String, -} - -type ResponseResult = Result, ResponseError>; - -struct RequestDropGuard<'a> { - id: u32, - message_with_payload: Arc>, - channel: &'a PayloadChannel, - removed: bool, -} - -impl<'a> Drop for RequestDropGuard<'a> { - fn drop(&mut self) { - if self.removed { - return; - } - - // Drop pending message from memory - self.message_with_payload.take(); - // Remove request handler from the container - if let Some(requests_container) = self.channel.inner.requests_container_weak.upgrade() { - requests_container.lock().handlers.remove(&self.id); - } - } -} - -impl<'a> RequestDropGuard<'a> { - fn remove(mut self) { - self.removed = true; - } -} - -#[derive(Default)] -struct RequestsContainer { - next_id: u32, - handlers: IntMap>>, -} - -struct OutgoingMessageRequest { - message: Vec, - payload: Vec, -} - -enum OutgoingMessage { - Request(Arc>), - Notification { message: Vec, payload: Vec }, -} - -struct OutgoingMessageBuffer { - handle: Option, - messages: VecDeque, -} - -#[derive(Debug, Error, Eq, PartialEq)] -pub enum NotificationError { - #[error("Channel already closed")] - ChannelClosed, -} - -struct Inner { - outgoing_message_buffer: Arc>, - internal_message_receiver: async_channel::Receiver, - requests_container_weak: Weak>, - #[allow(clippy::type_complexity)] - event_handlers_weak: WeakEventHandlers>, - worker_closed: Arc, -} - -impl Drop for Inner { - fn drop(&mut self) { - self.internal_message_receiver.close(); - } -} - -#[derive(Clone)] -pub(crate) struct PayloadChannel { - inner: Arc, -} - -impl PayloadChannel { - pub(super) fn new( - worker_closed: Arc, - ) -> ( - Self, - PreparedPayloadChannelRead, - PreparedPayloadChannelWrite, - ) { - let outgoing_message_buffer = Arc::new(Mutex::new(OutgoingMessageBuffer { - handle: None, - messages: VecDeque::with_capacity(10), - })); - let requests_container = Arc::>::default(); - let requests_container_weak = Arc::downgrade(&requests_container); - let event_handlers = EventHandlers::new(); - let event_handlers_weak = event_handlers.downgrade(); - - let prepared_payload_channel_read = utils::prepare_payload_channel_read_fn({ - let outgoing_message_buffer = Arc::clone(&outgoing_message_buffer); - - move |handle| { - let mut outgoing_message_buffer = outgoing_message_buffer.lock(); - if outgoing_message_buffer.handle.is_none() { - outgoing_message_buffer.handle.replace(handle); - } - - while let Some(outgoing_message) = outgoing_message_buffer.messages.pop_front() { - match outgoing_message { - OutgoingMessage::Request(maybe_message) => { - // Request might have already been cancelled - if let Some(OutgoingMessageRequest { message, payload }) = - maybe_message.take() - { - return Some((message, payload)); - } - } - OutgoingMessage::Notification { message, payload } => { - return Some((message, payload)); - } - } - } - - None - } - }); - - let (internal_message_sender, internal_message_receiver) = async_channel::bounded(1); - - let prepared_payload_channel_write = - utils::prepare_payload_channel_write_fn(move |message, payload| { - trace!("received raw message: {}", String::from_utf8_lossy(message)); - - match deserialize_message(message) { - PayloadChannelReceiveMessage::Notification { target_id } => { - trace!("received notification payload of {} bytes", payload.len()); - - event_handlers.call_callbacks_with_two_values(&target_id, message, payload); - } - PayloadChannelReceiveMessage::ResponseSuccess { id, data, .. } => { - let sender = requests_container.lock().handlers.remove(&id); - if let Some(mut sender) = sender { - let _ = sender.send(Ok(data)); - } else { - warn!( - "received success response does not match any sent request [id:{}]", - id, - ); - } - } - PayloadChannelReceiveMessage::ResponseError { id, reason } => { - let sender = requests_container.lock().handlers.remove(&id); - if let Some(mut sender) = sender { - let _ = sender.send(Err(ResponseError { reason })); - } else { - warn!( - "received error response does not match any sent request [id:{}]", - id, - ); - } - } - PayloadChannelReceiveMessage::Internal(internal_message) => { - let _ = internal_message_sender.try_send(internal_message); - } - } - }); - - let inner = Arc::new(Inner { - outgoing_message_buffer, - internal_message_receiver, - requests_container_weak, - event_handlers_weak, - worker_closed, - }); - - ( - Self { inner }, - prepared_payload_channel_read, - prepared_payload_channel_write, - ) - } - - pub(super) fn get_internal_message_receiver(&self) -> async_channel::Receiver { - self.inner.internal_message_receiver.clone() - } - - pub(crate) async fn request( - &self, - handler_id: HandlerId, - request: R, - payload: Vec, - ) -> Result - where - R: Request, - HandlerId: Display, - { - let method = request.as_method(); - - let id; - let (result_sender, result_receiver) = async_oneshot::oneshot(); - - { - let requests_container_lock = self - .inner - .requests_container_weak - .upgrade() - .ok_or(RequestError::ChannelClosed)?; - let mut requests_container = requests_container_lock.lock(); - - id = requests_container.next_id; - - requests_container.next_id = requests_container.next_id.wrapping_add(1); - requests_container.handlers.insert(id, result_sender); - } - - debug!("request() [method:{}, id:{}]: {:?}", method, id, request); - - // TODO: Todo pre-allocate fixed size string sufficient for most cases by default - // TODO: Refactor to avoid extra allocation during JSON serialization if possible - let message = format!( - "r:{id}:{}:{handler_id}:{}", - request.as_method(), - serde_json::to_string(&request).unwrap() - ) - .into_bytes(); - - let message_with_payload = - Arc::new(AtomicTake::new(OutgoingMessageRequest { message, payload })); - - { - let mut outgoing_message_buffer = self.inner.outgoing_message_buffer.lock(); - outgoing_message_buffer - .messages - .push_back(OutgoingMessage::Request(Arc::clone(&message_with_payload))); - if let Some(handle) = outgoing_message_buffer.handle { - if self.inner.worker_closed.load(Ordering::Acquire) { - return Err(RequestError::ChannelClosed); - } - unsafe { - // Notify worker that there is something to read - let ret = mediasoup_sys::uv_async_send(handle); - if ret != 0 { - error!("uv_async_send call failed with code {}", ret); - return Err(RequestError::ChannelClosed); - } - } - } - } - - // Drop guard to make sure to drop pending request when future is cancelled - let request_drop_guard = RequestDropGuard { - id, - message_with_payload, - channel: self, - removed: false, - }; - - let response_result_fut = result_receiver.await; - - request_drop_guard.remove(); - - match response_result_fut.map_err(|_| RequestError::ChannelClosed {})? { - Ok(data) => { - debug!("request succeeded [method:{}, id:{}]", method, id); - - // Default will work for `()` response - serde_json::from_value(data.unwrap_or_default()).map_err(|error| { - RequestError::FailedToParse { - error: error.to_string(), - } - }) - } - Err(ResponseError { reason }) => { - debug!("request failed [method:{}, id:{}]: {}", method, id, reason); - - Err(RequestError::Response { reason }) - } - } - } - - pub(crate) fn notify( - &self, - handler_id: HandlerId, - notification: N, - payload: Vec, - ) -> Result<(), NotificationError> - where - N: Notification, - HandlerId: Display, - { - debug!("notify() [event:{}]", notification.as_event()); - - // TODO: Todo pre-allocate fixed size string sufficient for most cases by default - // TODO: Refactor to avoid extra allocation during JSON serialization if possible - let message = format!( - "n:{}:{handler_id}:{}", - notification.as_event(), - serde_json::to_string(¬ification).unwrap() - ) - .into_bytes(); - - { - let mut outgoing_message_buffer = self.inner.outgoing_message_buffer.lock(); - outgoing_message_buffer - .messages - .push_back(OutgoingMessage::Notification { message, payload }); - if let Some(handle) = outgoing_message_buffer.handle { - if self.inner.worker_closed.load(Ordering::Acquire) { - return Err(NotificationError::ChannelClosed); - } - unsafe { - // Notify worker that there is something to read - let ret = mediasoup_sys::uv_async_send(handle); - if ret != 0 { - error!("uv_async_send call failed with code {}", ret); - return Err(NotificationError::ChannelClosed); - } - } - } - } - - Ok(()) - } - - pub(crate) fn subscribe_to_notifications( - &self, - target_id: SubscriptionTarget, - callback: F, - ) -> Option - where - F: Fn(&[u8], &[u8]) + Send + Sync + 'static, - { - self.inner - .event_handlers_weak - .upgrade() - .map(|event_handlers| event_handlers.add(target_id, Arc::new(callback))) - } -} diff --git a/rust/src/worker/utils.rs b/rust/src/worker/utils.rs index 0c9735737c..b04d74acee 100644 --- a/rust/src/worker/utils.rs +++ b/rust/src/worker/utils.rs @@ -2,15 +2,9 @@ mod channel_read_fn; mod channel_write_fn; use crate::worker::channel::BufferMessagesGuard; -use crate::worker::{Channel, PayloadChannel, WorkerId}; -pub(super) use channel_read_fn::{ - prepare_channel_read_fn, prepare_payload_channel_read_fn, PreparedChannelRead, - PreparedPayloadChannelRead, -}; -pub(super) use channel_write_fn::{ - prepare_channel_write_fn, prepare_payload_channel_write_fn, PreparedChannelWrite, - PreparedPayloadChannelWrite, -}; +use crate::worker::{Channel, SubscriptionTarget, WorkerId}; +pub(super) use channel_read_fn::{prepare_channel_read_fn, PreparedChannelRead}; +pub(super) use channel_write_fn::{prepare_channel_write_fn, PreparedChannelWrite}; use std::ffi::CString; use std::os::raw::{c_char, c_int}; use std::sync::atomic::AtomicBool; @@ -39,7 +33,6 @@ pub enum ExitError { pub(super) struct WorkerRunResult { pub(super) channel: Channel, - pub(super) payload_channel: PayloadChannel, pub(super) buffer_worker_messages_guard: BufferMessagesGuard, } @@ -55,9 +48,8 @@ where { let (channel, prepared_channel_read, prepared_channel_write) = Channel::new(Arc::clone(&worker_closed)); - let (payload_channel, prepared_payload_channel_read, prepared_payload_channel_write) = - PayloadChannel::new(worker_closed); - let buffer_worker_messages_guard = channel.buffer_messages_for(std::process::id().into()); + let buffer_worker_messages_guard = + channel.buffer_messages_for(SubscriptionTarget::String(std::process::id().to_string())); std::thread::Builder::new() .name(format!("mediasoup-worker-{id}")) @@ -81,16 +73,6 @@ where prepared_channel_read.deconstruct(); let (channel_write_fn, channel_write_ctx, _channel_read_callback) = prepared_channel_write.deconstruct(); - let ( - payload_channel_read_fn, - payload_channel_read_ctx, - _payload_channel_write_callback, - ) = prepared_payload_channel_read.deconstruct(); - let ( - payload_channel_write_fn, - payload_channel_write_ctx, - _payload_channel_read_callback, - ) = prepared_payload_channel_write.deconstruct(); mediasoup_sys::mediasoup_worker_run( argc, @@ -98,16 +80,10 @@ where version.as_ptr(), 0, 0, - 0, - 0, channel_read_fn, channel_read_ctx, channel_write_fn, channel_write_ctx, - payload_channel_read_fn, - payload_channel_read_ctx, - payload_channel_write_fn, - payload_channel_write_ctx, ) }; @@ -122,7 +98,6 @@ where WorkerRunResult { channel, - payload_channel, buffer_worker_messages_guard, } } diff --git a/rust/src/worker/utils/channel_read_fn.rs b/rust/src/worker/utils/channel_read_fn.rs index 902f87ef66..d84617cc7c 100644 --- a/rust/src/worker/utils/channel_read_fn.rs +++ b/rust/src/worker/utils/channel_read_fn.rs @@ -1,7 +1,5 @@ -pub(super) use mediasoup_sys::{ - ChannelReadCtx, ChannelReadFn, PayloadChannelReadCtx, PayloadChannelReadFn, -}; -use mediasoup_sys::{ChannelReadFreeFn, PayloadChannelReadFreeFn, UvAsyncT}; +pub(super) use mediasoup_sys::{ChannelReadCtx, ChannelReadFn}; +use mediasoup_sys::{ChannelReadFreeFn, UvAsyncT}; use std::mem; use std::os::raw::c_void; @@ -10,9 +8,18 @@ unsafe extern "C" fn free_vec(message: *mut u8, message_len: u32, message_capaci Vec::from_raw_parts(message, message_len as usize, message_capacity); } -pub(super) struct ChannelReadCallback( - Box Option>) + Send + 'static>, -); +pub(super) struct ChannelReadCallback { + // Silence clippy warnings + _callback: Box Option>) + Send + 'static>, +} + +impl ChannelReadCallback { + pub(super) fn new( + _callback: Box Option>) + Send + 'static>, + ) -> Self { + Self { _callback } + } +} pub(crate) struct PreparedChannelRead { channel_read_fn: ChannelReadFn, @@ -73,93 +80,6 @@ where PreparedChannelRead { channel_read_fn: wrapper::, channel_read_ctx: ChannelReadCtx(read_callback.as_ref() as *const F as *const c_void), - write_callback: ChannelReadCallback(read_callback), - } -} - -#[allow(clippy::type_complexity)] -pub(super) struct PayloadChannelReadCallback( - Box Option<(Vec, Vec)>) + Send + 'static>, -); - -pub(crate) struct PreparedPayloadChannelRead { - payload_channel_read_fn: PayloadChannelReadFn, - payload_channel_read_ctx: PayloadChannelReadCtx, - write_callback: PayloadChannelReadCallback, -} - -impl PreparedPayloadChannelRead { - /// SAFETY: - /// 1) `PayloadChannelReadCallback` returned must be dropped AFTER last usage of returned - /// function and context pointers - /// 2) `PayloadChannelReadCtx` should not be called from multiple threads concurrently - pub(super) unsafe fn deconstruct( - self, - ) -> ( - PayloadChannelReadFn, - PayloadChannelReadCtx, - PayloadChannelReadCallback, - ) { - let Self { - payload_channel_read_fn, - payload_channel_read_ctx, - write_callback, - } = self; - ( - payload_channel_read_fn, - payload_channel_read_ctx, - write_callback, - ) - } -} - -/// Given callback function, prepares a pair of channel read function and context, which can be -/// provided to of C++ worker and worker will effectively call the callback whenever it wants to -/// read something from Rust (so it is reading from C++ point of view and writing from Rust). -pub(crate) fn prepare_payload_channel_read_fn(read_callback: F) -> PreparedPayloadChannelRead -where - F: (FnMut(UvAsyncT) -> Option<(Vec, Vec)>) + Send + 'static, -{ - unsafe extern "C" fn wrapper( - message: *mut *mut u8, - message_len: *mut u32, - message_capacity: *mut usize, - payload: *mut *mut u8, - payload_len: *mut u32, - payload_capacity: *mut usize, - handle: UvAsyncT, - PayloadChannelReadCtx(ctx): PayloadChannelReadCtx, - ) -> PayloadChannelReadFreeFn - where - F: (FnMut(UvAsyncT) -> Option<(Vec, Vec)>) + Send + 'static, - { - // Call Rust and try to get a new message (if there is any) - let (mut new_message, mut new_payload) = (*(ctx as *mut F))(handle)?; - - // Set pointers, give out control over memory to C++ - *message = new_message.as_mut_ptr(); - *message_len = new_message.len() as u32; - *message_capacity = new_message.capacity(); - *payload = new_payload.as_mut_ptr(); - *payload_len = new_payload.len() as u32; - *payload_capacity = new_payload.capacity(); - - // Forget about vectors in Rust - mem::forget(new_message); - mem::forget(new_payload); - - // Function pointer that C++ can use to free vectors later - Some(free_vec) - } - - // Move to heap to make sure it doesn't change address later on - let read_callback = Box::new(read_callback); - - PreparedPayloadChannelRead { - payload_channel_read_fn: wrapper::, - payload_channel_read_ctx: PayloadChannelReadCtx( - read_callback.as_ref() as *const F as *const c_void - ), - write_callback: PayloadChannelReadCallback(read_callback), + write_callback: ChannelReadCallback::new(read_callback), } } diff --git a/rust/src/worker/utils/channel_write_fn.rs b/rust/src/worker/utils/channel_write_fn.rs index 968dafb79c..5cace7fd2e 100644 --- a/rust/src/worker/utils/channel_write_fn.rs +++ b/rust/src/worker/utils/channel_write_fn.rs @@ -1,11 +1,20 @@ -pub(super) use mediasoup_sys::{ - ChannelWriteCtx, ChannelWriteFn, PayloadChannelWriteCtx, PayloadChannelWriteFn, -}; +pub(super) use mediasoup_sys::{ChannelWriteCtx, ChannelWriteFn}; use std::os::raw::c_void; use std::slice; -#[allow(clippy::type_complexity)] -pub(super) struct ChannelReadCallback(Box); +/// TypeAlias to silience clippy::type_complexity warnings +type CallbackType = Box; + +pub(super) struct ChannelReadCallback { + // Silence clippy warnings + _callback: CallbackType, +} + +impl ChannelReadCallback { + pub(super) fn new(_callback: CallbackType) -> Self { + Self { _callback } + } +} pub(crate) struct PreparedChannelWrite { channel_write_fn: ChannelWriteFn, @@ -56,75 +65,6 @@ where PreparedChannelWrite { channel_write_fn: wrapper::, channel_write_ctx: ChannelWriteCtx(read_callback.as_ref() as *const F as *const c_void), - read_callback: ChannelReadCallback(read_callback), - } -} - -#[allow(clippy::type_complexity)] -pub(super) struct PayloadChannelReadCallback(Box); - -pub(crate) struct PreparedPayloadChannelWrite { - channel_write_fn: PayloadChannelWriteFn, - channel_write_ctx: PayloadChannelWriteCtx, - read_callback: PayloadChannelReadCallback, -} - -unsafe impl Send for PreparedPayloadChannelWrite {} - -impl PreparedPayloadChannelWrite { - /// SAFETY: - /// 1) `PayloadChannelReadCallback` returned must be dropped AFTER last usage of returned - /// function and context pointers - /// 2) `PayloadChannelWriteCtx` should not be called from multiple threads concurrently - pub(super) unsafe fn deconstruct( - self, - ) -> ( - PayloadChannelWriteFn, - PayloadChannelWriteCtx, - PayloadChannelReadCallback, - ) { - let Self { - channel_write_fn, - channel_write_ctx, - read_callback, - } = self; - (channel_write_fn, channel_write_ctx, read_callback) - } -} - -/// Given callback function, prepares a pair of channel write function and context, which can be -/// provided to of C++ worker and worker will effectively call the callback whenever it needs to -/// send something to Rust (so it is writing from C++ point of view and reading from Rust). -pub(crate) fn prepare_payload_channel_write_fn(read_callback: F) -> PreparedPayloadChannelWrite -where - F: FnMut(&[u8], &[u8]) + Send + 'static, -{ - unsafe extern "C" fn wrapper( - message: *const u8, - message_len: u32, - payload: *const u8, - payload_len: u32, - PayloadChannelWriteCtx(ctx): PayloadChannelWriteCtx, - ) where - F: FnMut(&[u8], &[u8]) + Send + 'static, - { - let message = slice::from_raw_parts(message, message_len as usize); - let payload = if payload_len == 0 { - &[] - } else { - slice::from_raw_parts(payload, payload_len as usize) - }; - (*(ctx as *mut F))(message, payload); - } - - // Move to heap to make sure it doesn't change address later on - let read_callback = Box::new(read_callback); - - PreparedPayloadChannelWrite { - channel_write_fn: wrapper::, - channel_write_ctx: PayloadChannelWriteCtx( - read_callback.as_ref() as *const F as *const c_void - ), - read_callback: PayloadChannelReadCallback(read_callback), + read_callback: ChannelReadCallback::new(read_callback), } } diff --git a/rust/tests/integration/consumer.rs b/rust/tests/integration/consumer.rs index 62c9b250ff..a2d3272f52 100644 --- a/rust/tests/integration/consumer.rs +++ b/rust/tests/integration/consumer.rs @@ -2,10 +2,8 @@ use async_executor::Executor; use async_io::Timer; use futures_lite::future; use hash_hasher::{HashedMap, HashedSet}; -use mediasoup::consumer::{ - ConsumableRtpEncoding, ConsumerLayers, ConsumerOptions, ConsumerScore, ConsumerType, -}; -use mediasoup::data_structures::{AppData, ListenIp}; +use mediasoup::consumer::{ConsumerLayers, ConsumerOptions, ConsumerScore, ConsumerType}; +use mediasoup::data_structures::{AppData, ListenInfo, Protocol}; use mediasoup::prelude::*; use mediasoup::producer::ProducerOptions; use mediasoup::router::{Router, RouterOptions}; @@ -18,7 +16,9 @@ use mediasoup::rtp_parameters::{ }; use mediasoup::scalability_modes::ScalabilityMode; use mediasoup::transport::ConsumeError; -use mediasoup::webrtc_transport::{TransportListenIps, WebRtcTransport, WebRtcTransportOptions}; +use mediasoup::webrtc_transport::{ + WebRtcTransport, WebRtcTransportListenInfos, WebRtcTransportOptions, +}; use mediasoup::worker::{Worker, WorkerSettings}; use mediasoup::worker_manager::WorkerManager; use parking_lot::Mutex; @@ -159,21 +159,25 @@ fn video_producer_options() -> ProducerOptions { encodings: vec![ RtpEncodingParameters { ssrc: Some(22222222), + scalability_mode: "L1T5".parse().unwrap(), rtx: Some(RtpEncodingParametersRtx { ssrc: 22222223 }), ..RtpEncodingParameters::default() }, RtpEncodingParameters { ssrc: Some(22222224), + scalability_mode: "L1T5".parse().unwrap(), rtx: Some(RtpEncodingParametersRtx { ssrc: 22222225 }), ..RtpEncodingParameters::default() }, RtpEncodingParameters { ssrc: Some(22222226), + scalability_mode: "L1T5".parse().unwrap(), rtx: Some(RtpEncodingParametersRtx { ssrc: 22222227 }), ..RtpEncodingParameters::default() }, RtpEncodingParameters { ssrc: Some(22222228), + scalability_mode: "L1T5".parse().unwrap(), rtx: Some(RtpEncodingParametersRtx { ssrc: 22222229 }), ..RtpEncodingParameters::default() }, @@ -199,7 +203,7 @@ fn consumer_device_capabilities() -> RtpCapabilities { clock_rate: NonZeroU32::new(48000).unwrap(), channels: NonZeroU8::new(2).unwrap(), parameters: RtpCodecParametersParameters::default(), - rtcp_feedback: vec![], + rtcp_feedback: vec![RtcpFeedback::Nack], }, RtpCodecCapability::Video { mime_type: MimeTypeVideo::H264, @@ -287,7 +291,16 @@ fn consumer_device_capabilities() -> RtpCapabilities { } // Keeps executor threads running until dropped -struct ExecutorGuard(Vec>); +struct ExecutorGuard { + // Silence clippy warnings + _senders: Vec>, +} + +impl ExecutorGuard { + fn new(_senders: Vec>) -> Self { + Self { _senders } + } +} fn create_executor() -> (ExecutorGuard, Arc>) { let executor = Arc::new(Executor::new()); @@ -314,7 +327,7 @@ fn create_executor() -> (ExecutorGuard, Arc>) { }) .collect(); - (ExecutorGuard(senders), executor) + (ExecutorGuard::new(senders), executor) } async fn init() -> ( @@ -346,10 +359,17 @@ async fn init() -> ( .await .expect("Failed to create router"); - let transport_options = WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - })); + let transport_options = + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })); let transport_1 = router .create_webrtc_transport(transport_options.clone()) @@ -493,7 +513,7 @@ fn consume_succeeds() { options.paused = true; options.preferred_layers = Some(ConsumerLayers { spatial_layer: 12, - temporal_layer: None, + temporal_layer: Some(0), }); options.app_data = AppData::new(ConsumerAppData { baz: "LOL" }); options @@ -698,6 +718,49 @@ fn consume_succeeds() { }); } +#[test] +fn consume_with_enable_rtx_succeeds() { + future::block_on(async move { + let (_executor_guard, _worker, _router, transport_1, transport_2) = init().await; + + let audio_producer = transport_1 + .produce(audio_producer_options()) + .await + .expect("Failed to produce audio"); + + let consumer_device_capabilities = consumer_device_capabilities(); + + let audio_consumer = transport_2 + .consume({ + let mut options = + ConsumerOptions::new(audio_producer.id(), consumer_device_capabilities.clone()); + options.enable_rtx = Some(true); + options + }) + .await + .expect("Failed to consume audio"); + + assert_eq!(audio_consumer.kind(), MediaKind::Audio); + assert_eq!(audio_consumer.rtp_parameters().mid, Some("0".to_string())); + assert_eq!( + audio_consumer.rtp_parameters().codecs, + vec![RtpCodecParameters::Audio { + mime_type: MimeTypeAudio::Opus, + payload_type: 100, + clock_rate: NonZeroU32::new(48000).unwrap(), + channels: NonZeroU8::new(2).unwrap(), + parameters: RtpCodecParametersParameters::from([ + ("useinbandfec", 1_u32.into()), + ("usedtx", 1_u32.into()), + ("foo", "222.222".into()), + ("bar", "333".into()), + ]), + rtcp_feedback: vec![RtcpFeedback::Nack], + }] + ); + }); +} + #[test] fn consumer_with_user_defined_mid() { future::block_on(async move { @@ -920,11 +983,10 @@ fn dump_succeeds() { rtx: None, dtx: None, scalability_mode: ScalabilityMode::None, - scale_resolution_down_by: None, ssrc: audio_consumer .rtp_parameters() .encodings - .get(0) + .first() .unwrap() .ssrc, rid: None, @@ -938,18 +1000,14 @@ fn dump_succeeds() { .consumable_rtp_parameters() .encodings .iter() - .map(|encoding| ConsumableRtpEncoding { + .map(|encoding| RtpEncodingParameters { ssrc: encoding.ssrc, rid: None, codec_payload_type: None, rtx: None, max_bitrate: None, - max_framerate: None, dtx: None, scalability_mode: ScalabilityMode::None, - spatial_layers: None, - temporal_layers: None, - ksvc: None }) .collect::>() ); @@ -1037,18 +1095,17 @@ fn dump_succeeds() { ssrc: video_consumer .rtp_parameters() .encodings - .get(0) + .first() .unwrap() .ssrc, rtx: video_consumer .rtp_parameters() .encodings - .get(0) + .first() .unwrap() .rtx, dtx: None, - scalability_mode: "S4T1".parse().unwrap(), - scale_resolution_down_by: None, + scalability_mode: "L4T5".parse().unwrap(), rid: None, max_bitrate: None, }], @@ -1060,18 +1117,14 @@ fn dump_succeeds() { .consumable_rtp_parameters() .encodings .iter() - .map(|encoding| ConsumableRtpEncoding { + .map(|encoding| RtpEncodingParameters { ssrc: encoding.ssrc, rid: None, codec_payload_type: None, rtx: None, max_bitrate: None, - max_framerate: None, dtx: None, - scalability_mode: ScalabilityMode::None, - spatial_layers: None, - temporal_layers: None, - ksvc: None, + scalability_mode: "L1T5".parse().unwrap(), }) .collect::>() ); @@ -1121,7 +1174,7 @@ fn get_stats_succeeds() { audio_consumer .rtp_parameters() .encodings - .get(0) + .first() .unwrap() .ssrc .unwrap() @@ -1170,7 +1223,7 @@ fn get_stats_succeeds() { video_consumer .rtp_parameters() .encodings - .get(0) + .first() .unwrap() .ssrc .unwrap() @@ -1285,8 +1338,56 @@ fn set_preferred_layers_succeeds() { video_consumer.preferred_layers(), Some(ConsumerLayers { spatial_layer: 2, + temporal_layer: Some(3), + }) + ); + + video_consumer + .set_preferred_layers(ConsumerLayers { + spatial_layer: 3, + temporal_layer: None, + }) + .await + .expect("Failed to set preferred layers consumer"); + + assert_eq!( + video_consumer.preferred_layers(), + Some(ConsumerLayers { + spatial_layer: 3, + temporal_layer: Some(4), + }) + ); + + video_consumer + .set_preferred_layers(ConsumerLayers { + spatial_layer: 3, temporal_layer: Some(0), }) + .await + .expect("Failed to set preferred layers consumer"); + + assert_eq!( + video_consumer.preferred_layers(), + Some(ConsumerLayers { + spatial_layer: 3, + temporal_layer: Some(0), + }) + ); + + video_consumer + .set_preferred_layers(ConsumerLayers { + spatial_layer: 66, + temporal_layer: Some(66), + }) + .await + .expect("Failed to set preferred layers consumer"); + + assert_eq!( + video_consumer.preferred_layers(), + Some(ConsumerLayers { + spatial_layer: 3, + temporal_layer: Some(4), + }) ); } }); diff --git a/rust/tests/integration/data_consumer.rs b/rust/tests/integration/data_consumer.rs index 37e510720d..1ded846105 100644 --- a/rust/tests/integration/data_consumer.rs +++ b/rust/tests/integration/data_consumer.rs @@ -3,13 +3,15 @@ use futures_lite::future; use hash_hasher::{HashedMap, HashedSet}; use mediasoup::data_consumer::{DataConsumerOptions, DataConsumerType}; use mediasoup::data_producer::{DataProducer, DataProducerOptions}; -use mediasoup::data_structures::{AppData, ListenIp}; +use mediasoup::data_structures::{AppData, ListenInfo, Protocol}; use mediasoup::direct_transport::DirectTransportOptions; use mediasoup::plain_transport::PlainTransportOptions; use mediasoup::prelude::*; use mediasoup::router::{Router, RouterOptions}; use mediasoup::sctp_parameters::SctpStreamParameters; -use mediasoup::webrtc_transport::{TransportListenIps, WebRtcTransport, WebRtcTransportOptions}; +use mediasoup::webrtc_transport::{ + WebRtcTransport, WebRtcTransportListenInfos, WebRtcTransportOptions, +}; use mediasoup::worker::{Worker, WorkerSettings}; use mediasoup::worker_manager::WorkerManager; use std::env; @@ -61,9 +63,15 @@ async fn init() -> (Worker, Router, WebRtcTransport, DataProducer) { let transport = router .create_webrtc_transport({ let mut transport_options = - WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, })); transport_options.enable_sctp = true; @@ -88,9 +96,15 @@ fn consume_data_succeeds() { let transport2 = router .create_plain_transport({ - let mut transport_options = PlainTransportOptions::new(ListenIp { + let mut transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); transport_options.enable_sctp = true; @@ -119,6 +133,7 @@ fn consume_data_succeeds() { 4000, ); + options.subchannels = Some(vec![0, 1, 1, 1, 2, 65535, 100]); options.app_data = AppData::new(CustomAppData { baz: "LOL" }); options @@ -142,6 +157,11 @@ fn consume_data_succeeds() { } assert_eq!(data_consumer.label().as_str(), "foo"); assert_eq!(data_consumer.protocol().as_str(), "bar"); + + let mut sorted_subchannels = data_consumer.subchannels(); + sorted_subchannels.sort(); + + assert_eq!(sorted_subchannels, [0, 1, 2, 100, 65535]); assert_eq!( data_consumer .app_data() @@ -187,9 +207,15 @@ fn weak() { let transport2 = router .create_plain_transport({ - let mut transport_options = PlainTransportOptions::new(ListenIp { + let mut transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); transport_options.enable_sctp = true; @@ -301,6 +327,117 @@ fn get_stats_succeeds() { }); } +#[test] +fn set_subchannels() { + future::block_on(async move { + let (_worker, _router, transport1, data_producer) = init().await; + + let data_consumer = transport1 + .consume_data(DataConsumerOptions::new_sctp_unordered_with_life_time( + data_producer.id(), + 4000, + )) + .await + .expect("Failed to consume data"); + + data_consumer + .set_subchannels([999, 999, 998, 0].to_vec()) + .await + .expect("Failed to set data consumer subchannels"); + + let mut sorted_subchannels = data_consumer.subchannels(); + sorted_subchannels.sort(); + + assert_eq!(sorted_subchannels, [0, 998, 999]); + }); +} + +#[test] +fn add_and_remove_subchannel() { + future::block_on(async move { + let (_worker, _router, transport1, data_producer) = init().await; + + let data_consumer = transport1 + .consume_data(DataConsumerOptions::new_sctp_unordered_with_life_time( + data_producer.id(), + 4000, + )) + .await + .expect("Failed to consume data"); + + data_consumer + .set_subchannels([].to_vec()) + .await + .expect("Failed to set data consumer subchannels"); + + assert_eq!(data_consumer.subchannels(), []); + + data_consumer + .add_subchannel(5) + .await + .expect("Failed to add data consumer subchannel"); + + assert_eq!(data_consumer.subchannels(), [5]); + + data_consumer + .add_subchannel(10) + .await + .expect("Failed to add data consumer subchannel"); + + let mut sorted_subchannels = data_consumer.subchannels(); + sorted_subchannels.sort(); + + assert_eq!(sorted_subchannels, [5, 10]); + + data_consumer + .add_subchannel(5) + .await + .expect("Failed to add data consumer subchannel"); + + sorted_subchannels = data_consumer.subchannels(); + sorted_subchannels.sort(); + + assert_eq!(sorted_subchannels, [5, 10]); + + data_consumer + .remove_subchannel(666) + .await + .expect("Failed to remove data consumer subchannel"); + + sorted_subchannels = data_consumer.subchannels(); + sorted_subchannels.sort(); + + assert_eq!(sorted_subchannels, [5, 10]); + + data_consumer + .remove_subchannel(5) + .await + .expect("Failed to remove data consumer subchannel"); + + sorted_subchannels = data_consumer.subchannels(); + sorted_subchannels.sort(); + + assert_eq!(sorted_subchannels, [10]); + + data_consumer + .add_subchannel(5) + .await + .expect("Failed to add data consumer subchannel"); + + sorted_subchannels = data_consumer.subchannels(); + sorted_subchannels.sort(); + + assert_eq!(sorted_subchannels, [5, 10]); + + data_consumer + .set_subchannels([].to_vec()) + .await + .expect("Failed to set data consumer subchannels"); + + assert_eq!(data_consumer.subchannels(), []); + }); +} + #[test] fn consume_data_on_direct_transport_succeeds() { future::block_on(async move { @@ -325,7 +462,7 @@ fn consume_data_on_direct_transport_succeeds() { let data_consumer = transport3 .consume_data({ - let mut options = DataConsumerOptions::new_direct(data_producer.id()); + let mut options = DataConsumerOptions::new_direct(data_producer.id(), None); options.app_data = AppData::new(CustomAppData2 { hehe: "HEHE" }); @@ -372,7 +509,7 @@ fn dump_on_direct_transport_succeeds() { let data_consumer = transport3 .consume_data({ - let mut options = DataConsumerOptions::new_direct(data_producer.id()); + let mut options = DataConsumerOptions::new_direct(data_producer.id(), None); options.app_data = AppData::new(CustomAppData2 { hehe: "HEHE" }); @@ -407,7 +544,7 @@ fn get_stats_on_direct_transport_succeeds() { let data_consumer = transport3 .consume_data({ - let mut options = DataConsumerOptions::new_direct(data_producer.id()); + let mut options = DataConsumerOptions::new_direct(data_producer.id(), None); options.app_data = AppData::new(CustomAppData2 { hehe: "HEHE" }); @@ -436,9 +573,15 @@ fn close_event() { let transport2 = router .create_plain_transport({ - let mut transport_options = PlainTransportOptions::new(ListenIp { + let mut transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); transport_options.enable_sctp = true; diff --git a/rust/tests/integration/data_producer.rs b/rust/tests/integration/data_producer.rs index 809e95bee8..409a70eade 100644 --- a/rust/tests/integration/data_producer.rs +++ b/rust/tests/integration/data_producer.rs @@ -2,13 +2,15 @@ use async_io::Timer; use futures_lite::future; use hash_hasher::{HashedMap, HashedSet}; use mediasoup::data_producer::{DataProducerOptions, DataProducerType}; -use mediasoup::data_structures::{AppData, ListenIp}; +use mediasoup::data_structures::{AppData, ListenInfo, Protocol}; use mediasoup::plain_transport::{PlainTransport, PlainTransportOptions}; use mediasoup::prelude::*; use mediasoup::router::{Router, RouterOptions}; use mediasoup::sctp_parameters::SctpStreamParameters; use mediasoup::transport::ProduceDataError; -use mediasoup::webrtc_transport::{TransportListenIps, WebRtcTransport, WebRtcTransportOptions}; +use mediasoup::webrtc_transport::{ + WebRtcTransport, WebRtcTransportListenInfos, WebRtcTransportOptions, +}; use mediasoup::worker::{RequestError, Worker, WorkerSettings}; use mediasoup::worker_manager::WorkerManager; use std::env; @@ -47,9 +49,15 @@ async fn init() -> (Worker, Router, WebRtcTransport, PlainTransport) { let transport1 = router .create_webrtc_transport({ let mut transport_options = - WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, })); transport_options.enable_sctp = true; @@ -61,9 +69,15 @@ async fn init() -> (Worker, Router, WebRtcTransport, PlainTransport) { let transport2 = router .create_plain_transport({ - let mut transport_options = PlainTransportOptions::new(ListenIp { + let mut transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); transport_options.enable_sctp = true; @@ -120,6 +134,7 @@ fn transport_1_produce_data_succeeds() { } assert_eq!(data_producer1.label().as_str(), "foo"); assert_eq!(data_producer1.protocol().as_str(), "bar"); + assert!(!data_producer1.paused()); assert_eq!( data_producer1 .app_data() @@ -177,6 +192,7 @@ fn transport_2_produce_data_succeeds() { options.label = "foo".to_string(); options.protocol = "bar".to_string(); + options.paused = true; options.app_data = AppData::new(CustomAppData { foo: 1, baz: "2" }); options @@ -197,6 +213,7 @@ fn transport_2_produce_data_succeeds() { } assert_eq!(data_producer2.label().as_str(), "foo"); assert_eq!(data_producer2.protocol().as_str(), "bar"); + assert!(data_producer2.paused()); assert_eq!( data_producer2 .app_data() @@ -318,6 +335,7 @@ fn dump_succeeds() { } assert_eq!(dump.label.as_str(), "foo"); assert_eq!(dump.protocol.as_str(), "bar"); + assert!(!dump.paused); } { @@ -329,6 +347,7 @@ fn dump_succeeds() { options.label = "foo".to_string(); options.protocol = "bar".to_string(); + options.paused = true; options.app_data = AppData::new(CustomAppData { foo: 1, baz: "2" }); options @@ -353,6 +372,7 @@ fn dump_succeeds() { } assert_eq!(dump.label.as_str(), "foo"); assert_eq!(dump.protocol.as_str(), "bar"); + assert!(dump.paused); } }); } @@ -419,6 +439,61 @@ fn get_stats_succeeds() { }); } +#[test] +fn pause_and_resume_succeed() { + future::block_on(async move { + let (_worker, _router, transport1, _) = init().await; + + { + let data_producer1 = transport1 + .produce_data({ + let mut options = + DataProducerOptions::new_sctp(SctpStreamParameters::new_ordered(666)); + + options.label = "foo".to_string(); + options.protocol = "bar".to_string(); + options.app_data = AppData::new(CustomAppData { foo: 1, baz: "2" }); + + options + }) + .await + .expect("Failed to produce data"); + + { + data_producer1 + .pause() + .await + .expect("Failed to pause data producer"); + + assert!(data_producer1.paused()); + + let dump = data_producer1 + .dump() + .await + .expect("Failed to dump data producer"); + + assert!(dump.paused); + } + + { + data_producer1 + .resume() + .await + .expect("Failed to resume data producer"); + + assert!(!data_producer1.paused()); + + let dump = data_producer1 + .dump() + .await + .expect("Failed to dump data producer"); + + assert!(!dump.paused); + } + } + }); +} + #[test] fn close_event() { future::block_on(async move { diff --git a/rust/tests/integration/direct_transport.rs b/rust/tests/integration/direct_transport.rs index 70e8730683..e3f9db4bf5 100644 --- a/rust/tests/integration/direct_transport.rs +++ b/rust/tests/integration/direct_transport.rs @@ -135,6 +135,7 @@ fn get_stats_succeeds() { .expect("Failed to get stats on Direct transport"); assert_eq!(stats.len(), 1); + assert_eq!(stats[0].transport_id, transport.id()); assert_eq!(stats[0].bytes_received, 0); assert_eq!(stats[0].recv_bitrate, 0); @@ -174,12 +175,21 @@ fn send_succeeds() { .expect("Failed to produce data"); let data_consumer = transport - .consume_data(DataConsumerOptions::new_direct(data_producer.id())) + .consume_data(DataConsumerOptions::new_direct(data_producer.id(), None)) .await .expect("Failed to consume data"); let num_messages = 200_usize; + let pause_sending_at_message = 10_usize; + let resume_sending_at_message = 20_usize; + let pause_receiving_at_message = 40_usize; + let resume_receiving_at_message = 60_usize; + let expected_received_num_messages = num_messages + - (resume_sending_at_message - pause_sending_at_message) + - (resume_receiving_at_message - pause_receiving_at_message); + let mut sent_message_bytes = 0_usize; + let mut effectively_sent_message_bytes = 0_usize; let recv_message_bytes = Arc::new(AtomicUsize::new(0)); let mut last_sent_message_id = 0_usize; let last_recv_message_id = Arc::new(AtomicUsize::new(0)); @@ -192,19 +202,19 @@ fn send_succeeds() { move |message| { let id: usize = match message { - WebRtcMessage::String(string) => { - recv_message_bytes.fetch_add(string.as_bytes().len(), Ordering::SeqCst); - string.parse().unwrap() + WebRtcMessage::String(binary) => { + recv_message_bytes.fetch_add(binary.len(), Ordering::SeqCst); + String::from_utf8(binary.to_vec()).unwrap().parse().unwrap() } WebRtcMessage::Binary(binary) => { recv_message_bytes.fetch_add(binary.len(), Ordering::SeqCst); String::from_utf8(binary.to_vec()).unwrap().parse().unwrap() } WebRtcMessage::EmptyString => { - panic!("Unexpected empty messages!"); + panic!("Unexpected empty message!"); } WebRtcMessage::EmptyBinary => { - panic!("Unexpected empty messages!"); + panic!("Unexpected empty message!"); } }; @@ -215,7 +225,6 @@ fn send_succeeds() { } last_recv_message_id.fetch_add(1, Ordering::SeqCst); - assert_eq!(id, last_recv_message_id.load(Ordering::SeqCst)); if id == num_messages { let _ = received_messages_tx.lock().take().unwrap().send(()); @@ -234,18 +243,50 @@ fn send_succeeds() { last_sent_message_id += 1; let id = last_sent_message_id; + if id == pause_sending_at_message { + data_producer + .pause() + .await + .expect("Failed to pause data producer"); + } else if id == resume_sending_at_message { + data_producer + .resume() + .await + .expect("Failed to resume data producer"); + } else if id == pause_receiving_at_message { + data_consumer + .pause() + .await + .expect("Failed to pause data consumer"); + } else if id == resume_receiving_at_message { + data_consumer + .resume() + .await + .expect("Failed to resume data consumer"); + } + let message = if id < num_messages / 2 { - let content = id.to_string(); + let content = id.to_string().into_bytes(); sent_message_bytes += content.len(); - WebRtcMessage::String(content) + + if !data_producer.paused() && !data_consumer.paused() { + effectively_sent_message_bytes += content.len(); + } + + WebRtcMessage::String(Cow::from(content)) } else { let content = id.to_string().into_bytes(); sent_message_bytes += content.len(); + + if !data_producer.paused() && !data_consumer.paused() { + effectively_sent_message_bytes += content.len(); + } + WebRtcMessage::Binary(Cow::from(content)) }; direct_data_producer - .send(message) + .send(message, None, None) .expect("Failed to send message"); if id == num_messages { @@ -258,10 +299,13 @@ fn send_succeeds() { .expect("Failed tor receive all messages"); assert_eq!(last_sent_message_id, num_messages); - assert_eq!(last_recv_message_id.load(Ordering::SeqCst), num_messages); + assert_eq!( + last_recv_message_id.load(Ordering::SeqCst), + expected_received_num_messages + ); assert_eq!( recv_message_bytes.load(Ordering::SeqCst), - sent_message_bytes, + effectively_sent_message_bytes, ); { @@ -273,8 +317,8 @@ fn send_succeeds() { assert_eq!(stats.len(), 1); assert_eq!(&stats[0].label, data_producer.label()); assert_eq!(&stats[0].protocol, data_producer.protocol()); - assert_eq!(stats[0].messages_received, num_messages); - assert_eq!(stats[0].bytes_received, sent_message_bytes); + assert_eq!(stats[0].messages_received, num_messages as u64); + assert_eq!(stats[0].bytes_received, sent_message_bytes as u64); } { @@ -286,15 +330,236 @@ fn send_succeeds() { assert_eq!(stats.len(), 1); assert_eq!(&stats[0].label, data_consumer.label()); assert_eq!(&stats[0].protocol, data_consumer.protocol()); - assert_eq!(stats[0].messages_sent, num_messages); + assert_eq!( + stats[0].messages_sent, + expected_received_num_messages as u64 + ); assert_eq!( stats[0].bytes_sent, - recv_message_bytes.load(Ordering::SeqCst), + recv_message_bytes.load(Ordering::SeqCst) as u64, ); } }); } +#[test] +fn send_with_subchannels_succeeds() { + future::block_on(async move { + let (_worker, _router, transport) = init().await; + + let data_producer = transport + .produce_data({ + let mut options = DataProducerOptions::new_direct(); + + options.label = "foo".to_string(); + options.protocol = "bar".to_string(); + options.app_data = AppData::new(CustomAppData { foo: "bar" }); + + options + }) + .await + .expect("Failed to produce data"); + + let data_consumer_1 = transport + .consume_data(DataConsumerOptions::new_direct( + data_producer.id(), + Some(vec![1, 11, 666]), + )) + .await + .expect("Failed to consume data"); + + let data_consumer_2 = transport + .consume_data(DataConsumerOptions::new_direct( + data_producer.id(), + Some(vec![2, 22, 666]), + )) + .await + .expect("Failed to consume data"); + + let expected_received_num_messages_1 = 7; + let expected_received_num_messages_2 = 5; + let received_messages_1: Arc>> = Arc::new(Mutex::new(vec![])); + let received_messages_2: Arc>> = Arc::new(Mutex::new(vec![])); + + let (received_messages_tx_1, received_messages_rx_1) = async_oneshot::oneshot::<()>(); + let _handler_1 = data_consumer_1.on_message({ + let received_messages_tx_1 = Mutex::new(Some(received_messages_tx_1)); + let received_messages_1 = Arc::clone(&received_messages_1); + + move |message| { + let text: String = match message { + WebRtcMessage::String(binary) => String::from_utf8(binary.to_vec()).unwrap(), + _ => { + panic!("Unexpected empty message!"); + } + }; + + received_messages_1.lock().push(text); + + if received_messages_1.lock().len() == expected_received_num_messages_1 { + let _ = received_messages_tx_1.lock().take().unwrap().send(()); + } + } + }); + + let (received_messages_tx_2, received_messages_rx_2) = async_oneshot::oneshot::<()>(); + let _handler_2 = data_consumer_2.on_message({ + let received_messages_tx_2 = Mutex::new(Some(received_messages_tx_2)); + let received_messages_2 = Arc::clone(&received_messages_2); + + move |message| { + let text: String = match message { + WebRtcMessage::String(binary) => String::from_utf8(binary.to_vec()).unwrap(), + _ => { + panic!("Unexpected empty message!"); + } + }; + + received_messages_2.lock().push(text); + + if received_messages_2.lock().len() == expected_received_num_messages_2 { + let _ = received_messages_tx_2.lock().take().unwrap().send(()); + } + } + }); + + let direct_data_producer = match &data_producer { + DataProducer::Direct(direct_data_producer) => direct_data_producer, + _ => { + panic!("Expected direct data producer") + } + }; + + let _ = match &data_consumer_2 { + DataConsumer::Direct(direct_data_consumer) => direct_data_consumer, + _ => { + panic!("Expected direct data consumer") + } + }; + + let both: &'static str = "both"; + let none: &'static str = "none"; + let dc1: &'static str = "dc1"; + let dc2: &'static str = "dc2"; + + // Must be received by dataConsumer1 and dataConsumer2. + direct_data_producer + .send( + WebRtcMessage::String(Cow::Borrowed(both.as_bytes())), + None, + None, + ) + .expect("Failed to send message"); + + // Must be received by dataConsumer1 and dataConsumer2. + direct_data_producer + .send( + WebRtcMessage::String(Cow::Borrowed(both.as_bytes())), + Some(vec![1, 2]), + None, + ) + .expect("Failed to send message"); + + // Must be received by dataConsumer1 and dataConsumer2. + direct_data_producer + .send( + WebRtcMessage::String(Cow::Borrowed(both.as_bytes())), + Some(vec![11, 22, 33]), + Some(666), + ) + .expect("Failed to send message"); + + // Must not be received by neither dataConsumer1 nor dataConsumer2. + direct_data_producer + .send( + WebRtcMessage::String(Cow::Borrowed(none.as_bytes())), + Some(vec![3]), + Some(666), + ) + .expect("Failed to send message"); + + // Must not be received by neither dataConsumer1 nor dataConsumer2. + direct_data_producer + .send( + WebRtcMessage::String(Cow::Borrowed(none.as_bytes())), + Some(vec![666]), + Some(3), + ) + .expect("Failed to send message"); + + // Must be received by dataConsumer1. + direct_data_producer + .send( + WebRtcMessage::String(Cow::Borrowed(dc1.as_bytes())), + Some(vec![1]), + None, + ) + .expect("Failed to send message"); + + // Must be received by dataConsumer1. + direct_data_producer + .send( + WebRtcMessage::String(Cow::Borrowed(dc1.as_bytes())), + Some(vec![11]), + None, + ) + .expect("Failed to send message"); + + // Must be received by dataConsumer1. + direct_data_producer + .send( + WebRtcMessage::String(Cow::Borrowed(dc1.as_bytes())), + Some(vec![666]), + Some(11), + ) + .expect("Failed to send message"); + + // Must be received by dataConsumer2. + direct_data_producer + .send( + WebRtcMessage::String(Cow::Borrowed(dc2.as_bytes())), + Some(vec![666]), + Some(2), + ) + .expect("Failed to send message"); + + let mut subchannels = data_consumer_2.subchannels(); + subchannels.push(1); + + data_consumer_2 + .set_subchannels(subchannels) + .await + .expect("Failed to set subchannels"); + + // Must be received by dataConsumer2. + direct_data_producer + .send( + WebRtcMessage::String(Cow::Borrowed(both.as_bytes())), + Some(vec![1]), + Some(666), + ) + .expect("Failed to send message"); + + received_messages_rx_1 + .await + .expect("Failed tor receive all messages"); + + received_messages_rx_2 + .await + .expect("Failed tor receive all messages"); + + let received_messages_1 = received_messages_1.lock().clone(); + assert!(received_messages_1.contains(&both.to_string())); + assert!(received_messages_1.contains(&dc1.to_string())); + assert!(!received_messages_1.contains(&dc2.to_string())); + + let received_messages_2 = received_messages_2.lock().clone(); + assert!(received_messages_2.contains(&both.to_string())); + assert!(!received_messages_2.contains(&dc1.to_string())); + assert!(received_messages_2.contains(&dc2.to_string())); + }); +} + #[test] fn close_event() { future::block_on(async move { diff --git a/rust/tests/integration/multiopus.rs b/rust/tests/integration/multiopus.rs index 16ea0ab483..5479be2077 100644 --- a/rust/tests/integration/multiopus.rs +++ b/rust/tests/integration/multiopus.rs @@ -1,5 +1,5 @@ use futures_lite::future; -use mediasoup::data_structures::ListenIp; +use mediasoup::data_structures::{ListenInfo, Protocol}; use mediasoup::prelude::*; use mediasoup::producer::ProducerOptions; use mediasoup::router::{Router, RouterOptions}; @@ -8,7 +8,9 @@ use mediasoup::rtp_parameters::{ RtpHeaderExtension, RtpHeaderExtensionDirection, RtpHeaderExtensionParameters, RtpHeaderExtensionUri, RtpParameters, }; -use mediasoup::webrtc_transport::{TransportListenIps, WebRtcTransport, WebRtcTransportOptions}; +use mediasoup::webrtc_transport::{ + WebRtcTransport, WebRtcTransportListenInfos, WebRtcTransportOptions, +}; use mediasoup::worker::WorkerSettings; use mediasoup::worker_manager::WorkerManager; use std::env; @@ -127,10 +129,17 @@ async fn init() -> (Router, WebRtcTransport) { .await .expect("Failed to create router"); - let transport_options = WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - })); + let transport_options = + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })); let transport = router .create_webrtc_transport(transport_options.clone()) diff --git a/rust/tests/integration/pipe_transport.rs b/rust/tests/integration/pipe_transport.rs index c6bc626d66..c11ff0d32a 100644 --- a/rust/tests/integration/pipe_transport.rs +++ b/rust/tests/integration/pipe_transport.rs @@ -2,7 +2,7 @@ use futures_lite::future; use mediasoup::consumer::{ConsumerOptions, ConsumerScore, ConsumerType}; use mediasoup::data_consumer::{DataConsumerOptions, DataConsumerType}; use mediasoup::data_producer::{DataProducerOptions, DataProducerType}; -use mediasoup::data_structures::{AppData, ListenIp}; +use mediasoup::data_structures::{AppData, ListenInfo, Protocol}; use mediasoup::pipe_transport::{PipeTransportOptions, PipeTransportRemoteParameters}; use mediasoup::prelude::*; use mediasoup::producer::ProducerOptions; @@ -19,7 +19,9 @@ use mediasoup::rtp_parameters::{ use mediasoup::sctp_parameters::SctpStreamParameters; use mediasoup::srtp_parameters::{SrtpCryptoSuite, SrtpParameters}; use mediasoup::transport::ProduceError; -use mediasoup::webrtc_transport::{TransportListenIps, WebRtcTransport, WebRtcTransportOptions}; +use mediasoup::webrtc_transport::{ + WebRtcTransport, WebRtcTransportListenInfos, WebRtcTransportOptions, +}; use mediasoup::worker::{RequestError, Worker, WorkerSettings}; use mediasoup::worker_manager::WorkerManager; use parking_lot::Mutex; @@ -253,10 +255,17 @@ async fn init() -> ( .await .expect("Failed to create router"); - let mut transport_options = WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - })); + let mut transport_options = + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })); transport_options.enable_sctp = true; let transport_1 = router1 @@ -341,6 +350,11 @@ fn pipe_to_router_succeeds_with_audio() { uri: RtpHeaderExtensionUri::AbsCaptureTime, id: 13, encrypt: false, + }, + RtpHeaderExtensionParameters { + uri: RtpHeaderExtensionUri::PlayoutDelay, + id: 14, + encrypt: false, } ], ); @@ -386,7 +400,12 @@ fn pipe_to_router_succeeds_with_audio() { uri: RtpHeaderExtensionUri::AbsCaptureTime, id: 13, encrypt: false, - } + }, + RtpHeaderExtensionParameters { + uri: RtpHeaderExtensionUri::PlayoutDelay, + id: 14, + encrypt: false, + }, ], ); assert!(!pipe_producer.paused()); @@ -490,6 +509,11 @@ fn pipe_to_router_succeeds_with_video() { id: 13, encrypt: false, }, + RtpHeaderExtensionParameters { + uri: RtpHeaderExtensionUri::PlayoutDelay, + id: 14, + encrypt: false, + }, ], ); assert_eq!(pipe_consumer.r#type(), ConsumerType::Pipe); @@ -547,6 +571,11 @@ fn pipe_to_router_succeeds_with_video() { id: 13, encrypt: false, }, + RtpHeaderExtensionParameters { + uri: RtpHeaderExtensionUri::PlayoutDelay, + id: 14, + encrypt: false, + }, ], ); assert!(pipe_producer.paused()); @@ -593,9 +622,15 @@ fn weak() { let pipe_transport = router1 .create_pipe_transport({ - let mut options = PipeTransportOptions::new(ListenIp { + let mut options = PipeTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); options.enable_rtx = true; @@ -623,13 +658,16 @@ fn create_with_fixed_port_succeeds() { let pipe_transport = router1 .create_pipe_transport({ - let mut options = PipeTransportOptions::new(ListenIp { + PipeTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - }); - options.port = Some(port); - - options + announced_address: None, + port: Some(port), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }) }) .await .expect("Failed to create Pipe transport"); @@ -645,9 +683,15 @@ fn create_with_enable_rtx_succeeds() { let pipe_transport = router1 .create_pipe_transport({ - let mut options = PipeTransportOptions::new(ListenIp { + let mut options = PipeTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); options.enable_rtx = true; @@ -729,6 +773,11 @@ fn create_with_enable_rtx_succeeds() { id: 13, encrypt: false, }, + RtpHeaderExtensionParameters { + uri: RtpHeaderExtensionUri::PlayoutDelay, + id: 14, + encrypt: false, + }, ], ); assert_eq!(pipe_consumer.r#type(), ConsumerType::Pipe); @@ -753,9 +802,15 @@ fn create_with_enable_srtp_succeeds() { let pipe_transport = router1 .create_pipe_transport({ - let mut options = PipeTransportOptions::new(ListenIp { + let mut options = PipeTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); options.enable_srtp = true; @@ -804,9 +859,15 @@ fn create_with_invalid_srtp_parameters_fails() { let (_worker1, _worker2, router1, _router2, _transport1, _transport2) = init().await; let pipe_transport = router1 - .create_pipe_transport(PipeTransportOptions::new(ListenIp { + .create_pipe_transport(PipeTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, })) .await .expect("Failed to create Pipe transport"); @@ -1126,9 +1187,15 @@ fn pipe_to_router_called_twice_generates_single_pair() { .expect("Failed to create router"); let mut transport_options = - WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, })); transport_options.enable_sctp = true; diff --git a/rust/tests/integration/plain_transport.rs b/rust/tests/integration/plain_transport.rs index fc4add8344..3fe38ed4a8 100644 --- a/rust/tests/integration/plain_transport.rs +++ b/rust/tests/integration/plain_transport.rs @@ -1,6 +1,8 @@ use futures_lite::future; use hash_hasher::HashedSet; -use mediasoup::data_structures::{AppData, ListenIp, Protocol, SctpState, TransportTuple}; +#[cfg(not(target_os = "windows"))] +use mediasoup::data_structures::SocketFlags; +use mediasoup::data_structures::{AppData, ListenInfo, Protocol, SctpState, TransportTuple}; use mediasoup::plain_transport::{PlainTransportOptions, PlainTransportRemoteParameters}; use mediasoup::prelude::*; use mediasoup::router::{Router, RouterOptions}; @@ -89,9 +91,15 @@ fn create_succeeds() { { let transport = router .create_plain_transport({ - let mut plain_transport_options = PlainTransportOptions::new(ListenIp { + let mut plain_transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("4.4.4.4".parse().unwrap()), + announced_address: Some("4.4.4.4".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); plain_transport_options.rtcp_mux = false; @@ -123,9 +131,15 @@ fn create_succeeds() { let transport1 = router .create_plain_transport({ - let mut plain_transport_options = PlainTransportOptions::new(ListenIp { + let mut plain_transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("9.9.9.1".parse().unwrap()), + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); plain_transport_options.rtcp_mux = true; plain_transport_options.enable_sctp = true; @@ -152,10 +166,12 @@ fn create_succeeds() { TransportTuple::LocalOnly { .. }, )); if let TransportTuple::LocalOnly { - local_ip, protocol, .. + local_address, + protocol, + .. } = transport1.tuple() { - assert_eq!(local_ip, "9.9.9.1".parse::().unwrap()); + assert_eq!(local_address, "9.9.9.1"); assert_eq!(protocol, Protocol::Udp); } assert_eq!(transport1.rtcp_tuple(), None); @@ -189,14 +205,32 @@ fn create_succeeds() { } { + let rtcp_port = pick_unused_port().unwrap(); let transport2 = router .create_plain_transport({ - let mut plain_transport_options = PlainTransportOptions::new(ListenIp { + let mut plain_transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); plain_transport_options.rtcp_mux = false; + plain_transport_options.rtcp_listen_info = Some(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, + port: Some(rtcp_port), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }); + plain_transport_options }) .await @@ -209,18 +243,24 @@ fn create_succeeds() { TransportTuple::LocalOnly { .. }, )); if let TransportTuple::LocalOnly { - local_ip, protocol, .. + local_address, + protocol, + .. } = transport2.tuple() { - assert_eq!(local_ip, "127.0.0.1".parse::().unwrap()); + assert_eq!(local_address, "127.0.0.1"); assert_eq!(protocol, Protocol::Udp); } assert!(transport2.rtcp_tuple().is_some()); if let TransportTuple::LocalOnly { - local_ip, protocol, .. + local_address, + local_port, + protocol, + .. } = transport2.rtcp_tuple().unwrap() { - assert_eq!(local_ip, "127.0.0.1".parse::().unwrap()); + assert_eq!(local_address, "127.0.0.1"); + assert_eq!(local_port, rtcp_port); assert_eq!(protocol, Protocol::Udp); } assert_eq!(transport2.srtp_parameters(), None); @@ -251,13 +291,16 @@ fn create_with_fixed_port_succeeds() { let transport = router .create_plain_transport({ - let mut plain_transport_options = PlainTransportOptions::new(ListenIp { + PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("4.4.4.4".parse().unwrap()), - }); - plain_transport_options.port = Some(port); - - plain_transport_options + announced_address: Some("4.4.4.4".to_string()), + port: Some(port), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }) }) .await .expect("Failed to create Plain transport"); @@ -273,9 +316,15 @@ fn weak() { let transport = router .create_plain_transport({ - let mut plain_transport_options = PlainTransportOptions::new(ListenIp { + let mut plain_transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("4.4.4.4".parse().unwrap()), + announced_address: Some("4.4.4.4".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); plain_transport_options.rtcp_mux = false; @@ -302,9 +351,15 @@ fn create_enable_srtp_succeeds() { // Use default cryptoSuite: 'AES_CM_128_HMAC_SHA1_80'. let transport1 = router .create_plain_transport({ - let mut plain_transport_options = PlainTransportOptions::new(ListenIp { + let mut plain_transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("9.9.9.1".parse().unwrap()), + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); plain_transport_options.enable_srtp = true; @@ -363,13 +418,123 @@ fn create_non_bindable_ip() { assert!(matches!( router - .create_plain_transport(PlainTransportOptions::new(ListenIp { + .create_plain_transport(PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: "8.8.8.8".parse().unwrap(), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })) + .await, + Err(RequestError::Response { .. }), + )); + }); +} + +#[cfg(not(target_os = "windows"))] +#[test] +fn create_two_transports_binding_to_same_ip_port_with_udp_reuse_port_flag_succeed() { + future::block_on(async move { + let (_worker, router) = init().await; + + let multicast_ip = "224.0.0.1".parse().unwrap(); + let port = pick_unused_port().unwrap(); + + let transport1 = router + .create_plain_transport({ + PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, + ip: multicast_ip, + announced_address: None, + port: Some(port), + port_range: None, + // NOTE: ipv6Only flag will be ignored since ip is IPv4. + flags: Some(SocketFlags { + ipv6_only: true, + udp_reuse_port: true, + }), + send_buffer_size: None, + recv_buffer_size: None, + }) + }) + .await + .expect("Failed to create first Plain transport"); + + let transport2 = router + .create_plain_transport({ + PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, + ip: multicast_ip, + announced_address: None, + port: Some(port), + port_range: None, + flags: Some(SocketFlags { + ipv6_only: false, + udp_reuse_port: true, + }), + send_buffer_size: None, + recv_buffer_size: None, + }) + }) + .await + .expect("Failed to create second Plain transport"); + + assert_eq!(transport1.tuple().local_port(), port); + assert_eq!(transport2.tuple().local_port(), port); + }); +} + +#[cfg(not(target_os = "windows"))] +#[test] +fn create_two_transports_binding_to_same_ip_port_without_udp_reuse_port_flag_fails() { + future::block_on(async move { + let (_worker, router) = init().await; + + let multicast_ip = "224.0.0.1".parse().unwrap(); + let port = pick_unused_port().unwrap(); + + let transport1 = router + .create_plain_transport({ + PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, + ip: multicast_ip, + announced_address: None, + port: Some(port), + port_range: None, + flags: Some(SocketFlags { + ipv6_only: false, + udp_reuse_port: false, + }), + send_buffer_size: None, + recv_buffer_size: None, + }) + }) + .await + .expect("Failed to create first Plain transport"); + + assert!(matches!( + router + .create_plain_transport(PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, + ip: multicast_ip, + announced_address: None, + port: Some(port), + port_range: None, + flags: Some(SocketFlags { + ipv6_only: false, + udp_reuse_port: false, + }), + send_buffer_size: None, + recv_buffer_size: None, })) .await, Err(RequestError::Response { .. }), )); + + assert_eq!(transport1.tuple().local_port(), port); }); } @@ -380,9 +545,15 @@ fn get_stats_succeeds() { let transport = router .create_plain_transport({ - let mut plain_transport_options = PlainTransportOptions::new(ListenIp { + let mut plain_transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("4.4.4.4".parse().unwrap()), + announced_address: Some("4.4.4.4".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); plain_transport_options.rtcp_mux = false; @@ -414,16 +585,15 @@ fn get_stats_succeeds() { assert_eq!(stats[0].probation_send_bitrate, 0); assert_eq!(stats[0].rtp_packet_loss_received, None); assert_eq!(stats[0].rtp_packet_loss_sent, None); - assert!(matches!( - stats[0].tuple, - Some(TransportTuple::LocalOnly { .. }), - )); + assert!(matches!(stats[0].tuple, TransportTuple::LocalOnly { .. },)); if let TransportTuple::LocalOnly { - local_ip, protocol, .. - } = stats[0].tuple.unwrap() + local_address, + protocol, + .. + } = &stats[0].tuple { - assert_eq!(local_ip, "4.4.4.4".parse::().unwrap()); - assert_eq!(protocol, Protocol::Udp); + assert_eq!(local_address, "4.4.4.4"); + assert_eq!(*protocol, Protocol::Udp); } assert_eq!(stats[0].rtcp_tuple, None); }); @@ -436,9 +606,15 @@ fn connect_succeeds() { let transport = router .create_plain_transport({ - let mut plain_transport_options = PlainTransportOptions::new(ListenIp { + let mut plain_transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("4.4.4.4".parse().unwrap()), + announced_address: Some("4.4.4.4".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); plain_transport_options.rtcp_mux = false; @@ -507,9 +683,15 @@ fn connect_wrong_arguments() { let transport = router .create_plain_transport({ - let mut plain_transport_options = PlainTransportOptions::new(ListenIp { + let mut plain_transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("4.4.4.4".parse().unwrap()), + announced_address: Some("4.4.4.4".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); plain_transport_options.rtcp_mux = false; @@ -543,9 +725,15 @@ fn close_event() { let transport = router .create_plain_transport({ - let mut plain_transport_options = PlainTransportOptions::new(ListenIp { + let mut plain_transport_options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("4.4.4.4".parse().unwrap()), + announced_address: Some("4.4.4.4".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); plain_transport_options.rtcp_mux = false; diff --git a/rust/tests/integration/producer.rs b/rust/tests/integration/producer.rs index ee5023b1ef..2fb6ac9276 100644 --- a/rust/tests/integration/producer.rs +++ b/rust/tests/integration/producer.rs @@ -1,7 +1,7 @@ use async_io::Timer; use futures_lite::future; use hash_hasher::{HashedMap, HashedSet}; -use mediasoup::data_structures::{AppData, ListenIp}; +use mediasoup::data_structures::{AppData, ListenInfo, Protocol}; use mediasoup::prelude::*; use mediasoup::producer::{ProducerOptions, ProducerTraceEventType, ProducerType}; use mediasoup::router::{Router, RouterOptions}; @@ -12,7 +12,9 @@ use mediasoup::rtp_parameters::{ }; use mediasoup::scalability_modes::ScalabilityMode; use mediasoup::transport::ProduceError; -use mediasoup::webrtc_transport::{TransportListenIps, WebRtcTransport, WebRtcTransportOptions}; +use mediasoup::webrtc_transport::{ + WebRtcTransport, WebRtcTransportListenInfos, WebRtcTransportOptions, +}; use mediasoup::worker::{Worker, WorkerSettings}; use mediasoup::worker_manager::WorkerManager; use std::env; @@ -199,10 +201,17 @@ async fn init() -> (Worker, Router, WebRtcTransport, WebRtcTransport) { .await .expect("Failed to create router"); - let transport_options = WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - })); + let transport_options = + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })); let transport_1 = router .create_webrtc_transport(transport_options.clone()) @@ -635,7 +644,7 @@ fn produce_already_used_mid_ssrc() { } #[test] -fn produce_no_mid_single_encoding_without_dir_or_ssrc() { +fn produce_no_mid_single_encoding_without_rid_or_ssrc() { future::block_on(async move { let (_worker, _router, transport_1, _transport_2) = init().await; @@ -699,7 +708,6 @@ fn dump_succeeds() { rtx: None, dtx: None, scalability_mode: ScalabilityMode::None, - scale_resolution_down_by: None, max_bitrate: None }], ); @@ -737,7 +745,6 @@ fn dump_succeeds() { rtx: Some(RtpEncodingParametersRtx { ssrc: 22222223 }), dtx: None, scalability_mode: "L1T3".parse().unwrap(), - scale_resolution_down_by: None, max_bitrate: None }, RtpEncodingParameters { @@ -747,7 +754,6 @@ fn dump_succeeds() { rtx: Some(RtpEncodingParametersRtx { ssrc: 22222225 }), dtx: None, scalability_mode: ScalabilityMode::None, - scale_resolution_down_by: None, max_bitrate: None }, RtpEncodingParameters { @@ -757,7 +763,6 @@ fn dump_succeeds() { rtx: Some(RtpEncodingParametersRtx { ssrc: 22222227 }), dtx: None, scalability_mode: ScalabilityMode::None, - scale_resolution_down_by: None, max_bitrate: None }, RtpEncodingParameters { @@ -767,7 +772,6 @@ fn dump_succeeds() { rtx: Some(RtpEncodingParametersRtx { ssrc: 22222229 }), dtx: None, scalability_mode: ScalabilityMode::None, - scale_resolution_down_by: None, max_bitrate: None }, ], @@ -880,7 +884,10 @@ fn enable_trace_event_succeeds() { .await .expect("Failed to dump audio producer"); - assert_eq!(dump.trace_event_types.as_str(), "rtp,pli"); + assert_eq!( + dump.trace_event_types, + vec![ProducerTraceEventType::Rtp, ProducerTraceEventType::Pli] + ); } { @@ -894,7 +901,7 @@ fn enable_trace_event_succeeds() { .await .expect("Failed to dump audio producer"); - assert_eq!(dump.trace_event_types.as_str(), ""); + assert_eq!(dump.trace_event_types, vec![]); } }); } diff --git a/rust/tests/integration/router.rs b/rust/tests/integration/router.rs index 66889e2f5b..56d056d5a9 100644 --- a/rust/tests/integration/router.rs +++ b/rust/tests/integration/router.rs @@ -111,8 +111,7 @@ fn create_router_succeeds() { worker_dump.channel_message_handlers, ChannelMessageHandlers { channel_request_handlers: vec![router.id().into()], - payload_channel_request_handlers: vec![], - payload_channel_notification_handlers: vec![] + channel_notification_handlers: vec![] } ); diff --git a/rust/tests/integration/smoke.rs b/rust/tests/integration/smoke.rs index e6527f3671..20f3bff81c 100644 --- a/rust/tests/integration/smoke.rs +++ b/rust/tests/integration/smoke.rs @@ -4,7 +4,7 @@ use mediasoup::audio_level_observer::AudioLevelObserverOptions; use mediasoup::consumer::{ConsumerLayers, ConsumerOptions, ConsumerTraceEventType}; use mediasoup::data_consumer::DataConsumerOptions; use mediasoup::data_producer::DataProducerOptions; -use mediasoup::data_structures::ListenIp; +use mediasoup::data_structures::{ListenInfo, Protocol}; use mediasoup::direct_transport::DirectTransportOptions; use mediasoup::plain_transport::PlainTransportOptions; use mediasoup::prelude::*; @@ -17,7 +17,7 @@ use mediasoup::rtp_parameters::{ }; use mediasoup::sctp_parameters::SctpStreamParameters; use mediasoup::transport::TransportTraceEventType; -use mediasoup::webrtc_transport::{TransportListenIps, WebRtcTransportOptions}; +use mediasoup::webrtc_transport::{WebRtcTransportListenInfos, WebRtcTransportOptions}; use mediasoup::worker::{WorkerLogLevel, WorkerSettings, WorkerUpdateSettings}; use mediasoup::worker_manager::WorkerManager; use std::net::{IpAddr, Ipv4Addr}; @@ -82,10 +82,17 @@ fn smoke() { let webrtc_transport = router .create_webrtc_transport({ - let mut options = WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - })); + let mut options = + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })); options.enable_sctp = true; @@ -264,9 +271,15 @@ fn smoke() { let plain_transport = router .create_plain_transport({ - let mut options = PlainTransportOptions::new(ListenIp { + let mut options = PlainTransportOptions::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); options.enable_sctp = true; diff --git a/rust/tests/integration/webrtc_server.rs b/rust/tests/integration/webrtc_server.rs index b4600cd09f..6e75f5a144 100644 --- a/rust/tests/integration/webrtc_server.rs +++ b/rust/tests/integration/webrtc_server.rs @@ -1,9 +1,7 @@ use futures_lite::future; use hash_hasher::HashedSet; -use mediasoup::data_structures::{AppData, ListenIp, Protocol}; -use mediasoup::webrtc_server::{ - WebRtcServerIpPort, WebRtcServerListenInfo, WebRtcServerListenInfos, WebRtcServerOptions, -}; +use mediasoup::data_structures::{AppData, ListenInfo, Protocol}; +use mediasoup::webrtc_server::{WebRtcServerIpPort, WebRtcServerListenInfos, WebRtcServerOptions}; use mediasoup::worker::{ChannelMessageHandlers, CreateWebRtcServerError, Worker, WorkerSettings}; use mediasoup::worker_manager::WorkerManager; use portpicker::pick_unused_port; @@ -12,6 +10,11 @@ use std::net::{IpAddr, Ipv4Addr}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; +#[derive(Debug, PartialEq)] +struct CustomAppData { + foo: u32, +} + async fn init() -> (Worker, Worker) { { let mut builder = env_logger::builder(); @@ -54,28 +57,27 @@ fn create_webrtc_server_succeeds() { }) .detach(); - #[derive(Debug, PartialEq)] - struct CustomAppData { - foo: u32, - } - let webrtc_server = worker1 .create_webrtc_server({ - let listen_infos = WebRtcServerListenInfos::new(WebRtcServerListenInfo { + let listen_infos = WebRtcServerListenInfos::new(ListenInfo { protocol: Protocol::Udp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - }, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, port: Some(port1), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); - let listen_infos = listen_infos.insert(WebRtcServerListenInfo { + let listen_infos = listen_infos.insert(ListenInfo { protocol: Protocol::Tcp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))), - }, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: Some("foo.bar.org".to_string()), port: Some(port2), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); let mut webrtc_server_options = WebRtcServerOptions::new(listen_infos); @@ -101,8 +103,7 @@ fn create_webrtc_server_succeeds() { worker_dump.channel_message_handlers, ChannelMessageHandlers { channel_request_handlers: vec![webrtc_server.id().into()], - payload_channel_request_handlers: vec![], - payload_channel_notification_handlers: vec![] + channel_notification_handlers: vec![] } ); @@ -149,28 +150,27 @@ fn create_webrtc_server_without_specifying_port_succeeds() { }) .detach(); - #[derive(Debug, PartialEq)] - struct CustomAppData { - foo: u32, - } - let webrtc_server = worker1 .create_webrtc_server({ - let listen_infos = WebRtcServerListenInfos::new(WebRtcServerListenInfo { + let listen_infos = WebRtcServerListenInfos::new(ListenInfo { protocol: Protocol::Udp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - }, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); - let listen_infos = listen_infos.insert(WebRtcServerListenInfo { + let listen_infos = listen_infos.insert(ListenInfo { protocol: Protocol::Tcp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))), - }, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: Some("1.2.3.4".to_string()), port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); let mut webrtc_server_options = WebRtcServerOptions::new(listen_infos); @@ -228,30 +228,29 @@ fn unavailable_infos_fails() { }) .detach(); - #[derive(Debug, PartialEq)] - struct CustomAppData { - foo: u32, - } - // Using an unavailable listen IP. { let create_result = worker1 .create_webrtc_server({ - let listen_infos = WebRtcServerListenInfos::new(WebRtcServerListenInfo { + let listen_infos = WebRtcServerListenInfos::new(ListenInfo { protocol: Protocol::Udp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - }, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, port: Some(port1), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); - let listen_infos = listen_infos.insert(WebRtcServerListenInfo { + let listen_infos = listen_infos.insert(ListenInfo { protocol: Protocol::Udp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), - announced_ip: None, - }, + ip: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), + announced_address: None, port: Some(port2), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); WebRtcServerOptions::new(listen_infos) @@ -268,21 +267,25 @@ fn unavailable_infos_fails() { { let create_result = worker1 .create_webrtc_server({ - let listen_infos = WebRtcServerListenInfos::new(WebRtcServerListenInfo { + let listen_infos = WebRtcServerListenInfos::new(ListenInfo { protocol: Protocol::Udp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - }, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, port: Some(port1), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); - let listen_infos = listen_infos.insert(WebRtcServerListenInfo { + let listen_infos = listen_infos.insert(ListenInfo { protocol: Protocol::Udp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - announced_ip: Some(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))), - }, + ip: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + announced_address: Some("1.2.3.4".to_string()), port: Some(port1), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }); WebRtcServerOptions::new(listen_infos) @@ -299,13 +302,15 @@ fn unavailable_infos_fails() { { let _webrtc_server = worker1 .create_webrtc_server(WebRtcServerOptions::new(WebRtcServerListenInfos::new( - WebRtcServerListenInfo { + ListenInfo { protocol: Protocol::Udp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - }, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, port: Some(port1), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }, ))) .await @@ -313,13 +318,15 @@ fn unavailable_infos_fails() { let create_result = worker2 .create_webrtc_server(WebRtcServerOptions::new(WebRtcServerListenInfos::new( - WebRtcServerListenInfo { + ListenInfo { protocol: Protocol::Udp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - }, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, port: Some(port1), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }, ))) .await; @@ -341,13 +348,15 @@ fn close_event() { let webrtc_server = worker1 .create_webrtc_server(WebRtcServerOptions::new(WebRtcServerListenInfos::new( - WebRtcServerListenInfo { + ListenInfo { protocol: Protocol::Udp, - listen_ip: ListenIp { - ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, - }, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: None, port: Some(port), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }, ))) .await diff --git a/rust/tests/integration/webrtc_transport.rs b/rust/tests/integration/webrtc_transport.rs index bf12e3ae49..a58c835439 100644 --- a/rust/tests/integration/webrtc_transport.rs +++ b/rust/tests/integration/webrtc_transport.rs @@ -1,8 +1,8 @@ use futures_lite::future; use hash_hasher::HashedSet; use mediasoup::data_structures::{ - AppData, DtlsFingerprint, DtlsParameters, DtlsRole, DtlsState, IceCandidateTcpType, - IceCandidateType, IceRole, IceState, ListenIp, Protocol, SctpState, + AppData, DtlsFingerprint, DtlsParameters, DtlsRole, DtlsState, IceCandidateType, IceRole, + IceState, ListenInfo, Protocol, SctpState, }; use mediasoup::prelude::*; use mediasoup::router::{Router, RouterOptions}; @@ -12,8 +12,7 @@ use mediasoup::rtp_parameters::{ use mediasoup::sctp_parameters::{NumSctpStreams, SctpParameters}; use mediasoup::transport::TransportTraceEventType; use mediasoup::webrtc_transport::{ - TransportListenIps, WebRtcTransportListen, WebRtcTransportOptions, - WebRtcTransportRemoteParameters, + WebRtcTransportListenInfos, WebRtcTransportOptions, WebRtcTransportRemoteParameters, }; use mediasoup::worker::{RequestError, Worker, WorkerSettings}; use mediasoup::worker_manager::WorkerManager; @@ -95,12 +94,18 @@ fn create_succeeds() { { let transport = router - .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - ListenIp { + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("9.9.9.1".parse().unwrap()), - }, - ))) + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) .await .expect("Failed to create WebRTC transport"); @@ -129,24 +134,40 @@ fn create_succeeds() { .create_webrtc_transport({ let mut webrtc_transport_options = WebRtcTransportOptions::new( vec![ - ListenIp { + ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("9.9.9.1".parse().unwrap()), + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }, - ListenIp { + ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::UNSPECIFIED), - announced_ip: Some("9.9.9.2".parse().unwrap()), + announced_address: Some("foo1.bar.org".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }, - ListenIp { + ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: None, + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, }, ] .try_into() .unwrap(), ); - webrtc_transport_options.enable_tcp = true; - webrtc_transport_options.prefer_udp = true; webrtc_transport_options.enable_sctp = true; webrtc_transport_options.num_sctp_streams = NumSctpStreams { os: 2048, @@ -182,42 +203,25 @@ fn create_succeeds() { ); { let ice_candidates = transport1.ice_candidates(); - assert_eq!(ice_candidates.len(), 6); - assert_eq!(ice_candidates[0].ip, "9.9.9.1".parse::().unwrap()); + assert_eq!(ice_candidates.len(), 3); + assert_eq!(ice_candidates[0].address, "9.9.9.1"); assert_eq!(ice_candidates[0].protocol, Protocol::Udp); assert_eq!(ice_candidates[0].r#type, IceCandidateType::Host); assert_eq!(ice_candidates[0].tcp_type, None); - assert_eq!(ice_candidates[1].ip, "9.9.9.1".parse::().unwrap()); - assert_eq!(ice_candidates[1].protocol, Protocol::Tcp); + assert_eq!(ice_candidates[1].address, "foo1.bar.org"); + assert_eq!(ice_candidates[1].protocol, Protocol::Udp); assert_eq!(ice_candidates[1].r#type, IceCandidateType::Host); - assert_eq!( - ice_candidates[1].tcp_type, - Some(IceCandidateTcpType::Passive), - ); - assert_eq!(ice_candidates[2].ip, "9.9.9.2".parse::().unwrap()); + assert_eq!(ice_candidates[1].tcp_type, None); + assert_eq!(ice_candidates[2].address, "127.0.0.1"); + assert_eq!(ice_candidates[2].protocol, Protocol::Udp); + assert_eq!(ice_candidates[2].r#type, IceCandidateType::Host); + assert_eq!(ice_candidates[2].tcp_type, None); + assert_eq!(ice_candidates[2].address, "127.0.0.1"); assert_eq!(ice_candidates[2].protocol, Protocol::Udp); assert_eq!(ice_candidates[2].r#type, IceCandidateType::Host); assert_eq!(ice_candidates[2].tcp_type, None); - assert_eq!(ice_candidates[3].ip, "9.9.9.2".parse::().unwrap()); - assert_eq!(ice_candidates[3].protocol, Protocol::Tcp); - assert_eq!(ice_candidates[3].r#type, IceCandidateType::Host); - assert_eq!( - ice_candidates[3].tcp_type, - Some(IceCandidateTcpType::Passive), - ); - assert_eq!(ice_candidates[4].ip, "127.0.0.1".parse::().unwrap()); - assert_eq!(ice_candidates[4].protocol, Protocol::Udp); - assert_eq!(ice_candidates[4].r#type, IceCandidateType::Host); - assert_eq!(ice_candidates[4].tcp_type, None); - assert_eq!(ice_candidates[4].ip, "127.0.0.1".parse::().unwrap()); - assert_eq!(ice_candidates[4].protocol, Protocol::Udp); - assert_eq!(ice_candidates[4].r#type, IceCandidateType::Host); - assert_eq!(ice_candidates[4].tcp_type, None); assert!(ice_candidates[0].priority > ice_candidates[1].priority); - assert!(ice_candidates[2].priority > ice_candidates[1].priority); - assert!(ice_candidates[2].priority > ice_candidates[3].priority); - assert!(ice_candidates[4].priority > ice_candidates[3].priority); - assert!(ice_candidates[4].priority > ice_candidates[5].priority); + assert!(ice_candidates[1].priority > ice_candidates[2].priority); } assert_eq!(transport1.ice_state(), IceState::New); @@ -258,29 +262,89 @@ fn create_with_fixed_port_succeeds() { future::block_on(async move { let (_worker, router) = init().await; - let port1 = pick_unused_port().unwrap(); + let port = pick_unused_port().unwrap(); let transport = router .create_webrtc_transport({ - let mut options = WebRtcTransportOptions::new(TransportListenIps::new(ListenIp { + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("9.9.9.1".parse().unwrap()), - })); - match &mut options.listen { - WebRtcTransportListen::Individual { port, .. } => { - port.replace(port1); - } - WebRtcTransportListen::Server { .. } => { - unreachable!(); - } - } + announced_address: Some("9.9.9.1".to_string()), + port: Some(port), + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })) + }) + .await + .expect("Failed to create WebRTC transport"); + + assert_eq!(transport.ice_candidates().first().unwrap().port, port); + }); +} - options +#[test] +fn create_with_port_range_succeeds() { + future::block_on(async move { + let (_worker, router) = init().await; + let port_range = 11111..=11112; + + let transport1 = router + .create_webrtc_transport({ + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: Some(port_range.clone()), + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })) }) .await .expect("Failed to create WebRTC transport"); - assert_eq!(transport.ice_candidates().get(0).unwrap().port, port1); + let port1 = transport1.ice_candidates().first().unwrap().port; + assert!(port1 >= *port_range.start() && port1 <= *port_range.end()); + + let transport2 = router + .create_webrtc_transport({ + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: Some(port_range.clone()), + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })) + }) + .await + .expect("Failed to create WebRTC transport"); + + let port2 = transport2.ice_candidates().first().unwrap().port; + assert!(port2 >= *port_range.start() && port2 <= *port_range.end()); + + assert!(matches!( + router + .create_webrtc_transport({ + WebRtcTransportOptions::new(WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: Some(port_range.clone()), + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + })) + }) + .await, + Err(RequestError::Response { .. }), + )); }); } @@ -290,12 +354,18 @@ fn weak() { let (_worker, router) = init().await; let transport = router - .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - ListenIp { + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("9.9.9.1".parse().unwrap()), - }, - ))) + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) .await .expect("Failed to create WebRTC transport"); @@ -316,12 +386,18 @@ fn create_non_bindable_ip() { assert!(matches!( router - .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - ListenIp { + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: "8.8.8.8".parse().unwrap(), - announced_ip: None, - }, - ))) + announced_address: None, + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + },) + )) .await, Err(RequestError::Response { .. }), )); @@ -334,12 +410,18 @@ fn get_stats_succeeds() { let (_worker, router) = init().await; let transport = router - .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - ListenIp { + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("9.9.9.1".parse().unwrap()), - }, - ))) + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) .await .expect("Failed to create WebRTC transport"); @@ -381,12 +463,18 @@ fn connect_succeeds() { let (_worker, router) = init().await; let transport = router - .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - ListenIp { + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("9.9.9.1".parse().unwrap()), - }, - ))) + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) .await .expect("Failed to create WebRTC transport"); @@ -426,19 +514,31 @@ fn set_max_incoming_bitrate_succeeds() { let (_worker, router) = init().await; let transport = router - .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - ListenIp { + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("9.9.9.1".parse().unwrap()), - }, - ))) + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) .await .expect("Failed to create WebRTC transport"); transport - .set_max_incoming_bitrate(100000) + .set_max_incoming_bitrate(1000000) .await .expect("Failed to set max incoming bitrate on WebRTC transport"); + + // Remove limit. + transport + .set_max_incoming_bitrate(0) + .await + .expect("Failed to remove limit in max incoming bitrate on WebRTC transport"); }); } @@ -448,19 +548,143 @@ fn set_max_outgoing_bitrate_succeeds() { let (_worker, router) = init().await; let transport = router - .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - ListenIp { + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) + .await + .expect("Failed to create WebRTC transport"); + + transport + .set_max_outgoing_bitrate(2000000) + .await + .expect("Failed to set max outgoing bitrate on WebRTC transport"); + + // Remove limit. + transport + .set_max_outgoing_bitrate(0) + .await + .expect("Failed to remove limit in max outgoing bitrate on WebRTC transport"); + }); +} + +#[test] +fn set_min_outgoing_bitrate_succeeds() { + future::block_on(async move { + let (_worker, router) = init().await; + + let transport = router + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) + .await + .expect("Failed to create WebRTC transport"); + + transport + .set_min_outgoing_bitrate(100000) + .await + .expect("Failed to set min outgoing bitrate on WebRTC transport"); + + // Remove limit. + transport + .set_min_outgoing_bitrate(0) + .await + .expect("Failed to remove limit in min outgoing bitrate on WebRTC transport"); + }); +} + +#[test] +fn set_max_outgoing_bitrate_fails_if_value_is_lower_than_current_min_limit() { + future::block_on(async move { + let (_worker, router) = init().await; + + let transport = router + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, + ip: IpAddr::V4(Ipv4Addr::LOCALHOST), + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) + .await + .expect("Failed to create WebRTC transport"); + + transport + .set_min_outgoing_bitrate(3000000) + .await + .expect("Failed to set min outgoing bitrate on WebRTC transport"); + + assert!(matches!( + transport.set_max_outgoing_bitrate(2000000).await, + Err(RequestError::Response { .. }), + )); + + // Remove limit. + transport + .set_min_outgoing_bitrate(0) + .await + .expect("Failed to remove limit in min outgoing bitrate on WebRTC transport"); + }); +} + +#[test] +fn set_min_outgoing_bitrate_fails_if_value_is_higher_than_current_max_limit() { + future::block_on(async move { + let (_worker, router) = init().await; + + let transport = router + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("9.9.9.1".parse().unwrap()), - }, - ))) + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) .await .expect("Failed to create WebRTC transport"); transport - .set_max_outgoing_bitrate(100000) + .set_max_outgoing_bitrate(2000000) .await .expect("Failed to set max outgoing bitrate on WebRTC transport"); + + assert!(matches!( + transport.set_min_outgoing_bitrate(3000000).await, + Err(RequestError::Response { .. }), + )); + + // Remove limit. + transport + .set_max_outgoing_bitrate(0) + .await + .expect("Failed to remove limit in max outgoing bitrate on WebRTC transport"); }); } @@ -470,12 +694,18 @@ fn restart_ice_succeeds() { let (_worker, router) = init().await; let transport = router - .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - ListenIp { + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("9.9.9.1".parse().unwrap()), - }, - ))) + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) .await .expect("Failed to create WebRTC transport"); @@ -501,12 +731,18 @@ fn enable_trace_event_succeeds() { let (_worker, router) = init().await; let transport = router - .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - ListenIp { + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("9.9.9.1".parse().unwrap()), - }, - ))) + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) .await .expect("Failed to create WebRTC transport"); @@ -521,7 +757,10 @@ fn enable_trace_event_succeeds() { .await .expect("Failed to dump WebRTC transport"); - assert_eq!(dump.trace_event_types.as_str(), "probation"); + assert_eq!( + dump.trace_event_types, + vec![TransportTraceEventType::Probation] + ); } { @@ -538,7 +777,13 @@ fn enable_trace_event_succeeds() { .await .expect("Failed to dump WebRTC transport"); - assert_eq!(dump.trace_event_types.as_str(), "probation,bwe"); + assert_eq!( + dump.trace_event_types, + vec![ + TransportTraceEventType::Probation, + TransportTraceEventType::Bwe, + ] + ); } { @@ -552,7 +797,7 @@ fn enable_trace_event_succeeds() { .await .expect("Failed to dump WebRTC transport"); - assert_eq!(dump.trace_event_types.as_str(), ""); + assert_eq!(dump.trace_event_types, vec![]); } }); } @@ -563,12 +808,18 @@ fn close_event() { let (_worker, router) = init().await; let transport = router - .create_webrtc_transport(WebRtcTransportOptions::new(TransportListenIps::new( - ListenIp { + .create_webrtc_transport(WebRtcTransportOptions::new( + WebRtcTransportListenInfos::new(ListenInfo { + protocol: Protocol::Udp, ip: IpAddr::V4(Ipv4Addr::LOCALHOST), - announced_ip: Some("9.9.9.1".parse().unwrap()), - }, - ))) + announced_address: Some("9.9.9.1".to_string()), + port: None, + port_range: None, + flags: None, + send_buffer_size: None, + recv_buffer_size: None, + }), + )) .await .expect("Failed to create WebRTC transport"); diff --git a/rust/tests/integration/worker.rs b/rust/tests/integration/worker.rs index 1ae513b746..9f905ae888 100644 --- a/rust/tests/integration/worker.rs +++ b/rust/tests/integration/worker.rs @@ -42,7 +42,7 @@ fn create_worker_succeeds() { settings.log_level = WorkerLogLevel::Debug; settings.log_tags = vec![WorkerLogTag::Info]; - settings.rtc_ports_range = 0..=9999; + settings.rtc_port_range = 0..=9999; settings.dtls_files = Some(WorkerDtlsFiles { certificate: "tests/integration/data/dtls-cert.pem".into(), private_key: "tests/integration/data/dtls-key.pem".into(), @@ -79,7 +79,7 @@ fn create_worker_wrong_settings() { // Intentionally incorrect range #[allow(clippy::reversed_empty_ranges)] { - settings.rtc_ports_range = 1000..=999; + settings.rtc_port_range = 1000..=999; } settings @@ -150,8 +150,7 @@ fn dump_succeeds() { dump.channel_message_handlers, ChannelMessageHandlers { channel_request_handlers: vec![], - payload_channel_request_handlers: vec![], - payload_channel_notification_handlers: vec![] + channel_notification_handlers: vec![] } ); }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..1ea8a48622 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "lib": ["es2022"], + "target": "esnext", + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "outDir": "node/lib", + "declaration": true, + "declarationMap": true + }, + "include": ["node/src"], + "watchOptions": { + "watchFile": "useFsEvents", + "watchDirectory": "useFsEvents", + "fallbackPolling": "dynamicPriority", + "synchronousWatchDirectory": true + } +} diff --git a/worker/.clang-format b/worker/.clang-format index 00181a08c8..953a0fdc73 100644 --- a/worker/.clang-format +++ b/worker/.clang-format @@ -47,20 +47,21 @@ DisableFormat: false ExperimentalAutoDetectBinPacking: true FixNamespaceComments: true IncludeCategories: - - Regex: '"common.hpp"' + - Regex: '"common.hpp"' Priority: 1 - - Regex: '^"(Channel|PayloadChannel|RTC|Utils|handles)/' + - Regex: '^"(Channel|PayloadChannel|FBS|RTC|Utils|handles)/' Priority: 3 - - Regex: '"*"' + - Regex: '"*"' Priority: 2 - - Regex: '^<(json|uv|openssl|srtp|usrsctp|libwebrtc)(.|/)' + - Regex: '^<(flatbuffers|uv|openssl|srtp|usrsctp|libwebrtc)(.|/)' Priority: 4 - - Regex: '<*>' + - Regex: '<*>' Priority: 5 IncludeIsMainRegex: '$' IndentCaseLabels: true IndentWidth: 2 IndentWrappedFunctionNames: false +InsertBraces: true KeepEmptyLinesAtTheStartOfBlocks: false MaxEmptyLinesToKeep: 1 NamespaceIndentation: All diff --git a/worker/.clang-tidy b/worker/.clang-tidy index 5e120619de..d079f6afe6 100644 --- a/worker/.clang-tidy +++ b/worker/.clang-tidy @@ -1,295 +1,365 @@ --- -Checks: '*,-llvmlibc-*,-boost-use-to-string,-cert-*,-clang-analyzer-osx.*,-clang-analyzer-optin.osx.*,-cppcoreguidelines-pro-bounds-array-to-pointer-decay,-cppcoreguidelines-pro-bounds-constant-array-index,-cppcoreguidelines-pro-bounds-pointer-arithmetic,-cppcoreguidelines-pro-type-vararg,-cppcoreguidelines-pro-type-const-cast,-cppcoreguidelines-pro-type-reinterpret-cast,-cppcoreguidelines-pro-type-static-cast-downcast,-cppcoreguidelines-pro-type-union-access,-cppcoreguidelines-special-member-functions,-google-readability-*,-llvm-include-order,-readability-else-after-return,-readability-implicit-bool-cast,-modernize-pass-by-value,-cppcoreguidelines-no-malloc,-modernize-make-unique,-google-default-arguments,-hicpp-uppercase-literal-suffix,-readability-uppercase-literal-suffix,-hicpp-braces-around-statements,-hicpp-vararg,-hicpp-no-array-decay,-cppcoreguidelines-avoid-c-arrays,-hicpp-avoid-c-arrays,-modernize-avoid-c-arrays,-fuchsia-default-arguments-calls,-readability-implicit-bool-conversion,-modernize-use-trailing-return-type,-cppcoreguidelines-avoid-magic-numbers,-cppcoreguidelines-init-variables,-readability-magic-numbers,-cppcoreguidelines-avoid-goto,-hicpp-avoid-goto,-cppcoreguidelines-owning-memory,-hicpp-special-member-functions,-google-runtime-references,-fuchsia-default-arguments-declarations,-cppcoreguidelines-non-private-member-variables-in-classes,-misc-non-private-member-variables-in-classes,-hicpp-signed-bitwise,-readability-function-cognitive-complexity,-altera*' +Checks: "*,\ + -altera*,\ + -android*,\ + -boost-use-to-string,\ + -bugprone-easily-swappable-parameters,\ + -bugprone-implicit-widening-of-multiplication-result,\ + -bugprone-lambda-function-name,\ + -bugprone-macro-parentheses,\ + -bugprone-reserved-identifier,\ + -cert-*,\ + -clang-analyzer-optin.osx.*,\ + -clang-analyzer-osx.*,\ + -concurrency-mt-unsafe,\ + -cppcoreguidelines-avoid-c-arrays,\ + -cppcoreguidelines-avoid-do-while,\ + -cppcoreguidelines-avoid-goto,\ + -cppcoreguidelines-avoid-non-const-global-variables,\ + -cppcoreguidelines-avoid-magic-numbers,\ + -cppcoreguidelines-init-variables,\ + -cppcoreguidelines-no-malloc,\ + -cppcoreguidelines-non-private-member-variables-in-classes,\ + -cppcoreguidelines-owning-memory,\ + -cppcoreguidelines-prefer-member-initializer,\ + -cppcoreguidelines-pro-bounds-array-to-pointer-decay,\ + -cppcoreguidelines-pro-bounds-constant-array-index,\ + -cppcoreguidelines-pro-bounds-pointer-arithmetic,\ + -cppcoreguidelines-pro-type-const-cast,\ + -cppcoreguidelines-pro-type-reinterpret-cast,\ + -cppcoreguidelines-pro-type-static-cast-downcast,\ + -cppcoreguidelines-pro-type-union-access,\ + -cppcoreguidelines-pro-type-vararg,\ + -cppcoreguidelines-special-member-functions,\ + -fuchsia-default-arguments-calls,\ + -fuchsia-default-arguments-declarations,\ + -fuchsia-overloaded-operator,\ + -google-default-arguments,\ + -google-readability-*,\ + -google-runtime-references,\ + -google-upgrade-googletest-case,\ + -hicpp-avoid-c-arrays,\ + -hicpp-avoid-goto,\ + -hicpp-braces-around-statements,\ + -hicpp-no-array-decay,\ + -hicpp-signed-bitwise,\ + -hicpp-special-member-functions,\ + -hicpp-uppercase-literal-suffix,\ + -hicpp-vararg,\ + -llvm-include-order,\ + -llvm-header-guard,\ + -llvm-else-after-return,\ + -llvmlibc-*,\ + -misc-confusable-identifiers,\ + -misc-include-cleaner,\ + -misc-non-private-member-variables-in-classes,\ + -misc-use-anonymous-namespace,\ + -modernize-avoid-c-arrays,\ + -modernize-concat-nested-namespaces,\ + -modernize-make-unique,\ + -modernize-pass-by-value,\ + -modernize-use-nodiscard, \ + -modernize-use-trailing-return-type,\ + -readability-else-after-return,\ + -readability-function-cognitive-complexity,\ + -readability-identifier-length,\ + -readability-implicit-bool-cast,\ + -readability-implicit-bool-conversion,\ + -readability-magic-numbers,\ + -readability-redundant-access-specifiers, \ + -readability-uppercase-literal-suffix,\ + " -HeaderFilterRegex: '' AnalyzeTemporaryDtors: false User: mediasoup FormatStyle: file + CheckOptions: - - key: cppcoreguidelines-pro-bounds-constant-array-index.GslHeader - value: '' - - key: cppcoreguidelines-pro-bounds-constant-array-index.IncludeStyle - value: '0' - - key: misc-assert-side-effect.AssertMacros - value: assert - - key: misc-assert-side-effect.CheckFunctionCalls - value: '0' - - key: misc-throw-by-value-catch-by-reference.CheckThrowTemporaries - value: '1' - - key: modernize-loop-convert.MaxCopySize - value: '16' - - key: modernize-loop-convert.MinConfidence - value: reasonable - - key: modernize-loop-convert.NamingStyle - value: CamelCase - - key: modernize-replace-auto-ptr.IncludeStyle - value: llvm - - key: modernize-use-nullptr.NullMacros - value: 'NULL' - - key: readability-braces-around-statements.ShortStatementLines - value: '3' - - key: readability-function-size.BranchThreshold - value: '4294967295' - - key: readability-function-size.LineThreshold - value: '4294967295' - - key: readability-function-size.StatementThreshold - value: '800' - - key: readability-identifier-naming.AbstractClassCase - value: CamelCase - - key: readability-identifier-naming.AbstractClassPrefix - value: '' - - key: readability-identifier-naming.AbstractClassSuffix - value: '' - - key: readability-identifier-naming.ClassCase - value: CamelCase - - key: readability-identifier-naming.ClassConstantCase - value: camelBack - - key: readability-identifier-naming.ClassConstantPrefix - value: '' - - key: readability-identifier-naming.ClassConstantSuffix - value: '' - - key: readability-identifier-naming.ClassMemberCase - value: camelBack - - key: readability-identifier-naming.ClassMemberPrefix - value: '' - - key: readability-identifier-naming.ClassMemberSuffix - value: '' - - key: readability-identifier-naming.ClassMethodCase - value: CamelCase - - key: readability-identifier-naming.ClassMethodPrefix - value: '' - - key: readability-identifier-naming.ClassMethodSuffix - value: '' - - key: readability-identifier-naming.ClassPrefix - value: '' - - key: readability-identifier-naming.ClassSuffix - value: '' - - key: readability-identifier-naming.ConstantCase - value: UPPER_CASE - - key: readability-identifier-naming.ConstantMemberCase - value: camelBack - - key: readability-identifier-naming.ConstantMemberPrefix - value: '' - - key: readability-identifier-naming.ConstantMemberSuffix - value: '' - - key: readability-identifier-naming.ConstantParameterCase - value: camelBack - - key: readability-identifier-naming.ConstantParameterPrefix - value: '' - - key: readability-identifier-naming.ConstantParameterSuffix - value: '' - - key: readability-identifier-naming.ConstantPrefix - value: '' - - key: readability-identifier-naming.ConstantSuffix - value: '' - - key: readability-identifier-naming.ConstexprFunctionCase - value: camelBack - - key: readability-identifier-naming.ConstexprFunctionPrefix - value: '' - - key: readability-identifier-naming.ConstexprFunctionSuffix - value: '' - - key: readability-identifier-naming.ConstexprMethodCase - value: camelBack - - key: readability-identifier-naming.ConstexprMethodPrefix - value: '' - - key: readability-identifier-naming.ConstexprMethodSuffix - value: '' - - key: readability-identifier-naming.ConstexprVariableCase - value: CamelCase - - key: readability-identifier-naming.ConstexprVariablePrefix - value: '' - - key: readability-identifier-naming.ConstexprVariableSuffix - value: '' - - key: readability-identifier-naming.EnumCase - value: aNy_Case - - key: readability-identifier-naming.EnumConstantCase - value: aNy_Case - - key: readability-identifier-naming.EnumConstantPrefix - value: '' - - key: readability-identifier-naming.EnumConstantSuffix - value: '' - - key: readability-identifier-naming.EnumPrefix - value: '' - - key: readability-identifier-naming.EnumSuffix - value: '' - - key: readability-identifier-naming.FunctionCase - value: camelBack - - key: readability-identifier-naming.FunctionPrefix - value: '' - - key: readability-identifier-naming.FunctionSuffix - value: '' - - key: readability-identifier-naming.GlobalConstantCase - value: CamelCase - - key: readability-identifier-naming.GlobalConstantPrefix - value: '' - - key: readability-identifier-naming.GlobalConstantSuffix - value: '' - - key: readability-identifier-naming.GlobalFunctionCase - value: CamelCase - - key: readability-identifier-naming.GlobalFunctionPrefix - value: '' - - key: readability-identifier-naming.GlobalFunctionSuffix - value: '' - - key: readability-identifier-naming.GlobalVariableCase - value: CamelCase - - key: readability-identifier-naming.GlobalVariablePrefix - value: '' - - key: readability-identifier-naming.GlobalVariableSuffix - value: '' - - key: readability-identifier-naming.IgnoreFailedSplit - value: '0' - - key: readability-identifier-naming.InlineNamespaceCase - value: aNy_Case - - key: readability-identifier-naming.InlineNamespacePrefix - value: '' - - key: readability-identifier-naming.InlineNamespaceSuffix - value: '' - - key: readability-identifier-naming.LocalConstantCase - value: camelBack - - key: readability-identifier-naming.LocalConstantPrefix - value: '' - - key: readability-identifier-naming.LocalConstantSuffix - value: '' - - key: readability-identifier-naming.LocalVariableCase - value: camelBack - - key: readability-identifier-naming.LocalVariablePrefix - value: '' - - key: readability-identifier-naming.LocalVariableSuffix - value: '' - - key: readability-identifier-naming.MemberCase - value: camelBack - - key: readability-identifier-naming.MemberPrefix - value: '' - - key: readability-identifier-naming.MemberSuffix - value: '' - - key: readability-identifier-naming.MethodCase - value: CamelCase - - key: readability-identifier-naming.MethodPrefix - value: '' - - key: readability-identifier-naming.MethodSuffix - value: '' - - key: readability-identifier-naming.NamespaceCase - value: aNy_Case - - key: readability-identifier-naming.NamespacePrefix - value: '' - - key: readability-identifier-naming.NamespaceSuffix - value: '' - - key: readability-identifier-naming.ParameterCase - value: camelBack - - key: readability-identifier-naming.ParameterPackCase - value: camelBack - - key: readability-identifier-naming.ParameterPackPrefix - value: '' - - key: readability-identifier-naming.ParameterPackSuffix - value: '' - - key: readability-identifier-naming.ParameterPrefix - value: '' - - key: readability-identifier-naming.ParameterSuffix - value: '' - - key: readability-identifier-naming.PrivateMemberCase - value: camelBack - - key: readability-identifier-naming.PrivateMemberPrefix - value: '' - - key: readability-identifier-naming.PrivateMemberSuffix - value: '' - - key: readability-identifier-naming.PrivateMethodCase - value: CamelCase - - key: readability-identifier-naming.PrivateMethodPrefix - value: '' - - key: readability-identifier-naming.PrivateMethodSuffix - value: '' - - key: readability-identifier-naming.ProtectedMemberCase - value: camelBack - - key: readability-identifier-naming.ProtectedMemberPrefix - value: '' - - key: readability-identifier-naming.ProtectedMemberSuffix - value: '' - - key: readability-identifier-naming.ProtectedMethodCase - value: CamelCase - - key: readability-identifier-naming.ProtectedMethodPrefix - value: '' - - key: readability-identifier-naming.ProtectedMethodSuffix - value: '' - - key: readability-identifier-naming.PublicMemberCase - value: camelBack - - key: readability-identifier-naming.PublicMemberPrefix - value: '' - - key: readability-identifier-naming.PublicMemberSuffix - value: '' - - key: readability-identifier-naming.PublicMethodCase - value: CamelCase - - key: readability-identifier-naming.PublicMethodPrefix - value: '' - - key: readability-identifier-naming.PublicMethodSuffix - value: '' - - key: readability-identifier-naming.StaticConstantCase - value: CamelCase - - key: readability-identifier-naming.StaticConstantPrefix - value: '' - - key: readability-identifier-naming.StaticConstantSuffix - value: '' - - key: readability-identifier-naming.StaticVariableCase - value: camelBack - - key: readability-identifier-naming.StaticVariablePrefix - value: '' - - key: readability-identifier-naming.StaticVariableSuffix - value: '' - - key: readability-identifier-naming.StructCase - value: CamelCase - - key: readability-identifier-naming.StructPrefix - value: '' - - key: readability-identifier-naming.StructSuffix - value: '' - - key: readability-identifier-naming.TemplateParameterCase - value: CamelCase - - key: readability-identifier-naming.TemplateParameterPrefix - value: '' - - key: readability-identifier-naming.TemplateParameterSuffix - value: '' - - key: readability-identifier-naming.TemplateTemplateParameterCase - value: CamelCase - - key: readability-identifier-naming.TemplateTemplateParameterPrefix - value: '' - - key: readability-identifier-naming.TemplateTemplateParameterSuffix - value: '' - - key: readability-identifier-naming.TypeTemplateParameterCase - value: CamelCase - - key: readability-identifier-naming.TypeTemplateParameterPrefix - value: '' - - key: readability-identifier-naming.TypeTemplateParameterSuffix - value: '' - - key: readability-identifier-naming.TypedefCase - value: CamelCase - - key: readability-identifier-naming.TypedefPrefix - value: '' - - key: readability-identifier-naming.TypedefSuffix - value: '' - - key: readability-identifier-naming.UnionCase - value: CamelCase - - key: readability-identifier-naming.UnionPrefix - value: '' - - key: readability-identifier-naming.UnionSuffix - value: '' - - key: readability-identifier-naming.ValueTemplateParameterCase - value: camelBack - - key: readability-identifier-naming.ValueTemplateParameterPrefix - value: '' - - key: readability-identifier-naming.ValueTemplateParameterSuffix - value: '' - - key: readability-identifier-naming.VariableCase - value: camelBack - - key: readability-identifier-naming.VariablePrefix - value: '' - - key: readability-identifier-naming.VariableSuffix - value: '' - - key: readability-identifier-naming.VirtualMethodCase - value: CamelCase - - key: readability-identifier-naming.VirtualMethodPrefix - value: '' - - key: readability-identifier-naming.VirtualMethodSuffix - value: '' - - key: readability-simplify-boolean-expr.ChainedConditionalAssignment - value: '1' - - key: readability-simplify-boolean-expr.ChainedConditionalReturn - value: '1' -... + - key: cppcoreguidelines-pro-bounds-constant-array-index.GslHeader + value: '' + - key: cppcoreguidelines-pro-bounds-constant-array-index.IncludeStyle + value: '0' + - key: cppcoreguidelines-macro-usage.AllowedRegexp + value: ^MS_* + - key: misc-assert-side-effect.AssertMacros + value: assert + - key: misc-assert-side-effect.CheckFunctionCalls + value: '0' + - key: misc-throw-by-value-catch-by-reference.CheckThrowTemporaries + value: '1' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: readability-braces-around-statements.ShortStatementLines + value: '3' + - key: readability-function-size.BranchThreshold + value: '4294967295' + - key: readability-function-size.LineThreshold + value: '4294967295' + - key: readability-function-size.StatementThreshold + value: '800' + - key: readability-identifier-naming.AbstractClassCase + value: CamelCase + - key: readability-identifier-naming.AbstractClassPrefix + value: '' + - key: readability-identifier-naming.AbstractClassSuffix + value: '' + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.ClassConstantCase + value: CamelCase + - key: readability-identifier-naming.ClassConstantPrefix + value: '' + - key: readability-identifier-naming.ClassConstantSuffix + value: '' + - key: readability-identifier-naming.ClassMemberCase + value: camelBack + - key: readability-identifier-naming.ClassMemberPrefix + value: '' + - key: readability-identifier-naming.ClassMemberSuffix + value: '' + - key: readability-identifier-naming.ClassMethodCase + value: CamelCase + - key: readability-identifier-naming.ClassMethodPrefix + value: '' + - key: readability-identifier-naming.ClassMethodSuffix + value: '' + - key: readability-identifier-naming.ClassPrefix + value: '' + - key: readability-identifier-naming.ClassSuffix + value: '' + - key: readability-identifier-naming.ConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.ConstantMemberCase + value: camelBack + - key: readability-identifier-naming.ConstantMemberPrefix + value: '' + - key: readability-identifier-naming.ConstantMemberSuffix + value: '' + - key: readability-identifier-naming.ConstantParameterCase + value: camelBack + - key: readability-identifier-naming.ConstantParameterPrefix + value: '' + - key: readability-identifier-naming.ConstantParameterSuffix + value: '' + - key: readability-identifier-naming.ConstantPrefix + value: '' + - key: readability-identifier-naming.ConstantSuffix + value: '' + - key: readability-identifier-naming.ConstexprFunctionCase + value: camelBack + - key: readability-identifier-naming.ConstexprFunctionPrefix + value: '' + - key: readability-identifier-naming.ConstexprFunctionSuffix + value: '' + - key: readability-identifier-naming.ConstexprMethodCase + value: camelBack + - key: readability-identifier-naming.ConstexprMethodPrefix + value: '' + - key: readability-identifier-naming.ConstexprMethodSuffix + value: '' + - key: readability-identifier-naming.ConstexprVariableCase + value: CamelCase + - key: readability-identifier-naming.ConstexprVariablePrefix + value: '' + - key: readability-identifier-naming.ConstexprVariableSuffix + value: '' + - key: readability-identifier-naming.EnumCase + value: aNy_CasE + - key: readability-identifier-naming.EnumConstantCase + value: aNy_CasE + - key: readability-identifier-naming.EnumConstantPrefix + value: '' + - key: readability-identifier-naming.EnumConstantSuffix + value: '' + - key: readability-identifier-naming.EnumPrefix + value: '' + - key: readability-identifier-naming.EnumSuffix + value: '' + - key: readability-identifier-naming.FunctionCase + value: camelBack + - key: readability-identifier-naming.FunctionPrefix + value: '' + - key: readability-identifier-naming.FunctionSuffix + value: '' + - key: readability-identifier-naming.GlobalConstantCase + value: CamelCase + - key: readability-identifier-naming.GlobalConstantPrefix + value: '' + - key: readability-identifier-naming.GlobalConstantSuffix + value: '' + - key: readability-identifier-naming.GlobalFunctionCase + value: CamelCase + - key: readability-identifier-naming.GlobalFunctionPrefix + value: '' + - key: readability-identifier-naming.GlobalFunctionSuffix + value: '' + - key: readability-identifier-naming.GlobalVariableCase + value: CamelCase + - key: readability-identifier-naming.GlobalVariablePrefix + value: '' + - key: readability-identifier-naming.GlobalVariableSuffix + value: '' + - key: readability-identifier-naming.IgnoreFailedSplit + value: '0' + - key: readability-identifier-naming.InlineNamespaceCase + value: aNy_CasE + - key: readability-identifier-naming.InlineNamespacePrefix + value: '' + - key: readability-identifier-naming.InlineNamespaceSuffix + value: '' + - key: readability-identifier-naming.LocalConstantCase + value: camelBack + - key: readability-identifier-naming.LocalConstantPrefix + value: '' + - key: readability-identifier-naming.LocalConstantSuffix + value: '' + - key: readability-identifier-naming.LocalVariableCase + value: camelBack + - key: readability-identifier-naming.LocalVariablePrefix + value: '' + - key: readability-identifier-naming.LocalVariableSuffix + value: '' + - key: readability-identifier-naming.MemberCase + value: camelBack + - key: readability-identifier-naming.MemberPrefix + value: '' + - key: readability-identifier-naming.MemberSuffix + value: '' + - key: readability-identifier-naming.MethodCase + value: CamelCase + - key: readability-identifier-naming.MethodPrefix + value: '' + - key: readability-identifier-naming.MethodSuffix + value: '' + - key: readability-identifier-naming.NamespaceCase + value: aNy_CasE + - key: readability-identifier-naming.NamespacePrefix + value: '' + - key: readability-identifier-naming.NamespaceSuffix + value: '' + - key: readability-identifier-naming.ParameterCase + value: camelBack + - key: readability-identifier-naming.ParameterPackCase + value: camelBack + - key: readability-identifier-naming.ParameterPackPrefix + value: '' + - key: readability-identifier-naming.ParameterPackSuffix + value: '' + - key: readability-identifier-naming.ParameterPrefix + value: '' + - key: readability-identifier-naming.ParameterSuffix + value: '' + - key: readability-identifier-naming.PrivateMemberCase + value: camelBack + - key: readability-identifier-naming.PrivateMemberPrefix + value: '' + - key: readability-identifier-naming.PrivateMemberSuffix + value: '' + - key: readability-identifier-naming.PrivateMethodCase + value: CamelCase + - key: readability-identifier-naming.PrivateMethodPrefix + value: '' + - key: readability-identifier-naming.PrivateMethodSuffix + value: '' + - key: readability-identifier-naming.ProtectedMemberCase + value: camelBack + - key: readability-identifier-naming.ProtectedMemberPrefix + value: '' + - key: readability-identifier-naming.ProtectedMemberSuffix + value: '' + - key: readability-identifier-naming.ProtectedMethodCase + value: CamelCase + - key: readability-identifier-naming.ProtectedMethodPrefix + value: '' + - key: readability-identifier-naming.ProtectedMethodSuffix + value: '' + - key: readability-identifier-naming.PublicMemberCase + value: camelBack + - key: readability-identifier-naming.PublicMemberPrefix + value: '' + - key: readability-identifier-naming.PublicMemberSuffix + value: '' + - key: readability-identifier-naming.PublicMethodCase + value: CamelCase + - key: readability-identifier-naming.PublicMethodPrefix + value: '' + - key: readability-identifier-naming.PublicMethodSuffix + value: '' + - key: readability-identifier-naming.StaticConstantCase + value: CamelCase + - key: readability-identifier-naming.StaticConstantPrefix + value: '' + - key: readability-identifier-naming.StaticConstantSuffix + value: '' + - key: readability-identifier-naming.StaticVariableCase + value: camelBack + - key: readability-identifier-naming.StaticVariablePrefix + value: '' + - key: readability-identifier-naming.StaticVariableSuffix + value: '' + - key: readability-identifier-naming.StructCase + value: CamelCase + - key: readability-identifier-naming.StructPrefix + value: '' + - key: readability-identifier-naming.StructSuffix + value: '' + - key: readability-identifier-naming.TemplateParameterCase + value: CamelCase + - key: readability-identifier-naming.TemplateParameterPrefix + value: '' + - key: readability-identifier-naming.TemplateParameterSuffix + value: '' + - key: readability-identifier-naming.TemplateTemplateParameterCase + value: CamelCase + - key: readability-identifier-naming.TemplateTemplateParameterPrefix + value: '' + - key: readability-identifier-naming.TemplateTemplateParameterSuffix + value: '' + - key: readability-identifier-naming.TypeTemplateParameterCase + value: CamelCase + - key: readability-identifier-naming.TypeTemplateParameterPrefix + value: '' + - key: readability-identifier-naming.TypeTemplateParameterSuffix + value: '' + - key: readability-identifier-naming.TypedefCase + value: CamelCase + - key: readability-identifier-naming.TypedefPrefix + value: '' + - key: readability-identifier-naming.TypedefSuffix + value: '' + - key: readability-identifier-naming.UnionCase + value: CamelCase + - key: readability-identifier-naming.UnionPrefix + value: '' + - key: readability-identifier-naming.UnionSuffix + value: '' + - key: readability-identifier-naming.ValueTemplateParameterCase + value: CamelCase + - key: readability-identifier-naming.ValueTemplateParameterPrefix + value: '' + - key: readability-identifier-naming.ValueTemplateParameterSuffix + value: '' + - key: readability-identifier-naming.VariableCase + value: camelBack + - key: readability-identifier-naming.VariablePrefix + value: '' + - key: readability-identifier-naming.VariableSuffix + value: '' + - key: readability-identifier-naming.VirtualMethodCase + value: CamelCase + - key: readability-identifier-naming.VirtualMethodPrefix + value: '' + - key: readability-identifier-naming.VirtualMethodSuffix + value: '' + - key: readability-simplify-boolean-expr.ChainedConditionalAssignment + value: '1' + - key: readability-simplify-boolean-expr.ChainedConditionalReturn + value: '1' diff --git a/worker/Cargo.toml b/worker/Cargo.toml index 1acefdea2e..d95f715da2 100644 --- a/worker/Cargo.toml +++ b/worker/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mediasoup-sys" -version = "0.5.3" +version = "0.9.1" description = "FFI bindings to C++ libmediasoup-worker" authors = ["Nazar Mokrynskyi "] edition = "2021" @@ -9,6 +9,7 @@ documentation = "https://docs.rs/mediasoup-sys" repository = "https://github.com/versatica/mediasoup/tree/v3/worker" include = [ "/deps/libwebrtc", + "/fbs", "/fuzzer/include", "/fuzzer/src", "/include", @@ -20,11 +21,22 @@ include = [ "/test/src", "/build.rs", "/Cargo.toml", - "/Makefile", "/meson.build", "/meson_options.txt", + "/tasks.py" ] [package.metadata.docs.rs] default-target = "x86_64-unknown-linux-gnu" targets = [] + +[build-dependencies] +planus-codegen = "0.4.0" +planus-translation = "0.4.0" + +[dependencies] +planus = "0.4.0" + +[dependencies.serde] +features = ["derive"] +version = "1.0.190" diff --git a/worker/Dockerfile b/worker/Dockerfile index 574be2d7b2..3a0dd68de8 100644 --- a/worker/Dockerfile +++ b/worker/Dockerfile @@ -1,25 +1,38 @@ FROM ubuntu:22.04 # Install dependencies. -RUN \ - set -x \ +RUN set -x \ && apt-get update \ && apt-get install --yes \ - bash-completion wget curl subversion screen gcc g++ cmake ninja-build golang \ - autoconf libtool apache2 python3-pip python3-dev pkg-config zlib1g-dev \ - libgss-dev libssl-dev libxml2-dev nasm libarchive-dev make automake \ - libdbus-1-dev libboost-dev autoconf-archive bash-completion python3-yaml \ - clang + gcc g++ clang pkg-config bash-completion wget curl \ + screen python3-pip python3-yaml pkg-config zlib1g-dev \ + libgss-dev libssl-dev libxml2-dev gdb -# Install node 16. -RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - -RUN apt-get install -y nodejs +# Install node 20. +RUN set -x \ + && apt-get update \ + && apt-get install --yes ca-certificates curl gnupg \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ + | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && NODE_MAJOR=20 \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" \ + > /etc/apt/sources.list.d/nodesource.list \ + && apt-get update \ + && apt-get install nodejs --yes + +# Enable core dumps. +RUN set -x \ + && echo "mkdir -p /tmp/cores && chmod 777 /tmp/cores && echo \"/tmp/cores/core.%e.sig%s.%p\" > /proc/sys/kernel/core_pattern && ulimit -c unlimited" >> ~/.bashrc # Make CC and CXX point to clang/clang++ installed above. ENV LANG="C.UTF-8" ENV CC="clang" ENV CXX="clang++" +ENV MEDIASOUP_LOCAL_DEV="true" +ENV KEEP_BUILD_ARTIFACTS="1" + WORKDIR /mediasoup CMD ["bash"] diff --git a/worker/Dockerfile.alpine b/worker/Dockerfile.alpine new file mode 100644 index 0000000000..68cf11664d --- /dev/null +++ b/worker/Dockerfile.alpine @@ -0,0 +1,17 @@ +FROM alpine + +# Install dependencies. +RUN set -x \ + && apk add gcc g++ nodejs-current npm python3 py3-pip linux-headers + +# Make CC and CXX point to gcc/g++. +ENV LANG="C.UTF-8" +ENV CC="gcc" +ENV CXX="g++" + +ENV MEDIASOUP_LOCAL_DEV="true" +ENV KEEP_BUILD_ARTIFACTS="1" + +WORKDIR /mediasoup + +CMD ["ash"] diff --git a/worker/Makefile b/worker/Makefile index 6329667836..1f27d6e65f 100644 --- a/worker/Makefile +++ b/worker/Makefile @@ -1,72 +1,22 @@ # # make tasks for mediasoup-worker. # - -# We need Python 3 here. -PYTHON ?= $(shell command -v python3 2> /dev/null || echo python) -ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) -CORES ?= $(shell ${ROOT_DIR}/scripts/cpu_cores.sh || echo 4) -MEDIASOUP_OUT_DIR ?= $(shell pwd)/out -# Controls build types, `Release` and `Debug` are presets optimized for those -# use cases. Other build types are possible too, but they are not presets and -# will require `MESON_ARGS` use to customize build configuration. -# Check the meaning of useful macros in the `worker/include/Logger.hpp` header -# file if you want to enable tracing or other debug information. -MEDIASOUP_BUILDTYPE ?= Release -GULP = ./scripts/node_modules/.bin/gulp -LCOV = ./deps/lcov/bin/lcov -DOCKER ?= docker -PIP_DIR = $(MEDIASOUP_OUT_DIR)/pip -INSTALL_DIR ?= $(MEDIASOUP_OUT_DIR)/$(MEDIASOUP_BUILDTYPE) -BUILD_DIR ?= $(MEDIASOUP_OUT_DIR)/$(MEDIASOUP_BUILDTYPE)/build -MESON ?= $(PIP_DIR)/bin/meson -MESON_VERSION ?= 0.61.5 -# `MESON_ARGS` can be used to provide extra configuration parameters to Meson, -# such as adding defines or changing optimization options. For instance, use -# `MESON_ARGS="-Dms_log_trace=true -Dms_log_file_line=true" npm i` to compile -# worker with tracing and enabled. +# NOTE: This Makefile is a proxy to pip invoke commands (see tasks.py). # -# NOTE: On Windows make sure to add `--vsenv` or have MSVS environment already -# active if you override this parameter. -MESON_ARGS ?= "" -# Workaround for NixOS and Guix that don't work with pre-built binaries, see: -# https://github.com/NixOS/nixpkgs/issues/142383. -PIP_BUILD_BINARIES = $(shell [ -f /etc/NIXOS -o -d /etc/guix ] && echo "--no-binary :all:") -# Let's use a specific version of ninja to avoid buggy version 1.11.1: -# https://mediasoup.discourse.group/t/partly-solved-could-not-detect-ninja-v1-8-2-or-newer/ -# https://github.com/ninja-build/ninja/issues/2211 -# https://github.com/ninja-build/ninja/issues/2212 -NINJA_VERSION ?= 1.10.2.4 -# Disable `*.pyc` files creation. -export PYTHONDONTWRITEBYTECODE = 1 -# Instruct `meson` where to look for ninja binary. -ifeq ($(OS),Windows_NT) - # Windows is, of course, special. - export NINJA = $(PIP_DIR)/bin/ninja.exe -else - export NINJA = $(PIP_DIR)/bin/ninja -endif +PYTHON ?= $(shell command -v python3 2> /dev/null || echo python) +PIP_INVOKE_DIR = $(shell pwd)/pip_invoke -# Instruct Python where to look for modules it needs, such that `meson` actually -# runs from installed location. -# -# NOTE: For some reason on Windows adding `:${PYTHONPATH}` breaks things. +# Instruct Python where to look for invoke module. ifeq ($(OS),Windows_NT) - export PYTHONPATH := $(PIP_DIR) + export PYTHONPATH := $(PIP_INVOKE_DIR);${PYTHONPATH} else - export PYTHONPATH := $(PIP_DIR):${PYTHONPATH} -endif - -# Activate VS environment on Windows by default. -ifeq ($(OS),Windows_NT) -ifeq ($(MESON_ARGS),"") - MESON_ARGS = $(subst $\",,"--vsenv") -endif + export PYTHONPATH := $(PIP_INVOKE_DIR):${PYTHONPATH} endif .PHONY: \ default \ + invoke \ meson-ninja \ setup \ clean \ @@ -77,182 +27,103 @@ endif update-wrap-file \ mediasoup-worker \ libmediasoup-worker \ + flatc \ xcode \ lint \ format \ test \ + test-asan-address \ + test-asan-undefined \ + test-asan-thread \ tidy \ fuzzer \ fuzzer-run-all \ docker \ - docker-run + docker-run \ + docker-alpine \ + docker-alpine-run default: mediasoup-worker -meson-ninja: -ifeq ($(wildcard $(PIP_DIR)),) - # Updated pip and setuptools are needed for meson. - # `--system` is not present everywhere and is only needed as workaround for - # Debian-specific issue (copied from https://github.com/gluster/gstatus/pull/33), - # fallback to command without `--system` if the first one fails. - $(PYTHON) -m pip install --system --target=$(PIP_DIR) pip setuptools || \ - $(PYTHON) -m pip install --target=$(PIP_DIR) pip setuptools || \ - echo "Installation failed, likely because PIP is unavailable, if you are on Debian/Ubuntu or derivative please install the python3-pip package" - # Install `meson` and `ninja` using `pip` into custom location, so we don't - # depend on system-wide installation. - $(PYTHON) -m pip install --upgrade --target=$(PIP_DIR) $(PIP_BUILD_BINARIES) meson==$(MESON_VERSION) ninja==$(NINJA_VERSION) +invoke: +ifeq ($(wildcard $(PIP_INVOKE_DIR)),) + # Install pip invoke into custom location, so we don't depend on system-wide + # installation. + "$(PYTHON)" -m pip install --upgrade --no-user --target "$(PIP_INVOKE_DIR)" invoke endif -setup: meson-ninja -# We try to call `--reconfigure` first as a workaround for this issue: -# https://github.com/ninja-build/ninja/issues/1997 -ifeq ($(MEDIASOUP_BUILDTYPE),Release) - $(MESON) setup \ - --prefix $(INSTALL_DIR) \ - --bindir '' \ - --libdir '' \ - --buildtype release \ - -Db_ndebug=true \ - -Db_pie=true \ - -Db_staticpic=true \ - --reconfigure \ - $(MESON_ARGS) \ - $(BUILD_DIR) || \ - $(MESON) setup \ - --prefix $(INSTALL_DIR) \ - --bindir '' \ - --libdir '' \ - --buildtype release \ - -Db_ndebug=true \ - -Db_pie=true \ - -Db_staticpic=true \ - $(MESON_ARGS) \ - $(BUILD_DIR) -else -ifeq ($(MEDIASOUP_BUILDTYPE),Debug) - $(MESON) setup \ - --prefix $(INSTALL_DIR) \ - --bindir '' \ - --libdir '' \ - --buildtype debug \ - -Db_pie=true \ - -Db_staticpic=true \ - --reconfigure \ - $(MESON_ARGS) \ - $(BUILD_DIR) || \ - $(MESON) setup \ - --prefix $(INSTALL_DIR) \ - --bindir '' \ - --libdir '' \ - --buildtype debug \ - -Db_pie=true \ - -Db_staticpic=true \ - $(MESON_ARGS) \ - $(BUILD_DIR) -else - $(MESON) setup \ - --prefix $(INSTALL_DIR) \ - --bindir '' \ - --libdir '' \ - --buildtype $(MEDIASOUP_BUILDTYPE) \ - -Db_ndebug=if-release \ - -Db_pie=true \ - -Db_staticpic=true \ - --reconfigure \ - $(MESON_ARGS) \ - $(BUILD_DIR) || \ - $(MESON) setup \ - --prefix $(INSTALL_DIR) \ - --bindir '' \ - --libdir '' \ - --buildtype $(MEDIASOUP_BUILDTYPE) \ - -Db_ndebug=if-release \ - -Db_pie=true \ - -Db_staticpic=true \ - $(MESON_ARGS) \ - $(BUILD_DIR) -endif -endif +meson-ninja: invoke + "$(PYTHON)" -m invoke meson-ninja -clean: - $(RM) -rf $(INSTALL_DIR) +setup: invoke + "$(PYTHON)" -m invoke setup -clean-build: - $(RM) -rf $(BUILD_DIR) +clean: invoke + "$(PYTHON)" -m invoke clean -clean-pip: - $(RM) -rf $(PIP_DIR) +clean-build: invoke + "$(PYTHON)" -m invoke clean-build -clean-subprojects: meson-ninja - $(MESON) subprojects purge --include-cache --confirm +clean-pip: invoke + "$(PYTHON)" -m invoke clean-pip -clean-all: clean-subprojects - $(RM) -rf $(MEDIASOUP_OUT_DIR) +clean-subprojects: invoke + "$(PYTHON)" -m invoke clean-subprojects -# Update the wrap file of a subproject. Usage example: -# make update-wrap-file SUBPROJECT=openssl -update-wrap-file: meson-ninja - $(MESON) subprojects update --reset $(SUBPROJECT) +clean-all: invoke + "$(PYTHON)" -m invoke clean-all -mediasoup-worker: setup -ifeq ($(MEDIASOUP_WORKER_BIN),) - $(MESON) compile -C $(BUILD_DIR) -j $(CORES) mediasoup-worker - $(MESON) install -C $(BUILD_DIR) --no-rebuild --tags mediasoup-worker -endif +# It requires the SUBPROJECT environment variable. +update-wrap-file: invoke + "$(PYTHON)" -m invoke subprojects $(SUBPROJECT) -libmediasoup-worker: setup - $(MESON) compile -C $(BUILD_DIR) -j $(CORES) libmediasoup-worker - $(MESON) install -C $(BUILD_DIR) --no-rebuild --tags libmediasoup-worker +mediasoup-worker: invoke + "$(PYTHON)" -m invoke mediasoup-worker -xcode: setup - $(MESON) setup --buildtype debug --backend xcode $(MEDIASOUP_OUT_DIR)/xcode +libmediasoup-worker: invoke + "$(PYTHON)" -m invoke libmediasoup-worker -lint: - $(GULP) --gulpfile ./scripts/gulpfile.js lint:worker +flatc: invoke + "$(PYTHON)" -m invoke flatc -format: - $(GULP) --gulpfile ./scripts/gulpfile.js format:worker +xcode: invoke + "$(PYTHON)" -m invoke xcode -test: setup - $(MESON) compile -C $(BUILD_DIR) -j $(CORES) mediasoup-worker-test - $(MESON) install -C $(BUILD_DIR) --no-rebuild --tags mediasoup-worker-test - # On Windows lcov doesn't work (at least not yet) and we need to add `.exe` to - # the binary path. -ifeq ($(OS),Windows_NT) - $(BUILD_DIR)/mediasoup-worker-test.exe --invisibles --use-colour=yes $(MEDIASOUP_TEST_TAGS) -else - $(LCOV) --directory ./ --zerocounters - $(BUILD_DIR)/mediasoup-worker-test --invisibles --use-colour=yes $(MEDIASOUP_TEST_TAGS) -endif +lint: invoke + "$(PYTHON)" -m invoke lint -tidy: - $(PYTHON) ./scripts/clang-tidy.py \ - -clang-tidy-binary=./scripts/node_modules/.bin/clang-tidy \ - -clang-apply-replacements-binary=./scripts/node_modules/.bin/clang-apply-replacements \ - -header-filter='(Channel/**/*.hpp|DepLibSRTP.hpp|DepLibUV.hpp|DepLibWebRTC.hpp|DepOpenSSL.hpp|DepUsrSCTP.hpp|LogLevel.hpp|Logger.hpp|MediaSoupError.hpp|RTC/**/*.hpp|Settings.hpp|Utils.hpp|Worker.hpp|common.hpp|handles/**/*.hpp)' \ - -p=$(BUILD_DIR) \ - -j=$(CORES) \ - -checks=$(MEDIASOUP_TIDY_CHECKS) \ - -quiet +format: invoke + "$(PYTHON)" -m invoke format -fuzzer: setup - $(MESON) compile -C $(BUILD_DIR) -j $(CORES) mediasoup-worker-fuzzer - $(MESON) install -C $(BUILD_DIR) --no-rebuild --tags mediasoup-worker-fuzzer +test: invoke + "$(PYTHON)" -m invoke test -fuzzer-run-all: - LSAN_OPTIONS=verbosity=1:log_threads=1 $(BUILD_DIR)/mediasoup-worker-fuzzer -artifact_prefix=fuzzer/reports/ -max_len=1400 fuzzer/new-corpus deps/webrtc-fuzzer-corpora/corpora/stun-corpus deps/webrtc-fuzzer-corpora/corpora/rtp-corpus deps/webrtc-fuzzer-corpora/corpora/rtcp-corpus +test-asan-address: invoke + "$(PYTHON)" -m invoke test-asan-address -docker: -ifeq ($(DOCKER_NO_CACHE),true) - $(DOCKER) build -f Dockerfile --no-cache --tag mediasoup/docker:latest . -else - $(DOCKER) build -f Dockerfile --tag mediasoup/docker:latest . -endif +test-asan-undefined: invoke + "$(PYTHON)" -m invoke test-asan-undefined + +test-asan-thread: invoke + "$(PYTHON)" -m invoke test-asan-thread + +tidy: invoke + "$(PYTHON)" -m invoke tidy + +fuzzer: invoke + "$(PYTHON)" -m invoke fuzzer + +fuzzer-run-all: invoke + "$(PYTHON)" -m invoke fuzzer-run-all + +docker: invoke + "$(PYTHON)" -m invoke docker + +docker-run: invoke + "$(PYTHON)" -m invoke docker-run + +docker-alpine: invoke + "$(PYTHON)" -m invoke docker-alpine -docker-run: - $(DOCKER) run \ - --name=mediasoupDocker -it --rm \ - --privileged \ - --cap-add SYS_PTRACE \ - -v $(shell pwd)/../:/mediasoup \ - mediasoup/docker:latest +docker-alpine-run: invoke + "$(PYTHON)" -m invoke docker-alpine-run diff --git a/worker/build.rs b/worker/build.rs index 1686acf30c..7fe5d544ea 100644 --- a/worker/build.rs +++ b/worker/build.rs @@ -1,14 +1,9 @@ -use std::env; use std::process::Command; +use std::{env, fs}; fn main() { - if env::var("DOCS_RS").is_ok() { - // Skip everything when building docs on docs.rs - return; - } - - // On Windows Rust always links against release version of MSVC runtime, thus requires Release - // build here. + // On Windows Rust always links against release version of MSVC runtime, thus requires + // Release build here let build_type = if cfg!(all(debug_assertions, not(windows))) { "Debug" } else { @@ -16,7 +11,40 @@ fn main() { }; let out_dir = env::var("OUT_DIR").unwrap(); - // Force forward slashes on Windows too so that is plays well with our dumb `Makefile` + + // Compile Rust flatbuffers + let flatbuffers_declarations = planus_translation::translate_files( + &fs::read_dir("fbs") + .expect("Failed to read `fbs` directory") + .filter_map(|maybe_entry| { + maybe_entry + .map(|entry| { + let path = entry.path(); + if path.extension() == Some("fbs".as_ref()) { + Some(path) + } else { + None + } + }) + .transpose() + }) + .collect::, _>>() + .expect("Failed to collect flatbuffers files"), + ) + .expect("Failed to translate flatbuffers files"); + + fs::write( + format!("{out_dir}/fbs.rs"), + planus_codegen::generate_rust(&flatbuffers_declarations) + .expect("Failed to generate Rust code from flatbuffers"), + ) + .expect("Failed to write generated Rust flatbuffers into fbs.rs"); + if env::var("DOCS_RS").is_ok() { + // Skip everything when building docs on docs.rs + return; + } + + // Force forward slashes on Windows too so that is plays well with our tasks.py let mediasoup_out_dir = format!("{}/out", out_dir.replace('\\', "/")); // Add C++ std lib @@ -75,18 +103,48 @@ fn main() { println!("cargo:rustc-link-lib=dylib=c++"); println!("cargo:rustc-link-lib=dylib=c++abi"); } - #[cfg(target_os = "windows")] + + // Install Python invoke package in custom folder + let pip_invoke_dir = format!("{out_dir}/pip_invoke"); + let python = env::var("PYTHON").unwrap_or("python3".to_string()); + let mut pythonpath = if let Ok(original_pythonpath) = env::var("PYTHONPATH") { + format!("{pip_invoke_dir}:{original_pythonpath}") + } else { + pip_invoke_dir.clone() + }; + + // Force ";" in PYTHONPATH on Windows + if cfg!(target_os = "windows") { + pythonpath = pythonpath.replace(':', ";"); + } + + if !Command::new(&python) + .arg("-m") + .arg("pip") + .arg("install") + .arg("--upgrade") + .arg("--target") + .arg(pip_invoke_dir) + .arg("invoke") + .spawn() + .expect("Failed to start") + .wait() + .expect("Wasn't running") + .success() { - // Nothing special is needed so far + panic!("Failed to install Python invoke package") } // Build - if !Command::new("make") + if !Command::new(&python) + .arg("-m") + .arg("invoke") .arg("libmediasoup-worker") + .env("PYTHONPATH", &pythonpath) .env("MEDIASOUP_OUT_DIR", &mediasoup_out_dir) .env("MEDIASOUP_BUILDTYPE", build_type) // Force forward slashes on Windows too, otherwise Meson thinks path is not absolute 🤷 - .env("INSTALL_DIR", &out_dir.replace('\\', "/")) + .env("MEDIASOUP_INSTALL_DIR", &out_dir.replace('\\', "/")) .spawn() .expect("Failed to start") .wait() @@ -115,6 +173,10 @@ fn main() { println!("cargo:rustc-link-lib=iphlpapi"); println!("cargo:rustc-link-lib=userenv"); println!("cargo:rustc-link-lib=ws2_32"); + println!("cargo:rustc-link-lib=dbghelp"); + println!("cargo:rustc-link-lib=ole32"); + println!("cargo:rustc-link-lib=uuid"); + println!("cargo:rustc-link-lib=shell32"); // These are required by OpenSSL on Windows println!("cargo:rustc-link-lib=ws2_32"); @@ -126,8 +188,11 @@ fn main() { if env::var("KEEP_BUILD_ARTIFACTS") != Ok("1".to_string()) { // Clean - if !Command::new("make") + if !Command::new(python) + .arg("-m") + .arg("invoke") .arg("clean-all") + .env("PYTHONPATH", &pythonpath) .env("MEDIASOUP_OUT_DIR", &mediasoup_out_dir) .spawn() .expect("Failed to start") diff --git a/worker/deps/lcov/.gitignore b/worker/deps/lcov/.gitignore deleted file mode 100644 index 51f952052d..0000000000 --- a/worker/deps/lcov/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -.version -*.gcda -*.gcno -*.info -*.tar.gz -*.rpm diff --git a/worker/deps/lcov/CONTRIBUTING b/worker/deps/lcov/CONTRIBUTING deleted file mode 100644 index 6890789bd1..0000000000 --- a/worker/deps/lcov/CONTRIBUTING +++ /dev/null @@ -1,93 +0,0 @@ -Contributing to LCOV -==================== - -Please read this document if you would like to help improving the LTP GCOV -extension (LCOV). In general, all types of contributions are welcome, for -example: - - * Fixes for code or documentation - * Performance and compatibility improvements - * Functional enhancements - -There are some rules that these contributions must follow to be acceptable for -inclusion: - - 1. The contribution must align with the project goals of LCOV. - 2. The contribution must follow a particular format. - 3. The contribution must be signed. - -Once you have made sure that your contribution follows these rules, send it via -e-mail to the LTP coverage mailing list [1]. - - -Signing your work -================= - -All contributions to LCOV must be signed by putting the following line at the -end of the explanation of a patch: - - Signed-off-by: Your Name - -By signing a patch, you certify the following: - - By making a contribution to the LTP GCOV extension (LCOV) on - http://ltp.sourceforge.net, I certify that: - - a) The contribution was created by me and I have the right to submit it - under the terms and conditions of the open source license - "GNU General Public License, version 2 or later". - (http://www.gnu.org/licenses/old-licenses/gpl-2.0.html). - - b) The contribution is made free of any other party's intellectual property - claims or rights. - - c) I understand and agree that this project and the contribution are public - and that a record of the contribution (including all personal information - I submit with it, including my sign-off) is maintained indefinitely and - may be redistributed consistent with this project or the open source - license(s) involved. - - -Project goals -============= - -The goal of LCOV is to provide a set of command line tools that can be used to -collect, process and visualize code coverage data as produced by the gcov tool -that is part of the GNU Compiler Collection (GCC) [2]. - -If you have an idea for a contribution but are unsure if it aligns with the -project goals, feel free to discuss the idea on the LTP coverage mailing -list [1]. - - -Contribution format -=================== - -To contribute a change, please create a patch using 'git format-patch'. -Alternatively you can use the diff utility with the following command line -options: - - diff -Naurp - -Please base your changes on the most current version of LCOV. You can use the -following command line to obtain this version from the lcov Git repository: - - git clone https://github.com/linux-test-project/lcov.git - -Add a meaningful description of the contribution to the top of the patch. The -description should follow this format: - - component: short description - - detailed description - - Signed-off-by: Your Name - -With your Signed-off-by, you certify the rules stated in section -"Signing your work". - - --- - -[1] ltp-coverage@lists.sourceforge.net -[2] http://gcc.gnu.org diff --git a/worker/deps/lcov/COPYING b/worker/deps/lcov/COPYING deleted file mode 100644 index d511905c16..0000000000 --- a/worker/deps/lcov/COPYING +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/worker/deps/lcov/Makefile b/worker/deps/lcov/Makefile deleted file mode 100644 index 1207cb19ad..0000000000 --- a/worker/deps/lcov/Makefile +++ /dev/null @@ -1,122 +0,0 @@ -# -# Makefile for LCOV -# -# Make targets: -# - install: install LCOV tools and man pages on the system -# - uninstall: remove tools and man pages from the system -# - dist: create files required for distribution, i.e. the lcov.tar.gz -# and the lcov.rpm file. Just make sure to adjust the VERSION -# and RELEASE variables below - both version and date strings -# will be updated in all necessary files. -# - clean: remove all generated files -# - -VERSION := $(shell bin/get_version.sh --version) -RELEASE := $(shell bin/get_version.sh --release) -FULL := $(shell bin/get_version.sh --full) - -# Set this variable during 'make install' to specify the Perl interpreter used in -# installed scripts, or leave empty to keep the current interpreter. -export LCOV_PERL_PATH := /usr/bin/perl - -PREFIX := /usr/local - -CFG_DIR := $(PREFIX)/etc -BIN_DIR := $(PREFIX)/bin -MAN_DIR := $(PREFIX)/share/man -TMP_DIR := $(shell mktemp -d) -FILES := $(wildcard bin/*) $(wildcard man/*) README Makefile \ - $(wildcard rpm/*) lcovrc - -.PHONY: all info clean install uninstall rpms test - -all: info - -info: - @echo "Available make targets:" - @echo " install : install binaries and man pages in DESTDIR (default /)" - @echo " uninstall : delete binaries and man pages from DESTDIR (default /)" - @echo " dist : create packages (RPM, tarball) ready for distribution" - @echo " test : perform self-tests" - -clean: - rm -f lcov-*.tar.gz - rm -f lcov-*.rpm - make -C example clean - make -C test -s clean - -install: - bin/install.sh bin/lcov $(DESTDIR)$(BIN_DIR)/lcov -m 755 - bin/install.sh bin/genhtml $(DESTDIR)$(BIN_DIR)/genhtml -m 755 - bin/install.sh bin/geninfo $(DESTDIR)$(BIN_DIR)/geninfo -m 755 - bin/install.sh bin/genpng $(DESTDIR)$(BIN_DIR)/genpng -m 755 - bin/install.sh bin/gendesc $(DESTDIR)$(BIN_DIR)/gendesc -m 755 - bin/install.sh man/lcov.1 $(DESTDIR)$(MAN_DIR)/man1/lcov.1 -m 644 - bin/install.sh man/genhtml.1 $(DESTDIR)$(MAN_DIR)/man1/genhtml.1 -m 644 - bin/install.sh man/geninfo.1 $(DESTDIR)$(MAN_DIR)/man1/geninfo.1 -m 644 - bin/install.sh man/genpng.1 $(DESTDIR)$(MAN_DIR)/man1/genpng.1 -m 644 - bin/install.sh man/gendesc.1 $(DESTDIR)$(MAN_DIR)/man1/gendesc.1 -m 644 - bin/install.sh man/lcovrc.5 $(DESTDIR)$(MAN_DIR)/man5/lcovrc.5 -m 644 - bin/install.sh lcovrc $(DESTDIR)$(CFG_DIR)/lcovrc -m 644 - bin/updateversion.pl $(DESTDIR)$(BIN_DIR)/lcov $(VERSION) $(RELEASE) $(FULL) - bin/updateversion.pl $(DESTDIR)$(BIN_DIR)/genhtml $(VERSION) $(RELEASE) $(FULL) - bin/updateversion.pl $(DESTDIR)$(BIN_DIR)/geninfo $(VERSION) $(RELEASE) $(FULL) - bin/updateversion.pl $(DESTDIR)$(BIN_DIR)/genpng $(VERSION) $(RELEASE) $(FULL) - bin/updateversion.pl $(DESTDIR)$(BIN_DIR)/gendesc $(VERSION) $(RELEASE) $(FULL) - bin/updateversion.pl $(DESTDIR)$(MAN_DIR)/man1/lcov.1 $(VERSION) $(RELEASE) $(FULL) - bin/updateversion.pl $(DESTDIR)$(MAN_DIR)/man1/genhtml.1 $(VERSION) $(RELEASE) $(FULL) - bin/updateversion.pl $(DESTDIR)$(MAN_DIR)/man1/geninfo.1 $(VERSION) $(RELEASE) $(FULL) - bin/updateversion.pl $(DESTDIR)$(MAN_DIR)/man1/genpng.1 $(VERSION) $(RELEASE) $(FULL) - bin/updateversion.pl $(DESTDIR)$(MAN_DIR)/man1/gendesc.1 $(VERSION) $(RELEASE) $(FULL) - bin/updateversion.pl $(DESTDIR)$(MAN_DIR)/man5/lcovrc.5 $(VERSION) $(RELEASE) $(FULL) - -uninstall: - bin/install.sh --uninstall bin/lcov $(DESTDIR)$(BIN_DIR)/lcov - bin/install.sh --uninstall bin/genhtml $(DESTDIR)$(BIN_DIR)/genhtml - bin/install.sh --uninstall bin/geninfo $(DESTDIR)$(BIN_DIR)/geninfo - bin/install.sh --uninstall bin/genpng $(DESTDIR)$(BIN_DIR)/genpng - bin/install.sh --uninstall bin/gendesc $(DESTDIR)$(BIN_DIR)/gendesc - bin/install.sh --uninstall man/lcov.1 $(DESTDIR)$(MAN_DIR)/man1/lcov.1 - bin/install.sh --uninstall man/genhtml.1 $(DESTDIR)$(MAN_DIR)/man1/genhtml.1 - bin/install.sh --uninstall man/geninfo.1 $(DESTDIR)$(MAN_DIR)/man1/geninfo.1 - bin/install.sh --uninstall man/genpng.1 $(DESTDIR)$(MAN_DIR)/man1/genpng.1 - bin/install.sh --uninstall man/gendesc.1 $(DESTDIR)$(MAN_DIR)/man1/gendesc.1 - bin/install.sh --uninstall man/lcovrc.5 $(DESTDIR)$(MAN_DIR)/man5/lcovrc.5 - bin/install.sh --uninstall lcovrc $(DESTDIR)$(CFG_DIR)/lcovrc - -dist: lcov-$(VERSION).tar.gz lcov-$(VERSION)-$(RELEASE).noarch.rpm \ - lcov-$(VERSION)-$(RELEASE).src.rpm - -lcov-$(VERSION).tar.gz: $(FILES) - mkdir $(TMP_DIR)/lcov-$(VERSION) - cp -r * $(TMP_DIR)/lcov-$(VERSION) - bin/copy_dates.sh . $(TMP_DIR)/lcov-$(VERSION) - make -C $(TMP_DIR)/lcov-$(VERSION) clean - bin/updateversion.pl $(TMP_DIR)/lcov-$(VERSION) $(VERSION) $(RELEASE) $(FULL) - bin/get_changes.sh > $(TMP_DIR)/lcov-$(VERSION)/CHANGES - cd $(TMP_DIR) ; \ - tar cfz $(TMP_DIR)/lcov-$(VERSION).tar.gz lcov-$(VERSION) - mv $(TMP_DIR)/lcov-$(VERSION).tar.gz . - rm -rf $(TMP_DIR) - -lcov-$(VERSION)-$(RELEASE).noarch.rpm: rpms -lcov-$(VERSION)-$(RELEASE).src.rpm: rpms - -rpms: lcov-$(VERSION).tar.gz - mkdir $(TMP_DIR) - mkdir $(TMP_DIR)/BUILD - mkdir $(TMP_DIR)/RPMS - mkdir $(TMP_DIR)/SOURCES - mkdir $(TMP_DIR)/SRPMS - cp lcov-$(VERSION).tar.gz $(TMP_DIR)/SOURCES - cd $(TMP_DIR)/BUILD ; \ - tar xfz $(TMP_DIR)/SOURCES/lcov-$(VERSION).tar.gz \ - lcov-$(VERSION)/rpm/lcov.spec - rpmbuild --define '_topdir $(TMP_DIR)' \ - -ba $(TMP_DIR)/BUILD/lcov-$(VERSION)/rpm/lcov.spec - mv $(TMP_DIR)/RPMS/noarch/lcov-$(VERSION)-$(RELEASE).noarch.rpm . - mv $(TMP_DIR)/SRPMS/lcov-$(VERSION)-$(RELEASE).src.rpm . - rm -rf $(TMP_DIR) - -test: - @make -C test -s all diff --git a/worker/deps/lcov/README b/worker/deps/lcov/README deleted file mode 100644 index bd3c4f2af9..0000000000 --- a/worker/deps/lcov/README +++ /dev/null @@ -1,135 +0,0 @@ -------------------------------------------------- -- README file for the LTP GCOV extension (LCOV) - -- Last changes: 2016-12-19 - -------------------------------------------------- - -Description ------------ - LCOV is an extension of GCOV, a GNU tool which provides information about - what parts of a program are actually executed (i.e. "covered") while running - a particular test case. The extension consists of a set of Perl scripts - which build on the textual GCOV output to implement the following enhanced - functionality: - - * HTML based output: coverage rates are additionally indicated using bar - graphs and specific colors. - - * Support for large projects: overview pages allow quick browsing of - coverage data by providing three levels of detail: directory view, - file view and source code view. - - LCOV was initially designed to support Linux kernel coverage measurements, - but works as well for coverage measurements on standard user space - applications. - - -Further README contents ------------------------ - 1. Included files - 2. Installing LCOV - 3. An example of how to access kernel coverage data - 4. An example of how to access coverage data for a user space program - 5. Questions and Comments - - - -1. Important files ------------------- - README - This README file - CHANGES - List of changes between releases - bin/lcov - Tool for capturing LCOV coverage data - bin/genhtml - Tool for creating HTML output from LCOV data - bin/gendesc - Tool for creating description files as used by genhtml - bin/geninfo - Internal tool (creates LCOV data files) - bin/genpng - Internal tool (creates png overviews of source files) - bin/install.sh - Internal tool (takes care of un-/installing) - man - Directory containing man pages for included tools - example - Directory containing an example to demonstrate LCOV - lcovrc - LCOV configuration file - Makefile - Makefile providing 'install' and 'uninstall' targets - - -2. Installing LCOV ------------------- -The LCOV package is available as either RPM or tarball from: - - http://ltp.sourceforge.net/coverage/lcov.php - -To install the tarball, unpack it to a directory and run: - - make install - -Use Git for the most recent (but possibly unstable) version: - - git clone https://github.com/linux-test-project/lcov.git - -Change to the resulting lcov directory and type: - - make install - - -3. An example of how to access kernel coverage data ---------------------------------------------------- -Requirements: get and install the gcov-kernel package from - - http://sourceforge.net/projects/ltp - -Copy the resulting gcov kernel module file to either the system wide modules -directory or the same directory as the Perl scripts. As root, do the following: - - a) Resetting counters - - lcov --zerocounters - - b) Capturing the current coverage state to a file - - lcov --capture --output-file kernel.info - - c) Getting HTML output - - genhtml kernel.info - -Point the web browser of your choice to the resulting index.html file. - - -4. An example of how to access coverage data for a user space program ---------------------------------------------------------------------- -Requirements: compile the program in question using GCC with the options --fprofile-arcs and -ftest-coverage. During linking, make sure to specify --lgcov or -coverage. - -Assuming the compile directory is called "appdir", do the following: - - a) Resetting counters - - lcov --directory appdir --zerocounters - - b) Capturing the current coverage state to a file - - lcov --directory appdir --capture --output-file app.info - - Note that this step only works after the application has - been started and stopped at least once. Otherwise lcov will - abort with an error mentioning that there are no data/.gcda files. - - c) Getting HTML output - - genhtml app.info - -Point the web browser of your choice to the resulting index.html file. - -Please note that independently of where the application is installed or -from which directory it is run, the --directory statement needs to -point to the directory in which the application was compiled. - -For further information on the gcc profiling mechanism, please also -consult the gcov man page. - - -5. Questions and comments -------------------------- -See the included man pages for more information on how to use the LCOV tools. - -Please email further questions or comments regarding this tool to the -LTP Mailing list at ltp-coverage@lists.sourceforge.net - diff --git a/worker/deps/lcov/bin/copy_dates.sh b/worker/deps/lcov/bin/copy_dates.sh deleted file mode 100755 index aef5f5ed36..0000000000 --- a/worker/deps/lcov/bin/copy_dates.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -# -# Usage: copy_dates.sh SOURCE TARGET -# -# For each file found in SOURCE, set the modification time of the copy of that -# file in TARGET to either the time of the latest Git commit (if SOURCE contains -# a Git repository and the file was not modified after the last commit), or the -# modification time of the original file. - -SOURCE="$1" -TARGET="$2" - -if [ -z "$SOURCE" -o -z "$TARGET" ] ; then - echo "Usage: $0 SOURCE TARGET" >&2 - exit 1 -fi - -[ -d "$SOURCE/.git" ] ; NOGIT=$? - -echo "Copying modification/commit times from $SOURCE to $TARGET" - -cd "$SOURCE" || exit 1 -find * -type f | while read FILENAME ; do - [ ! -e "$TARGET/$FILENAME" ] && continue - - # Copy modification time - touch -m "$TARGET/$FILENAME" -r "$FILENAME" - - [ $NOGIT -eq 1 ] && continue # No Git - git diff --quiet -- "$FILENAME" || continue # Modified - git diff --quiet --cached -- "$FILENAME" || continue # Modified - - # Apply modification time from Git commit time - TIME=$(git log --pretty=format:%cd -n 1 --date=iso -- "$FILENAME") - [ -n "$TIME" ] && touch -m "$TARGET/$FILENAME" --date "$TIME" -done diff --git a/worker/deps/lcov/bin/gendesc b/worker/deps/lcov/bin/gendesc deleted file mode 100755 index ea07b4e9b7..0000000000 --- a/worker/deps/lcov/bin/gendesc +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env perl -# -# Copyright (c) International Business Machines Corp., 2002 -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or (at -# your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# -# gendesc -# -# This script creates a description file as understood by genhtml. -# Input file format: -# -# For each test case: -# -# -# -# Actual description may consist of several lines. By default, output is -# written to stdout. Test names consist of alphanumeric characters -# including _ and -. -# -# -# History: -# 2002-09-02: created by Peter Oberparleiter -# - -use strict; -use warnings; -use File::Basename; -use Getopt::Long; -use Cwd qw/abs_path/; - - -# Constants -our $tool_dir = abs_path(dirname($0)); -our $lcov_version = 'LCOV version '.`$tool_dir/get_version.sh --full`; -our $lcov_url = "http://ltp.sourceforge.net/coverage/lcov.php"; -our $tool_name = basename($0); - - -# Prototypes -sub print_usage(*); -sub gen_desc(); -sub warn_handler($); -sub die_handler($); - - -# Global variables -our $help; -our $version; -our $output_filename; -our $input_filename; - - -# -# Code entry point -# - -$SIG{__WARN__} = \&warn_handler; -$SIG{__DIE__} = \&die_handler; - -# Parse command line options -if (!GetOptions("output-filename=s" => \$output_filename, - "version" =>\$version, - "help|?" => \$help - )) -{ - print(STDERR "Use $tool_name --help to get usage information\n"); - exit(1); -} - -$input_filename = $ARGV[0]; - -# Check for help option -if ($help) -{ - print_usage(*STDOUT); - exit(0); -} - -# Check for version option -if ($version) -{ - print("$tool_name: $lcov_version\n"); - exit(0); -} - - -# Check for input filename -if (!$input_filename) -{ - die("No input filename specified\n". - "Use $tool_name --help to get usage information\n"); -} - -# Do something -gen_desc(); - - -# -# print_usage(handle) -# -# Write out command line usage information to given filehandle. -# - -sub print_usage(*) -{ - local *HANDLE = $_[0]; - - print(HANDLE < -# TD: -# -# If defined, write output to OUTPUT_FILENAME, otherwise to stdout. -# -# Die on error. -# - -sub gen_desc() -{ - local *INPUT_HANDLE; - local *OUTPUT_HANDLE; - my $empty_line = "ignore"; - - open(INPUT_HANDLE, "<", $input_filename) - or die("ERROR: cannot open $input_filename!\n"); - - # Open output file for writing - if ($output_filename) - { - open(OUTPUT_HANDLE, ">", $output_filename) - or die("ERROR: cannot create $output_filename!\n"); - } - else - { - *OUTPUT_HANDLE = *STDOUT; - } - - # Process all lines in input file - while () - { - chomp($_); - - if (/^(\w[\w-]*)(\s*)$/) - { - # Matched test name - # Name starts with alphanum or _, continues with - # alphanum, _ or - - print(OUTPUT_HANDLE "TN: $1\n"); - $empty_line = "ignore"; - } - elsif (/^(\s+)(\S.*?)\s*$/) - { - # Matched test description - if ($empty_line eq "insert") - { - # Write preserved empty line - print(OUTPUT_HANDLE "TD: \n"); - } - print(OUTPUT_HANDLE "TD: $2\n"); - $empty_line = "observe"; - } - elsif (/^\s*$/) - { - # Matched empty line to preserve paragraph separation - # inside description text - if ($empty_line eq "observe") - { - $empty_line = "insert"; - } - } - } - - # Close output file if defined - if ($output_filename) - { - close(OUTPUT_HANDLE); - } - - close(INPUT_HANDLE); -} - -sub warn_handler($) -{ - my ($msg) = @_; - - warn("$tool_name: $msg"); -} - -sub die_handler($) -{ - my ($msg) = @_; - - die("$tool_name: $msg"); -} diff --git a/worker/deps/lcov/bin/genhtml b/worker/deps/lcov/bin/genhtml deleted file mode 100755 index 578c66ef6b..0000000000 --- a/worker/deps/lcov/bin/genhtml +++ /dev/null @@ -1,5978 +0,0 @@ -#!/usr/bin/env perl -# -# Copyright (c) International Business Machines Corp., 2002,2012 -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or (at -# your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# -# genhtml -# -# This script generates HTML output from .info files as created by the -# geninfo script. Call it with --help and refer to the genhtml man page -# to get information on usage and available options. -# -# -# History: -# 2002-08-23 created by Peter Oberparleiter -# IBM Lab Boeblingen -# based on code by Manoj Iyer and -# Megan Bock -# IBM Austin -# 2002-08-27 / Peter Oberparleiter: implemented frame view -# 2002-08-29 / Peter Oberparleiter: implemented test description filtering -# so that by default only descriptions for test cases which -# actually hit some source lines are kept -# 2002-09-05 / Peter Oberparleiter: implemented --no-sourceview -# 2002-09-05 / Mike Kobler: One of my source file paths includes a "+" in -# the directory name. I found that genhtml.pl died when it -# encountered it. I was able to fix the problem by modifying -# the string with the escape character before parsing it. -# 2002-10-26 / Peter Oberparleiter: implemented --num-spaces -# 2003-04-07 / Peter Oberparleiter: fixed bug which resulted in an error -# when trying to combine .info files containing data without -# a test name -# 2003-04-10 / Peter Oberparleiter: extended fix by Mike to also cover -# other special characters -# 2003-04-30 / Peter Oberparleiter: made info write to STDERR, not STDOUT -# 2003-07-10 / Peter Oberparleiter: added line checksum support -# 2004-08-09 / Peter Oberparleiter: added configuration file support -# 2005-03-04 / Cal Pierog: added legend to HTML output, fixed coloring of -# "good coverage" background -# 2006-03-18 / Marcus Boerger: added --custom-intro, --custom-outro and -# overwrite --no-prefix if --prefix is present -# 2006-03-20 / Peter Oberparleiter: changes to custom_* function (rename -# to html_prolog/_epilog, minor modifications to implementation), -# changed prefix/noprefix handling to be consistent with current -# logic -# 2006-03-20 / Peter Oberparleiter: added --html-extension option -# 2008-07-14 / Tom Zoerner: added --function-coverage command line option; -# added function table to source file page -# 2008-08-13 / Peter Oberparleiter: modified function coverage -# implementation (now enabled per default), -# introduced sorting option (enabled per default) -# - -use strict; -use warnings; -use File::Basename; -use File::Temp qw(tempfile); -use Getopt::Long; -use Digest::MD5 qw(md5_base64); -use Cwd qw/abs_path cwd/; - - -# Global constants -our $title = "LCOV - code coverage report"; -our $tool_dir = abs_path(dirname($0)); -our $lcov_version = 'LCOV version '.`$tool_dir/get_version.sh --full`; -our $lcov_url = "http://ltp.sourceforge.net/coverage/lcov.php"; -our $tool_name = basename($0); - -# Specify coverage rate default precision -our $default_precision = 1; - -# Specify coverage rate limits (in %) for classifying file entries -# HI: $hi_limit <= rate <= 100 graph color: green -# MED: $med_limit <= rate < $hi_limit graph color: orange -# LO: 0 <= rate < $med_limit graph color: red - -# For line coverage/all coverage types if not specified -our $hi_limit = 90; -our $med_limit = 75; - -# For function coverage -our $fn_hi_limit; -our $fn_med_limit; - -# For branch coverage -our $br_hi_limit; -our $br_med_limit; - -# Width of overview image -our $overview_width = 80; - -# Resolution of overview navigation: this number specifies the maximum -# difference in lines between the position a user selected from the overview -# and the position the source code window is scrolled to. -our $nav_resolution = 4; - -# Clicking a line in the overview image should show the source code view at -# a position a bit further up so that the requested line is not the first -# line in the window. This number specifies that offset in lines. -our $nav_offset = 10; - -# Clicking on a function name should show the source code at a position a -# few lines before the first line of code of that function. This number -# specifies that offset in lines. -our $func_offset = 2; - -our $overview_title = "top level"; - -# Width for line coverage information in the source code view -our $line_field_width = 12; - -# Width for branch coverage information in the source code view -our $br_field_width = 16; - -# Internal Constants - -# Header types -our $HDR_DIR = 0; -our $HDR_FILE = 1; -our $HDR_SOURCE = 2; -our $HDR_TESTDESC = 3; -our $HDR_FUNC = 4; - -# Sort types -our $SORT_FILE = 0; -our $SORT_LINE = 1; -our $SORT_FUNC = 2; -our $SORT_BRANCH = 3; - -# Fileview heading types -our $HEAD_NO_DETAIL = 1; -our $HEAD_DETAIL_HIDDEN = 2; -our $HEAD_DETAIL_SHOWN = 3; - -# Additional offsets used when converting branch coverage data to HTML -our $BR_LEN = 3; -our $BR_OPEN = 4; -our $BR_CLOSE = 5; - -# Branch data combination types -our $BR_SUB = 0; -our $BR_ADD = 1; - -# Block value used for unnamed blocks -our $UNNAMED_BLOCK = vec(pack('b*', 1 x 32), 0, 32); - -# Error classes which users may specify to ignore during processing -our $ERROR_SOURCE = 0; -our %ERROR_ID = ( - "source" => $ERROR_SOURCE, -); - -# Data related prototypes -sub print_usage(*); -sub gen_html(); -sub html_create($$); -sub process_dir($); -sub process_file($$$); -sub info(@); -sub read_info_file($); -sub get_info_entry($); -sub set_info_entry($$$$$$$$$;$$$$$$); -sub get_prefix($@); -sub shorten_prefix($); -sub get_dir_list(@); -sub get_relative_base_path($); -sub read_testfile($); -sub get_date_string(); -sub create_sub_dir($); -sub subtract_counts($$); -sub add_counts($$); -sub apply_baseline($$); -sub remove_unused_descriptions(); -sub get_found_and_hit($); -sub get_affecting_tests($$$); -sub combine_info_files($$); -sub merge_checksums($$$); -sub combine_info_entries($$$); -sub apply_prefix($@); -sub system_no_output($@); -sub read_config($); -sub apply_config($); -sub get_html_prolog($); -sub get_html_epilog($); -sub write_dir_page($$$$$$$$$$$$$$$$$); -sub classify_rate($$$$); -sub combine_brcount($$$;$); -sub get_br_found_and_hit($); -sub warn_handler($); -sub die_handler($); -sub parse_ignore_errors(@); -sub parse_dir_prefix(@); -sub rate($$;$$$); - - -# HTML related prototypes -sub escape_html($); -sub get_bar_graph_code($$$); - -sub write_png_files(); -sub write_htaccess_file(); -sub write_css_file(); -sub write_description_file($$$$$$$); -sub write_function_table(*$$$$$$$$$$); - -sub write_html(*$); -sub write_html_prolog(*$$); -sub write_html_epilog(*$;$); - -sub write_header(*$$$$$$$$$$); -sub write_header_prolog(*$); -sub write_header_line(*@); -sub write_header_epilog(*$); - -sub write_file_table(*$$$$$$$); -sub write_file_table_prolog(*$@); -sub write_file_table_entry(*$$$@); -sub write_file_table_detail_entry(*$@); -sub write_file_table_epilog(*); - -sub write_test_table_prolog(*$); -sub write_test_table_entry(*$$); -sub write_test_table_epilog(*); - -sub write_source($$$$$$$); -sub write_source_prolog(*); -sub write_source_line(*$$$$$); -sub write_source_epilog(*); - -sub write_frameset(*$$$); -sub write_overview_line(*$$$); -sub write_overview(*$$$$); - -# External prototype (defined in genpng) -sub gen_png($$$@); - - -# Global variables & initialization -our %info_data; # Hash containing all data from .info file -our @opt_dir_prefix; # Array of prefixes to remove from all sub directories -our @dir_prefix; -our %test_description; # Hash containing test descriptions if available -our $date = get_date_string(); - -our @info_filenames; # List of .info files to use as data source -our $test_title; # Title for output as written to each page header -our $output_directory; # Name of directory in which to store output -our $base_filename; # Optional name of file containing baseline data -our $desc_filename; # Name of file containing test descriptions -our $css_filename; # Optional name of external stylesheet file to use -our $quiet; # If set, suppress information messages -our $help; # Help option flag -our $version; # Version option flag -our $show_details; # If set, generate detailed directory view -our $no_prefix; # If set, do not remove filename prefix -our $func_coverage; # If set, generate function coverage statistics -our $no_func_coverage; # Disable func_coverage -our $br_coverage; # If set, generate branch coverage statistics -our $no_br_coverage; # Disable br_coverage -our $sort = 1; # If set, provide directory listings with sorted entries -our $no_sort; # Disable sort -our $frames; # If set, use frames for source code view -our $keep_descriptions; # If set, do not remove unused test case descriptions -our $no_sourceview; # If set, do not create a source code view for each file -our $highlight; # If set, highlight lines covered by converted data only -our $legend; # If set, include legend in output -our $tab_size = 8; # Number of spaces to use in place of tab -our $config; # Configuration file contents -our $html_prolog_file; # Custom HTML prolog file (up to and including ) -our $html_epilog_file; # Custom HTML epilog file (from onwards) -our $html_prolog; # Actual HTML prolog -our $html_epilog; # Actual HTML epilog -our $html_ext = "html"; # Extension for generated HTML files -our $html_gzip = 0; # Compress with gzip -our $demangle_cpp = 0; # Demangle C++ function names -our @opt_ignore_errors; # Ignore certain error classes during processing -our @ignore; -our $opt_config_file; # User-specified configuration file location -our %opt_rc; -our $opt_missed; # List/sort lines by missed counts -our $charset = "UTF-8"; # Default charset for HTML pages -our @fileview_sortlist; -our @fileview_sortname = ("", "-sort-l", "-sort-f", "-sort-b"); -our @funcview_sortlist; -our @rate_name = ("Lo", "Med", "Hi"); -our @rate_png = ("ruby.png", "amber.png", "emerald.png"); -our $lcov_func_coverage = 1; -our $lcov_branch_coverage = 0; -our $rc_desc_html = 0; # lcovrc: genhtml_desc_html - -our $cwd = cwd(); # Current working directory - - -# -# Code entry point -# - -$SIG{__WARN__} = \&warn_handler; -$SIG{__DIE__} = \&die_handler; - -# Check command line for a configuration file name -Getopt::Long::Configure("pass_through", "no_auto_abbrev"); -GetOptions("config-file=s" => \$opt_config_file, - "rc=s%" => \%opt_rc); -Getopt::Long::Configure("default"); - -{ - # Remove spaces around rc options - my %new_opt_rc; - - while (my ($key, $value) = each(%opt_rc)) { - $key =~ s/^\s+|\s+$//g; - $value =~ s/^\s+|\s+$//g; - - $new_opt_rc{$key} = $value; - } - %opt_rc = %new_opt_rc; -} - -# Read configuration file if available -if (defined($opt_config_file)) { - $config = read_config($opt_config_file); -} elsif (defined($ENV{"HOME"}) && (-r $ENV{"HOME"}."/.lcovrc")) -{ - $config = read_config($ENV{"HOME"}."/.lcovrc"); -} -elsif (-r "/etc/lcovrc") -{ - $config = read_config("/etc/lcovrc"); -} elsif (-r "/usr/local/etc/lcovrc") -{ - $config = read_config("/usr/local/etc/lcovrc"); -} - -if ($config || %opt_rc) -{ - # Copy configuration file and --rc values to variables - apply_config({ - "genhtml_css_file" => \$css_filename, - "genhtml_hi_limit" => \$hi_limit, - "genhtml_med_limit" => \$med_limit, - "genhtml_line_field_width" => \$line_field_width, - "genhtml_overview_width" => \$overview_width, - "genhtml_nav_resolution" => \$nav_resolution, - "genhtml_nav_offset" => \$nav_offset, - "genhtml_keep_descriptions" => \$keep_descriptions, - "genhtml_no_prefix" => \$no_prefix, - "genhtml_no_source" => \$no_sourceview, - "genhtml_num_spaces" => \$tab_size, - "genhtml_highlight" => \$highlight, - "genhtml_legend" => \$legend, - "genhtml_html_prolog" => \$html_prolog_file, - "genhtml_html_epilog" => \$html_epilog_file, - "genhtml_html_extension" => \$html_ext, - "genhtml_html_gzip" => \$html_gzip, - "genhtml_precision" => \$default_precision, - "genhtml_function_hi_limit" => \$fn_hi_limit, - "genhtml_function_med_limit" => \$fn_med_limit, - "genhtml_function_coverage" => \$func_coverage, - "genhtml_branch_hi_limit" => \$br_hi_limit, - "genhtml_branch_med_limit" => \$br_med_limit, - "genhtml_branch_coverage" => \$br_coverage, - "genhtml_branch_field_width" => \$br_field_width, - "genhtml_sort" => \$sort, - "genhtml_charset" => \$charset, - "genhtml_desc_html" => \$rc_desc_html, - "genhtml_demangle_cpp" => \$demangle_cpp, - "genhtml_missed" => \$opt_missed, - "lcov_function_coverage" => \$lcov_func_coverage, - "lcov_branch_coverage" => \$lcov_branch_coverage, - }); -} - -# Copy related values if not specified -$fn_hi_limit = $hi_limit if (!defined($fn_hi_limit)); -$fn_med_limit = $med_limit if (!defined($fn_med_limit)); -$br_hi_limit = $hi_limit if (!defined($br_hi_limit)); -$br_med_limit = $med_limit if (!defined($br_med_limit)); -$func_coverage = $lcov_func_coverage if (!defined($func_coverage)); -$br_coverage = $lcov_branch_coverage if (!defined($br_coverage)); - -# Parse command line options -if (!GetOptions("output-directory|o=s" => \$output_directory, - "title|t=s" => \$test_title, - "description-file|d=s" => \$desc_filename, - "keep-descriptions|k" => \$keep_descriptions, - "css-file|c=s" => \$css_filename, - "baseline-file|b=s" => \$base_filename, - "prefix|p=s" => \@opt_dir_prefix, - "num-spaces=i" => \$tab_size, - "no-prefix" => \$no_prefix, - "no-sourceview" => \$no_sourceview, - "show-details|s" => \$show_details, - "frames|f" => \$frames, - "highlight" => \$highlight, - "legend" => \$legend, - "quiet|q" => \$quiet, - "help|h|?" => \$help, - "version|v" => \$version, - "html-prolog=s" => \$html_prolog_file, - "html-epilog=s" => \$html_epilog_file, - "html-extension=s" => \$html_ext, - "html-gzip" => \$html_gzip, - "function-coverage" => \$func_coverage, - "no-function-coverage" => \$no_func_coverage, - "branch-coverage" => \$br_coverage, - "no-branch-coverage" => \$no_br_coverage, - "sort" => \$sort, - "no-sort" => \$no_sort, - "demangle-cpp" => \$demangle_cpp, - "ignore-errors=s" => \@opt_ignore_errors, - "config-file=s" => \$opt_config_file, - "rc=s%" => \%opt_rc, - "precision=i" => \$default_precision, - "missed" => \$opt_missed, - )) -{ - print(STDERR "Use $tool_name --help to get usage information\n"); - exit(1); -} else { - # Merge options - if ($no_func_coverage) { - $func_coverage = 0; - } - if ($no_br_coverage) { - $br_coverage = 0; - } - - # Merge sort options - if ($no_sort) { - $sort = 0; - } -} - -@info_filenames = @ARGV; - -# Check for help option -if ($help) -{ - print_usage(*STDOUT); - exit(0); -} - -# Check for version option -if ($version) -{ - print("$tool_name: $lcov_version\n"); - exit(0); -} - -# Determine which errors the user wants us to ignore -parse_ignore_errors(@opt_ignore_errors); - -# Split the list of prefixes if needed -parse_dir_prefix(@opt_dir_prefix); - -# Check for info filename -if (!@info_filenames) -{ - die("No filename specified\n". - "Use $tool_name --help to get usage information\n"); -} - -# Generate a title if none is specified -if (!$test_title) -{ - if (scalar(@info_filenames) == 1) - { - # Only one filename specified, use it as title - $test_title = basename($info_filenames[0]); - } - else - { - # More than one filename specified, used default title - $test_title = "unnamed"; - } -} - -# Make sure css_filename is an absolute path (in case we're changing -# directories) -if ($css_filename) -{ - if (!($css_filename =~ /^\/(.*)$/)) - { - $css_filename = $cwd."/".$css_filename; - } -} - -# Make sure tab_size is within valid range -if ($tab_size < 1) -{ - print(STDERR "ERROR: invalid number of spaces specified: ". - "$tab_size!\n"); - exit(1); -} - -# Get HTML prolog and epilog -$html_prolog = get_html_prolog($html_prolog_file); -$html_epilog = get_html_epilog($html_epilog_file); - -# Issue a warning if --no-sourceview is enabled together with --frames -if ($no_sourceview && defined($frames)) -{ - warn("WARNING: option --frames disabled because --no-sourceview ". - "was specified!\n"); - $frames = undef; -} - -# Issue a warning if --no-prefix is enabled together with --prefix -if ($no_prefix && @dir_prefix) -{ - warn("WARNING: option --prefix disabled because --no-prefix was ". - "specified!\n"); - @dir_prefix = undef; -} - -@fileview_sortlist = ($SORT_FILE); -@funcview_sortlist = ($SORT_FILE); - -if ($sort) { - push(@fileview_sortlist, $SORT_LINE); - push(@fileview_sortlist, $SORT_FUNC) if ($func_coverage); - push(@fileview_sortlist, $SORT_BRANCH) if ($br_coverage); - push(@funcview_sortlist, $SORT_LINE); -} - -if ($frames) -{ - # Include genpng code needed for overview image generation - do("$tool_dir/genpng"); -} - -# Ensure that the c++filt tool is available when using --demangle-cpp -if ($demangle_cpp) -{ - if (system_no_output(3, "c++filt", "--version")) { - die("ERROR: could not find c++filt tool needed for ". - "--demangle-cpp\n"); - } -} - -# Make sure precision is within valid range -if ($default_precision < 1 || $default_precision > 4) -{ - die("ERROR: specified precision is out of range (1 to 4)\n"); -} - - -# Make sure output_directory exists, create it if necessary -if ($output_directory) -{ - stat($output_directory); - - if (! -e _) - { - create_sub_dir($output_directory); - } -} - -# Do something -gen_html(); - -exit(0); - - - -# -# print_usage(handle) -# -# Print usage information. -# - -sub print_usage(*) -{ - local *HANDLE = $_[0]; - - print(HANDLE <{$filename}; - my $funcdata = $data->{"func"}; - my $sumfnccount = $data->{"sumfnc"}; - - if (defined($funcdata)) { - foreach my $func_name (keys(%{$funcdata})) { - $fns{$func_name} = 1; - } - } - - if (defined($sumfnccount)) { - foreach my $func_name (keys(%{$sumfnccount})) { - $fns{$func_name} = 1; - } - } - } - - @result = keys(%fns); - - return \@result; -} - -# -# rename_functions(info, conv) -# -# Rename all function names in INFO according to CONV: OLD_NAME -> NEW_NAME. -# In case two functions demangle to the same name, assume that they are -# different object code implementations for the same source function. -# - -sub rename_functions($$) -{ - my ($info, $conv) = @_; - - foreach my $filename (keys(%{$info})) { - my $data = $info->{$filename}; - my $funcdata; - my $testfncdata; - my $sumfnccount; - my %newfuncdata; - my %newsumfnccount; - my $f_found; - my $f_hit; - - # funcdata: function name -> line number - $funcdata = $data->{"func"}; - foreach my $fn (keys(%{$funcdata})) { - my $cn = $conv->{$fn}; - - # Abort if two functions on different lines map to the - # same demangled name. - if (defined($newfuncdata{$cn}) && - $newfuncdata{$cn} != $funcdata->{$fn}) { - die("ERROR: Demangled function name $cn ". - "maps to different lines (". - $newfuncdata{$cn}." vs ". - $funcdata->{$fn}.") in $filename\n"); - } - $newfuncdata{$cn} = $funcdata->{$fn}; - } - $data->{"func"} = \%newfuncdata; - - # testfncdata: test name -> testfnccount - # testfnccount: function name -> execution count - $testfncdata = $data->{"testfnc"}; - foreach my $tn (keys(%{$testfncdata})) { - my $testfnccount = $testfncdata->{$tn}; - my %newtestfnccount; - - foreach my $fn (keys(%{$testfnccount})) { - my $cn = $conv->{$fn}; - - # Add counts for different functions that map - # to the same name. - $newtestfnccount{$cn} += - $testfnccount->{$fn}; - } - $testfncdata->{$tn} = \%newtestfnccount; - } - - # sumfnccount: function name -> execution count - $sumfnccount = $data->{"sumfnc"}; - foreach my $fn (keys(%{$sumfnccount})) { - my $cn = $conv->{$fn}; - - # Add counts for different functions that map - # to the same name. - $newsumfnccount{$cn} += $sumfnccount->{$fn}; - } - $data->{"sumfnc"} = \%newsumfnccount; - - # Update function found and hit counts since they may have - # changed - $f_found = 0; - $f_hit = 0; - foreach my $fn (keys(%newsumfnccount)) { - $f_found++; - $f_hit++ if ($newsumfnccount{$fn} > 0); - } - $data->{"f_found"} = $f_found; - $data->{"f_hit"} = $f_hit; - } -} - -# -# gen_html() -# -# Generate a set of HTML pages from contents of .info file INFO_FILENAME. -# Files will be written to the current directory. If provided, test case -# descriptions will be read from .tests file TEST_FILENAME and included -# in ouput. -# -# Die on error. -# - -sub gen_html() -{ - local *HTML_HANDLE; - my %overview; - my %base_data; - my $lines_found; - my $lines_hit; - my $fn_found; - my $fn_hit; - my $br_found; - my $br_hit; - my $overall_found = 0; - my $overall_hit = 0; - my $total_fn_found = 0; - my $total_fn_hit = 0; - my $total_br_found = 0; - my $total_br_hit = 0; - my $dir_name; - my $link_name; - my @dir_list; - my %new_info; - - # Read in all specified .info files - foreach (@info_filenames) - { - %new_info = %{read_info_file($_)}; - - # Combine %new_info with %info_data - %info_data = %{combine_info_files(\%info_data, \%new_info)}; - } - - info("Found %d entries.\n", scalar(keys(%info_data))); - - # Read and apply baseline data if specified - if ($base_filename) - { - # Read baseline file - info("Reading baseline file $base_filename\n"); - %base_data = %{read_info_file($base_filename)}; - info("Found %d entries.\n", scalar(keys(%base_data))); - - # Apply baseline - info("Subtracting baseline data.\n"); - %info_data = %{apply_baseline(\%info_data, \%base_data)}; - } - - @dir_list = get_dir_list(keys(%info_data)); - - if ($no_prefix) - { - # User requested that we leave filenames alone - info("User asked not to remove filename prefix\n"); - } - elsif (! @dir_prefix) - { - # Get prefix common to most directories in list - my $prefix = get_prefix(1, keys(%info_data)); - - if ($prefix) - { - info("Found common filename prefix \"$prefix\"\n"); - $dir_prefix[0] = $prefix; - - } - else - { - info("No common filename prefix found!\n"); - $no_prefix=1; - } - } - else - { - my $msg = "Using user-specified filename prefix "; - for my $i (0 .. $#dir_prefix) - { - $dir_prefix[$i] =~ s/\/+$//; - $msg .= ", " unless 0 == $i; - $msg .= "\"" . $dir_prefix[$i] . "\""; - } - info($msg . "\n"); - } - - - # Read in test description file if specified - if ($desc_filename) - { - info("Reading test description file $desc_filename\n"); - %test_description = %{read_testfile($desc_filename)}; - - # Remove test descriptions which are not referenced - # from %info_data if user didn't tell us otherwise - if (!$keep_descriptions) - { - remove_unused_descriptions(); - } - } - - # Change to output directory if specified - if ($output_directory) - { - chdir($output_directory) - or die("ERROR: cannot change to directory ". - "$output_directory!\n"); - } - - info("Writing .css and .png files.\n"); - write_css_file(); - write_png_files(); - - if ($html_gzip) - { - info("Writing .htaccess file.\n"); - write_htaccess_file(); - } - - info("Generating output.\n"); - - # Process each subdirectory and collect overview information - foreach $dir_name (@dir_list) - { - ($lines_found, $lines_hit, $fn_found, $fn_hit, - $br_found, $br_hit) - = process_dir($dir_name); - - # Handle files in root directory gracefully - $dir_name = "root" if ($dir_name eq ""); - - # Remove prefix if applicable - if (!$no_prefix && @dir_prefix) - { - # Match directory names beginning with one of @dir_prefix - $dir_name = apply_prefix($dir_name,@dir_prefix); - } - - # Generate name for directory overview HTML page - if ($dir_name =~ /^\/(.*)$/) - { - $link_name = substr($dir_name, 1)."/index.$html_ext"; - } - else - { - $link_name = $dir_name."/index.$html_ext"; - } - - $overview{$dir_name} = [$lines_found, $lines_hit, $fn_found, - $fn_hit, $br_found, $br_hit, $link_name, - get_rate($lines_found, $lines_hit), - get_rate($fn_found, $fn_hit), - get_rate($br_found, $br_hit)]; - $overall_found += $lines_found; - $overall_hit += $lines_hit; - $total_fn_found += $fn_found; - $total_fn_hit += $fn_hit; - $total_br_found += $br_found; - $total_br_hit += $br_hit; - } - - # Generate overview page - info("Writing directory view page.\n"); - - # Create sorted pages - foreach (@fileview_sortlist) { - write_dir_page($fileview_sortname[$_], ".", "", $test_title, - undef, $overall_found, $overall_hit, - $total_fn_found, $total_fn_hit, $total_br_found, - $total_br_hit, \%overview, {}, {}, {}, 0, $_); - } - - # Check if there are any test case descriptions to write out - if (%test_description) - { - info("Writing test case description file.\n"); - write_description_file( \%test_description, - $overall_found, $overall_hit, - $total_fn_found, $total_fn_hit, - $total_br_found, $total_br_hit); - } - - print_overall_rate(1, $overall_found, $overall_hit, - $func_coverage, $total_fn_found, $total_fn_hit, - $br_coverage, $total_br_found, $total_br_hit); - - chdir($cwd); -} - -# -# html_create(handle, filename) -# - -sub html_create($$) -{ - my $handle = $_[0]; - my $filename = $_[1]; - - if ($html_gzip) - { - open($handle, "|-", "gzip -c >'$filename'") - or die("ERROR: cannot open $filename for writing ". - "(gzip)!\n"); - } - else - { - open($handle, ">", $filename) - or die("ERROR: cannot open $filename for writing!\n"); - } -} - -sub write_dir_page($$$$$$$$$$$$$$$$$) -{ - my ($name, $rel_dir, $base_dir, $title, $trunc_dir, $overall_found, - $overall_hit, $total_fn_found, $total_fn_hit, $total_br_found, - $total_br_hit, $overview, $testhash, $testfnchash, $testbrhash, - $view_type, $sort_type) = @_; - - # Generate directory overview page including details - html_create(*HTML_HANDLE, "$rel_dir/index$name.$html_ext"); - if (!defined($trunc_dir)) { - $trunc_dir = ""; - } - $title .= " - " if ($trunc_dir ne ""); - write_html_prolog(*HTML_HANDLE, $base_dir, "LCOV - $title$trunc_dir"); - write_header(*HTML_HANDLE, $view_type, $trunc_dir, $rel_dir, - $overall_found, $overall_hit, $total_fn_found, - $total_fn_hit, $total_br_found, $total_br_hit, $sort_type); - write_file_table(*HTML_HANDLE, $base_dir, $overview, $testhash, - $testfnchash, $testbrhash, $view_type, $sort_type); - write_html_epilog(*HTML_HANDLE, $base_dir); - close(*HTML_HANDLE); -} - - -# -# process_dir(dir_name) -# - -sub process_dir($) -{ - my $abs_dir = $_[0]; - my $trunc_dir; - my $rel_dir = $abs_dir; - my $base_dir; - my $filename; - my %overview; - my $lines_found; - my $lines_hit; - my $fn_found; - my $fn_hit; - my $br_found; - my $br_hit; - my $overall_found=0; - my $overall_hit=0; - my $total_fn_found=0; - my $total_fn_hit=0; - my $total_br_found = 0; - my $total_br_hit = 0; - my $base_name; - my $extension; - my $testdata; - my %testhash; - my $testfncdata; - my %testfnchash; - my $testbrdata; - my %testbrhash; - my @sort_list; - local *HTML_HANDLE; - - # Remove prefix if applicable - if (!$no_prefix) - { - # Match directory name beginning with one of @dir_prefix - $rel_dir = apply_prefix($rel_dir,@dir_prefix); - } - - $trunc_dir = $rel_dir; - - # Remove leading / - if ($rel_dir =~ /^\/(.*)$/) - { - $rel_dir = substr($rel_dir, 1); - } - - # Handle files in root directory gracefully - $rel_dir = "root" if ($rel_dir eq ""); - $trunc_dir = "root" if ($trunc_dir eq ""); - - $base_dir = get_relative_base_path($rel_dir); - - create_sub_dir($rel_dir); - - # Match filenames which specify files in this directory, not including - # sub-directories - foreach $filename (grep(/^\Q$abs_dir\E\/[^\/]*$/,keys(%info_data))) - { - my $page_link; - my $func_link; - - ($lines_found, $lines_hit, $fn_found, $fn_hit, $br_found, - $br_hit, $testdata, $testfncdata, $testbrdata) = - process_file($trunc_dir, $rel_dir, $filename); - - $base_name = basename($filename); - - if ($no_sourceview) { - $page_link = ""; - } elsif ($frames) { - # Link to frameset page - $page_link = "$base_name.gcov.frameset.$html_ext"; - } else { - # Link directory to source code view page - $page_link = "$base_name.gcov.$html_ext"; - } - $overview{$base_name} = [$lines_found, $lines_hit, $fn_found, - $fn_hit, $br_found, $br_hit, - $page_link, - get_rate($lines_found, $lines_hit), - get_rate($fn_found, $fn_hit), - get_rate($br_found, $br_hit)]; - - $testhash{$base_name} = $testdata; - $testfnchash{$base_name} = $testfncdata; - $testbrhash{$base_name} = $testbrdata; - - $overall_found += $lines_found; - $overall_hit += $lines_hit; - - $total_fn_found += $fn_found; - $total_fn_hit += $fn_hit; - - $total_br_found += $br_found; - $total_br_hit += $br_hit; - } - - # Create sorted pages - foreach (@fileview_sortlist) { - # Generate directory overview page (without details) - write_dir_page($fileview_sortname[$_], $rel_dir, $base_dir, - $test_title, $trunc_dir, $overall_found, - $overall_hit, $total_fn_found, $total_fn_hit, - $total_br_found, $total_br_hit, \%overview, {}, - {}, {}, 1, $_); - if (!$show_details) { - next; - } - # Generate directory overview page including details - write_dir_page("-detail".$fileview_sortname[$_], $rel_dir, - $base_dir, $test_title, $trunc_dir, - $overall_found, $overall_hit, $total_fn_found, - $total_fn_hit, $total_br_found, $total_br_hit, - \%overview, \%testhash, \%testfnchash, - \%testbrhash, 1, $_); - } - - # Calculate resulting line counts - return ($overall_found, $overall_hit, $total_fn_found, $total_fn_hit, - $total_br_found, $total_br_hit); -} - - -# -# get_converted_lines(testdata) -# -# Return hash of line numbers of those lines which were only covered in -# converted data sets. -# - -sub get_converted_lines($) -{ - my $testdata = $_[0]; - my $testcount; - my %converted; - my %nonconverted; - my $hash; - my $testcase; - my $line; - my %result; - - - # Get a hash containing line numbers with positive counts both for - # converted and original data sets - foreach $testcase (keys(%{$testdata})) - { - # Check to see if this is a converted data set - if ($testcase =~ /,diff$/) - { - $hash = \%converted; - } - else - { - $hash = \%nonconverted; - } - - $testcount = $testdata->{$testcase}; - # Add lines with a positive count to hash - foreach $line (keys%{$testcount}) - { - if ($testcount->{$line} > 0) - { - $hash->{$line} = 1; - } - } - } - - # Combine both hashes to resulting list - foreach $line (keys(%converted)) - { - if (!defined($nonconverted{$line})) - { - $result{$line} = 1; - } - } - - return \%result; -} - - -sub write_function_page($$$$$$$$$$$$$$$$$$) -{ - my ($base_dir, $rel_dir, $trunc_dir, $base_name, $title, - $lines_found, $lines_hit, $fn_found, $fn_hit, $br_found, $br_hit, - $sumcount, $funcdata, $sumfnccount, $testfncdata, $sumbrcount, - $testbrdata, $sort_type) = @_; - my $pagetitle; - my $filename; - - # Generate function table for this file - if ($sort_type == 0) { - $filename = "$rel_dir/$base_name.func.$html_ext"; - } else { - $filename = "$rel_dir/$base_name.func-sort-c.$html_ext"; - } - html_create(*HTML_HANDLE, $filename); - $pagetitle = "LCOV - $title - $trunc_dir/$base_name - functions"; - write_html_prolog(*HTML_HANDLE, $base_dir, $pagetitle); - write_header(*HTML_HANDLE, 4, "$trunc_dir/$base_name", - "$rel_dir/$base_name", $lines_found, $lines_hit, - $fn_found, $fn_hit, $br_found, $br_hit, $sort_type); - write_function_table(*HTML_HANDLE, "$base_name.gcov.$html_ext", - $sumcount, $funcdata, - $sumfnccount, $testfncdata, $sumbrcount, - $testbrdata, $base_name, - $base_dir, $sort_type); - write_html_epilog(*HTML_HANDLE, $base_dir, 1); - close(*HTML_HANDLE); -} - - -# -# process_file(trunc_dir, rel_dir, filename) -# - -sub process_file($$$) -{ - info("Processing file ".apply_prefix($_[2], @dir_prefix)."\n"); - - my $trunc_dir = $_[0]; - my $rel_dir = $_[1]; - my $filename = $_[2]; - my $base_name = basename($filename); - my $base_dir = get_relative_base_path($rel_dir); - my $testdata; - my $testcount; - my $sumcount; - my $funcdata; - my $checkdata; - my $testfncdata; - my $sumfnccount; - my $testbrdata; - my $sumbrcount; - my $lines_found; - my $lines_hit; - my $fn_found; - my $fn_hit; - my $br_found; - my $br_hit; - my $converted; - my @source; - my $pagetitle; - local *HTML_HANDLE; - - ($testdata, $sumcount, $funcdata, $checkdata, $testfncdata, - $sumfnccount, $testbrdata, $sumbrcount, $lines_found, $lines_hit, - $fn_found, $fn_hit, $br_found, $br_hit) - = get_info_entry($info_data{$filename}); - - # Return after this point in case user asked us not to generate - # source code view - if ($no_sourceview) - { - return ($lines_found, $lines_hit, $fn_found, $fn_hit, - $br_found, $br_hit, $testdata, $testfncdata, - $testbrdata); - } - - $converted = get_converted_lines($testdata); - # Generate source code view for this file - html_create(*HTML_HANDLE, "$rel_dir/$base_name.gcov.$html_ext"); - $pagetitle = "LCOV - $test_title - $trunc_dir/$base_name"; - write_html_prolog(*HTML_HANDLE, $base_dir, $pagetitle); - write_header(*HTML_HANDLE, 2, "$trunc_dir/$base_name", - "$rel_dir/$base_name", $lines_found, $lines_hit, - $fn_found, $fn_hit, $br_found, $br_hit, 0); - @source = write_source(*HTML_HANDLE, $filename, $sumcount, $checkdata, - $converted, $funcdata, $sumbrcount); - - write_html_epilog(*HTML_HANDLE, $base_dir, 1); - close(*HTML_HANDLE); - - if ($func_coverage) { - # Create function tables - foreach (@funcview_sortlist) { - write_function_page($base_dir, $rel_dir, $trunc_dir, - $base_name, $test_title, - $lines_found, $lines_hit, - $fn_found, $fn_hit, $br_found, - $br_hit, $sumcount, - $funcdata, $sumfnccount, - $testfncdata, $sumbrcount, - $testbrdata, $_); - } - } - - # Additional files are needed in case of frame output - if (!$frames) - { - return ($lines_found, $lines_hit, $fn_found, $fn_hit, - $br_found, $br_hit, $testdata, $testfncdata, - $testbrdata); - } - - # Create overview png file - gen_png("$rel_dir/$base_name.gcov.png", $overview_width, $tab_size, - @source); - - # Create frameset page - html_create(*HTML_HANDLE, - "$rel_dir/$base_name.gcov.frameset.$html_ext"); - write_frameset(*HTML_HANDLE, $base_dir, $base_name, $pagetitle); - close(*HTML_HANDLE); - - # Write overview frame - html_create(*HTML_HANDLE, - "$rel_dir/$base_name.gcov.overview.$html_ext"); - write_overview(*HTML_HANDLE, $base_dir, $base_name, $pagetitle, - scalar(@source)); - close(*HTML_HANDLE); - - return ($lines_found, $lines_hit, $fn_found, $fn_hit, $br_found, - $br_hit, $testdata, $testfncdata, $testbrdata); -} - - -sub compress_brcount($) -{ - my ($brcount) = @_; - my $db; - - $db = brcount_to_db($brcount); - return db_to_brcount($db, $brcount); -} - - -# -# read_info_file(info_filename) -# -# Read in the contents of the .info file specified by INFO_FILENAME. Data will -# be returned as a reference to a hash containing the following mappings: -# -# %result: for each filename found in file -> \%data -# -# %data: "test" -> \%testdata -# "sum" -> \%sumcount -# "func" -> \%funcdata -# "found" -> $lines_found (number of instrumented lines found in file) -# "hit" -> $lines_hit (number of executed lines in file) -# "f_found" -> $fn_found (number of instrumented functions found in file) -# "f_hit" -> $fn_hit (number of executed functions in file) -# "b_found" -> $br_found (number of instrumented branches found in file) -# "b_hit" -> $br_hit (number of executed branches in file) -# "check" -> \%checkdata -# "testfnc" -> \%testfncdata -# "sumfnc" -> \%sumfnccount -# "testbr" -> \%testbrdata -# "sumbr" -> \%sumbrcount -# -# %testdata : name of test affecting this file -> \%testcount -# %testfncdata: name of test affecting this file -> \%testfnccount -# %testbrdata: name of test affecting this file -> \%testbrcount -# -# %testcount : line number -> execution count for a single test -# %testfnccount: function name -> execution count for a single test -# %testbrcount : line number -> branch coverage data for a single test -# %sumcount : line number -> execution count for all tests -# %sumfnccount : function name -> execution count for all tests -# %sumbrcount : line number -> branch coverage data for all tests -# %funcdata : function name -> line number -# %checkdata : line number -> checksum of source code line -# $brdata : vector of items: block, branch, taken -# -# Note that .info file sections referring to the same file and test name -# will automatically be combined by adding all execution counts. -# -# Note that if INFO_FILENAME ends with ".gz", it is assumed that the file -# is compressed using GZIP. If available, GUNZIP will be used to decompress -# this file. -# -# Die on error. -# - -sub read_info_file($) -{ - my $tracefile = $_[0]; # Name of tracefile - my %result; # Resulting hash: file -> data - my $data; # Data handle for current entry - my $testdata; # " " - my $testcount; # " " - my $sumcount; # " " - my $funcdata; # " " - my $checkdata; # " " - my $testfncdata; - my $testfnccount; - my $sumfnccount; - my $testbrdata; - my $testbrcount; - my $sumbrcount; - my $line; # Current line read from .info file - my $testname; # Current test name - my $filename; # Current filename - my $hitcount; # Count for lines hit - my $count; # Execution count of current line - my $negative; # If set, warn about negative counts - my $changed_testname; # If set, warn about changed testname - my $line_checksum; # Checksum of current line - my $notified_about_relative_paths; - local *INFO_HANDLE; # Filehandle for .info file - - info("Reading data file $tracefile\n"); - - # Check if file exists and is readable - stat($_[0]); - if (!(-r _)) - { - die("ERROR: cannot read file $_[0]!\n"); - } - - # Check if this is really a plain file - if (!(-f _)) - { - die("ERROR: not a plain file: $_[0]!\n"); - } - - # Check for .gz extension - if ($_[0] =~ /\.gz$/) - { - # Check for availability of GZIP tool - system_no_output(1, "gunzip" ,"-h") - and die("ERROR: gunzip command not available!\n"); - - # Check integrity of compressed file - system_no_output(1, "gunzip", "-t", $_[0]) - and die("ERROR: integrity check failed for ". - "compressed file $_[0]!\n"); - - # Open compressed file - open(INFO_HANDLE, "-|", "gunzip -c '$_[0]'") - or die("ERROR: cannot start gunzip to decompress ". - "file $_[0]!\n"); - } - else - { - # Open decompressed file - open(INFO_HANDLE, "<", $_[0]) - or die("ERROR: cannot read file $_[0]!\n"); - } - - $testname = ""; - while () - { - chomp($_); - $line = $_; - - # Switch statement - foreach ($line) - { - /^TN:([^,]*)(,diff)?/ && do - { - # Test name information found - $testname = defined($1) ? $1 : ""; - if ($testname =~ s/\W/_/g) - { - $changed_testname = 1; - } - $testname .= $2 if (defined($2)); - last; - }; - - /^[SK]F:(.*)/ && do - { - # Filename information found - # Retrieve data for new entry - $filename = File::Spec->rel2abs($1, $cwd); - - if (!File::Spec->file_name_is_absolute($1) && - !$notified_about_relative_paths) - { - info("Resolved relative source file ". - "path \"$1\" with CWD to ". - "\"$filename\".\n"); - $notified_about_relative_paths = 1; - } - - $data = $result{$filename}; - ($testdata, $sumcount, $funcdata, $checkdata, - $testfncdata, $sumfnccount, $testbrdata, - $sumbrcount) = - get_info_entry($data); - - if (defined($testname)) - { - $testcount = $testdata->{$testname}; - $testfnccount = $testfncdata->{$testname}; - $testbrcount = $testbrdata->{$testname}; - } - else - { - $testcount = {}; - $testfnccount = {}; - $testbrcount = {}; - } - last; - }; - - /^DA:(\d+),(-?\d+)(,[^,\s]+)?/ && do - { - # Fix negative counts - $count = $2 < 0 ? 0 : $2; - if ($2 < 0) - { - $negative = 1; - } - # Execution count found, add to structure - # Add summary counts - $sumcount->{$1} += $count; - - # Add test-specific counts - if (defined($testname)) - { - $testcount->{$1} += $count; - } - - # Store line checksum if available - if (defined($3)) - { - $line_checksum = substr($3, 1); - - # Does it match a previous definition - if (defined($checkdata->{$1}) && - ($checkdata->{$1} ne - $line_checksum)) - { - die("ERROR: checksum mismatch ". - "at $filename:$1\n"); - } - - $checkdata->{$1} = $line_checksum; - } - last; - }; - - /^FN:(\d+),([^,]+)/ && do - { - last if (!$func_coverage); - - # Function data found, add to structure - $funcdata->{$2} = $1; - - # Also initialize function call data - if (!defined($sumfnccount->{$2})) { - $sumfnccount->{$2} = 0; - } - if (defined($testname)) - { - if (!defined($testfnccount->{$2})) { - $testfnccount->{$2} = 0; - } - } - last; - }; - - /^FNDA:(\d+),([^,]+)/ && do - { - last if (!$func_coverage); - # Function call count found, add to structure - # Add summary counts - $sumfnccount->{$2} += $1; - - # Add test-specific counts - if (defined($testname)) - { - $testfnccount->{$2} += $1; - } - last; - }; - - /^BRDA:(\d+),(\d+),(\d+),(\d+|-)/ && do { - # Branch coverage data found - my ($line, $block, $branch, $taken) = - ($1, $2, $3, $4); - - last if (!$br_coverage); - $block = -1 if ($block == $UNNAMED_BLOCK); - $sumbrcount->{$line} .= - "$block,$branch,$taken:"; - - # Add test-specific counts - if (defined($testname)) { - $testbrcount->{$line} .= - "$block,$branch,$taken:"; - } - last; - }; - - /^end_of_record/ && do - { - # Found end of section marker - if ($filename) - { - # Store current section data - if (defined($testname)) - { - $testdata->{$testname} = - $testcount; - $testfncdata->{$testname} = - $testfnccount; - $testbrdata->{$testname} = - $testbrcount; - } - - set_info_entry($data, $testdata, - $sumcount, $funcdata, - $checkdata, $testfncdata, - $sumfnccount, - $testbrdata, - $sumbrcount); - $result{$filename} = $data; - last; - } - }; - - # default - last; - } - } - close(INFO_HANDLE); - - # Calculate lines_found and lines_hit for each file - foreach $filename (keys(%result)) - { - $data = $result{$filename}; - - ($testdata, $sumcount, undef, undef, $testfncdata, - $sumfnccount, $testbrdata, $sumbrcount) = - get_info_entry($data); - - # Filter out empty files - if (scalar(keys(%{$sumcount})) == 0) - { - delete($result{$filename}); - next; - } - # Filter out empty test cases - foreach $testname (keys(%{$testdata})) - { - if (!defined($testdata->{$testname}) || - scalar(keys(%{$testdata->{$testname}})) == 0) - { - delete($testdata->{$testname}); - delete($testfncdata->{$testname}); - } - } - - $data->{"found"} = scalar(keys(%{$sumcount})); - $hitcount = 0; - - foreach (keys(%{$sumcount})) - { - if ($sumcount->{$_} > 0) { $hitcount++; } - } - - $data->{"hit"} = $hitcount; - - # Get found/hit values for function call data - $data->{"f_found"} = scalar(keys(%{$sumfnccount})); - $hitcount = 0; - - foreach (keys(%{$sumfnccount})) { - if ($sumfnccount->{$_} > 0) { - $hitcount++; - } - } - $data->{"f_hit"} = $hitcount; - - # Combine branch data for the same branches - (undef, $data->{"b_found"}, $data->{"b_hit"}) = - compress_brcount($sumbrcount); - foreach $testname (keys(%{$testbrdata})) { - compress_brcount($testbrdata->{$testname}); - } - } - - if (scalar(keys(%result)) == 0) - { - die("ERROR: no valid records found in tracefile $tracefile\n"); - } - if ($negative) - { - warn("WARNING: negative counts found in tracefile ". - "$tracefile\n"); - } - if ($changed_testname) - { - warn("WARNING: invalid characters removed from testname in ". - "tracefile $tracefile\n"); - } - - return(\%result); -} - - -# -# get_info_entry(hash_ref) -# -# Retrieve data from an entry of the structure generated by read_info_file(). -# Return a list of references to hashes: -# (test data hash ref, sum count hash ref, funcdata hash ref, checkdata hash -# ref, testfncdata hash ref, sumfnccount hash ref, lines found, lines hit, -# functions found, functions hit) -# - -sub get_info_entry($) -{ - my $testdata_ref = $_[0]->{"test"}; - my $sumcount_ref = $_[0]->{"sum"}; - my $funcdata_ref = $_[0]->{"func"}; - my $checkdata_ref = $_[0]->{"check"}; - my $testfncdata = $_[0]->{"testfnc"}; - my $sumfnccount = $_[0]->{"sumfnc"}; - my $testbrdata = $_[0]->{"testbr"}; - my $sumbrcount = $_[0]->{"sumbr"}; - my $lines_found = $_[0]->{"found"}; - my $lines_hit = $_[0]->{"hit"}; - my $fn_found = $_[0]->{"f_found"}; - my $fn_hit = $_[0]->{"f_hit"}; - my $br_found = $_[0]->{"b_found"}; - my $br_hit = $_[0]->{"b_hit"}; - - return ($testdata_ref, $sumcount_ref, $funcdata_ref, $checkdata_ref, - $testfncdata, $sumfnccount, $testbrdata, $sumbrcount, - $lines_found, $lines_hit, $fn_found, $fn_hit, - $br_found, $br_hit); -} - - -# -# set_info_entry(hash_ref, testdata_ref, sumcount_ref, funcdata_ref, -# checkdata_ref, testfncdata_ref, sumfcncount_ref, -# testbrdata_ref, sumbrcount_ref[,lines_found, -# lines_hit, f_found, f_hit, $b_found, $b_hit]) -# -# Update the hash referenced by HASH_REF with the provided data references. -# - -sub set_info_entry($$$$$$$$$;$$$$$$) -{ - my $data_ref = $_[0]; - - $data_ref->{"test"} = $_[1]; - $data_ref->{"sum"} = $_[2]; - $data_ref->{"func"} = $_[3]; - $data_ref->{"check"} = $_[4]; - $data_ref->{"testfnc"} = $_[5]; - $data_ref->{"sumfnc"} = $_[6]; - $data_ref->{"testbr"} = $_[7]; - $data_ref->{"sumbr"} = $_[8]; - - if (defined($_[9])) { $data_ref->{"found"} = $_[9]; } - if (defined($_[10])) { $data_ref->{"hit"} = $_[10]; } - if (defined($_[11])) { $data_ref->{"f_found"} = $_[11]; } - if (defined($_[12])) { $data_ref->{"f_hit"} = $_[12]; } - if (defined($_[13])) { $data_ref->{"b_found"} = $_[13]; } - if (defined($_[14])) { $data_ref->{"b_hit"} = $_[14]; } -} - - -# -# add_counts(data1_ref, data2_ref) -# -# DATA1_REF and DATA2_REF are references to hashes containing a mapping -# -# line number -> execution count -# -# Return a list (RESULT_REF, LINES_FOUND, LINES_HIT) where RESULT_REF -# is a reference to a hash containing the combined mapping in which -# execution counts are added. -# - -sub add_counts($$) -{ - my $data1_ref = $_[0]; # Hash 1 - my $data2_ref = $_[1]; # Hash 2 - my %result; # Resulting hash - my $line; # Current line iteration scalar - my $data1_count; # Count of line in hash1 - my $data2_count; # Count of line in hash2 - my $found = 0; # Total number of lines found - my $hit = 0; # Number of lines with a count > 0 - - foreach $line (keys(%$data1_ref)) - { - $data1_count = $data1_ref->{$line}; - $data2_count = $data2_ref->{$line}; - - # Add counts if present in both hashes - if (defined($data2_count)) { $data1_count += $data2_count; } - - # Store sum in %result - $result{$line} = $data1_count; - - $found++; - if ($data1_count > 0) { $hit++; } - } - - # Add lines unique to data2_ref - foreach $line (keys(%$data2_ref)) - { - # Skip lines already in data1_ref - if (defined($data1_ref->{$line})) { next; } - - # Copy count from data2_ref - $result{$line} = $data2_ref->{$line}; - - $found++; - if ($result{$line} > 0) { $hit++; } - } - - return (\%result, $found, $hit); -} - - -# -# merge_checksums(ref1, ref2, filename) -# -# REF1 and REF2 are references to hashes containing a mapping -# -# line number -> checksum -# -# Merge checksum lists defined in REF1 and REF2 and return reference to -# resulting hash. Die if a checksum for a line is defined in both hashes -# but does not match. -# - -sub merge_checksums($$$) -{ - my $ref1 = $_[0]; - my $ref2 = $_[1]; - my $filename = $_[2]; - my %result; - my $line; - - foreach $line (keys(%{$ref1})) - { - if (defined($ref2->{$line}) && - ($ref1->{$line} ne $ref2->{$line})) - { - die("ERROR: checksum mismatch at $filename:$line\n"); - } - $result{$line} = $ref1->{$line}; - } - - foreach $line (keys(%{$ref2})) - { - $result{$line} = $ref2->{$line}; - } - - return \%result; -} - - -# -# merge_func_data(funcdata1, funcdata2, filename) -# - -sub merge_func_data($$$) -{ - my ($funcdata1, $funcdata2, $filename) = @_; - my %result; - my $func; - - if (defined($funcdata1)) { - %result = %{$funcdata1}; - } - - foreach $func (keys(%{$funcdata2})) { - my $line1 = $result{$func}; - my $line2 = $funcdata2->{$func}; - - if (defined($line1) && ($line1 != $line2)) { - warn("WARNING: function data mismatch at ". - "$filename:$line2\n"); - next; - } - $result{$func} = $line2; - } - - return \%result; -} - - -# -# add_fnccount(fnccount1, fnccount2) -# -# Add function call count data. Return list (fnccount_added, f_found, f_hit) -# - -sub add_fnccount($$) -{ - my ($fnccount1, $fnccount2) = @_; - my %result; - my $fn_found; - my $fn_hit; - my $function; - - if (defined($fnccount1)) { - %result = %{$fnccount1}; - } - foreach $function (keys(%{$fnccount2})) { - $result{$function} += $fnccount2->{$function}; - } - $fn_found = scalar(keys(%result)); - $fn_hit = 0; - foreach $function (keys(%result)) { - if ($result{$function} > 0) { - $fn_hit++; - } - } - - return (\%result, $fn_found, $fn_hit); -} - -# -# add_testfncdata(testfncdata1, testfncdata2) -# -# Add function call count data for several tests. Return reference to -# added_testfncdata. -# - -sub add_testfncdata($$) -{ - my ($testfncdata1, $testfncdata2) = @_; - my %result; - my $testname; - - foreach $testname (keys(%{$testfncdata1})) { - if (defined($testfncdata2->{$testname})) { - my $fnccount; - - # Function call count data for this testname exists - # in both data sets: add - ($fnccount) = add_fnccount( - $testfncdata1->{$testname}, - $testfncdata2->{$testname}); - $result{$testname} = $fnccount; - next; - } - # Function call count data for this testname is unique to - # data set 1: copy - $result{$testname} = $testfncdata1->{$testname}; - } - - # Add count data for testnames unique to data set 2 - foreach $testname (keys(%{$testfncdata2})) { - if (!defined($result{$testname})) { - $result{$testname} = $testfncdata2->{$testname}; - } - } - return \%result; -} - - -# -# brcount_to_db(brcount) -# -# Convert brcount data to the following format: -# -# db: line number -> block hash -# block hash: block number -> branch hash -# branch hash: branch number -> taken value -# - -sub brcount_to_db($) -{ - my ($brcount) = @_; - my $line; - my $db; - - # Add branches to database - foreach $line (keys(%{$brcount})) { - my $brdata = $brcount->{$line}; - - foreach my $entry (split(/:/, $brdata)) { - my ($block, $branch, $taken) = split(/,/, $entry); - my $old = $db->{$line}->{$block}->{$branch}; - - if (!defined($old) || $old eq "-") { - $old = $taken; - } elsif ($taken ne "-") { - $old += $taken; - } - - $db->{$line}->{$block}->{$branch} = $old; - } - } - - return $db; -} - - -# -# db_to_brcount(db[, brcount]) -# -# Convert branch coverage data back to brcount format. If brcount is specified, -# the converted data is directly inserted in brcount. -# - -sub db_to_brcount($;$) -{ - my ($db, $brcount) = @_; - my $line; - my $br_found = 0; - my $br_hit = 0; - - # Convert database back to brcount format - foreach $line (sort({$a <=> $b} keys(%{$db}))) { - my $ldata = $db->{$line}; - my $brdata; - my $block; - - foreach $block (sort({$a <=> $b} keys(%{$ldata}))) { - my $bdata = $ldata->{$block}; - my $branch; - - foreach $branch (sort({$a <=> $b} keys(%{$bdata}))) { - my $taken = $bdata->{$branch}; - - $br_found++; - $br_hit++ if ($taken ne "-" && $taken > 0); - $brdata .= "$block,$branch,$taken:"; - } - } - $brcount->{$line} = $brdata; - } - - return ($brcount, $br_found, $br_hit); -} - - -# -# brcount_db_combine(db1, db2, op) -# -# db1 := db1 op db2, where -# db1, db2: brcount data as returned by brcount_to_db -# op: one of $BR_ADD and BR_SUB -# -sub brcount_db_combine($$$) -{ - my ($db1, $db2, $op) = @_; - - foreach my $line (keys(%{$db2})) { - my $ldata = $db2->{$line}; - - foreach my $block (keys(%{$ldata})) { - my $bdata = $ldata->{$block}; - - foreach my $branch (keys(%{$bdata})) { - my $taken = $bdata->{$branch}; - my $new = $db1->{$line}->{$block}->{$branch}; - - if (!defined($new) || $new eq "-") { - $new = $taken; - } elsif ($taken ne "-") { - if ($op == $BR_ADD) { - $new += $taken; - } elsif ($op == $BR_SUB) { - $new -= $taken; - $new = 0 if ($new < 0); - } - } - - $db1->{$line}->{$block}->{$branch} = $new; - } - } - } -} - - -# -# brcount_db_get_found_and_hit(db) -# -# Return (br_found, br_hit) for db. -# - -sub brcount_db_get_found_and_hit($) -{ - my ($db) = @_; - my ($br_found , $br_hit) = (0, 0); - - foreach my $line (keys(%{$db})) { - my $ldata = $db->{$line}; - - foreach my $block (keys(%{$ldata})) { - my $bdata = $ldata->{$block}; - - foreach my $branch (keys(%{$bdata})) { - my $taken = $bdata->{$branch}; - - $br_found++; - $br_hit++ if ($taken ne "-" && $taken > 0); - } - } - } - - return ($br_found, $br_hit); -} - - -# combine_brcount(brcount1, brcount2, type, inplace) -# -# If add is BR_ADD, add branch coverage data and return list brcount_added. -# If add is BR_SUB, subtract the taken values of brcount2 from brcount1 and -# return brcount_sub. If inplace is set, the result is inserted into brcount1. -# - -sub combine_brcount($$$;$) -{ - my ($brcount1, $brcount2, $type, $inplace) = @_; - my ($db1, $db2); - - $db1 = brcount_to_db($brcount1); - $db2 = brcount_to_db($brcount2); - brcount_db_combine($db1, $db2, $type); - - return db_to_brcount($db1, $inplace ? $brcount1 : undef); -} - - -# -# add_testbrdata(testbrdata1, testbrdata2) -# -# Add branch coverage data for several tests. Return reference to -# added_testbrdata. -# - -sub add_testbrdata($$) -{ - my ($testbrdata1, $testbrdata2) = @_; - my %result; - my $testname; - - foreach $testname (keys(%{$testbrdata1})) { - if (defined($testbrdata2->{$testname})) { - my $brcount; - - # Branch coverage data for this testname exists - # in both data sets: add - ($brcount) = combine_brcount($testbrdata1->{$testname}, - $testbrdata2->{$testname}, $BR_ADD); - $result{$testname} = $brcount; - next; - } - # Branch coverage data for this testname is unique to - # data set 1: copy - $result{$testname} = $testbrdata1->{$testname}; - } - - # Add count data for testnames unique to data set 2 - foreach $testname (keys(%{$testbrdata2})) { - if (!defined($result{$testname})) { - $result{$testname} = $testbrdata2->{$testname}; - } - } - return \%result; -} - - -# -# combine_info_entries(entry_ref1, entry_ref2, filename) -# -# Combine .info data entry hashes referenced by ENTRY_REF1 and ENTRY_REF2. -# Return reference to resulting hash. -# - -sub combine_info_entries($$$) -{ - my $entry1 = $_[0]; # Reference to hash containing first entry - my $testdata1; - my $sumcount1; - my $funcdata1; - my $checkdata1; - my $testfncdata1; - my $sumfnccount1; - my $testbrdata1; - my $sumbrcount1; - - my $entry2 = $_[1]; # Reference to hash containing second entry - my $testdata2; - my $sumcount2; - my $funcdata2; - my $checkdata2; - my $testfncdata2; - my $sumfnccount2; - my $testbrdata2; - my $sumbrcount2; - - my %result; # Hash containing combined entry - my %result_testdata; - my $result_sumcount = {}; - my $result_funcdata; - my $result_testfncdata; - my $result_sumfnccount; - my $result_testbrdata; - my $result_sumbrcount; - my $lines_found; - my $lines_hit; - my $fn_found; - my $fn_hit; - my $br_found; - my $br_hit; - - my $testname; - my $filename = $_[2]; - - # Retrieve data - ($testdata1, $sumcount1, $funcdata1, $checkdata1, $testfncdata1, - $sumfnccount1, $testbrdata1, $sumbrcount1) = get_info_entry($entry1); - ($testdata2, $sumcount2, $funcdata2, $checkdata2, $testfncdata2, - $sumfnccount2, $testbrdata2, $sumbrcount2) = get_info_entry($entry2); - - # Merge checksums - $checkdata1 = merge_checksums($checkdata1, $checkdata2, $filename); - - # Combine funcdata - $result_funcdata = merge_func_data($funcdata1, $funcdata2, $filename); - - # Combine function call count data - $result_testfncdata = add_testfncdata($testfncdata1, $testfncdata2); - ($result_sumfnccount, $fn_found, $fn_hit) = - add_fnccount($sumfnccount1, $sumfnccount2); - - # Combine branch coverage data - $result_testbrdata = add_testbrdata($testbrdata1, $testbrdata2); - ($result_sumbrcount, $br_found, $br_hit) = - combine_brcount($sumbrcount1, $sumbrcount2, $BR_ADD); - - # Combine testdata - foreach $testname (keys(%{$testdata1})) - { - if (defined($testdata2->{$testname})) - { - # testname is present in both entries, requires - # combination - ($result_testdata{$testname}) = - add_counts($testdata1->{$testname}, - $testdata2->{$testname}); - } - else - { - # testname only present in entry1, add to result - $result_testdata{$testname} = $testdata1->{$testname}; - } - - # update sum count hash - ($result_sumcount, $lines_found, $lines_hit) = - add_counts($result_sumcount, - $result_testdata{$testname}); - } - - foreach $testname (keys(%{$testdata2})) - { - # Skip testnames already covered by previous iteration - if (defined($testdata1->{$testname})) { next; } - - # testname only present in entry2, add to result hash - $result_testdata{$testname} = $testdata2->{$testname}; - - # update sum count hash - ($result_sumcount, $lines_found, $lines_hit) = - add_counts($result_sumcount, - $result_testdata{$testname}); - } - - # Calculate resulting sumcount - - # Store result - set_info_entry(\%result, \%result_testdata, $result_sumcount, - $result_funcdata, $checkdata1, $result_testfncdata, - $result_sumfnccount, $result_testbrdata, - $result_sumbrcount, $lines_found, $lines_hit, - $fn_found, $fn_hit, $br_found, $br_hit); - - return(\%result); -} - - -# -# combine_info_files(info_ref1, info_ref2) -# -# Combine .info data in hashes referenced by INFO_REF1 and INFO_REF2. Return -# reference to resulting hash. -# - -sub combine_info_files($$) -{ - my %hash1 = %{$_[0]}; - my %hash2 = %{$_[1]}; - my $filename; - - foreach $filename (keys(%hash2)) - { - if ($hash1{$filename}) - { - # Entry already exists in hash1, combine them - $hash1{$filename} = - combine_info_entries($hash1{$filename}, - $hash2{$filename}, - $filename); - } - else - { - # Entry is unique in both hashes, simply add to - # resulting hash - $hash1{$filename} = $hash2{$filename}; - } - } - - return(\%hash1); -} - - -# -# get_prefix(min_dir, filename_list) -# -# Search FILENAME_LIST for a directory prefix which is common to as many -# list entries as possible, so that removing this prefix will minimize the -# sum of the lengths of all resulting shortened filenames while observing -# that no filename has less than MIN_DIR parent directories. -# - -sub get_prefix($@) -{ - my ($min_dir, @filename_list) = @_; - my %prefix; # mapping: prefix -> sum of lengths - my $current; # Temporary iteration variable - - # Find list of prefixes - foreach (@filename_list) - { - # Need explicit assignment to get a copy of $_ so that - # shortening the contained prefix does not affect the list - $current = $_; - while ($current = shorten_prefix($current)) - { - $current .= "/"; - - # Skip rest if the remaining prefix has already been - # added to hash - if (exists($prefix{$current})) { last; } - - # Initialize with 0 - $prefix{$current}="0"; - } - - } - - # Remove all prefixes that would cause filenames to have less than - # the minimum number of parent directories - foreach my $filename (@filename_list) { - my $dir = dirname($filename); - - for (my $i = 0; $i < $min_dir; $i++) { - delete($prefix{$dir."/"}); - $dir = shorten_prefix($dir); - } - } - - # Check if any prefix remains - return undef if (!%prefix); - - # Calculate sum of lengths for all prefixes - foreach $current (keys(%prefix)) - { - foreach (@filename_list) - { - # Add original length - $prefix{$current} += length($_); - - # Check whether prefix matches - if (substr($_, 0, length($current)) eq $current) - { - # Subtract prefix length for this filename - $prefix{$current} -= length($current); - } - } - } - - # Find and return prefix with minimal sum - $current = (keys(%prefix))[0]; - - foreach (keys(%prefix)) - { - if ($prefix{$_} < $prefix{$current}) - { - $current = $_; - } - } - - $current =~ s/\/$//; - - return($current); -} - - -# -# shorten_prefix(prefix) -# -# Return PREFIX shortened by last directory component. -# - -sub shorten_prefix($) -{ - my @list = split("/", $_[0]); - - pop(@list); - return join("/", @list); -} - - - -# -# get_dir_list(filename_list) -# -# Return sorted list of directories for each entry in given FILENAME_LIST. -# - -sub get_dir_list(@) -{ - my %result; - - foreach (@_) - { - $result{shorten_prefix($_)} = ""; - } - - return(sort(keys(%result))); -} - - -# -# get_relative_base_path(subdirectory) -# -# Return a relative path string which references the base path when applied -# in SUBDIRECTORY. -# -# Example: get_relative_base_path("fs/mm") -> "../../" -# - -sub get_relative_base_path($) -{ - my $result = ""; - my $index; - - # Make an empty directory path a special case - if (!$_[0]) { return(""); } - - # Count number of /s in path - $index = ($_[0] =~ s/\//\//g); - - # Add a ../ to $result for each / in the directory path + 1 - for (; $index>=0; $index--) - { - $result .= "../"; - } - - return $result; -} - - -# -# read_testfile(test_filename) -# -# Read in file TEST_FILENAME which contains test descriptions in the format: -# -# TN: -# TD: -# -# for each test case. Return a reference to a hash containing a mapping -# -# test name -> test description. -# -# Die on error. -# - -sub read_testfile($) -{ - my %result; - my $test_name; - my $changed_testname; - local *TEST_HANDLE; - - open(TEST_HANDLE, "<", $_[0]) - or die("ERROR: cannot open $_[0]!\n"); - - while () - { - chomp($_); - - # Match lines beginning with TN: - if (/^TN:\s+(.*?)\s*$/) - { - # Store name for later use - $test_name = $1; - if ($test_name =~ s/\W/_/g) - { - $changed_testname = 1; - } - } - - # Match lines beginning with TD: - if (/^TD:\s+(.*?)\s*$/) - { - if (!defined($test_name)) { - die("ERROR: Found test description without prior test name in $_[0]:$.\n"); - } - # Check for empty line - if ($1) - { - # Add description to hash - $result{$test_name} .= " $1"; - } - else - { - # Add empty line - $result{$test_name} .= "\n\n"; - } - } - } - - close(TEST_HANDLE); - - if ($changed_testname) - { - warn("WARNING: invalid characters removed from testname in ". - "descriptions file $_[0]\n"); - } - - return \%result; -} - - -# -# escape_html(STRING) -# -# Return a copy of STRING in which all occurrences of HTML special characters -# are escaped. -# - -sub escape_html($) -{ - my $string = $_[0]; - - if (!$string) { return ""; } - - $string =~ s/&/&/g; # & -> & - $string =~ s/ < - $string =~ s/>/>/g; # > -> > - $string =~ s/\"/"/g; # " -> " - - while ($string =~ /^([^\t]*)(\t)/) - { - my $replacement = " "x($tab_size - (length($1) % $tab_size)); - $string =~ s/^([^\t]*)(\t)/$1$replacement/; - } - - $string =~ s/\n/
/g; # \n ->
- - return $string; -} - - -# -# get_date_string() -# -# Return the current date in the form: yyyy-mm-dd -# - -sub get_date_string() -{ - my $year; - my $month; - my $day; - my $hour; - my $min; - my $sec; - my @timeresult; - - if (defined $ENV{'SOURCE_DATE_EPOCH'}) - { - @timeresult = gmtime($ENV{'SOURCE_DATE_EPOCH'}); - } - else - { - @timeresult = localtime(); - } - ($year, $month, $day, $hour, $min, $sec) = - @timeresult[5, 4, 3, 2, 1, 0]; - - return sprintf("%d-%02d-%02d %02d:%02d:%02d", $year+1900, $month+1, - $day, $hour, $min, $sec); -} - - -# -# create_sub_dir(dir_name) -# -# Create subdirectory DIR_NAME if it does not already exist, including all its -# parent directories. -# -# Die on error. -# - -sub create_sub_dir($) -{ - my ($dir) = @_; - - system("mkdir", "-p" ,$dir) - and die("ERROR: cannot create directory $dir!\n"); -} - - -# -# write_description_file(descriptions, overall_found, overall_hit, -# total_fn_found, total_fn_hit, total_br_found, -# total_br_hit) -# -# Write HTML file containing all test case descriptions. DESCRIPTIONS is a -# reference to a hash containing a mapping -# -# test case name -> test case description -# -# Die on error. -# - -sub write_description_file($$$$$$$) -{ - my %description = %{$_[0]}; - my $found = $_[1]; - my $hit = $_[2]; - my $fn_found = $_[3]; - my $fn_hit = $_[4]; - my $br_found = $_[5]; - my $br_hit = $_[6]; - my $test_name; - local *HTML_HANDLE; - - html_create(*HTML_HANDLE,"descriptions.$html_ext"); - write_html_prolog(*HTML_HANDLE, "", "LCOV - test case descriptions"); - write_header(*HTML_HANDLE, 3, "", "", $found, $hit, $fn_found, - $fn_hit, $br_found, $br_hit, 0); - - write_test_table_prolog(*HTML_HANDLE, - "Test case descriptions - alphabetical list"); - - foreach $test_name (sort(keys(%description))) - { - my $desc = $description{$test_name}; - - $desc = escape_html($desc) if (!$rc_desc_html); - write_test_table_entry(*HTML_HANDLE, $test_name, $desc); - } - - write_test_table_epilog(*HTML_HANDLE); - write_html_epilog(*HTML_HANDLE, ""); - - close(*HTML_HANDLE); -} - - - -# -# write_png_files() -# -# Create all necessary .png files for the HTML-output in the current -# directory. .png-files are used as bar graphs. -# -# Die on error. -# - -sub write_png_files() -{ - my %data; - local *PNG_HANDLE; - - $data{"ruby.png"} = - [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, - 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, - 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, - 0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x18, 0x10, 0x5d, 0x57, - 0x34, 0x6e, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, - 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, - 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, - 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, - 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0x35, 0x2f, - 0x00, 0x00, 0x00, 0xd0, 0x33, 0x9a, 0x9d, 0x00, 0x00, 0x00, - 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00, - 0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00, - 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, - 0x82]; - $data{"amber.png"} = - [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, - 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, - 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, - 0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x28, 0x04, 0x98, 0xcb, - 0xd6, 0xe0, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, - 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, - 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, - 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, - 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0xe0, 0x50, - 0x00, 0x00, 0x00, 0xa2, 0x7a, 0xda, 0x7e, 0x00, 0x00, 0x00, - 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00, - 0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00, - 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, - 0x82]; - $data{"emerald.png"} = - [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, - 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, - 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, - 0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x22, 0x2b, 0xc9, 0xf5, - 0x03, 0x33, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, - 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, - 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, - 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, - 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0x1b, 0xea, 0x59, - 0x0a, 0x0a, 0x0a, 0x0f, 0xba, 0x50, 0x83, 0x00, 0x00, 0x00, - 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00, - 0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00, - 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, - 0x82]; - $data{"snow.png"} = - [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, - 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, - 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, - 0x45, 0x07, 0xd2, 0x07, 0x11, 0x0f, 0x1e, 0x1d, 0x75, 0xbc, - 0xef, 0x55, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, - 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, 0xd2, - 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, - 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, - 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x55, 0xc2, 0xd3, 0x7e, 0x00, 0x00, 0x00, - 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x60, 0x00, - 0x00, 0x00, 0x02, 0x00, 0x01, 0xe5, 0x27, 0xde, 0xfc, 0x00, - 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, - 0x82]; - $data{"glass.png"} = - [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, - 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, - 0xdb, 0x56, 0xca, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, - 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, - 0x00, 0x00, 0x06, 0x50, 0x4c, 0x54, 0x45, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x55, 0xc2, 0xd3, 0x7e, 0x00, 0x00, 0x00, - 0x01, 0x74, 0x52, 0x4e, 0x53, 0x00, 0x40, 0xe6, 0xd8, 0x66, - 0x00, 0x00, 0x00, 0x01, 0x62, 0x4b, 0x47, 0x44, 0x00, 0x88, - 0x05, 0x1d, 0x48, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, - 0x73, 0x00, 0x00, 0x0b, 0x12, 0x00, 0x00, 0x0b, 0x12, 0x01, - 0xd2, 0xdd, 0x7e, 0xfc, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, - 0x4d, 0x45, 0x07, 0xd2, 0x07, 0x13, 0x0f, 0x08, 0x19, 0xc4, - 0x40, 0x56, 0x10, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, - 0x54, 0x78, 0x9c, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, - 0x01, 0x48, 0xaf, 0xa4, 0x71, 0x00, 0x00, 0x00, 0x00, 0x49, - 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82]; - $data{"updown.png"} = - [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, - 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x0a, - 0x00, 0x00, 0x00, 0x0e, 0x08, 0x06, 0x00, 0x00, 0x00, 0x16, - 0xa3, 0x8d, 0xab, 0x00, 0x00, 0x00, 0x3c, 0x49, 0x44, 0x41, - 0x54, 0x28, 0xcf, 0x63, 0x60, 0x40, 0x03, 0xff, 0xa1, 0x00, - 0x5d, 0x9c, 0x11, 0x5d, 0x11, 0x8a, 0x24, 0x23, 0x23, 0x23, - 0x86, 0x42, 0x6c, 0xa6, 0x20, 0x2b, 0x66, 0xc4, 0xa7, 0x08, - 0x59, 0x31, 0x23, 0x21, 0x45, 0x30, 0xc0, 0xc4, 0x30, 0x60, - 0x80, 0xfa, 0x6e, 0x24, 0x3e, 0x78, 0x48, 0x0a, 0x70, 0x62, - 0xa2, 0x90, 0x81, 0xd8, 0x44, 0x01, 0x00, 0xe9, 0x5c, 0x2f, - 0xf5, 0xe2, 0x9d, 0x0f, 0xf9, 0x00, 0x00, 0x00, 0x00, 0x49, - 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82] if ($sort); - foreach (keys(%data)) - { - open(PNG_HANDLE, ">", $_) - or die("ERROR: cannot create $_!\n"); - binmode(PNG_HANDLE); - print(PNG_HANDLE map(chr,@{$data{$_}})); - close(PNG_HANDLE); - } -} - - -# -# write_htaccess_file() -# - -sub write_htaccess_file() -{ - local *HTACCESS_HANDLE; - my $htaccess_data; - - open(*HTACCESS_HANDLE, ">", ".htaccess") - or die("ERROR: cannot open .htaccess for writing!\n"); - - $htaccess_data = (<<"END_OF_HTACCESS") -AddEncoding x-gzip .html -END_OF_HTACCESS - ; - - print(HTACCESS_HANDLE $htaccess_data); - close(*HTACCESS_HANDLE); -} - - -# -# write_css_file() -# -# Write the cascading style sheet file gcov.css to the current directory. -# This file defines basic layout attributes of all generated HTML pages. -# - -sub write_css_file() -{ - local *CSS_HANDLE; - - # Check for a specified external style sheet file - if ($css_filename) - { - # Simply copy that file - system("cp", $css_filename, "gcov.css") - and die("ERROR: cannot copy file $css_filename!\n"); - return; - } - - open(CSS_HANDLE, ">", "gcov.css") - or die ("ERROR: cannot open gcov.css for writing!\n"); - - - # ************************************************************* - - my $css_data = ($_=<<"END_OF_CSS") - /* All views: initial background and text color */ - body - { - color: #000000; - background-color: #FFFFFF; - } - - /* All views: standard link format*/ - a:link - { - color: #284FA8; - text-decoration: underline; - } - - /* All views: standard link - visited format */ - a:visited - { - color: #00CB40; - text-decoration: underline; - } - - /* All views: standard link - activated format */ - a:active - { - color: #FF0040; - text-decoration: underline; - } - - /* All views: main title format */ - td.title - { - text-align: center; - padding-bottom: 10px; - font-family: sans-serif; - font-size: 20pt; - font-style: italic; - font-weight: bold; - } - - /* All views: header item format */ - td.headerItem - { - text-align: right; - padding-right: 6px; - font-family: sans-serif; - font-weight: bold; - vertical-align: top; - white-space: nowrap; - } - - /* All views: header item value format */ - td.headerValue - { - text-align: left; - color: #284FA8; - font-family: sans-serif; - font-weight: bold; - white-space: nowrap; - } - - /* All views: header item coverage table heading */ - td.headerCovTableHead - { - text-align: center; - padding-right: 6px; - padding-left: 6px; - padding-bottom: 0px; - font-family: sans-serif; - font-size: 80%; - white-space: nowrap; - } - - /* All views: header item coverage table entry */ - td.headerCovTableEntry - { - text-align: right; - color: #284FA8; - font-family: sans-serif; - font-weight: bold; - white-space: nowrap; - padding-left: 12px; - padding-right: 4px; - background-color: #DAE7FE; - } - - /* All views: header item coverage table entry for high coverage rate */ - td.headerCovTableEntryHi - { - text-align: right; - color: #000000; - font-family: sans-serif; - font-weight: bold; - white-space: nowrap; - padding-left: 12px; - padding-right: 4px; - background-color: #A7FC9D; - } - - /* All views: header item coverage table entry for medium coverage rate */ - td.headerCovTableEntryMed - { - text-align: right; - color: #000000; - font-family: sans-serif; - font-weight: bold; - white-space: nowrap; - padding-left: 12px; - padding-right: 4px; - background-color: #FFEA20; - } - - /* All views: header item coverage table entry for ow coverage rate */ - td.headerCovTableEntryLo - { - text-align: right; - color: #000000; - font-family: sans-serif; - font-weight: bold; - white-space: nowrap; - padding-left: 12px; - padding-right: 4px; - background-color: #FF0000; - } - - /* All views: header legend value for legend entry */ - td.headerValueLeg - { - text-align: left; - color: #000000; - font-family: sans-serif; - font-size: 80%; - white-space: nowrap; - padding-top: 4px; - } - - /* All views: color of horizontal ruler */ - td.ruler - { - background-color: #6688D4; - } - - /* All views: version string format */ - td.versionInfo - { - text-align: center; - padding-top: 2px; - font-family: sans-serif; - font-style: italic; - } - - /* Directory view/File view (all)/Test case descriptions: - table headline format */ - td.tableHead - { - text-align: center; - color: #FFFFFF; - background-color: #6688D4; - font-family: sans-serif; - font-size: 120%; - font-weight: bold; - white-space: nowrap; - padding-left: 4px; - padding-right: 4px; - } - - span.tableHeadSort - { - padding-right: 4px; - } - - /* Directory view/File view (all): filename entry format */ - td.coverFile - { - text-align: left; - padding-left: 10px; - padding-right: 20px; - color: #284FA8; - background-color: #DAE7FE; - font-family: monospace; - } - - /* Directory view/File view (all): bar-graph entry format*/ - td.coverBar - { - padding-left: 10px; - padding-right: 10px; - background-color: #DAE7FE; - } - - /* Directory view/File view (all): bar-graph outline color */ - td.coverBarOutline - { - background-color: #000000; - } - - /* Directory view/File view (all): percentage entry for files with - high coverage rate */ - td.coverPerHi - { - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #A7FC9D; - font-weight: bold; - font-family: sans-serif; - } - - /* Directory view/File view (all): line count entry for files with - high coverage rate */ - td.coverNumHi - { - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #A7FC9D; - white-space: nowrap; - font-family: sans-serif; - } - - /* Directory view/File view (all): percentage entry for files with - medium coverage rate */ - td.coverPerMed - { - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #FFEA20; - font-weight: bold; - font-family: sans-serif; - } - - /* Directory view/File view (all): line count entry for files with - medium coverage rate */ - td.coverNumMed - { - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #FFEA20; - white-space: nowrap; - font-family: sans-serif; - } - - /* Directory view/File view (all): percentage entry for files with - low coverage rate */ - td.coverPerLo - { - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #FF0000; - font-weight: bold; - font-family: sans-serif; - } - - /* Directory view/File view (all): line count entry for files with - low coverage rate */ - td.coverNumLo - { - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #FF0000; - white-space: nowrap; - font-family: sans-serif; - } - - /* File view (all): "show/hide details" link format */ - a.detail:link - { - color: #B8D0FF; - font-size:80%; - } - - /* File view (all): "show/hide details" link - visited format */ - a.detail:visited - { - color: #B8D0FF; - font-size:80%; - } - - /* File view (all): "show/hide details" link - activated format */ - a.detail:active - { - color: #FFFFFF; - font-size:80%; - } - - /* File view (detail): test name entry */ - td.testName - { - text-align: right; - padding-right: 10px; - background-color: #DAE7FE; - font-family: sans-serif; - } - - /* File view (detail): test percentage entry */ - td.testPer - { - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #DAE7FE; - font-family: sans-serif; - } - - /* File view (detail): test lines count entry */ - td.testNum - { - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #DAE7FE; - font-family: sans-serif; - } - - /* Test case descriptions: test name format*/ - dt - { - font-family: sans-serif; - font-weight: bold; - } - - /* Test case descriptions: description table body */ - td.testDescription - { - padding-top: 10px; - padding-left: 30px; - padding-bottom: 10px; - padding-right: 30px; - background-color: #DAE7FE; - } - - /* Source code view: function entry */ - td.coverFn - { - text-align: left; - padding-left: 10px; - padding-right: 20px; - color: #284FA8; - background-color: #DAE7FE; - font-family: monospace; - } - - /* Source code view: function entry zero count*/ - td.coverFnLo - { - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #FF0000; - font-weight: bold; - font-family: sans-serif; - } - - /* Source code view: function entry nonzero count*/ - td.coverFnHi - { - text-align: right; - padding-left: 10px; - padding-right: 10px; - background-color: #DAE7FE; - font-weight: bold; - font-family: sans-serif; - } - - /* Source code view: source code format */ - pre.source - { - font-family: monospace; - white-space: pre; - margin-top: 2px; - } - - /* Source code view: line number format */ - span.lineNum - { - background-color: #EFE383; - } - - /* Source code view: format for lines which were executed */ - td.lineCov, - span.lineCov - { - background-color: #CAD7FE; - } - - /* Source code view: format for Cov legend */ - span.coverLegendCov - { - padding-left: 10px; - padding-right: 10px; - padding-bottom: 2px; - background-color: #CAD7FE; - } - - /* Source code view: format for lines which were not executed */ - td.lineNoCov, - span.lineNoCov - { - background-color: #FF6230; - } - - /* Source code view: format for NoCov legend */ - span.coverLegendNoCov - { - padding-left: 10px; - padding-right: 10px; - padding-bottom: 2px; - background-color: #FF6230; - } - - /* Source code view (function table): standard link - visited format */ - td.lineNoCov > a:visited, - td.lineCov > a:visited - { - color: black; - text-decoration: underline; - } - - /* Source code view: format for lines which were executed only in a - previous version */ - span.lineDiffCov - { - background-color: #B5F7AF; - } - - /* Source code view: format for branches which were executed - * and taken */ - span.branchCov - { - background-color: #CAD7FE; - } - - /* Source code view: format for branches which were executed - * but not taken */ - span.branchNoCov - { - background-color: #FF6230; - } - - /* Source code view: format for branches which were not executed */ - span.branchNoExec - { - background-color: #FF6230; - } - - /* Source code view: format for the source code heading line */ - pre.sourceHeading - { - white-space: pre; - font-family: monospace; - font-weight: bold; - margin: 0px; - } - - /* All views: header legend value for low rate */ - td.headerValueLegL - { - font-family: sans-serif; - text-align: center; - white-space: nowrap; - padding-left: 4px; - padding-right: 2px; - background-color: #FF0000; - font-size: 80%; - } - - /* All views: header legend value for med rate */ - td.headerValueLegM - { - font-family: sans-serif; - text-align: center; - white-space: nowrap; - padding-left: 2px; - padding-right: 2px; - background-color: #FFEA20; - font-size: 80%; - } - - /* All views: header legend value for hi rate */ - td.headerValueLegH - { - font-family: sans-serif; - text-align: center; - white-space: nowrap; - padding-left: 2px; - padding-right: 4px; - background-color: #A7FC9D; - font-size: 80%; - } - - /* All views except source code view: legend format for low coverage */ - span.coverLegendCovLo - { - padding-left: 10px; - padding-right: 10px; - padding-top: 2px; - background-color: #FF0000; - } - - /* All views except source code view: legend format for med coverage */ - span.coverLegendCovMed - { - padding-left: 10px; - padding-right: 10px; - padding-top: 2px; - background-color: #FFEA20; - } - - /* All views except source code view: legend format for hi coverage */ - span.coverLegendCovHi - { - padding-left: 10px; - padding-right: 10px; - padding-top: 2px; - background-color: #A7FC9D; - } -END_OF_CSS - ; - - # ************************************************************* - - - # Remove leading tab from all lines - $css_data =~ s/^\t//gm; - - print(CSS_HANDLE $css_data); - - close(CSS_HANDLE); -} - - -# -# get_bar_graph_code(base_dir, cover_found, cover_hit) -# -# Return a string containing HTML code which implements a bar graph display -# for a coverage rate of cover_hit * 100 / cover_found. -# - -sub get_bar_graph_code($$$) -{ - my ($base_dir, $found, $hit) = @_; - my $rate; - my $alt; - my $width; - my $remainder; - my $png_name; - my $graph_code; - - # Check number of instrumented lines - if ($_[1] == 0) { return ""; } - - $alt = rate($hit, $found, "%"); - $width = rate($hit, $found, undef, 0); - $remainder = 100 - $width; - - # Decide which .png file to use - $png_name = $rate_png[classify_rate($found, $hit, $med_limit, - $hi_limit)]; - - if ($width == 0) - { - # Zero coverage - $graph_code = (<$alt -END_OF_HTML - ; - } - elsif ($width == 100) - { - # Full coverage - $graph_code = (<$alt -END_OF_HTML - ; - } - else - { - # Positive coverage - $graph_code = (<$alt$alt -END_OF_HTML - ; - } - - # Remove leading tabs from all lines - $graph_code =~ s/^\t+//gm; - chomp($graph_code); - - return($graph_code); -} - -# -# sub classify_rate(found, hit, med_limit, high_limit) -# -# Return 0 for low rate, 1 for medium rate and 2 for hi rate. -# - -sub classify_rate($$$$) -{ - my ($found, $hit, $med, $hi) = @_; - my $rate; - - if ($found == 0) { - return 2; - } - $rate = rate($hit, $found); - if ($rate < $med) { - return 0; - } elsif ($rate < $hi) { - return 1; - } - return 2; -} - - -# -# write_html(filehandle, html_code) -# -# Write out HTML_CODE to FILEHANDLE while removing a leading tabulator mark -# in each line of HTML_CODE. -# - -sub write_html(*$) -{ - local *HTML_HANDLE = $_[0]; - my $html_code = $_[1]; - - # Remove leading tab from all lines - $html_code =~ s/^\t//gm; - - print(HTML_HANDLE $html_code) - or die("ERROR: cannot write HTML data ($!)\n"); -} - - -# -# write_html_prolog(filehandle, base_dir, pagetitle) -# -# Write an HTML prolog common to all HTML files to FILEHANDLE. PAGETITLE will -# be used as HTML page title. BASE_DIR contains a relative path which points -# to the base directory. -# - -sub write_html_prolog(*$$) -{ - my $basedir = $_[1]; - my $pagetitle = $_[2]; - my $prolog; - - $prolog = $html_prolog; - $prolog =~ s/\@pagetitle\@/$pagetitle/g; - $prolog =~ s/\@basedir\@/$basedir/g; - - write_html($_[0], $prolog); -} - - -# -# write_header_prolog(filehandle, base_dir) -# -# Write beginning of page header HTML code. -# - -sub write_header_prolog(*$) -{ - # ************************************************************* - - write_html($_[0], < - $title - - - - - -END_OF_HTML - ; - - # ************************************************************* -} - - -# -# write_header_line(handle, content) -# -# Write a header line with the specified table contents. -# - -sub write_header_line(*@) -{ - my ($handle, @content) = @_; - my $entry; - - write_html($handle, " \n"); - foreach $entry (@content) { - my ($width, $class, $text, $colspan) = @{$entry}; - - if (defined($width)) { - $width = " width=\"$width\""; - } else { - $width = ""; - } - if (defined($class)) { - $class = " class=\"$class\""; - } else { - $class = ""; - } - if (defined($colspan)) { - $colspan = " colspan=\"$colspan\""; - } else { - $colspan = ""; - } - $text = "" if (!defined($text)); - write_html($handle, - " $text\n"); - } - write_html($handle, " \n"); -} - - -# -# write_header_epilog(filehandle, base_dir) -# -# Write end of page header HTML code. -# - -sub write_header_epilog(*$) -{ - # ************************************************************* - - write_html($_[0], < -
- - - - - - -END_OF_HTML - ; - - # ************************************************************* -} - - -# -# write_file_table_prolog(handle, file_heading, ([heading, num_cols], ...)) -# -# Write heading for file table. -# - -sub write_file_table_prolog(*$@) -{ - my ($handle, $file_heading, @columns) = @_; - my $num_columns = 0; - my $file_width; - my $col; - my $width; - - $width = 20 if (scalar(@columns) == 1); - $width = 10 if (scalar(@columns) == 2); - $width = 8 if (scalar(@columns) > 2); - - foreach $col (@columns) { - my ($heading, $cols) = @{$col}; - - $num_columns += $cols; - } - $file_width = 100 - $num_columns * $width; - - # Table definition - write_html($handle, < - - - - -END_OF_HTML - # Empty first row - foreach $col (@columns) { - my ($heading, $cols) = @{$col}; - - while ($cols-- > 0) { - write_html($handle, < -END_OF_HTML - } - } - # Next row - write_html($handle, < - - - -END_OF_HTML - # Heading row - foreach $col (@columns) { - my ($heading, $cols) = @{$col}; - my $colspan = ""; - - $colspan = " colspan=$cols" if ($cols > 1); - write_html($handle, <$heading -END_OF_HTML - } - write_html($handle, < -END_OF_HTML -} - - -# write_file_table_entry(handle, base_dir, filename, page_link, -# ([ found, hit, med_limit, hi_limit, graph ], ..) -# -# Write an entry of the file table. -# - -sub write_file_table_entry(*$$$@) -{ - my ($handle, $base_dir, $filename, $page_link, @entries) = @_; - my $file_code; - my $entry; - my $esc_filename = escape_html($filename); - - # Add link to source if provided - if (defined($page_link) && $page_link ne "") { - $file_code = "$esc_filename"; - } else { - $file_code = $esc_filename; - } - - # First column: filename - write_html($handle, < - -END_OF_HTML - # Columns as defined - foreach $entry (@entries) { - my ($found, $hit, $med, $hi, $graph) = @{$entry}; - my $bar_graph; - my $class; - my $rate; - - # Generate bar graph if requested - if ($graph) { - $bar_graph = get_bar_graph_code($base_dir, $found, - $hit); - write_html($handle, < - $bar_graph - -END_OF_HTML - } - # Get rate color and text - if ($found == 0) { - $rate = "-"; - $class = "Hi"; - } else { - $rate = rate($hit, $found, " %"); - $class = $rate_name[classify_rate($found, $hit, - $med, $hi)]; - } - if ($opt_missed) { - # Show negative number of items without coverage - $hit = -($found - $hit); - } - write_html($handle, <$rate - -END_OF_HTML - } - # End of row - write_html($handle, < -END_OF_HTML -} - - -# -# write_file_table_detail_entry(filehandle, test_name, ([found, hit], ...)) -# -# Write entry for detail section in file table. -# - -sub write_file_table_detail_entry(*$@) -{ - my ($handle, $test, @entries) = @_; - my $entry; - - if ($test eq "") { - $test = "<unnamed>"; - } elsif ($test =~ /^(.*),diff$/) { - $test = $1." (converted)"; - } - # Testname - write_html($handle, < - -END_OF_HTML - # Test data - foreach $entry (@entries) { - my ($found, $hit) = @{$entry}; - my $rate = rate($hit, $found, " %"); - - write_html($handle, <$rate - -END_OF_HTML - } - - write_html($handle, < - -END_OF_HTML - - # ************************************************************* -} - - -# -# write_file_table_epilog(filehandle) -# -# Write end of file table HTML code. -# - -sub write_file_table_epilog(*) -{ - # ************************************************************* - - write_html($_[0], < - -
- -END_OF_HTML - ; - - # ************************************************************* -} - - -# -# write_test_table_prolog(filehandle, table_heading) -# -# Write heading for test case description table. -# - -sub write_test_table_prolog(*$) -{ - # ************************************************************* - - write_html($_[0], < -

$file_heading$file_code$hit / $found$test$hit / $found
- - - - - - - - - - - - -

$_[1]
-
-END_OF_HTML - ; - - # ************************************************************* -} - - -# -# write_test_table_entry(filehandle, test_name, test_description) -# -# Write entry for the test table. -# - -sub write_test_table_entry(*$$) -{ - # ************************************************************* - - write_html($_[0], <$_[1]  -
$_[2]

-END_OF_HTML - ; - - # ************************************************************* -} - - -# -# write_test_table_epilog(filehandle) -# -# Write end of test description table HTML code. -# - -sub write_test_table_epilog(*) -{ - # ************************************************************* - - write_html($_[0], < -
- -
- -END_OF_HTML - ; - - # ************************************************************* -} - - -sub fmt_centered($$) -{ - my ($width, $text) = @_; - my $w0 = length($text); - my $w1 = $width > $w0 ? int(($width - $w0) / 2) : 0; - my $w2 = $width > $w0 ? $width - $w0 - $w1 : 0; - - return (" "x$w1).$text.(" "x$w2); -} - - -# -# write_source_prolog(filehandle) -# -# Write start of source code table. -# - -sub write_source_prolog(*) -{ - my $lineno_heading = " "; - my $branch_heading = ""; - my $line_heading = fmt_centered($line_field_width, "Line data"); - my $source_heading = " Source code"; - - if ($br_coverage) { - $branch_heading = fmt_centered($br_field_width, "Branch data"). - " "; - } - # ************************************************************* - - write_html($_[0], < - -
- - - -
${lineno_heading}${branch_heading}${line_heading} ${source_heading}
-
-END_OF_HTML
-	;
-
-	# *************************************************************
-}
-
-sub cmp_blocks($$)
-{
-	my ($a, $b) = @_;
-	my ($fa, $fb) = ($a->[0], $b->[0]);
-
-	return $fa->[0] <=> $fb->[0] if ($fa->[0] != $fb->[0]);
-	return $fa->[1] <=> $fb->[1];
-}
-
-#
-# get_branch_blocks(brdata)
-#
-# Group branches that belong to the same basic block.
-#
-# Returns: [block1, block2, ...]
-# block:   [branch1, branch2, ...]
-# branch:  [block_num, branch_num, taken_count, text_length, open, close]
-#
-
-sub get_branch_blocks($)
-{
-	my ($brdata) = @_;
-	my $last_block_num;
-	my $block = [];
-	my @blocks;
-
-	return () if (!defined($brdata));
-
-	# Group branches
-	foreach my $entry (split(/:/, $brdata)) {
-		my ($block_num, $branch, $taken) = split(/,/, $entry);
-		my $br;
-
-		if (defined($last_block_num) && $block_num != $last_block_num) {
-			push(@blocks, $block);
-			$block = [];
-		}
-		$br = [$block_num, $branch, $taken, 3, 0, 0];
-		push(@{$block}, $br);
-		$last_block_num = $block_num;
-	}
-	push(@blocks, $block) if (scalar(@{$block}) > 0);
-
-	# Add braces to first and last branch in group
-	foreach $block (@blocks) {
-		$block->[0]->[$BR_OPEN] = 1;
-		$block->[0]->[$BR_LEN]++;
-		$block->[scalar(@{$block}) - 1]->[$BR_CLOSE] = 1;
-		$block->[scalar(@{$block}) - 1]->[$BR_LEN]++;
-	}
-
-	return sort(cmp_blocks @blocks);
-}
-
-#
-# get_block_len(block)
-#
-# Calculate total text length of all branches in a block of branches.
-#
-
-sub get_block_len($)
-{
-	my ($block) = @_;
-	my $len = 0;
-	my $branch;
-
-	foreach $branch (@{$block}) {
-		$len += $branch->[$BR_LEN];
-	}
-
-	return $len;
-}
-
-
-#
-# get_branch_html(brdata)
-#
-# Return a list of HTML lines which represent the specified branch coverage
-# data in source code view.
-#
-
-sub get_branch_html($)
-{
-	my ($brdata) = @_;
-	my @blocks = get_branch_blocks($brdata);
-	my $block;
-	my $branch;
-	my $line_len = 0;
-	my $line = [];	# [branch2|" ", branch|" ", ...]
-	my @lines;	# [line1, line2, ...]
-	my @result;
-
-	# Distribute blocks to lines
-	foreach $block (@blocks) {
-		my $block_len = get_block_len($block);
-
-		# Does this block fit into the current line?
-		if ($line_len + $block_len <= $br_field_width) {
-			# Add it
-			$line_len += $block_len;
-			push(@{$line}, @{$block});
-			next;
-		} elsif ($block_len <= $br_field_width) {
-			# It would fit if the line was empty - add it to new
-			# line
-			push(@lines, $line);
-			$line_len = $block_len;
-			$line = [ @{$block} ];
-			next;
-		}
-		# Split the block into several lines
-		foreach $branch (@{$block}) {
-			if ($line_len + $branch->[$BR_LEN] >= $br_field_width) {
-				# Start a new line
-				if (($line_len + 1 <= $br_field_width) &&
-				    scalar(@{$line}) > 0 &&
-				    !$line->[scalar(@$line) - 1]->[$BR_CLOSE]) {
-					# Try to align branch symbols to be in
-					# one # row
-					push(@{$line}, " ");
-				}
-				push(@lines, $line);
-				$line_len = 0;
-				$line = [];
-			}
-			push(@{$line}, $branch);
-			$line_len += $branch->[$BR_LEN];
-		}
-	}
-	push(@lines, $line);
-
-	# Convert to HTML
-	foreach $line (@lines) {
-		my $current = "";
-		my $current_len = 0;
-
-		foreach $branch (@$line) {
-			# Skip alignment space
-			if ($branch eq " ") {
-				$current .= " ";
-				$current_len++;
-				next;
-			}
-
-			my ($block_num, $br_num, $taken, $len, $open, $close) =
-			   @{$branch};
-			my $class;
-			my $title;
-			my $text;
-
-			if ($taken eq '-') {
-				$class	= "branchNoExec";
-				$text	= " # ";
-				$title	= "Branch $br_num was not executed";
-			} elsif ($taken == 0) {
-				$class	= "branchNoCov";
-				$text	= " - ";
-				$title	= "Branch $br_num was not taken";
-			} else {
-				$class	= "branchCov";
-				$text	= " + ";
-				$title	= "Branch $br_num was taken $taken ".
-					  "time";
-				$title .= "s" if ($taken > 1);
-			}
-			$current .= "[" if ($open);
-			$current .= "";
-			$current .= $text."";
-			$current .= "]" if ($close);
-			$current_len += $len;
-		}
-
-		# Right-align result text
-		if ($current_len < $br_field_width) {
-			$current = (" "x($br_field_width - $current_len)).
-				   $current;
-		}
-		push(@result, $current);
-	}
-
-	return @result;
-}
-
-
-#
-# format_count(count, width)
-#
-# Return a right-aligned representation of count that fits in width characters.
-#
-
-sub format_count($$)
-{
-	my ($count, $width) = @_;
-	my $result;
-	my $exp;
-
-	$result = sprintf("%*.0f", $width, $count);
-	while (length($result) > $width) {
-		last if ($count < 10);
-		$exp++;
-		$count = int($count/10);
-		$result = sprintf("%*s", $width, ">$count*10^$exp");
-	}
-	return $result;
-}
-
-#
-# write_source_line(filehandle, line_num, source, hit_count, converted,
-#                   brdata)
-#
-# Write formatted source code line. Return a line in a format as needed
-# by gen_png()
-#
-
-sub write_source_line(*$$$$$)
-{
-	my ($handle, $line, $source, $count, $converted, $brdata) = @_;
-	my $source_format;
-	my $count_format;
-	my $result;
-	my $anchor_start = "";
-	my $anchor_end = "";
-	my $count_field_width = $line_field_width - 1;
-	my @br_html;
-	my $html;
-
-	# Get branch HTML data for this line
-	@br_html = get_branch_html($brdata) if ($br_coverage);
-
-	if (!defined($count)) {
-		$result		= "";
-		$source_format	= "";
-		$count_format	= " "x$count_field_width;
-	}
-	elsif ($count == 0) {
-		$result		= $count;
-		$source_format	= '';
-		$count_format	= format_count($count, $count_field_width);
-	}
-	elsif ($converted && defined($highlight)) {
-		$result		= "*".$count;
-		$source_format	= '';
-		$count_format	= format_count($count, $count_field_width);
-	}
-	else {
-		$result		= $count;
-		$source_format	= '';
-		$count_format	= format_count($count, $count_field_width);
-	}
-	$result .= ":".$source;
-
-	# Write out a line number navigation anchor every $nav_resolution
-	# lines if necessary
-	$anchor_start	= "";
-	$anchor_end	= "";
-
-
-	# *************************************************************
-
-	$html = $anchor_start;
-	$html .= "".sprintf("%8d", $line)." ";
-	$html .= shift(@br_html).":" if ($br_coverage);
-	$html .= "$source_format$count_format : ";
-	$html .= escape_html($source);
-	$html .= "" if ($source_format);
-	$html .= $anchor_end."\n";
-
-	write_html($handle, $html);
-
-	if ($br_coverage) {
-		# Add lines for overlong branch information
-		foreach (@br_html) {
-			write_html($handle, "".
-				   "         $_\n");
-		}
-	}
-	# *************************************************************
-
-	return($result);
-}
-
-
-#
-# write_source_epilog(filehandle)
-#
-# Write end of source code table.
-#
-
-sub write_source_epilog(*)
-{
-	# *************************************************************
-
-	write_html($_[0], <
-	      
-	    
-	  
-	  
- -END_OF_HTML - ; - - # ************************************************************* -} - - -# -# write_html_epilog(filehandle, base_dir[, break_frames]) -# -# Write HTML page footer to FILEHANDLE. BREAK_FRAMES should be set when -# this page is embedded in a frameset, clicking the URL link will then -# break this frameset. -# - -sub write_html_epilog(*$;$) -{ - my $basedir = $_[1]; - my $break_code = ""; - my $epilog; - - if (defined($_[2])) - { - $break_code = " target=\"_parent\""; - } - - # ************************************************************* - - write_html($_[0], < - - Generated by: $lcov_version - -
-END_OF_HTML - ; - - $epilog = $html_epilog; - $epilog =~ s/\@basedir\@/$basedir/g; - - write_html($_[0], $epilog); -} - - -# -# write_frameset(filehandle, basedir, basename, pagetitle) -# -# - -sub write_frameset(*$$$) -{ - my $frame_width = $overview_width + 40; - - # ************************************************************* - - write_html($_[0], < - - - - - - $_[3] - - - - - - - - <center>Frames not supported by your browser!<br></center> - - - - -END_OF_HTML - ; - - # ************************************************************* -} - - -# -# sub write_overview_line(filehandle, basename, line, link) -# -# - -sub write_overview_line(*$$$) -{ - my $y1 = $_[2] - 1; - my $y2 = $y1 + $nav_resolution - 1; - my $x2 = $overview_width - 1; - - # ************************************************************* - - write_html($_[0], < -END_OF_HTML - ; - - # ************************************************************* -} - - -# -# write_overview(filehandle, basedir, basename, pagetitle, lines) -# -# - -sub write_overview(*$$$$) -{ - my $index; - my $max_line = $_[4] - 1; - my $offset; - - # ************************************************************* - - write_html($_[0], < - - - - - $_[3] - - - - - - -END_OF_HTML - ; - - # ************************************************************* - - # Make $offset the next higher multiple of $nav_resolution - $offset = ($nav_offset + $nav_resolution - 1) / $nav_resolution; - $offset = sprintf("%d", $offset ) * $nav_resolution; - - # Create image map for overview image - for ($index = 1; $index <= $_[4]; $index += $nav_resolution) - { - # Enforce nav_offset - if ($index < $offset + 1) - { - write_overview_line($_[0], $_[2], $index, 1); - } - else - { - write_overview_line($_[0], $_[2], $index, $index - $offset); - } - } - - # ************************************************************* - - write_html($_[0], < - -
- Top

- Overview -
- - -END_OF_HTML - ; - - # ************************************************************* -} - - -sub max($$) -{ - my ($a, $b) = @_; - - return $a if ($a > $b); - return $b; -} - - -# -# write_header(filehandle, type, trunc_file_name, rel_file_name, lines_found, -# lines_hit, funcs_found, funcs_hit, sort_type) -# -# Write a complete standard page header. TYPE may be (0, 1, 2, 3, 4) -# corresponding to (directory view header, file view header, source view -# header, test case description header, function view header) -# - -sub write_header(*$$$$$$$$$$) -{ - local *HTML_HANDLE = $_[0]; - my $type = $_[1]; - my $trunc_name = $_[2]; - my $rel_filename = $_[3]; - my $lines_found = $_[4]; - my $lines_hit = $_[5]; - my $fn_found = $_[6]; - my $fn_hit = $_[7]; - my $br_found = $_[8]; - my $br_hit = $_[9]; - my $sort_type = $_[10]; - my $base_dir; - my $view; - my $test; - my $base_name; - my $style; - my $rate; - my @row_left; - my @row_right; - my $num_rows; - my $i; - my $esc_trunc_name = escape_html($trunc_name); - - $base_name = basename($rel_filename); - - # Prepare text for "current view" field - if ($type == $HDR_DIR) - { - # Main overview - $base_dir = ""; - $view = $overview_title; - } - elsif ($type == $HDR_FILE) - { - # Directory overview - $base_dir = get_relative_base_path($rel_filename); - $view = "". - "$overview_title - $esc_trunc_name"; - } - elsif ($type == $HDR_SOURCE || $type == $HDR_FUNC) - { - # File view - my $dir_name = dirname($rel_filename); - my $esc_base_name = escape_html($base_name); - my $esc_dir_name = escape_html($dir_name); - - $base_dir = get_relative_base_path($dir_name); - if ($frames) - { - # Need to break frameset when clicking any of these - # links - $view = "$overview_title - ". - "". - "$esc_dir_name - $esc_base_name"; - } - else - { - $view = "". - "$overview_title - ". - "". - "$esc_dir_name - $esc_base_name"; - } - - # Add function suffix - if ($func_coverage) { - $view .= ""; - if ($type == $HDR_SOURCE) { - if ($sort) { - $view .= " (source / functions)"; - } else { - $view .= " (source / functions)"; - } - } elsif ($type == $HDR_FUNC) { - $view .= " (source / functions)"; - } - $view .= ""; - } - } - elsif ($type == $HDR_TESTDESC) - { - # Test description header - $base_dir = ""; - $view = "". - "$overview_title - test case descriptions"; - } - - # Prepare text for "test" field - $test = escape_html($test_title); - - # Append link to test description page if available - if (%test_description && ($type != $HDR_TESTDESC)) - { - if ($frames && ($type == $HDR_SOURCE || $type == $HDR_FUNC)) - { - # Need to break frameset when clicking this link - $test .= " ( ". - "". - "view descriptions )"; - } - else - { - $test .= " ( ". - "". - "view descriptions )"; - } - } - - # Write header - write_header_prolog(*HTML_HANDLE, $base_dir); - - # Left row - push(@row_left, [[ "10%", "headerItem", "Current view:" ], - [ "35%", "headerValue", $view ]]); - push(@row_left, [[undef, "headerItem", "Test:"], - [undef, "headerValue", $test]]); - push(@row_left, [[undef, "headerItem", "Date:"], - [undef, "headerValue", $date]]); - - # Right row - if ($legend && ($type == $HDR_SOURCE || $type == $HDR_FUNC)) { - my $text = <hit
- not hit -END_OF_HTML - if ($br_coverage) { - $text .= <+
taken - - not taken - # not executed -END_OF_HTML - } - push(@row_left, [[undef, "headerItem", "Legend:"], - [undef, "headerValueLeg", $text]]); - } elsif ($legend && ($type != $HDR_TESTDESC)) { - my $text = <low: < $med_limit % - medium: >= $med_limit % - high: >= $hi_limit % -END_OF_HTML - push(@row_left, [[undef, "headerItem", "Legend:"], - [undef, "headerValueLeg", $text]]); - } - if ($type == $HDR_TESTDESC) { - push(@row_right, [[ "55%" ]]); - } else { - push(@row_right, [["15%", undef, undef ], - ["10%", "headerCovTableHead", "Hit" ], - ["10%", "headerCovTableHead", "Total" ], - ["15%", "headerCovTableHead", "Coverage"]]); - } - # Line coverage - $style = $rate_name[classify_rate($lines_found, $lines_hit, - $med_limit, $hi_limit)]; - $rate = rate($lines_hit, $lines_found, " %"); - push(@row_right, [[undef, "headerItem", "Lines:"], - [undef, "headerCovTableEntry", $lines_hit], - [undef, "headerCovTableEntry", $lines_found], - [undef, "headerCovTableEntry$style", $rate]]) - if ($type != $HDR_TESTDESC); - # Function coverage - if ($func_coverage) { - $style = $rate_name[classify_rate($fn_found, $fn_hit, - $fn_med_limit, $fn_hi_limit)]; - $rate = rate($fn_hit, $fn_found, " %"); - push(@row_right, [[undef, "headerItem", "Functions:"], - [undef, "headerCovTableEntry", $fn_hit], - [undef, "headerCovTableEntry", $fn_found], - [undef, "headerCovTableEntry$style", $rate]]) - if ($type != $HDR_TESTDESC); - } - # Branch coverage - if ($br_coverage) { - $style = $rate_name[classify_rate($br_found, $br_hit, - $br_med_limit, $br_hi_limit)]; - $rate = rate($br_hit, $br_found, " %"); - push(@row_right, [[undef, "headerItem", "Branches:"], - [undef, "headerCovTableEntry", $br_hit], - [undef, "headerCovTableEntry", $br_found], - [undef, "headerCovTableEntry$style", $rate]]) - if ($type != $HDR_TESTDESC); - } - - # Print rows - $num_rows = max(scalar(@row_left), scalar(@row_right)); - for ($i = 0; $i < $num_rows; $i++) { - my $left = $row_left[$i]; - my $right = $row_right[$i]; - - if (!defined($left)) { - $left = [[undef, undef, undef], [undef, undef, undef]]; - } - if (!defined($right)) { - $right = []; - } - write_header_line(*HTML_HANDLE, @{$left}, - [ $i == 0 ? "5%" : undef, undef, undef], - @{$right}); - } - - # Fourth line - write_header_epilog(*HTML_HANDLE, $base_dir); -} - -sub get_sorted_by_rate($$) -{ - my ($hash, $type) = @_; - - if ($type == $SORT_LINE) { - # Sort by line coverage - return sort({$hash->{$a}[7] <=> $hash->{$b}[7]} keys(%{$hash})); - } elsif ($type == $SORT_FUNC) { - # Sort by function coverage; - return sort({$hash->{$a}[8] <=> $hash->{$b}[8]} keys(%{$hash})); - } elsif ($type == $SORT_BRANCH) { - # Sort by br coverage; - return sort({$hash->{$a}[9] <=> $hash->{$b}[9]} keys(%{$hash})); - } -} - -sub get_sorted_by_missed($$) -{ - my ($hash, $type) = @_; - - if ($type == $SORT_LINE) { - # Sort by number of instrumented lines without coverage - return sort( - { - ($hash->{$b}[0] - $hash->{$b}[1]) <=> - ($hash->{$a}[0] - $hash->{$a}[1]) - } keys(%{$hash})); - } elsif ($type == $SORT_FUNC) { - # Sort by number of instrumented functions without coverage - return sort( - { - ($hash->{$b}[2] - $hash->{$b}[3]) <=> - ($hash->{$a}[2] - $hash->{$a}[3]) - } keys(%{$hash})); - } elsif ($type == $SORT_BRANCH) { - # Sort by number of instrumented branches without coverage - return sort( - { - ($hash->{$b}[4] - $hash->{$b}[5]) <=> - ($hash->{$a}[4] - $hash->{$a}[5]) - } keys(%{$hash})); - } -} - -# -# get_sorted_keys(hash_ref, sort_type) -# -# hash_ref: filename -> stats -# stats: [ lines_found, lines_hit, fn_found, fn_hit, br_found, br_hit, -# link_name, line_rate, fn_rate, br_rate ] -# - -sub get_sorted_keys($$) -{ - my ($hash, $type) = @_; - - if ($type == $SORT_FILE) { - # Sort by name - return sort(keys(%{$hash})); - } elsif ($opt_missed) { - return get_sorted_by_missed($hash, $type); - } else { - return get_sorted_by_rate($hash, $type); - } -} - -sub get_sort_code($$$) -{ - my ($link, $alt, $base) = @_; - my $png; - my $link_start; - my $link_end; - - if (!defined($link)) { - $png = "glass.png"; - $link_start = ""; - $link_end = ""; - } else { - $png = "updown.png"; - $link_start = ''; - $link_end = ""; - } - - return ' '.$link_start. - ''.$link_end.''; -} - -sub get_file_code($$$$) -{ - my ($type, $text, $sort_button, $base) = @_; - my $result = $text; - my $link; - - if ($sort_button) { - if ($type == $HEAD_NO_DETAIL) { - $link = "index.$html_ext"; - } else { - $link = "index-detail.$html_ext"; - } - } - $result .= get_sort_code($link, "Sort by name", $base); - - return $result; -} - -sub get_line_code($$$$$) -{ - my ($type, $sort_type, $text, $sort_button, $base) = @_; - my $result = $text; - my $sort_link; - - if ($type == $HEAD_NO_DETAIL) { - # Just text - if ($sort_button) { - $sort_link = "index-sort-l.$html_ext"; - } - } elsif ($type == $HEAD_DETAIL_HIDDEN) { - # Text + link to detail view - $result .= ' ( show details )'; - if ($sort_button) { - $sort_link = "index-sort-l.$html_ext"; - } - } else { - # Text + link to standard view - $result .= ' ( hide details )'; - if ($sort_button) { - $sort_link = "index-detail-sort-l.$html_ext"; - } - } - # Add sort button - $result .= get_sort_code($sort_link, "Sort by line coverage", $base); - - return $result; -} - -sub get_func_code($$$$) -{ - my ($type, $text, $sort_button, $base) = @_; - my $result = $text; - my $link; - - if ($sort_button) { - if ($type == $HEAD_NO_DETAIL) { - $link = "index-sort-f.$html_ext"; - } else { - $link = "index-detail-sort-f.$html_ext"; - } - } - $result .= get_sort_code($link, "Sort by function coverage", $base); - return $result; -} - -sub get_br_code($$$$) -{ - my ($type, $text, $sort_button, $base) = @_; - my $result = $text; - my $link; - - if ($sort_button) { - if ($type == $HEAD_NO_DETAIL) { - $link = "index-sort-b.$html_ext"; - } else { - $link = "index-detail-sort-b.$html_ext"; - } - } - $result .= get_sort_code($link, "Sort by branch coverage", $base); - return $result; -} - -# -# write_file_table(filehandle, base_dir, overview, testhash, testfnchash, -# testbrhash, fileview, sort_type) -# -# Write a complete file table. OVERVIEW is a reference to a hash containing -# the following mapping: -# -# filename -> "lines_found,lines_hit,funcs_found,funcs_hit,page_link, -# func_link" -# -# TESTHASH is a reference to the following hash: -# -# filename -> \%testdata -# %testdata: name of test affecting this file -> \%testcount -# %testcount: line number -> execution count for a single test -# -# Heading of first column is "Filename" if FILEVIEW is true, "Directory name" -# otherwise. -# - -sub write_file_table(*$$$$$$$) -{ - local *HTML_HANDLE = $_[0]; - my $base_dir = $_[1]; - my $overview = $_[2]; - my $testhash = $_[3]; - my $testfnchash = $_[4]; - my $testbrhash = $_[5]; - my $fileview = $_[6]; - my $sort_type = $_[7]; - my $filename; - my $bar_graph; - my $hit; - my $found; - my $fn_found; - my $fn_hit; - my $br_found; - my $br_hit; - my $page_link; - my $testname; - my $testdata; - my $testfncdata; - my $testbrdata; - my %affecting_tests; - my $line_code = ""; - my $func_code; - my $br_code; - my $file_code; - my @head_columns; - - # Determine HTML code for column headings - if (($base_dir ne "") && $show_details) - { - my $detailed = keys(%{$testhash}); - - $file_code = get_file_code($detailed ? $HEAD_DETAIL_HIDDEN : - $HEAD_NO_DETAIL, - $fileview ? "Filename" : "Directory", - $sort && $sort_type != $SORT_FILE, - $base_dir); - $line_code = get_line_code($detailed ? $HEAD_DETAIL_SHOWN : - $HEAD_DETAIL_HIDDEN, - $sort_type, - "Line Coverage", - $sort && $sort_type != $SORT_LINE, - $base_dir); - $func_code = get_func_code($detailed ? $HEAD_DETAIL_HIDDEN : - $HEAD_NO_DETAIL, - "Functions", - $sort && $sort_type != $SORT_FUNC, - $base_dir); - $br_code = get_br_code($detailed ? $HEAD_DETAIL_HIDDEN : - $HEAD_NO_DETAIL, - "Branches", - $sort && $sort_type != $SORT_BRANCH, - $base_dir); - } else { - $file_code = get_file_code($HEAD_NO_DETAIL, - $fileview ? "Filename" : "Directory", - $sort && $sort_type != $SORT_FILE, - $base_dir); - $line_code = get_line_code($HEAD_NO_DETAIL, $sort_type, "Line Coverage", - $sort && $sort_type != $SORT_LINE, - $base_dir); - $func_code = get_func_code($HEAD_NO_DETAIL, "Functions", - $sort && $sort_type != $SORT_FUNC, - $base_dir); - $br_code = get_br_code($HEAD_NO_DETAIL, "Branches", - $sort && $sort_type != $SORT_BRANCH, - $base_dir); - } - push(@head_columns, [ $line_code, 3 ]); - push(@head_columns, [ $func_code, 2]) if ($func_coverage); - push(@head_columns, [ $br_code, 2]) if ($br_coverage); - - write_file_table_prolog(*HTML_HANDLE, $file_code, @head_columns); - - foreach $filename (get_sorted_keys($overview, $sort_type)) - { - my @columns; - ($found, $hit, $fn_found, $fn_hit, $br_found, $br_hit, - $page_link) = @{$overview->{$filename}}; - - # Line coverage - push(@columns, [$found, $hit, $med_limit, $hi_limit, 1]); - # Function coverage - if ($func_coverage) { - push(@columns, [$fn_found, $fn_hit, $fn_med_limit, - $fn_hi_limit, 0]); - } - # Branch coverage - if ($br_coverage) { - push(@columns, [$br_found, $br_hit, $br_med_limit, - $br_hi_limit, 0]); - } - write_file_table_entry(*HTML_HANDLE, $base_dir, $filename, - $page_link, @columns); - - $testdata = $testhash->{$filename}; - $testfncdata = $testfnchash->{$filename}; - $testbrdata = $testbrhash->{$filename}; - - # Check whether we should write test specific coverage - # as well - if (!($show_details && $testdata)) { next; } - - # Filter out those tests that actually affect this file - %affecting_tests = %{ get_affecting_tests($testdata, - $testfncdata, $testbrdata) }; - - # Does any of the tests affect this file at all? - if (!%affecting_tests) { next; } - - foreach $testname (keys(%affecting_tests)) - { - my @results; - ($found, $hit, $fn_found, $fn_hit, $br_found, $br_hit) = - split(",", $affecting_tests{$testname}); - - # Insert link to description of available - if ($test_description{$testname}) - { - $testname = "". - "$testname"; - } - - push(@results, [$found, $hit]); - push(@results, [$fn_found, $fn_hit]) if ($func_coverage); - push(@results, [$br_found, $br_hit]) if ($br_coverage); - write_file_table_detail_entry(*HTML_HANDLE, $testname, - @results); - } - } - - write_file_table_epilog(*HTML_HANDLE); -} - - -# -# get_found_and_hit(hash) -# -# Return the count for entries (found) and entries with an execution count -# greater than zero (hit) in a hash (linenumber -> execution count) as -# a list (found, hit) -# - -sub get_found_and_hit($) -{ - my %hash = %{$_[0]}; - my $found = 0; - my $hit = 0; - - # Calculate sum - $found = 0; - $hit = 0; - - foreach (keys(%hash)) - { - $found++; - if ($hash{$_}>0) { $hit++; } - } - - return ($found, $hit); -} - - -# -# get_func_found_and_hit(sumfnccount) -# -# Return (f_found, f_hit) for sumfnccount -# - -sub get_func_found_and_hit($) -{ - my ($sumfnccount) = @_; - my $function; - my $fn_found; - my $fn_hit; - - $fn_found = scalar(keys(%{$sumfnccount})); - $fn_hit = 0; - foreach $function (keys(%{$sumfnccount})) { - if ($sumfnccount->{$function} > 0) { - $fn_hit++; - } - } - return ($fn_found, $fn_hit); -} - - -sub get_br_found_and_hit($) -{ - my ($brcount) = @_; - my $db; - - $db = brcount_to_db($brcount); - - return brcount_db_get_found_and_hit($db); -} - - -# -# get_affecting_tests(testdata, testfncdata, testbrdata) -# -# HASHREF contains a mapping filename -> (linenumber -> exec count). Return -# a hash containing mapping filename -> "lines found, lines hit" for each -# filename which has a nonzero hit count. -# - -sub get_affecting_tests($$$) -{ - my ($testdata, $testfncdata, $testbrdata) = @_; - my $testname; - my $testcount; - my $testfnccount; - my $testbrcount; - my %result; - my $found; - my $hit; - my $fn_found; - my $fn_hit; - my $br_found; - my $br_hit; - - foreach $testname (keys(%{$testdata})) - { - # Get (line number -> count) hash for this test case - $testcount = $testdata->{$testname}; - $testfnccount = $testfncdata->{$testname}; - $testbrcount = $testbrdata->{$testname}; - - # Calculate sum - ($found, $hit) = get_found_and_hit($testcount); - ($fn_found, $fn_hit) = get_func_found_and_hit($testfnccount); - ($br_found, $br_hit) = get_br_found_and_hit($testbrcount); - - if ($hit>0) - { - $result{$testname} = "$found,$hit,$fn_found,$fn_hit,". - "$br_found,$br_hit"; - } - } - - return(\%result); -} - - -sub get_hash_reverse($) -{ - my ($hash) = @_; - my %result; - - foreach (keys(%{$hash})) { - $result{$hash->{$_}} = $_; - } - - return \%result; -} - -# -# write_source(filehandle, source_filename, count_data, checksum_data, -# converted_data, func_data, sumbrcount) -# -# Write an HTML view of a source code file. Returns a list containing -# data as needed by gen_png(). -# -# Die on error. -# - -sub write_source($$$$$$$) -{ - local *HTML_HANDLE = $_[0]; - local *SOURCE_HANDLE; - my $source_filename = $_[1]; - my %count_data; - my $line_number; - my @result; - my $checkdata = $_[3]; - my $converted = $_[4]; - my $funcdata = $_[5]; - my $sumbrcount = $_[6]; - my $datafunc = get_hash_reverse($funcdata); - my @file; - - if ($_[2]) - { - %count_data = %{$_[2]}; - } - - if (!open(SOURCE_HANDLE, "<", $source_filename)) { - my @lines; - my $last_line = 0; - - if (!$ignore[$ERROR_SOURCE]) { - die("ERROR: cannot read $source_filename\n"); - } - - # Continue without source file - warn("WARNING: cannot read $source_filename!\n"); - - @lines = sort( { $a <=> $b } keys(%count_data)); - if (@lines) { - $last_line = $lines[scalar(@lines) - 1]; - } - return ( ":" ) if ($last_line < 1); - - # Simulate gcov behavior - for ($line_number = 1; $line_number <= $last_line; - $line_number++) { - push(@file, "/* EOF */"); - } - } else { - @file = ; - } - - write_source_prolog(*HTML_HANDLE); - $line_number = 0; - foreach (@file) { - $line_number++; - chomp($_); - - # Also remove CR from line-end - s/\015$//; - - # Source code matches coverage data? - if (defined($checkdata->{$line_number}) && - ($checkdata->{$line_number} ne md5_base64($_))) - { - die("ERROR: checksum mismatch at $source_filename:". - "$line_number\n"); - } - - push (@result, - write_source_line(HTML_HANDLE, $line_number, - $_, $count_data{$line_number}, - $converted->{$line_number}, - $sumbrcount->{$line_number})); - } - - close(SOURCE_HANDLE); - write_source_epilog(*HTML_HANDLE); - return(@result); -} - - -sub funcview_get_func_code($$$) -{ - my ($name, $base, $type) = @_; - my $result; - my $link; - - if ($sort && $type == 1) { - $link = "$name.func.$html_ext"; - } - $result = "Function Name"; - $result .= get_sort_code($link, "Sort by function name", $base); - - return $result; -} - -sub funcview_get_count_code($$$) -{ - my ($name, $base, $type) = @_; - my $result; - my $link; - - if ($sort && $type == 0) { - $link = "$name.func-sort-c.$html_ext"; - } - $result = "Hit count"; - $result .= get_sort_code($link, "Sort by hit count", $base); - - return $result; -} - -# -# funcview_get_sorted(funcdata, sumfncdata, sort_type) -# -# Depending on the value of sort_type, return a list of functions sorted -# by name (type 0) or by the associated call count (type 1). -# - -sub funcview_get_sorted($$$) -{ - my ($funcdata, $sumfncdata, $type) = @_; - - if ($type == 0) { - return sort(keys(%{$funcdata})); - } - return sort({ - $sumfncdata->{$b} == $sumfncdata->{$a} ? - $a cmp $b : $sumfncdata->{$a} <=> $sumfncdata->{$b} - } keys(%{$sumfncdata})); -} - -sub demangle_list($) -{ - my ($list) = @_; - my $tmpfile; - my $handle; - my %demangle; - my $demangle_arg = ""; - my %versions; - - # Write function names to file - ($handle, $tmpfile) = tempfile(); - die("ERROR: could not create temporary file") if (!defined($tmpfile)); - print($handle join("\n", @$list)); - close($handle); - - # Extra flag necessary on OS X so that symbols listed by gcov get demangled - # properly. - if ($^O eq "darwin") { - $demangle_arg = "--no-strip-underscores"; - } - - # Build translation hash from c++filt output - open($handle, "-|", "c++filt $demangle_arg < $tmpfile") or - die("ERROR: could not run c++filt: $!\n"); - foreach my $func (@$list) { - my $translated = <$handle>; - my $version; - - last if (!defined($translated)); - chomp($translated); - - $version = ++$versions{$translated}; - $translated .= ".$version" if ($version > 1); - $demangle{$func} = $translated; - } - close($handle); - - if (scalar(keys(%demangle)) != scalar(@$list)) { - die("ERROR: c++filt output not as expected (". - scalar(keys(%demangle))." vs ".scalar(@$list).") lines\n"); - } - - unlink($tmpfile) or - warn("WARNING: could not remove temporary file $tmpfile: $!\n"); - - return \%demangle; -} - -# -# write_function_table(filehandle, source_file, sumcount, funcdata, -# sumfnccount, testfncdata, sumbrcount, testbrdata, -# base_name, base_dir, sort_type) -# -# Write an HTML table listing all functions in a source file, including -# also function call counts and line coverages inside of each function. -# -# Die on error. -# - -sub write_function_table(*$$$$$$$$$$) -{ - local *HTML_HANDLE = $_[0]; - my $source = $_[1]; - my $sumcount = $_[2]; - my $funcdata = $_[3]; - my $sumfncdata = $_[4]; - my $testfncdata = $_[5]; - my $sumbrcount = $_[6]; - my $testbrdata = $_[7]; - my $name = $_[8]; - my $base = $_[9]; - my $type = $_[10]; - my $func; - my $func_code; - my $count_code; - my $demangle; - - # Get HTML code for headings - $func_code = funcview_get_func_code($name, $base, $type); - $count_code = funcview_get_count_code($name, $base, $type); - write_html(*HTML_HANDLE, < - - - - - - -END_OF_HTML - ; - - # Get demangle translation hash - if ($demangle_cpp) { - $demangle = demangle_list([ sort(keys(%{$funcdata})) ]); - } - - # Get a sorted table - foreach $func (funcview_get_sorted($funcdata, $sumfncdata, $type)) { - if (!defined($funcdata->{$func})) - { - next; - } - - my $startline = $funcdata->{$func} - $func_offset; - my $name = $func; - my $count = $sumfncdata->{$name}; - my $countstyle; - - # Replace function name with demangled version if available - $name = $demangle->{$name} if (exists($demangle->{$name})); - - # Escape special characters - $name = escape_html($name); - if ($startline < 1) { - $startline = 1; - } - if ($count == 0) { - $countstyle = "coverFnLo"; - } else { - $countstyle = "coverFnHi"; - } - - write_html(*HTML_HANDLE, < - - - -END_OF_HTML - ; - } - write_html(*HTML_HANDLE, < -
- -END_OF_HTML - ; -} - - -# -# info(printf_parameter) -# -# Use printf to write PRINTF_PARAMETER to stdout only when the $quiet flag -# is not set. -# - -sub info(@) -{ - if (!$quiet) - { - # Print info string - printf(@_); - } -} - - -# -# subtract_counts(data_ref, base_ref) -# - -sub subtract_counts($$) -{ - my %data = %{$_[0]}; - my %base = %{$_[1]}; - my $line; - my $data_count; - my $base_count; - my $hit = 0; - my $found = 0; - - foreach $line (keys(%data)) - { - $found++; - $data_count = $data{$line}; - $base_count = $base{$line}; - - if (defined($base_count)) - { - $data_count -= $base_count; - - # Make sure we don't get negative numbers - if ($data_count<0) { $data_count = 0; } - } - - $data{$line} = $data_count; - if ($data_count > 0) { $hit++; } - } - - return (\%data, $found, $hit); -} - - -# -# subtract_fnccounts(data, base) -# -# Subtract function call counts found in base from those in data. -# Return (data, f_found, f_hit). -# - -sub subtract_fnccounts($$) -{ - my %data; - my %base; - my $func; - my $data_count; - my $base_count; - my $fn_hit = 0; - my $fn_found = 0; - - %data = %{$_[0]} if (defined($_[0])); - %base = %{$_[1]} if (defined($_[1])); - foreach $func (keys(%data)) { - $fn_found++; - $data_count = $data{$func}; - $base_count = $base{$func}; - - if (defined($base_count)) { - $data_count -= $base_count; - - # Make sure we don't get negative numbers - if ($data_count < 0) { - $data_count = 0; - } - } - - $data{$func} = $data_count; - if ($data_count > 0) { - $fn_hit++; - } - } - - return (\%data, $fn_found, $fn_hit); -} - - -# -# apply_baseline(data_ref, baseline_ref) -# -# Subtract the execution counts found in the baseline hash referenced by -# BASELINE_REF from actual data in DATA_REF. -# - -sub apply_baseline($$) -{ - my %data_hash = %{$_[0]}; - my %base_hash = %{$_[1]}; - my $filename; - my $testname; - my $data; - my $data_testdata; - my $data_funcdata; - my $data_checkdata; - my $data_testfncdata; - my $data_testbrdata; - my $data_count; - my $data_testfnccount; - my $data_testbrcount; - my $base; - my $base_checkdata; - my $base_sumfnccount; - my $base_sumbrcount; - my $base_count; - my $sumcount; - my $sumfnccount; - my $sumbrcount; - my $found; - my $hit; - my $fn_found; - my $fn_hit; - my $br_found; - my $br_hit; - - foreach $filename (keys(%data_hash)) - { - # Get data set for data and baseline - $data = $data_hash{$filename}; - $base = $base_hash{$filename}; - - # Skip data entries for which no base entry exists - if (!defined($base)) - { - next; - } - - # Get set entries for data and baseline - ($data_testdata, undef, $data_funcdata, $data_checkdata, - $data_testfncdata, undef, $data_testbrdata) = - get_info_entry($data); - (undef, $base_count, undef, $base_checkdata, undef, - $base_sumfnccount, undef, $base_sumbrcount) = - get_info_entry($base); - - # Check for compatible checksums - merge_checksums($data_checkdata, $base_checkdata, $filename); - - # sumcount has to be calculated anew - $sumcount = {}; - $sumfnccount = {}; - $sumbrcount = {}; - - # For each test case, subtract test specific counts - foreach $testname (keys(%{$data_testdata})) - { - # Get counts of both data and baseline - $data_count = $data_testdata->{$testname}; - $data_testfnccount = $data_testfncdata->{$testname}; - $data_testbrcount = $data_testbrdata->{$testname}; - - ($data_count, undef, $hit) = - subtract_counts($data_count, $base_count); - ($data_testfnccount) = - subtract_fnccounts($data_testfnccount, - $base_sumfnccount); - ($data_testbrcount) = - combine_brcount($data_testbrcount, - $base_sumbrcount, $BR_SUB); - - - # Check whether this test case did hit any line at all - if ($hit > 0) - { - # Write back resulting hash - $data_testdata->{$testname} = $data_count; - $data_testfncdata->{$testname} = - $data_testfnccount; - $data_testbrdata->{$testname} = - $data_testbrcount; - } - else - { - # Delete test case which did not impact this - # file - delete($data_testdata->{$testname}); - delete($data_testfncdata->{$testname}); - delete($data_testbrdata->{$testname}); - } - - # Add counts to sum of counts - ($sumcount, $found, $hit) = - add_counts($sumcount, $data_count); - ($sumfnccount, $fn_found, $fn_hit) = - add_fnccount($sumfnccount, $data_testfnccount); - ($sumbrcount, $br_found, $br_hit) = - combine_brcount($sumbrcount, $data_testbrcount, - $BR_ADD); - } - - # Write back resulting entry - set_info_entry($data, $data_testdata, $sumcount, $data_funcdata, - $data_checkdata, $data_testfncdata, $sumfnccount, - $data_testbrdata, $sumbrcount, $found, $hit, - $fn_found, $fn_hit, $br_found, $br_hit); - - $data_hash{$filename} = $data; - } - - return (\%data_hash); -} - - -# -# remove_unused_descriptions() -# -# Removes all test descriptions from the global hash %test_description which -# are not present in %info_data. -# - -sub remove_unused_descriptions() -{ - my $filename; # The current filename - my %test_list; # Hash containing found test names - my $test_data; # Reference to hash test_name -> count_data - my $before; # Initial number of descriptions - my $after; # Remaining number of descriptions - - $before = scalar(keys(%test_description)); - - foreach $filename (keys(%info_data)) - { - ($test_data) = get_info_entry($info_data{$filename}); - foreach (keys(%{$test_data})) - { - $test_list{$_} = ""; - } - } - - # Remove descriptions for tests which are not in our list - foreach (keys(%test_description)) - { - if (!defined($test_list{$_})) - { - delete($test_description{$_}); - } - } - - $after = scalar(keys(%test_description)); - if ($after < $before) - { - info("Removed ".($before - $after). - " unused descriptions, $after remaining.\n"); - } -} - - -# -# apply_prefix(filename, PREFIXES) -# -# If FILENAME begins with PREFIX from PREFIXES, remove PREFIX from FILENAME -# and return resulting string, otherwise return FILENAME. -# - -sub apply_prefix($@) -{ - my $filename = shift; - my @dir_prefix = @_; - - if (@dir_prefix) - { - foreach my $prefix (@dir_prefix) - { - if ($prefix ne "" && $filename =~ /^\Q$prefix\E\/(.*)$/) - { - return substr($filename, length($prefix) + 1); - } - } - } - - return $filename; -} - - -# -# system_no_output(mode, parameters) -# -# Call an external program using PARAMETERS while suppressing depending on -# the value of MODE: -# -# MODE & 1: suppress STDOUT -# MODE & 2: suppress STDERR -# -# Return 0 on success, non-zero otherwise. -# - -sub system_no_output($@) -{ - my $mode = shift; - my $result; - local *OLD_STDERR; - local *OLD_STDOUT; - - # Save old stdout and stderr handles - ($mode & 1) && open(OLD_STDOUT, ">>&", "STDOUT"); - ($mode & 2) && open(OLD_STDERR, ">>&", "STDERR"); - - # Redirect to /dev/null - ($mode & 1) && open(STDOUT, ">", "/dev/null"); - ($mode & 2) && open(STDERR, ">", "/dev/null"); - - system(@_); - $result = $?; - - # Close redirected handles - ($mode & 1) && close(STDOUT); - ($mode & 2) && close(STDERR); - - # Restore old handles - ($mode & 1) && open(STDOUT, ">>&", "OLD_STDOUT"); - ($mode & 2) && open(STDERR, ">>&", "OLD_STDERR"); - - return $result; -} - - -# -# read_config(filename) -# -# Read configuration file FILENAME and return a reference to a hash containing -# all valid key=value pairs found. -# - -sub read_config($) -{ - my $filename = $_[0]; - my %result; - my $key; - my $value; - local *HANDLE; - - if (!open(HANDLE, "<", $filename)) - { - warn("WARNING: cannot read configuration file $filename\n"); - return undef; - } - while () - { - chomp; - # Skip comments - s/#.*//; - # Remove leading blanks - s/^\s+//; - # Remove trailing blanks - s/\s+$//; - next unless length; - ($key, $value) = split(/\s*=\s*/, $_, 2); - if (defined($key) && defined($value)) - { - $result{$key} = $value; - } - else - { - warn("WARNING: malformed statement in line $. ". - "of configuration file $filename\n"); - } - } - close(HANDLE); - return \%result; -} - - -# -# apply_config(REF) -# -# REF is a reference to a hash containing the following mapping: -# -# key_string => var_ref -# -# where KEY_STRING is a keyword and VAR_REF is a reference to an associated -# variable. If the global configuration hashes CONFIG or OPT_RC contain a value -# for keyword KEY_STRING, VAR_REF will be assigned the value for that keyword. -# - -sub apply_config($) -{ - my $ref = $_[0]; - - foreach (keys(%{$ref})) - { - if (defined($opt_rc{$_})) { - ${$ref->{$_}} = $opt_rc{$_}; - } elsif (defined($config->{$_})) { - ${$ref->{$_}} = $config->{$_}; - } - } -} - - -# -# get_html_prolog(FILENAME) -# -# If FILENAME is defined, return contents of file. Otherwise return default -# HTML prolog. Die on error. -# - -sub get_html_prolog($) -{ - my $filename = $_[0]; - my $result = ""; - - if (defined($filename)) - { - local *HANDLE; - - open(HANDLE, "<", $filename) - or die("ERROR: cannot open html prolog $filename!\n"); - while () - { - $result .= $_; - } - close(HANDLE); - } - else - { - $result = < - - - - - - \@pagetitle\@ - - - - - -END_OF_HTML - ; - } - - return $result; -} - - -# -# get_html_epilog(FILENAME) -# -# If FILENAME is defined, return contents of file. Otherwise return default -# HTML epilog. Die on error. -# -sub get_html_epilog($) -{ - my $filename = $_[0]; - my $result = ""; - - if (defined($filename)) - { - local *HANDLE; - - open(HANDLE, "<", $filename) - or die("ERROR: cannot open html epilog $filename!\n"); - while () - { - $result .= $_; - } - close(HANDLE); - } - else - { - $result = < - -END_OF_HTML - ; - } - - return $result; - -} - -sub warn_handler($) -{ - my ($msg) = @_; - - warn("$tool_name: $msg"); -} - -sub die_handler($) -{ - my ($msg) = @_; - - die("$tool_name: $msg"); -} - -# -# parse_ignore_errors(@ignore_errors) -# -# Parse user input about which errors to ignore. -# - -sub parse_ignore_errors(@) -{ - my (@ignore_errors) = @_; - my @items; - my $item; - - return if (!@ignore_errors); - - foreach $item (@ignore_errors) { - $item =~ s/\s//g; - if ($item =~ /,/) { - # Split and add comma-separated parameters - push(@items, split(/,/, $item)); - } else { - # Add single parameter - push(@items, $item); - } - } - foreach $item (@items) { - my $item_id = $ERROR_ID{lc($item)}; - - if (!defined($item_id)) { - die("ERROR: unknown argument for --ignore-errors: ". - "$item\n"); - } - $ignore[$item_id] = 1; - } -} - -# -# parse_dir_prefix(@dir_prefix) -# -# Parse user input about the prefix list -# - -sub parse_dir_prefix(@) -{ - my (@opt_dir_prefix) = @_; - my $item; - - return if (!@opt_dir_prefix); - - foreach $item (@opt_dir_prefix) { - if ($item =~ /,/) { - # Split and add comma-separated parameters - push(@dir_prefix, split(/,/, $item)); - } else { - # Add single parameter - push(@dir_prefix, $item); - } - } -} - -# -# rate(hit, found[, suffix, precision, width]) -# -# Return the coverage rate [0..100] for HIT and FOUND values. 0 is only -# returned when HIT is 0. 100 is only returned when HIT equals FOUND. -# PRECISION specifies the precision of the result. SUFFIX defines a -# string that is appended to the result if FOUND is non-zero. Spaces -# are added to the start of the resulting string until it is at least WIDTH -# characters wide. -# - -sub rate($$;$$$) -{ - my ($hit, $found, $suffix, $precision, $width) = @_; - my $rate; - - # Assign defaults if necessary - $precision = $default_precision if (!defined($precision)); - $suffix = "" if (!defined($suffix)); - $width = 0 if (!defined($width)); - - return sprintf("%*s", $width, "-") if (!defined($found) || $found == 0); - $rate = sprintf("%.*f", $precision, $hit * 100 / $found); - - # Adjust rates if necessary - if ($rate == 0 && $hit > 0) { - $rate = sprintf("%.*f", $precision, 1 / 10 ** $precision); - } elsif ($rate == 100 && $hit != $found) { - $rate = sprintf("%.*f", $precision, 100 - 1 / 10 ** $precision); - } - - return sprintf("%*s", $width, $rate.$suffix); -} diff --git a/worker/deps/lcov/bin/geninfo b/worker/deps/lcov/bin/geninfo deleted file mode 100755 index 4e7a293146..0000000000 --- a/worker/deps/lcov/bin/geninfo +++ /dev/null @@ -1,4593 +0,0 @@ -#!/usr/bin/env perl -# -# Copyright (c) International Business Machines Corp., 2002,2012 -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or (at -# your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# -# geninfo -# -# This script generates .info files from data files as created by code -# instrumented with gcc's built-in profiling mechanism. Call it with -# --help and refer to the geninfo man page to get information on usage -# and available options. -# -# -# Authors: -# 2002-08-23 created by Peter Oberparleiter -# IBM Lab Boeblingen -# based on code by Manoj Iyer and -# Megan Bock -# IBM Austin -# 2002-09-05 / Peter Oberparleiter: implemented option that allows file list -# 2003-04-16 / Peter Oberparleiter: modified read_gcov so that it can also -# parse the new gcov format which is to be introduced in gcc 3.3 -# 2003-04-30 / Peter Oberparleiter: made info write to STDERR, not STDOUT -# 2003-07-03 / Peter Oberparleiter: added line checksum support, added -# --no-checksum -# 2003-09-18 / Nigel Hinds: capture branch coverage data from GCOV -# 2003-12-11 / Laurent Deniel: added --follow option -# workaround gcov (<= 3.2.x) bug with empty .da files -# 2004-01-03 / Laurent Deniel: Ignore empty .bb files -# 2004-02-16 / Andreas Krebbel: Added support for .gcno/.gcda files and -# gcov versioning -# 2004-08-09 / Peter Oberparleiter: added configuration file support -# 2008-07-14 / Tom Zoerner: added --function-coverage command line option -# 2008-08-13 / Peter Oberparleiter: modified function coverage -# implementation (now enabled per default) -# - -use strict; -use warnings; -use File::Basename; -use File::Spec::Functions qw /abs2rel catdir file_name_is_absolute splitdir - splitpath catpath/; -use File::Temp qw(tempfile tempdir); -use File::Copy qw(copy); -use Getopt::Long; -use Digest::MD5 qw(md5_base64); -use Cwd qw/abs_path/; -use PerlIO::gzip; -use JSON qw(decode_json); - -if( $^O eq "msys" ) -{ - require File::Spec::Win32; -} - -# Constants -our $tool_dir = abs_path(dirname($0)); -our $lcov_version = 'LCOV version '.`$tool_dir/get_version.sh --full`; -our $lcov_url = "http://ltp.sourceforge.net/coverage/lcov.php"; -our $gcov_tool = "gcov"; -our $tool_name = basename($0); - -our $GCOV_VERSION_8_0_0 = 0x80000; -our $GCOV_VERSION_4_7_0 = 0x40700; -our $GCOV_VERSION_3_4_0 = 0x30400; -our $GCOV_VERSION_3_3_0 = 0x30300; -our $GCNO_FUNCTION_TAG = 0x01000000; -our $GCNO_LINES_TAG = 0x01450000; -our $GCNO_FILE_MAGIC = 0x67636e6f; -our $BBG_FILE_MAGIC = 0x67626267; - -# Error classes which users may specify to ignore during processing -our $ERROR_GCOV = 0; -our $ERROR_SOURCE = 1; -our $ERROR_GRAPH = 2; -our %ERROR_ID = ( - "gcov" => $ERROR_GCOV, - "source" => $ERROR_SOURCE, - "graph" => $ERROR_GRAPH, -); - -our $EXCL_START = "LCOV_EXCL_START"; -our $EXCL_STOP = "LCOV_EXCL_STOP"; - -# Marker to exclude branch coverage but keep function and line coveage -our $EXCL_BR_START = "LCOV_EXCL_BR_START"; -our $EXCL_BR_STOP = "LCOV_EXCL_BR_STOP"; - -# Compatibility mode values -our $COMPAT_VALUE_OFF = 0; -our $COMPAT_VALUE_ON = 1; -our $COMPAT_VALUE_AUTO = 2; - -# Compatibility mode value names -our %COMPAT_NAME_TO_VALUE = ( - "off" => $COMPAT_VALUE_OFF, - "on" => $COMPAT_VALUE_ON, - "auto" => $COMPAT_VALUE_AUTO, -); - -# Compatiblity modes -our $COMPAT_MODE_LIBTOOL = 1 << 0; -our $COMPAT_MODE_HAMMER = 1 << 1; -our $COMPAT_MODE_SPLIT_CRC = 1 << 2; - -# Compatibility mode names -our %COMPAT_NAME_TO_MODE = ( - "libtool" => $COMPAT_MODE_LIBTOOL, - "hammer" => $COMPAT_MODE_HAMMER, - "split_crc" => $COMPAT_MODE_SPLIT_CRC, - "android_4_4_0" => $COMPAT_MODE_SPLIT_CRC, -); - -# Map modes to names -our %COMPAT_MODE_TO_NAME = ( - $COMPAT_MODE_LIBTOOL => "libtool", - $COMPAT_MODE_HAMMER => "hammer", - $COMPAT_MODE_SPLIT_CRC => "split_crc", -); - -# Compatibility mode default values -our %COMPAT_MODE_DEFAULTS = ( - $COMPAT_MODE_LIBTOOL => $COMPAT_VALUE_ON, - $COMPAT_MODE_HAMMER => $COMPAT_VALUE_AUTO, - $COMPAT_MODE_SPLIT_CRC => $COMPAT_VALUE_AUTO, -); - -# Compatibility mode auto-detection routines -sub compat_hammer_autodetect(); -our %COMPAT_MODE_AUTO = ( - $COMPAT_MODE_HAMMER => \&compat_hammer_autodetect, - $COMPAT_MODE_SPLIT_CRC => 1, # will be done later -); - -our $BR_LINE = 0; -our $BR_BLOCK = 1; -our $BR_BRANCH = 2; -our $BR_TAKEN = 3; -our $BR_VEC_ENTRIES = 4; -our $BR_VEC_WIDTH = 32; -our $BR_VEC_MAX = vec(pack('b*', 1 x $BR_VEC_WIDTH), 0, $BR_VEC_WIDTH); - -our $UNNAMED_BLOCK = -1; - -# Prototypes -sub print_usage(*); -sub transform_pattern($); -sub gen_info($); -sub process_dafile($$); -sub match_filename($@); -sub solve_ambiguous_match($$$); -sub split_filename($); -sub solve_relative_path($$); -sub read_gcov_header($); -sub read_gcov_file($); -sub info(@); -sub process_intermediate($$$); -sub map_llvm_version($); -sub version_to_str($); -sub get_gcov_version(); -sub system_no_output($@); -sub read_config($); -sub apply_config($); -sub apply_exclusion_data($$); -sub process_graphfile($$); -sub filter_fn_name($); -sub warn_handler($); -sub die_handler($); -sub graph_error($$); -sub graph_expect($); -sub graph_read(*$;$$); -sub graph_skip(*$;$); -sub uniq(@); -sub sort_uniq(@); -sub sort_uniq_lex(@); -sub graph_cleanup($); -sub graph_find_base($); -sub graph_from_bb($$$$); -sub graph_add_order($$$); -sub read_bb_word(*;$); -sub read_bb_value(*;$); -sub read_bb_string(*$); -sub read_bb($); -sub read_bbg_word(*;$); -sub read_bbg_value(*;$); -sub read_bbg_string(*); -sub read_bbg_lines_record(*$$$$$); -sub read_bbg($); -sub read_gcno_word(*;$$); -sub read_gcno_value(*$;$$); -sub read_gcno_string(*$); -sub read_gcno_lines_record(*$$$$$$); -sub determine_gcno_split_crc($$$$); -sub read_gcno_function_record(*$$$$$); -sub read_gcno($); -sub get_gcov_capabilities(); -sub get_overall_line($$$$); -sub print_overall_rate($$$$$$$$$); -sub br_gvec_len($); -sub br_gvec_get($$); -sub debug($); -sub int_handler(); -sub parse_ignore_errors(@); -sub is_external($); -sub compat_name($); -sub parse_compat_modes($); -sub is_compat($); -sub is_compat_auto($); - - -# Global variables -our $gcov_version; -our $gcov_version_string; -our $graph_file_extension; -our $data_file_extension; -our @data_directory; -our $test_name = ""; -our $quiet; -our $help; -our $output_filename; -our $base_directory; -our $version; -our $follow; -our $checksum; -our $no_checksum; -our $opt_compat_libtool; -our $opt_no_compat_libtool; -our $rc_adjust_src_path;# Regexp specifying parts to remove from source path -our $adjust_src_pattern; -our $adjust_src_replace; -our $adjust_testname; -our $config; # Configuration file contents -our @ignore_errors; # List of errors to ignore (parameter) -our @ignore; # List of errors to ignore (array) -our $initial; -our @include_patterns; # List of source file patterns to include -our @exclude_patterns; # List of source file patterns to exclude -our %excluded_files; # Files excluded due to include/exclude options -our $no_recursion = 0; -our $maxdepth; -our $no_markers = 0; -our $opt_derive_func_data = 0; -our $opt_external = 1; -our $opt_no_external; -our $debug = 0; -our $gcov_caps; -our @gcov_options; -our @internal_dirs; -our $opt_config_file; -our $opt_gcov_all_blocks = 1; -our $opt_compat; -our %opt_rc; -our %compat_value; -our $gcno_split_crc; -our $func_coverage = 1; -our $br_coverage = 0; -our $rc_auto_base = 1; -our $rc_intermediate = "auto"; -our $intermediate; -our $excl_line = "LCOV_EXCL_LINE"; -our $excl_br_line = "LCOV_EXCL_BR_LINE"; - -our $cwd = `pwd`; -chomp($cwd); - - -# -# Code entry point -# - -# Register handler routine to be called when interrupted -$SIG{"INT"} = \&int_handler; -$SIG{__WARN__} = \&warn_handler; -$SIG{__DIE__} = \&die_handler; - -# Set LC_ALL so that gcov output will be in a unified format -$ENV{"LC_ALL"} = "C"; - -# Check command line for a configuration file name -Getopt::Long::Configure("pass_through", "no_auto_abbrev"); -GetOptions("config-file=s" => \$opt_config_file, - "rc=s%" => \%opt_rc); -Getopt::Long::Configure("default"); - -{ - # Remove spaces around rc options - my %new_opt_rc; - - while (my ($key, $value) = each(%opt_rc)) { - $key =~ s/^\s+|\s+$//g; - $value =~ s/^\s+|\s+$//g; - - $new_opt_rc{$key} = $value; - } - %opt_rc = %new_opt_rc; -} - -# Read configuration file if available -if (defined($opt_config_file)) { - $config = read_config($opt_config_file); -} elsif (defined($ENV{"HOME"}) && (-r $ENV{"HOME"}."/.lcovrc")) -{ - $config = read_config($ENV{"HOME"}."/.lcovrc"); -} -elsif (-r "/etc/lcovrc") -{ - $config = read_config("/etc/lcovrc"); -} elsif (-r "/usr/local/etc/lcovrc") -{ - $config = read_config("/usr/local/etc/lcovrc"); -} - -if ($config || %opt_rc) -{ - # Copy configuration file and --rc values to variables - apply_config({ - "geninfo_gcov_tool" => \$gcov_tool, - "geninfo_adjust_testname" => \$adjust_testname, - "geninfo_checksum" => \$checksum, - "geninfo_no_checksum" => \$no_checksum, # deprecated - "geninfo_compat_libtool" => \$opt_compat_libtool, - "geninfo_external" => \$opt_external, - "geninfo_gcov_all_blocks" => \$opt_gcov_all_blocks, - "geninfo_compat" => \$opt_compat, - "geninfo_adjust_src_path" => \$rc_adjust_src_path, - "geninfo_auto_base" => \$rc_auto_base, - "geninfo_intermediate" => \$rc_intermediate, - "lcov_function_coverage" => \$func_coverage, - "lcov_branch_coverage" => \$br_coverage, - "lcov_excl_line" => \$excl_line, - "lcov_excl_br_line" => \$excl_br_line, - }); - - # Merge options - if (defined($no_checksum)) - { - $checksum = ($no_checksum ? 0 : 1); - $no_checksum = undef; - } - - # Check regexp - if (defined($rc_adjust_src_path)) { - my ($pattern, $replace) = split(/\s*=>\s*/, - $rc_adjust_src_path); - local $SIG{__DIE__}; - eval '$adjust_src_pattern = qr>'.$pattern.'>;'; - if (!defined($adjust_src_pattern)) { - my $msg = $@; - - chomp($msg); - $msg =~ s/at \(eval.*$//; - warn("WARNING: invalid pattern in ". - "geninfo_adjust_src_path: $msg\n"); - } elsif (!defined($replace)) { - # If no replacement is specified, simply remove pattern - $adjust_src_replace = ""; - } else { - $adjust_src_replace = $replace; - } - } - for my $regexp (($excl_line, $excl_br_line)) { - eval 'qr/'.$regexp.'/'; - my $error = $@; - chomp($error); - $error =~ s/at \(eval.*$//; - die("ERROR: invalid exclude pattern: $error") if $error; - } -} - -# Parse command line options -if (!GetOptions("test-name|t=s" => \$test_name, - "output-filename|o=s" => \$output_filename, - "checksum" => \$checksum, - "no-checksum" => \$no_checksum, - "base-directory|b=s" => \$base_directory, - "version|v" =>\$version, - "quiet|q" => \$quiet, - "help|h|?" => \$help, - "follow|f" => \$follow, - "compat-libtool" => \$opt_compat_libtool, - "no-compat-libtool" => \$opt_no_compat_libtool, - "gcov-tool=s" => \$gcov_tool, - "ignore-errors=s" => \@ignore_errors, - "initial|i" => \$initial, - "include=s" => \@include_patterns, - "exclude=s" => \@exclude_patterns, - "no-recursion" => \$no_recursion, - "no-markers" => \$no_markers, - "derive-func-data" => \$opt_derive_func_data, - "debug" => \$debug, - "external|e" => \$opt_external, - "no-external" => \$opt_no_external, - "compat=s" => \$opt_compat, - "config-file=s" => \$opt_config_file, - "rc=s%" => \%opt_rc, - )) -{ - print(STDERR "Use $tool_name --help to get usage information\n"); - exit(1); -} -else -{ - # Merge options - if (defined($no_checksum)) - { - $checksum = ($no_checksum ? 0 : 1); - $no_checksum = undef; - } - - if (defined($opt_no_compat_libtool)) - { - $opt_compat_libtool = ($opt_no_compat_libtool ? 0 : 1); - $opt_no_compat_libtool = undef; - } - - if (defined($opt_no_external)) { - $opt_external = 0; - $opt_no_external = undef; - } - - if(@include_patterns) { - # Need perlreg expressions instead of shell pattern - @include_patterns = map({ transform_pattern($_); } @include_patterns); - } - - if(@exclude_patterns) { - # Need perlreg expressions instead of shell pattern - @exclude_patterns = map({ transform_pattern($_); } @exclude_patterns); - } -} - -@data_directory = @ARGV; - -debug("$lcov_version\n"); - -# Check for help option -if ($help) -{ - print_usage(*STDOUT); - exit(0); -} - -# Check for version option -if ($version) -{ - print("$tool_name: $lcov_version\n"); - exit(0); -} - -# Check gcov tool -if (system_no_output(3, $gcov_tool, "--help") == -1) -{ - die("ERROR: need tool $gcov_tool!\n"); -} - -($gcov_version, $gcov_version_string) = get_gcov_version(); -$gcov_caps = get_gcov_capabilities(); - -# Determine intermediate mode -if ($rc_intermediate eq "0") { - $intermediate = 0; -} elsif ($rc_intermediate eq "1") { - $intermediate = 1; -} elsif (lc($rc_intermediate) eq "auto") { - # Use intermediate format if supported by gcov - $intermediate = ($gcov_caps->{'intermediate-format'} || - $gcov_caps->{'json-format'}) ? 1 : 0; -} else { - die("ERROR: invalid value for geninfo_intermediate: ". - "'$rc_intermediate'\n"); -} - -if ($intermediate) { - info("Using intermediate gcov format\n"); - if ($opt_derive_func_data) { - warn("WARNING: --derive-func-data is not compatible with ". - "intermediate format - ignoring\n"); - $opt_derive_func_data = 0; - } -} - -# Determine gcov options -push(@gcov_options, "-b") if ($gcov_caps->{'branch-probabilities'} && - ($br_coverage || $func_coverage)); -push(@gcov_options, "-c") if ($gcov_caps->{'branch-counts'} && - $br_coverage); -push(@gcov_options, "-a") if ($gcov_caps->{'all-blocks'} && - $opt_gcov_all_blocks && $br_coverage && - !$intermediate); -if ($gcov_caps->{'hash-filenames'}) -{ - push(@gcov_options, "-x"); -} else { - push(@gcov_options, "-p") if ($gcov_caps->{'preserve-paths'}); -} - -# Determine compatibility modes -parse_compat_modes($opt_compat); - -# Determine which errors the user wants us to ignore -parse_ignore_errors(@ignore_errors); - -# Make sure test names only contain valid characters -if ($test_name =~ s/\W/_/g) -{ - warn("WARNING: invalid characters removed from testname!\n"); -} - -# Adjust test name to include uname output if requested -if ($adjust_testname) -{ - $test_name .= "__".`uname -a`; - $test_name =~ s/\W/_/g; -} - -# Make sure base_directory contains an absolute path specification -if ($base_directory) -{ - $base_directory = solve_relative_path($cwd, $base_directory); -} - -# Check for follow option -if ($follow) -{ - $follow = "-follow" -} -else -{ - $follow = ""; -} - -# Determine checksum mode -if (defined($checksum)) -{ - # Normalize to boolean - $checksum = ($checksum ? 1 : 0); -} -else -{ - # Default is off - $checksum = 0; -} - -# Determine max depth for recursion -if ($no_recursion) -{ - $maxdepth = "-maxdepth 1"; -} -else -{ - $maxdepth = ""; -} - -# Check for directory name -if (!@data_directory) -{ - die("No directory specified\n". - "Use $tool_name --help to get usage information\n"); -} -else -{ - foreach (@data_directory) - { - stat($_); - if (!-r _) - { - die("ERROR: cannot read $_!\n"); - } - } -} - -if ($gcov_version < $GCOV_VERSION_3_4_0) -{ - if (is_compat($COMPAT_MODE_HAMMER)) - { - $data_file_extension = ".da"; - $graph_file_extension = ".bbg"; - } - else - { - $data_file_extension = ".da"; - $graph_file_extension = ".bb"; - } -} -else -{ - $data_file_extension = ".gcda"; - $graph_file_extension = ".gcno"; -} - -# Check output filename -if (defined($output_filename) && ($output_filename ne "-")) -{ - # Initially create output filename, data is appended - # for each data file processed - local *DUMMY_HANDLE; - open(DUMMY_HANDLE, ">", $output_filename) - or die("ERROR: cannot create $output_filename!\n"); - close(DUMMY_HANDLE); - - # Make $output_filename an absolute path because we're going - # to change directories while processing files - if (!($output_filename =~ /^\/(.*)$/)) - { - $output_filename = $cwd."/".$output_filename; - } -} - -# Build list of directories to identify external files -foreach my $entry(@data_directory, $base_directory) { - next if (!defined($entry)); - push(@internal_dirs, solve_relative_path($cwd, $entry)); -} - -# Do something -foreach my $entry (@data_directory) { - gen_info($entry); -} - -if ($initial && $br_coverage && !$intermediate) { - warn("Note: --initial does not generate branch coverage ". - "data\n"); -} -info("Finished .info-file creation\n"); - -exit(0); - - - -# -# print_usage(handle) -# -# Print usage information. -# - -sub print_usage(*) -{ - local *HANDLE = $_[0]; - - print(HANDLE < (.) and * => (.*) - - $pattern =~ s/\*/\(\.\*\)/g; - $pattern =~ s/\?/\(\.\)/g; - - return $pattern; -} - - -# -# get_common_prefix(min_dir, filenames) -# -# Return the longest path prefix shared by all filenames. MIN_DIR specifies -# the minimum number of directories that a filename may have after removing -# the prefix. -# - -sub get_common_prefix($@) -{ - my ($min_dir, @files) = @_; - my $file; - my @prefix; - my $i; - - foreach $file (@files) { - my ($v, $d, $f) = splitpath($file); - my @comp = splitdir($d); - - if (!@prefix) { - @prefix = @comp; - next; - } - for ($i = 0; $i < scalar(@comp) && $i < scalar(@prefix); $i++) { - if ($comp[$i] ne $prefix[$i] || - ((scalar(@comp) - ($i + 1)) <= $min_dir)) { - delete(@prefix[$i..scalar(@prefix)]); - last; - } - } - } - - return catdir(@prefix); -} - -# -# gen_info(directory) -# -# Traverse DIRECTORY and create a .info file for each data file found. -# The .info file contains TEST_NAME in the following format: -# -# TN: -# -# For each source file name referenced in the data file, there is a section -# containing source code and coverage data: -# -# SF: -# FN:, for each function -# DA:, for each instrumented line -# LH: greater than 0 -# LF: -# -# Sections are separated by: -# -# end_of_record -# -# In addition to the main source code file there are sections for each -# #included file containing executable code. Note that the absolute path -# of a source file is generated by interpreting the contents of the respective -# graph file. Relative filenames are prefixed with the directory in which the -# graph file is found. Note also that symbolic links to the graph file will be -# resolved so that the actual file path is used instead of the path to a link. -# This approach is necessary for the mechanism to work with the /proc/gcov -# files. -# -# Die on error. -# - -sub gen_info($) -{ - my $directory = $_[0]; - my @file_list; - my $file; - my $prefix; - my $type; - my $ext; - my $tempdir; - - if ($initial) { - $type = "graph"; - $ext = $graph_file_extension; - } else { - $type = "data"; - $ext = $data_file_extension; - } - - if (-d $directory) - { - info("Scanning $directory for $ext files ...\n"); - - @file_list = `find "$directory" $maxdepth $follow -name \\*$ext -type f -o -name \\*$ext -type l 2>/dev/null`; - chomp(@file_list); - if (!@file_list) { - warn("WARNING: no $ext files found in $directory - ". - "skipping!\n"); - return; - } - $prefix = get_common_prefix(1, @file_list); - info("Found %d %s files in %s\n", $#file_list+1, $type, - $directory); - } - else - { - @file_list = ($directory); - $prefix = ""; - } - - $tempdir = tempdir(CLEANUP => 1); - - # Process all files in list - foreach $file (@file_list) { - # Process file - if ($intermediate) { - process_intermediate($file, $prefix, $tempdir); - } elsif ($initial) { - process_graphfile($file, $prefix); - } else { - process_dafile($file, $prefix); - } - } - - unlink($tempdir); - - # Report whether files were excluded. - if (%excluded_files) { - info("Excluded data for %d files due to include/exclude options\n", - scalar keys %excluded_files); - } -} - - -# -# derive_data(contentdata, funcdata, bbdata) -# -# Calculate function coverage data by combining line coverage data and the -# list of lines belonging to a function. -# -# contentdata: [ instr1, count1, source1, instr2, count2, source2, ... ] -# instr: Instrumentation flag for line n -# count: Execution count for line n -# source: Source code for line n -# -# funcdata: [ count1, func1, count2, func2, ... ] -# count: Execution count for function number n -# func: Function name for function number n -# -# bbdata: function_name -> [ line1, line2, ... ] -# line: Line number belonging to the corresponding function -# - -sub derive_data($$$) -{ - my ($contentdata, $funcdata, $bbdata) = @_; - my @gcov_content = @{$contentdata}; - my @gcov_functions = @{$funcdata}; - my %fn_count; - my %ln_fn; - my $line; - my $maxline; - my %fn_name; - my $fn; - my $count; - - if (!defined($bbdata)) { - return @gcov_functions; - } - - # First add existing function data - while (@gcov_functions) { - $count = shift(@gcov_functions); - $fn = shift(@gcov_functions); - - $fn_count{$fn} = $count; - } - - # Convert line coverage data to function data - foreach $fn (keys(%{$bbdata})) { - my $line_data = $bbdata->{$fn}; - my $line; - my $fninstr = 0; - - if ($fn eq "") { - next; - } - # Find the lowest line count for this function - $count = 0; - foreach $line (@$line_data) { - my $linstr = $gcov_content[ ( $line - 1 ) * 3 + 0 ]; - my $lcount = $gcov_content[ ( $line - 1 ) * 3 + 1 ]; - - next if (!$linstr); - $fninstr = 1; - if (($lcount > 0) && - (($count == 0) || ($lcount < $count))) { - $count = $lcount; - } - } - next if (!$fninstr); - $fn_count{$fn} = $count; - } - - - # Check if we got data for all functions - foreach $fn (keys(%fn_name)) { - if ($fn eq "") { - next; - } - if (defined($fn_count{$fn})) { - next; - } - warn("WARNING: no derived data found for function $fn\n"); - } - - # Convert hash to list in @gcov_functions format - foreach $fn (sort(keys(%fn_count))) { - push(@gcov_functions, $fn_count{$fn}, $fn); - } - - return @gcov_functions; -} - -# -# get_filenames(directory, pattern) -# -# Return a list of filenames found in directory which match the specified -# pattern. -# -# Die on error. -# - -sub get_filenames($$) -{ - my ($dirname, $pattern) = @_; - my @result; - my $directory; - local *DIR; - - opendir(DIR, $dirname) or - die("ERROR: cannot read directory $dirname\n"); - while ($directory = readdir(DIR)) { - push(@result, $directory) if ($directory =~ /$pattern/); - } - closedir(DIR); - - return @result; -} - -# -# process_dafile(da_filename, dir) -# -# Create a .info file for a single data file. -# -# Die on error. -# - -sub process_dafile($$) -{ - my ($file, $dir) = @_; - my $da_filename; # Name of data file to process - my $da_dir; # Directory of data file - my $source_dir; # Directory of source file - my $da_basename; # data filename without ".da/.gcda" extension - my $bb_filename; # Name of respective graph file - my $bb_basename; # Basename of the original graph file - my $graph; # Contents of graph file - my $instr; # Contents of graph file part 2 - my $gcov_error; # Error code of gcov tool - my $object_dir; # Directory containing all object files - my $source_filename; # Name of a source code file - my $gcov_file; # Name of a .gcov file - my @gcov_content; # Content of a .gcov file - my $gcov_branches; # Branch content of a .gcov file - my @gcov_functions; # Function calls of a .gcov file - my @gcov_list; # List of generated .gcov files - my $line_number; # Line number count - my $lines_hit; # Number of instrumented lines hit - my $lines_found; # Number of instrumented lines found - my $funcs_hit; # Number of instrumented functions hit - my $funcs_found; # Number of instrumented functions found - my $br_hit; - my $br_found; - my $source; # gcov source header information - my $object; # gcov object header information - my @matches; # List of absolute paths matching filename - my $base_dir; # Base directory for current file - my @tmp_links; # Temporary links to be cleaned up - my @result; - my $index; - my $da_renamed; # If data file is to be renamed - local *INFO_HANDLE; - - info("Processing %s\n", abs2rel($file, $dir)); - # Get path to data file in absolute and normalized form (begins with /, - # contains no more ../ or ./) - $da_filename = solve_relative_path($cwd, $file); - - # Get directory and basename of data file - ($da_dir, $da_basename) = split_filename($da_filename); - - $source_dir = $da_dir; - if (is_compat($COMPAT_MODE_LIBTOOL)) { - # Avoid files from .libs dirs - $source_dir =~ s/\.libs$//; - } - - if (-z $da_filename) - { - $da_renamed = 1; - } - else - { - $da_renamed = 0; - } - - # Construct base_dir for current file - if ($base_directory) - { - $base_dir = $base_directory; - } - else - { - $base_dir = $source_dir; - } - - # Check for writable $base_dir (gcov will try to write files there) - stat($base_dir); - if (!-w _) - { - die("ERROR: cannot write to directory $base_dir!\n"); - } - - # Construct name of graph file - $bb_basename = $da_basename.$graph_file_extension; - $bb_filename = "$da_dir/$bb_basename"; - - # Find out the real location of graph file in case we're just looking at - # a link - while (readlink($bb_filename)) - { - my $last_dir = dirname($bb_filename); - - $bb_filename = readlink($bb_filename); - $bb_filename = solve_relative_path($last_dir, $bb_filename); - } - - # Ignore empty graph file (e.g. source file with no statement) - if (-z $bb_filename) - { - warn("WARNING: empty $bb_filename (skipped)\n"); - return; - } - - # Read contents of graph file into hash. We need it later to find out - # the absolute path to each .gcov file created as well as for - # information about functions and their source code positions. - if ($gcov_version < $GCOV_VERSION_3_4_0) - { - if (is_compat($COMPAT_MODE_HAMMER)) - { - ($instr, $graph) = read_bbg($bb_filename); - } - else - { - ($instr, $graph) = read_bb($bb_filename); - } - } - else - { - ($instr, $graph) = read_gcno($bb_filename); - } - - # Try to find base directory automatically if requested by user - if ($rc_auto_base) { - $base_dir = find_base_from_source($base_dir, - [ keys(%{$instr}), keys(%{$graph}) ]); - } - - adjust_source_filenames($instr, $base_dir); - adjust_source_filenames($graph, $base_dir); - - # Set $object_dir to real location of object files. This may differ - # from $da_dir if the graph file is just a link to the "real" object - # file location. - $object_dir = dirname($bb_filename); - - # Is the data file in a different directory? (this happens e.g. with - # the gcov-kernel patch) - if ($object_dir ne $da_dir) - { - # Need to create link to data file in $object_dir - system("ln", "-s", $da_filename, - "$object_dir/$da_basename$data_file_extension") - and die ("ERROR: cannot create link $object_dir/". - "$da_basename$data_file_extension!\n"); - push(@tmp_links, - "$object_dir/$da_basename$data_file_extension"); - # Need to create link to graph file if basename of link - # and file are different (CONFIG_MODVERSION compat) - if ((basename($bb_filename) ne $bb_basename) && - (! -e "$object_dir/$bb_basename")) { - symlink($bb_filename, "$object_dir/$bb_basename") or - warn("WARNING: cannot create link ". - "$object_dir/$bb_basename\n"); - push(@tmp_links, "$object_dir/$bb_basename"); - } - } - - # Change to directory containing data files and apply GCOV - debug("chdir($base_dir)\n"); - chdir($base_dir); - - if ($da_renamed) - { - # Need to rename empty data file to workaround - # gcov <= 3.2.x bug (Abort) - system_no_output(3, "mv", "$da_filename", "$da_filename.ori") - and die ("ERROR: cannot rename $da_filename\n"); - } - - # Execute gcov command and suppress standard output - $gcov_error = system_no_output(1, $gcov_tool, $da_filename, - "-o", $object_dir, @gcov_options); - - if ($da_renamed) - { - system_no_output(3, "mv", "$da_filename.ori", "$da_filename") - and die ("ERROR: cannot rename $da_filename.ori"); - } - - # Clean up temporary links - foreach (@tmp_links) { - unlink($_); - } - - if ($gcov_error) - { - if ($ignore[$ERROR_GCOV]) - { - warn("WARNING: GCOV failed for $da_filename!\n"); - return; - } - die("ERROR: GCOV failed for $da_filename!\n"); - } - - # Collect data from resulting .gcov files and create .info file - @gcov_list = get_filenames('.', '\.gcov$'); - - # Check for files - if (!@gcov_list) - { - warn("WARNING: gcov did not create any files for ". - "$da_filename!\n"); - } - - # Check whether we're writing to a single file - if ($output_filename) - { - if ($output_filename eq "-") - { - *INFO_HANDLE = *STDOUT; - } - else - { - # Append to output file - open(INFO_HANDLE, ">>", $output_filename) - or die("ERROR: cannot write to ". - "$output_filename!\n"); - } - } - else - { - # Open .info file for output - open(INFO_HANDLE, ">", "$da_filename.info") - or die("ERROR: cannot create $da_filename.info!\n"); - } - - # Write test name - printf(INFO_HANDLE "TN:%s\n", $test_name); - - # Traverse the list of generated .gcov files and combine them into a - # single .info file - foreach $gcov_file (sort(@gcov_list)) - { - my $i; - my $num; - - # Skip gcov file for gcc built-in code - next if ($gcov_file eq ".gcov"); - - ($source, $object) = read_gcov_header($gcov_file); - - if (!defined($source)) { - # Derive source file name from gcov file name if - # header format could not be parsed - $source = $gcov_file; - $source =~ s/\.gcov$//; - } - - $source = solve_relative_path($base_dir, $source); - - if (defined($adjust_src_pattern)) { - # Apply transformation as specified by user - $source =~ s/$adjust_src_pattern/$adjust_src_replace/g; - } - - # gcov will happily create output even if there's no source code - # available - this interferes with checksum creation so we need - # to pull the emergency brake here. - if (! -r $source && $checksum) - { - if ($ignore[$ERROR_SOURCE]) - { - warn("WARNING: could not read source file ". - "$source\n"); - next; - } - die("ERROR: could not read source file $source\n"); - } - - @matches = match_filename($source, keys(%{$instr})); - - # Skip files that are not mentioned in the graph file - if (!@matches) - { - warn("WARNING: cannot find an entry for ".$gcov_file. - " in $graph_file_extension file, skipping ". - "file!\n"); - unlink($gcov_file); - next; - } - - # Read in contents of gcov file - @result = read_gcov_file($gcov_file); - if (!defined($result[0])) { - warn("WARNING: skipping unreadable file ". - $gcov_file."\n"); - unlink($gcov_file); - next; - } - @gcov_content = @{$result[0]}; - $gcov_branches = $result[1]; - @gcov_functions = @{$result[2]}; - - # Skip empty files - if (!@gcov_content) - { - warn("WARNING: skipping empty file ".$gcov_file."\n"); - unlink($gcov_file); - next; - } - - if (scalar(@matches) == 1) - { - # Just one match - $source_filename = $matches[0]; - } - else - { - # Try to solve the ambiguity - $source_filename = solve_ambiguous_match($gcov_file, - \@matches, \@gcov_content); - } - - if (@include_patterns) - { - my $keep = 0; - - foreach my $pattern (@include_patterns) - { - $keep ||= ($source_filename =~ (/^$pattern$/)); - } - - if (!$keep) - { - $excluded_files{$source_filename} = (); - unlink($gcov_file); - next; - } - } - - if (@exclude_patterns) - { - my $exclude = 0; - - foreach my $pattern (@exclude_patterns) - { - $exclude ||= ($source_filename =~ (/^$pattern$/)); - } - - if ($exclude) - { - $excluded_files{$source_filename} = (); - unlink($gcov_file); - next; - } - } - - # Skip external files if requested - if (!$opt_external) { - if (is_external($source_filename)) { - info(" ignoring data for external file ". - "$source_filename\n"); - unlink($gcov_file); - next; - } - } - - # Write absolute path of source file - printf(INFO_HANDLE "SF:%s\n", $source_filename); - - # If requested, derive function coverage data from - # line coverage data of the first line of a function - if ($opt_derive_func_data) { - @gcov_functions = - derive_data(\@gcov_content, \@gcov_functions, - $graph->{$source_filename}); - } - - # Write function-related information - if (defined($graph->{$source_filename})) - { - my $fn_data = $graph->{$source_filename}; - my $fn; - - foreach $fn (sort - {$fn_data->{$a}->[0] <=> $fn_data->{$b}->[0]} - keys(%{$fn_data})) { - my $ln_data = $fn_data->{$fn}; - my $line = $ln_data->[0]; - - # Skip empty function - if ($fn eq "") { - next; - } - # Remove excluded functions - if (!$no_markers) { - my $gfn; - my $found = 0; - - foreach $gfn (@gcov_functions) { - if ($gfn eq $fn) { - $found = 1; - last; - } - } - if (!$found) { - next; - } - } - - # Normalize function name - $fn = filter_fn_name($fn); - - print(INFO_HANDLE "FN:$line,$fn\n"); - } - } - - #-- - #-- FNDA: , - #-- FNF: overall count of functions - #-- FNH: overall count of functions with non-zero call count - #-- - $funcs_found = 0; - $funcs_hit = 0; - while (@gcov_functions) - { - my $count = shift(@gcov_functions); - my $fn = shift(@gcov_functions); - - $fn = filter_fn_name($fn); - printf(INFO_HANDLE "FNDA:$count,$fn\n"); - $funcs_found++; - $funcs_hit++ if ($count > 0); - } - if ($funcs_found > 0) { - printf(INFO_HANDLE "FNF:%s\n", $funcs_found); - printf(INFO_HANDLE "FNH:%s\n", $funcs_hit); - } - - # Write coverage information for each instrumented branch: - # - # BRDA:,,, - # - # where 'taken' is the number of times the branch was taken - # or '-' if the block to which the branch belongs was never - # executed - $br_found = 0; - $br_hit = 0; - $num = br_gvec_len($gcov_branches); - for ($i = 0; $i < $num; $i++) { - my ($line, $block, $branch, $taken) = - br_gvec_get($gcov_branches, $i); - - $block = $BR_VEC_MAX if ($block < 0); - print(INFO_HANDLE "BRDA:$line,$block,$branch,$taken\n"); - $br_found++; - $br_hit++ if ($taken ne '-' && $taken > 0); - } - if ($br_found > 0) { - printf(INFO_HANDLE "BRF:%s\n", $br_found); - printf(INFO_HANDLE "BRH:%s\n", $br_hit); - } - - # Reset line counters - $line_number = 0; - $lines_found = 0; - $lines_hit = 0; - - # Write coverage information for each instrumented line - # Note: @gcov_content contains a list of (flag, count, source) - # tuple for each source code line - while (@gcov_content) - { - $line_number++; - - # Check for instrumented line - if ($gcov_content[0]) - { - $lines_found++; - printf(INFO_HANDLE "DA:".$line_number.",". - $gcov_content[1].($checksum ? - ",". md5_base64($gcov_content[2]) : ""). - "\n"); - - # Increase $lines_hit in case of an execution - # count>0 - if ($gcov_content[1] > 0) { $lines_hit++; } - } - - # Remove already processed data from array - splice(@gcov_content,0,3); - } - - # Write line statistics and section separator - printf(INFO_HANDLE "LF:%s\n", $lines_found); - printf(INFO_HANDLE "LH:%s\n", $lines_hit); - print(INFO_HANDLE "end_of_record\n"); - - # Remove .gcov file after processing - unlink($gcov_file); - } - - if (!($output_filename && ($output_filename eq "-"))) - { - close(INFO_HANDLE); - } - - # Change back to initial directory - chdir($cwd); -} - - -# -# solve_relative_path(path, dir) -# -# Solve relative path components of DIR which, if not absolute, resides in PATH. -# - -sub solve_relative_path($$) -{ - my $path = $_[0]; - my $dir = $_[1]; - my $volume; - my $directories; - my $filename; - my @dirs; # holds path elements - my $result; - - # Convert from Windows path to msys path - if( $^O eq "msys" ) - { - # search for a windows drive letter at the beginning - ($volume, $directories, $filename) = File::Spec::Win32->splitpath( $dir ); - if( $volume ne '' ) - { - my $uppercase_volume; - # transform c/d\../e/f\g to Windows style c\d\..\e\f\g - $dir = File::Spec::Win32->canonpath( $dir ); - # use Win32 module to retrieve path components - # $uppercase_volume is not used any further - ( $uppercase_volume, $directories, $filename ) = File::Spec::Win32->splitpath( $dir ); - @dirs = File::Spec::Win32->splitdir( $directories ); - - # prepend volume, since in msys C: is always mounted to /c - $volume =~ s|^([a-zA-Z]+):|/\L$1\E|; - unshift( @dirs, $volume ); - - # transform to Unix style '/' path - $directories = File::Spec->catdir( @dirs ); - $dir = File::Spec->catpath( '', $directories, $filename ); - } else { - # eliminate '\' path separators - $dir = File::Spec->canonpath( $dir ); - } - } - - $result = $dir; - # Prepend path if not absolute - if ($dir =~ /^[^\/]/) - { - $result = "$path/$result"; - } - - # Remove // - $result =~ s/\/\//\//g; - - # Remove . - while ($result =~ s/\/\.\//\//g) - { - } - $result =~ s/\/\.$/\//g; - - # Remove trailing / - $result =~ s/\/$//g; - - # Solve .. - while ($result =~ s/\/[^\/]+\/\.\.\//\//) - { - } - - # Remove preceding .. - $result =~ s/^\/\.\.\//\//g; - - return $result; -} - - -# -# match_filename(gcov_filename, list) -# -# Return a list of those entries of LIST which match the relative filename -# GCOV_FILENAME. -# - -sub match_filename($@) -{ - my ($filename, @list) = @_; - my ($vol, $dir, $file) = splitpath($filename); - my @comp = splitdir($dir); - my $comps = scalar(@comp); - my $entry; - my @result; - -entry: - foreach $entry (@list) { - my ($evol, $edir, $efile) = splitpath($entry); - my @ecomp; - my $ecomps; - my $i; - - # Filename component must match - if ($efile ne $file) { - next; - } - # Check directory components last to first for match - @ecomp = splitdir($edir); - $ecomps = scalar(@ecomp); - if ($ecomps < $comps) { - next; - } - for ($i = 0; $i < $comps; $i++) { - if ($comp[$comps - $i - 1] ne - $ecomp[$ecomps - $i - 1]) { - next entry; - } - } - push(@result, $entry), - } - - return @result; -} - -# -# solve_ambiguous_match(rel_filename, matches_ref, gcov_content_ref) -# -# Try to solve ambiguous matches of mapping (gcov file) -> (source code) file -# by comparing source code provided in the GCOV file with that of the files -# in MATCHES. REL_FILENAME identifies the relative filename of the gcov -# file. -# -# Return the one real match or die if there is none. -# - -sub solve_ambiguous_match($$$) -{ - my $rel_name = $_[0]; - my $matches = $_[1]; - my $content = $_[2]; - my $filename; - my $index; - my $no_match; - local *SOURCE; - - # Check the list of matches - foreach $filename (@$matches) - { - - # Compare file contents - open(SOURCE, "<", $filename) - or die("ERROR: cannot read $filename!\n"); - - $no_match = 0; - for ($index = 2; ; $index += 3) - { - chomp; - - # Also remove CR from line-end - s/\015$//; - - if ($_ ne @$content[$index]) - { - $no_match = 1; - last; - } - } - - close(SOURCE); - - if (!$no_match) - { - info("Solved source file ambiguity for $rel_name\n"); - return $filename; - } - } - - die("ERROR: could not match gcov data for $rel_name!\n"); -} - - -# -# split_filename(filename) -# -# Return (path, filename, extension) for a given FILENAME. -# - -sub split_filename($) -{ - my @path_components = split('/', $_[0]); - my @file_components = split('\.', pop(@path_components)); - my $extension = pop(@file_components); - - return (join("/",@path_components), join(".",@file_components), - $extension); -} - - -# -# read_gcov_header(gcov_filename) -# -# Parse file GCOV_FILENAME and return a list containing the following -# information: -# -# (source, object) -# -# where: -# -# source: complete relative path of the source code file (gcc >= 3.3 only) -# object: name of associated graph file -# -# Die on error. -# - -sub read_gcov_header($) -{ - my $source; - my $object; - local *INPUT; - - if (!open(INPUT, "<", $_[0])) - { - if ($ignore_errors[$ERROR_GCOV]) - { - warn("WARNING: cannot read $_[0]!\n"); - return (undef,undef); - } - die("ERROR: cannot read $_[0]!\n"); - } - - while () - { - chomp($_); - - # Also remove CR from line-end - s/\015$//; - - if (/^\s+-:\s+0:Source:(.*)$/) - { - # Source: header entry - $source = $1; - } - elsif (/^\s+-:\s+0:Object:(.*)$/) - { - # Object: header entry - $object = $1; - } - else - { - last; - } - } - - close(INPUT); - - return ($source, $object); -} - - -# -# br_gvec_len(vector) -# -# Return the number of entries in the branch coverage vector. -# - -sub br_gvec_len($) -{ - my ($vec) = @_; - - return 0 if (!defined($vec)); - return (length($vec) * 8 / $BR_VEC_WIDTH) / $BR_VEC_ENTRIES; -} - - -# -# br_gvec_get(vector, number) -# -# Return an entry from the branch coverage vector. -# - -sub br_gvec_get($$) -{ - my ($vec, $num) = @_; - my $line; - my $block; - my $branch; - my $taken; - my $offset = $num * $BR_VEC_ENTRIES; - - # Retrieve data from vector - $line = vec($vec, $offset + $BR_LINE, $BR_VEC_WIDTH); - $block = vec($vec, $offset + $BR_BLOCK, $BR_VEC_WIDTH); - $block = -1 if ($block == $BR_VEC_MAX); - $branch = vec($vec, $offset + $BR_BRANCH, $BR_VEC_WIDTH); - $taken = vec($vec, $offset + $BR_TAKEN, $BR_VEC_WIDTH); - - # Decode taken value from an integer - if ($taken == 0) { - $taken = "-"; - } else { - $taken--; - } - - return ($line, $block, $branch, $taken); -} - - -# -# br_gvec_push(vector, line, block, branch, taken) -# -# Add an entry to the branch coverage vector. -# - -sub br_gvec_push($$$$$) -{ - my ($vec, $line, $block, $branch, $taken) = @_; - my $offset; - - $vec = "" if (!defined($vec)); - $offset = br_gvec_len($vec) * $BR_VEC_ENTRIES; - $block = $BR_VEC_MAX if $block < 0; - - # Encode taken value into an integer - if ($taken eq "-") { - $taken = 0; - } else { - $taken++; - } - - # Add to vector - vec($vec, $offset + $BR_LINE, $BR_VEC_WIDTH) = $line; - vec($vec, $offset + $BR_BLOCK, $BR_VEC_WIDTH) = $block; - vec($vec, $offset + $BR_BRANCH, $BR_VEC_WIDTH) = $branch; - vec($vec, $offset + $BR_TAKEN, $BR_VEC_WIDTH) = $taken; - - return $vec; -} - - -# -# read_gcov_file(gcov_filename) -# -# Parse file GCOV_FILENAME (.gcov file format) and return the list: -# (reference to gcov_content, reference to gcov_branch, reference to gcov_func) -# -# gcov_content is a list of 3 elements -# (flag, count, source) for each source code line: -# -# $result[($line_number-1)*3+0] = instrumentation flag for line $line_number -# $result[($line_number-1)*3+1] = execution count for line $line_number -# $result[($line_number-1)*3+2] = source code text for line $line_number -# -# gcov_branch is a vector of 4 4-byte long elements for each branch: -# line number, block number, branch number, count + 1 or 0 -# -# gcov_func is a list of 2 elements -# (number of calls, function name) for each function -# -# Die on error. -# - -sub read_gcov_file($) -{ - my $filename = $_[0]; - my @result = (); - my $branches = ""; - my @functions = (); - my $number; - my $exclude_flag = 0; - my $exclude_line = 0; - my $exclude_br_flag = 0; - my $exclude_branch = 0; - my $last_block = $UNNAMED_BLOCK; - my $last_line = 0; - local *INPUT; - - if (!open(INPUT, "<", $filename)) { - if ($ignore_errors[$ERROR_GCOV]) - { - warn("WARNING: cannot read $filename!\n"); - return (undef, undef, undef); - } - die("ERROR: cannot read $filename!\n"); - } - - if ($gcov_version < $GCOV_VERSION_3_3_0) - { - # Expect gcov format as used in gcc < 3.3 - while () - { - chomp($_); - - # Also remove CR from line-end - s/\015$//; - - if (/^branch\s+(\d+)\s+taken\s+=\s+(\d+)/) { - next if (!$br_coverage); - next if ($exclude_line); - next if ($exclude_branch); - $branches = br_gvec_push($branches, $last_line, - $last_block, $1, $2); - } elsif (/^branch\s+(\d+)\s+never\s+executed/) { - next if (!$br_coverage); - next if ($exclude_line); - next if ($exclude_branch); - $branches = br_gvec_push($branches, $last_line, - $last_block, $1, '-'); - } - elsif (/^call/ || /^function/) - { - # Function call return data - } - else - { - $last_line++; - # Check for exclusion markers - if (!$no_markers) { - if (/$EXCL_STOP/) { - $exclude_flag = 0; - } elsif (/$EXCL_START/) { - $exclude_flag = 1; - } - if (/$excl_line/ || $exclude_flag) { - $exclude_line = 1; - } else { - $exclude_line = 0; - } - } - # Check for exclusion markers (branch exclude) - if (!$no_markers) { - if (/$EXCL_BR_STOP/) { - $exclude_br_flag = 0; - } elsif (/$EXCL_BR_START/) { - $exclude_br_flag = 1; - } - if (/$excl_br_line/ || $exclude_br_flag) { - $exclude_branch = 1; - } else { - $exclude_branch = 0; - } - } - # Source code execution data - if (/^\t\t(.*)$/) - { - # Uninstrumented line - push(@result, 0); - push(@result, 0); - push(@result, $1); - next; - } - $number = (split(" ",substr($_, 0, 16)))[0]; - - # Check for zero count which is indicated - # by ###### - if ($number eq "######") { $number = 0; } - - if ($exclude_line) { - # Register uninstrumented line instead - push(@result, 0); - push(@result, 0); - } else { - push(@result, 1); - push(@result, $number); - } - push(@result, substr($_, 16)); - } - } - } - else - { - # Expect gcov format as used in gcc >= 3.3 - while () - { - chomp($_); - - # Also remove CR from line-end - s/\015$//; - - if (/^\s*(\d+|\$+|\%+):\s*(\d+)-block\s+(\d+)\s*$/) { - # Block information - used to group related - # branches - $last_line = $2; - $last_block = $3; - } elsif (/^branch\s+(\d+)\s+taken\s+(\d+)/) { - next if (!$br_coverage); - next if ($exclude_line); - next if ($exclude_branch); - $branches = br_gvec_push($branches, $last_line, - $last_block, $1, $2); - } elsif (/^branch\s+(\d+)\s+never\s+executed/) { - next if (!$br_coverage); - next if ($exclude_line); - next if ($exclude_branch); - $branches = br_gvec_push($branches, $last_line, - $last_block, $1, '-'); - } - elsif (/^function\s+(.+)\s+called\s+(\d+)\s+/) - { - next if (!$func_coverage); - if ($exclude_line) { - next; - } - push(@functions, $2, $1); - } - elsif (/^call/) - { - # Function call return data - } - elsif (/^\s*([^:]+):\s*([^:]+):(.*)$/) - { - my ($count, $line, $code) = ($1, $2, $3); - - # Skip instance-specific counts - next if ($line <= (scalar(@result) / 3)); - - $last_line = $line; - $last_block = $UNNAMED_BLOCK; - # Check for exclusion markers - if (!$no_markers) { - if (/$EXCL_STOP/) { - $exclude_flag = 0; - } elsif (/$EXCL_START/) { - $exclude_flag = 1; - } - if (/$excl_line/ || $exclude_flag) { - $exclude_line = 1; - } else { - $exclude_line = 0; - } - } - # Check for exclusion markers (branch exclude) - if (!$no_markers) { - if (/$EXCL_BR_STOP/) { - $exclude_br_flag = 0; - } elsif (/$EXCL_BR_START/) { - $exclude_br_flag = 1; - } - if (/$excl_br_line/ || $exclude_br_flag) { - $exclude_branch = 1; - } else { - $exclude_branch = 0; - } - } - - # Strip unexecuted basic block marker - $count =~ s/\*$//; - - # :: - if ($line eq "0") - { - # Extra data - } - elsif ($count eq "-") - { - # Uninstrumented line - push(@result, 0); - push(@result, 0); - push(@result, $code); - } - else - { - if ($exclude_line) { - push(@result, 0); - push(@result, 0); - } else { - # Check for zero count - if ($count =~ /^[#=]/) { - $count = 0; - } - push(@result, 1); - push(@result, $count); - } - push(@result, $code); - } - } - } - } - - close(INPUT); - if ($exclude_flag || $exclude_br_flag) { - warn("WARNING: unterminated exclusion section in $filename\n"); - } - return(\@result, $branches, \@functions); -} - - -# -# read_intermediate_text(gcov_filename, data) -# -# Read gcov intermediate text format in GCOV_FILENAME and add the resulting -# data to DATA in the following format: -# -# data: source_filename -> file_data -# file_data: concatenated lines of intermediate text data -# - -sub read_intermediate_text($$) -{ - my ($gcov_filename, $data) = @_; - my $fd; - my $filename; - - open($fd, "<", $gcov_filename) or - die("ERROR: Could not read $gcov_filename: $!\n"); - while (my $line = <$fd>) { - if ($line =~ /^file:(.*)$/) { - $filename = $1; - chomp($filename); - } elsif (defined($filename)) { - $data->{$filename} .= $line; - } - } - close($fd); -} - - -# -# read_intermediate_json(gcov_filename, data, basedir_ref) -# -# Read gcov intermediate JSON format in GCOV_FILENAME and add the resulting -# data to DATA in the following format: -# -# data: source_filename -> file_data -# file_data: GCOV JSON data for file -# -# Also store the value for current_working_directory to BASEDIR_REF. -# - -sub read_intermediate_json($$$) -{ - my ($gcov_filename, $data, $basedir_ref) = @_; - my $fd; - my $text; - my $json; - - open($fd, "<:gzip", $gcov_filename) or - die("ERROR: Could not read $gcov_filename: $!\n"); - local $/; - $text = <$fd>; - close($fd); - - $json = decode_json($text); - if (!defined($json) || !exists($json->{"files"}) || - ref($json->{"files"} ne "ARRAY")) { - die("ERROR: Unrecognized JSON output format in ". - "$gcov_filename\n"); - } - - $$basedir_ref = $json->{"current_working_directory"}; - - for my $file (@{$json->{"files"}}) { - my $filename = $file->{"file"}; - - $data->{$filename} = $file; - } -} - - -# -# intermediate_text_to_info(fd, data, srcdata) -# -# Write DATA in info format to file descriptor FD. -# -# data: filename -> file_data: -# file_data: concatenated lines of intermediate text data -# -# srcdata: filename -> [ excl, brexcl, checksums ] -# excl: lineno -> 1 for all lines for which to exclude all data -# brexcl: lineno -> 1 for all lines for which to exclude branch data -# checksums: lineno -> source code checksum -# -# Note: To simplify processing, gcov data is not combined here, that is counts -# that appear multiple times for the same lines/branches are not added. -# This is done by lcov/genhtml when reading the data files. -# - -sub intermediate_text_to_info($$$) -{ - my ($fd, $data, $srcdata) = @_; - my $branch_num = 0; - my $c; - - return if (!%{$data}); - - print($fd "TN:$test_name\n"); - for my $filename (keys(%{$data})) { - my ($excl, $brexcl, $checksums); - - if (defined($srcdata->{$filename})) { - ($excl, $brexcl, $checksums) = @{$srcdata->{$filename}}; - } - - print($fd "SF:$filename\n"); - for my $line (split(/\n/, $data->{$filename})) { - if ($line =~ /^lcount:(\d+),(\d+),?/) { - # lcount:, - # lcount:,, - if ($checksum && exists($checksums->{$1})) { - $c = ",".$checksums->{$1}; - } else { - $c = ""; - } - print($fd "DA:$1,$2$c\n") if (!$excl->{$1}); - - # Intermediate text format does not provide - # branch numbers, and the same branch may appear - # multiple times on the same line (e.g. in - # template instances). Synthesize a branch - # number based on the assumptions: - # a) the order of branches is fixed across - # instances - # b) an instance starts with an lcount line - $branch_num = 0; - } elsif ($line =~ /^function:(\d+),(\d+),([^,]+)$/) { - next if (!$func_coverage || $excl->{$1}); - - # function:,, - print($fd "FN:$1,$3\n"); - print($fd "FNDA:$2,$3\n"); - } elsif ($line =~ /^function:(\d+),\d+,(\d+),([^,]+)$/) { - next if (!$func_coverage || $excl->{$1}); - - # function:,,, - # - print($fd "FN:$1,$3\n"); - print($fd "FNDA:$2,$3\n"); - } elsif ($line =~ /^branch:(\d+),(taken|nottaken|notexec)/) { - next if (!$br_coverage || $excl->{$1} || - $brexcl->{$1}); - - # branch:,taken|nottaken|notexec - if ($2 eq "taken") { - $c = 1; - } elsif ($2 eq "nottaken") { - $c = 0; - } else { - $c = "-"; - } - print($fd "BRDA:$1,0,$branch_num,$c\n"); - $branch_num++; - } - } - print($fd "end_of_record\n"); - } -} - - -# -# intermediate_json_to_info(fd, data, srcdata) -# -# Write DATA in info format to file descriptor FD. -# -# data: filename -> file_data: -# file_data: GCOV JSON data for file -# -# srcdata: filename -> [ excl, brexcl, checksums ] -# excl: lineno -> 1 for all lines for which to exclude all data -# brexcl: lineno -> 1 for all lines for which to exclude branch data -# checksums: lineno -> source code checksum -# -# Note: To simplify processing, gcov data is not combined here, that is counts -# that appear multiple times for the same lines/branches are not added. -# This is done by lcov/genhtml when reading the data files. -# - -sub intermediate_json_to_info($$$) -{ - my ($fd, $data, $srcdata) = @_; - my $branch_num = 0; - - return if (!%{$data}); - - print($fd "TN:$test_name\n"); - for my $filename (keys(%{$data})) { - my ($excl, $brexcl, $checksums); - my $file_data = $data->{$filename}; - - if (defined($srcdata->{$filename})) { - ($excl, $brexcl, $checksums) = @{$srcdata->{$filename}}; - } - - print($fd "SF:$filename\n"); - - # Function data - if ($func_coverage) { - for my $d (@{$file_data->{"functions"}}) { - my $line = $d->{"start_line"}; - my $count = $d->{"execution_count"}; - my $name = $d->{"name"}; - - next if (!defined($line) || !defined($count) || - !defined($name) || $excl->{$line}); - - print($fd "FN:$line,$name\n"); - print($fd "FNDA:$count,$name\n"); - } - } - - # Line data - for my $d (@{$file_data->{"lines"}}) { - my $line = $d->{"line_number"}; - my $count = $d->{"count"}; - my $c; - my $branches = $d->{"branches"}; - my $unexec = $d->{"unexecuted_block"}; - - next if (!defined($line) || !defined($count) || - $excl->{$line}); - - if (defined($unexec) && $unexec && $count == 0) { - $unexec = 1; - } else { - $unexec = 0; - } - - if ($checksum && exists($checksums->{$line})) { - $c = ",".$checksums->{$line}; - } else { - $c = ""; - } - print($fd "DA:$line,$count$c\n"); - - $branch_num = 0; - # Branch data - if ($br_coverage && !$brexcl->{$line}) { - for my $b (@$branches) { - my $brcount = $b->{"count"}; - - if (!defined($brcount) || $unexec) { - $brcount = "-"; - } - print($fd "BRDA:$line,0,$branch_num,". - "$brcount\n"); - - $branch_num++; - } - } - - } - - print($fd "end_of_record\n"); - } -} - - -sub get_output_fd($$) -{ - my ($outfile, $file) = @_; - my $fd; - - if (!defined($outfile)) { - open($fd, ">", "$file.info") or - die("ERROR: Cannot create file $file.info: $!\n"); - } elsif ($outfile eq "-") { - open($fd, ">&STDOUT") or - die("ERROR: Cannot duplicate stdout: $!\n"); - } else { - open($fd, ">>", $outfile) or - die("ERROR: Cannot write to file $outfile: $!\n"); - } - - return $fd; -} - - -# -# print_gcov_warnings(stderr_file, is_graph, map) -# -# Print GCOV warnings in file STDERR_FILE to STDERR. If IS_GRAPH is non-zero, -# suppress warnings about missing as these are expected. Replace keys found -# in MAP with their values. -# - -sub print_gcov_warnings($$$) -{ - my ($stderr_file, $is_graph, $map) = @_; - my $fd; - - if (!open($fd, "<", $stderr_file)) { - warn("WARNING: Could not open GCOV stderr file ". - "$stderr_file: $!\n"); - return; - } - while (my $line = <$fd>) { - next if ($is_graph && $line =~ /cannot open data file/); - - for my $key (keys(%{$map})) { - $line =~ s/\Q$key\E/$map->{$key}/g; - } - - print(STDERR $line); - } - close($fd); -} - - -# -# process_intermediate(file, dir, tempdir) -# -# Create output for a single file (either a data file or a graph file) using -# gcov's intermediate option. -# - -sub process_intermediate($$$) -{ - my ($file, $dir, $tempdir) = @_; - my ($fdir, $fbase, $fext); - my $data_file; - my $errmsg; - my %data; - my $fd; - my $base; - my $srcdata; - my $is_graph = 0; - my ($out, $err, $rc); - my $json_basedir; - my $json_format; - - info("Processing %s\n", abs2rel($file, $dir)); - - $file = solve_relative_path($cwd, $file); - ($fdir, $fbase, $fext) = split_filename($file); - - $is_graph = 1 if (".$fext" eq $graph_file_extension); - - if ($is_graph) { - # Process graph file - copy to temp directory to prevent - # accidental processing of associated data file - $data_file = "$tempdir/$fbase$graph_file_extension"; - if (!copy($file, $data_file)) { - $errmsg = "ERROR: Could not copy file $file"; - goto err; - } - } else { - # Process data file in place - $data_file = $file; - } - - # Change directory - if (!chdir($tempdir)) { - $errmsg = "Could not change to directory $tempdir: $!"; - goto err; - } - - # Run gcov on data file - ($out, $err, $rc) = system_no_output(1 + 2 + 4, $gcov_tool, - $data_file, @gcov_options, "-i"); - defined($out) && unlink($out); - if (defined($err)) { - print_gcov_warnings($err, $is_graph, { - $data_file => $file, - }); - unlink($err); - } - if ($rc) { - $errmsg = "GCOV failed for $file"; - goto err; - } - - if ($is_graph) { - # Remove graph file copy - unlink($data_file); - } - - # Parse resulting file(s) - for my $gcov_filename (glob("*.gcov")) { - read_intermediate_text($gcov_filename, \%data); - unlink($gcov_filename); - } - - for my $gcov_filename (glob("*.gcov.json.gz")) { - read_intermediate_json($gcov_filename, \%data, \$json_basedir); - unlink($gcov_filename); - $json_format = 1; - } - - if (!%data) { - warn("WARNING: GCOV did not produce any data for $file\n"); - return; - } - - # Determine base directory - if (defined($base_directory)) { - $base = $base_directory; - } elsif (defined($json_basedir)) { - $base = $json_basedir; - } else { - $base = $fdir; - - if (is_compat($COMPAT_MODE_LIBTOOL)) { - # Avoid files from .libs dirs - $base =~ s/\.libs$//; - } - - # Try to find base directory automatically if requested by user - if ($rc_auto_base) { - $base = find_base_from_source($base, [ keys(%data) ]); - } - } - - # Apply base file name to relative source files - adjust_source_filenames(\%data, $base); - - # Remove excluded source files - filter_source_files(\%data); - - # Get data on exclusion markers and checksums if requested - if (!$no_markers || $checksum) { - $srcdata = get_all_source_data(keys(%data)); - } - - # Generate output - $fd = get_output_fd($output_filename, $file); - if ($json_format) { - intermediate_json_to_info($fd, \%data, $srcdata); - } else { - intermediate_text_to_info($fd, \%data, $srcdata); - } - close($fd); - - chdir($cwd); - - return; - -err: - if ($ignore[$ERROR_GCOV]) { - warn("WARNING: $errmsg!\n"); - } else { - die("ERROR: $errmsg!\n") - } -} - - -# Map LLVM versions to the version of GCC gcov which they emulate. - -sub map_llvm_version($) -{ - my ($ver) = @_; - - return 0x040200 if ($ver >= 0x030400); - - warn("WARNING: This version of LLVM's gcov is unknown. ". - "Assuming it emulates GCC gcov version 4.2.\n"); - - return 0x040200; -} - - -# Return a readable version of encoded gcov version. - -sub version_to_str($) -{ - my ($ver) = @_; - my ($a, $b, $c); - - $a = $ver >> 16 & 0xff; - $b = $ver >> 8 & 0xff; - $c = $ver & 0xff; - - return "$a.$b.$c"; -} - - -# -# Get the GCOV tool version. Return an integer number which represents the -# GCOV version. Version numbers can be compared using standard integer -# operations. -# - -sub get_gcov_version() -{ - local *HANDLE; - my $version_string; - my $result; - my ($a, $b, $c) = (4, 2, 0); # Fallback version - - # Examples for gcov version output: - # - # gcov (GCC) 4.4.7 20120313 (Red Hat 4.4.7-3) - # - # gcov (crosstool-NG 1.18.0) 4.7.2 - # - # LLVM (http://llvm.org/): - # LLVM version 3.4svn - # - # Apple LLVM version 8.0.0 (clang-800.0.38) - # Optimized build. - # Default target: x86_64-apple-darwin16.0.0 - # Host CPU: haswell - - open(GCOV_PIPE, "-|", "$gcov_tool --version") - or die("ERROR: cannot retrieve gcov version!\n"); - local $/; - $version_string = ; - close(GCOV_PIPE); - - # Remove all bracketed information - $version_string =~ s/\([^\)]*\)//g; - - if ($version_string =~ /(\d+)\.(\d+)(\.(\d+))?/) { - ($a, $b, $c) = ($1, $2, $4); - $c = 0 if (!defined($c)); - } else { - warn("WARNING: cannot determine gcov version - ". - "assuming $a.$b.$c\n"); - } - $result = $a << 16 | $b << 8 | $c; - - if ($version_string =~ /LLVM/) { - $result = map_llvm_version($result); - info("Found LLVM gcov version $a.$b.$c, which emulates gcov ". - "version ".version_to_str($result)."\n"); - } else { - info("Found gcov version: ".version_to_str($result)."\n"); - } - - return ($result, $version_string); -} - - -# -# info(printf_parameter) -# -# Use printf to write PRINTF_PARAMETER to stdout only when the $quiet flag -# is not set. -# - -sub info(@) -{ - if (!$quiet) - { - # Print info string - if (defined($output_filename) && ($output_filename eq "-")) - { - # Don't interfere with the .info output to STDOUT - printf(STDERR @_); - } - else - { - printf(@_); - } - } -} - - -# -# int_handler() -# -# Called when the script was interrupted by an INT signal (e.g. CTRl-C) -# - -sub int_handler() -{ - if ($cwd) { chdir($cwd); } - info("Aborted.\n"); - exit(1); -} - - -# -# system_no_output(mode, parameters) -# -# Call an external program using PARAMETERS while suppressing depending on -# the value of MODE: -# -# MODE & 1: suppress STDOUT -# MODE & 2: suppress STDERR -# MODE & 4: redirect to temporary files instead of suppressing -# -# Return (stdout, stderr, rc): -# stdout: path to tempfile containing stdout or undef -# stderr: path to tempfile containing stderr or undef -# 0 on success, non-zero otherwise -# - -sub system_no_output($@) -{ - my $mode = shift; - my $result; - local *OLD_STDERR; - local *OLD_STDOUT; - my $stdout_file; - my $stderr_file; - my $fd; - - # Save old stdout and stderr handles - ($mode & 1) && open(OLD_STDOUT, ">>&", "STDOUT"); - ($mode & 2) && open(OLD_STDERR, ">>&", "STDERR"); - - if ($mode & 4) { - # Redirect to temporary files - if ($mode & 1) { - ($fd, $stdout_file) = tempfile(UNLINK => 1); - open(STDOUT, ">", $stdout_file) || warn("$!\n"); - close($fd); - } - if ($mode & 2) { - ($fd, $stderr_file) = tempfile(UNLINK => 1); - open(STDERR, ">", $stderr_file) || warn("$!\n"); - close($fd); - } - } else { - # Redirect to /dev/null - ($mode & 1) && open(STDOUT, ">", "/dev/null"); - ($mode & 2) && open(STDERR, ">", "/dev/null"); - } - - debug("system(".join(' ', @_).")\n"); - system(@_); - $result = $?; - - # Close redirected handles - ($mode & 1) && close(STDOUT); - ($mode & 2) && close(STDERR); - - # Restore old handles - ($mode & 1) && open(STDOUT, ">>&", "OLD_STDOUT"); - ($mode & 2) && open(STDERR, ">>&", "OLD_STDERR"); - - # Remove empty output files - if (defined($stdout_file) && -z $stdout_file) { - unlink($stdout_file); - $stdout_file = undef; - } - if (defined($stderr_file) && -z $stderr_file) { - unlink($stderr_file); - $stderr_file = undef; - } - - return ($stdout_file, $stderr_file, $result); -} - - -# -# read_config(filename) -# -# Read configuration file FILENAME and return a reference to a hash containing -# all valid key=value pairs found. -# - -sub read_config($) -{ - my $filename = $_[0]; - my %result; - my $key; - my $value; - local *HANDLE; - - if (!open(HANDLE, "<", $filename)) - { - warn("WARNING: cannot read configuration file $filename\n"); - return undef; - } - while () - { - chomp; - # Skip comments - s/#.*//; - # Remove leading blanks - s/^\s+//; - # Remove trailing blanks - s/\s+$//; - next unless length; - ($key, $value) = split(/\s*=\s*/, $_, 2); - if (defined($key) && defined($value)) - { - $result{$key} = $value; - } - else - { - warn("WARNING: malformed statement in line $. ". - "of configuration file $filename\n"); - } - } - close(HANDLE); - return \%result; -} - - -# -# apply_config(REF) -# -# REF is a reference to a hash containing the following mapping: -# -# key_string => var_ref -# -# where KEY_STRING is a keyword and VAR_REF is a reference to an associated -# variable. If the global configuration hashes CONFIG or OPT_RC contain a value -# for keyword KEY_STRING, VAR_REF will be assigned the value for that keyword. -# - -sub apply_config($) -{ - my $ref = $_[0]; - - foreach (keys(%{$ref})) - { - if (defined($opt_rc{$_})) { - ${$ref->{$_}} = $opt_rc{$_}; - } elsif (defined($config->{$_})) { - ${$ref->{$_}} = $config->{$_}; - } - } -} - - -# -# get_source_data(filename) -# -# Scan specified source code file for exclusion markers and checksums. Return -# ( excl, brexcl, checksums ) where -# excl: lineno -> 1 for all lines for which to exclude all data -# brexcl: lineno -> 1 for all lines for which to exclude branch data -# checksums: lineno -> source code checksum -# - -sub get_source_data($) -{ - my ($filename) = @_; - my %list; - my $flag = 0; - my %brdata; - my $brflag = 0; - my %checksums; - local *HANDLE; - - if (!open(HANDLE, "<", $filename)) { - warn("WARNING: could not open $filename\n"); - return; - } - while () { - if (/$EXCL_STOP/) { - $flag = 0; - } elsif (/$EXCL_START/) { - $flag = 1; - } - if (/$excl_line/ || $flag) { - $list{$.} = 1; - } - if (/$EXCL_BR_STOP/) { - $brflag = 0; - } elsif (/$EXCL_BR_START/) { - $brflag = 1; - } - if (/$excl_br_line/ || $brflag) { - $brdata{$.} = 1; - } - if ($checksum) { - chomp(); - $checksums{$.} = md5_base64($_); - } - } - close(HANDLE); - - if ($flag || $brflag) { - warn("WARNING: unterminated exclusion section in $filename\n"); - } - - return (\%list, \%brdata, \%checksums); -} - - -# -# get_all_source_data(filenames) -# -# Scan specified source code files for exclusion markers and return -# filename -> [ excl, brexcl, checksums ] -# excl: lineno -> 1 for all lines for which to exclude all data -# brexcl: lineno -> 1 for all lines for which to exclude branch data -# checksums: lineno -> source code checksum -# - -sub get_all_source_data(@) -{ - my @filenames = @_; - my %data; - my $failed = 0; - - for my $filename (@filenames) { - my @d; - next if (exists($data{$filename})); - - @d = get_source_data($filename); - if (@d) { - $data{$filename} = [ @d ]; - } else { - $failed = 1; - } - } - - if ($failed) { - warn("WARNING: some exclusion markers may be ignored\n"); - } - - return \%data; -} - - -# -# apply_exclusion_data(instr, graph) -# -# Remove lines from instr and graph data structures which are marked -# for exclusion in the source code file. -# -# Return adjusted (instr, graph). -# -# graph : file name -> function data -# function data : function name -> line data -# line data : [ line1, line2, ... ] -# -# instr : filename -> line data -# line data : [ line1, line2, ... ] -# - -sub apply_exclusion_data($$) -{ - my ($instr, $graph) = @_; - my $filename; - my $excl_data; - - ($excl_data) = get_all_source_data(keys(%{$graph}), keys(%{$instr})); - - # Skip if no markers were found - return ($instr, $graph) if (!%$excl_data); - - # Apply exclusion marker data to graph - foreach $filename (keys(%$excl_data)) { - my $function_data = $graph->{$filename}; - my $excl = $excl_data->{$filename}->[0]; - my $function; - - next if (!defined($function_data)); - - foreach $function (keys(%{$function_data})) { - my $line_data = $function_data->{$function}; - my $line; - my @new_data; - - # To be consistent with exclusion parser in non-initial - # case we need to remove a function if the first line - # was excluded - if ($excl->{$line_data->[0]}) { - delete($function_data->{$function}); - next; - } - # Copy only lines which are not excluded - foreach $line (@{$line_data}) { - push(@new_data, $line) if (!$excl->{$line}); - } - - # Store modified list - if (scalar(@new_data) > 0) { - $function_data->{$function} = \@new_data; - } else { - # All of this function was excluded - delete($function_data->{$function}); - } - } - - # Check if all functions of this file were excluded - if (keys(%{$function_data}) == 0) { - delete($graph->{$filename}); - } - } - - # Apply exclusion marker data to instr - foreach $filename (keys(%$excl_data)) { - my $line_data = $instr->{$filename}; - my $excl = $excl_data->{$filename}->[0]; - my $line; - my @new_data; - - next if (!defined($line_data)); - - # Copy only lines which are not excluded - foreach $line (@{$line_data}) { - push(@new_data, $line) if (!$excl->{$line}); - } - - # Store modified list - $instr->{$filename} = \@new_data; - } - - return ($instr, $graph); -} - - -sub process_graphfile($$) -{ - my ($file, $dir) = @_; - my $graph_filename = $file; - my $graph_dir; - my $graph_basename; - my $source_dir; - my $base_dir; - my $graph; - my $instr; - my $filename; - local *INFO_HANDLE; - - info("Processing %s\n", abs2rel($file, $dir)); - - # Get path to data file in absolute and normalized form (begins with /, - # contains no more ../ or ./) - $graph_filename = solve_relative_path($cwd, $graph_filename); - - # Get directory and basename of data file - ($graph_dir, $graph_basename) = split_filename($graph_filename); - - $source_dir = $graph_dir; - if (is_compat($COMPAT_MODE_LIBTOOL)) { - # Avoid files from .libs dirs - $source_dir =~ s/\.libs$//; - } - - # Construct base_dir for current file - if ($base_directory) - { - $base_dir = $base_directory; - } - else - { - $base_dir = $source_dir; - } - - # Ignore empty graph file (e.g. source file with no statement) - if (-z $graph_filename) - { - warn("WARNING: empty $graph_filename (skipped)\n"); - return; - } - - if ($gcov_version < $GCOV_VERSION_3_4_0) - { - if (is_compat($COMPAT_MODE_HAMMER)) - { - ($instr, $graph) = read_bbg($graph_filename); - } - else - { - ($instr, $graph) = read_bb($graph_filename); - } - } - else - { - ($instr, $graph) = read_gcno($graph_filename); - } - - # Try to find base directory automatically if requested by user - if ($rc_auto_base) { - $base_dir = find_base_from_source($base_dir, - [ keys(%{$instr}), keys(%{$graph}) ]); - } - - adjust_source_filenames($instr, $base_dir); - adjust_source_filenames($graph, $base_dir); - - if (!$no_markers) { - # Apply exclusion marker data to graph file data - ($instr, $graph) = apply_exclusion_data($instr, $graph); - } - - # Check whether we're writing to a single file - if ($output_filename) - { - if ($output_filename eq "-") - { - *INFO_HANDLE = *STDOUT; - } - else - { - # Append to output file - open(INFO_HANDLE, ">>", $output_filename) - or die("ERROR: cannot write to ". - "$output_filename!\n"); - } - } - else - { - # Open .info file for output - open(INFO_HANDLE, ">", "$graph_filename.info") - or die("ERROR: cannot create $graph_filename.info!\n"); - } - - # Write test name - printf(INFO_HANDLE "TN:%s\n", $test_name); - foreach $filename (sort(keys(%{$instr}))) - { - my $funcdata = $graph->{$filename}; - my $line; - my $linedata; - - # Skip external files if requested - if (!$opt_external) { - if (is_external($filename)) { - info(" ignoring data for external file ". - "$filename\n"); - next; - } - } - - print(INFO_HANDLE "SF:$filename\n"); - - if (defined($funcdata) && $func_coverage) { - my @functions = sort {$funcdata->{$a}->[0] <=> - $funcdata->{$b}->[0]} - keys(%{$funcdata}); - my $func; - - # Gather list of instrumented lines and functions - foreach $func (@functions) { - $linedata = $funcdata->{$func}; - - # Print function name and starting line - print(INFO_HANDLE "FN:".$linedata->[0]. - ",".filter_fn_name($func)."\n"); - } - # Print zero function coverage data - foreach $func (@functions) { - print(INFO_HANDLE "FNDA:0,". - filter_fn_name($func)."\n"); - } - # Print function summary - print(INFO_HANDLE "FNF:".scalar(@functions)."\n"); - print(INFO_HANDLE "FNH:0\n"); - } - # Print zero line coverage data - foreach $line (@{$instr->{$filename}}) { - print(INFO_HANDLE "DA:$line,0\n"); - } - # Print line summary - print(INFO_HANDLE "LF:".scalar(@{$instr->{$filename}})."\n"); - print(INFO_HANDLE "LH:0\n"); - - print(INFO_HANDLE "end_of_record\n"); - } - if (!($output_filename && ($output_filename eq "-"))) - { - close(INFO_HANDLE); - } -} - -sub filter_fn_name($) -{ - my ($fn) = @_; - - # Remove characters used internally as function name delimiters - $fn =~ s/[,=]/_/g; - - return $fn; -} - -sub warn_handler($) -{ - my ($msg) = @_; - - warn("$tool_name: $msg"); -} - -sub die_handler($) -{ - my ($msg) = @_; - - die("$tool_name: $msg"); -} - - -# -# graph_error(filename, message) -# -# Print message about error in graph file. If ignore_graph_error is set, return. -# Otherwise abort. -# - -sub graph_error($$) -{ - my ($filename, $msg) = @_; - - if ($ignore[$ERROR_GRAPH]) { - warn("WARNING: $filename: $msg - skipping\n"); - return; - } - die("ERROR: $filename: $msg\n"); -} - -# -# graph_expect(description) -# -# If debug is set to a non-zero value, print the specified description of what -# is expected to be read next from the graph file. -# - -sub graph_expect($) -{ - my ($msg) = @_; - - if (!$debug || !defined($msg)) { - return; - } - - print(STDERR "DEBUG: expecting $msg\n"); -} - -# -# graph_read(handle, bytes[, description, peek]) -# -# Read and return the specified number of bytes from handle. Return undef -# if the number of bytes could not be read. If PEEK is non-zero, reset -# file position after read. -# - -sub graph_read(*$;$$) -{ - my ($handle, $length, $desc, $peek) = @_; - my $data; - my $result; - my $pos; - - graph_expect($desc); - if ($peek) { - $pos = tell($handle); - if ($pos == -1) { - warn("Could not get current file position: $!\n"); - return undef; - } - } - $result = read($handle, $data, $length); - if ($debug) { - my $op = $peek ? "peek" : "read"; - my $ascii = ""; - my $hex = ""; - my $i; - - print(STDERR "DEBUG: $op($length)=$result: "); - for ($i = 0; $i < length($data); $i++) { - my $c = substr($data, $i, 1);; - my $n = ord($c); - - $hex .= sprintf("%02x ", $n); - if ($n >= 32 && $n <= 127) { - $ascii .= $c; - } else { - $ascii .= "."; - } - } - print(STDERR "$hex |$ascii|"); - print(STDERR "\n"); - } - if ($peek) { - if (!seek($handle, $pos, 0)) { - warn("Could not set file position: $!\n"); - return undef; - } - } - if ($result != $length) { - return undef; - } - return $data; -} - -# -# graph_skip(handle, bytes[, description]) -# -# Read and discard the specified number of bytes from handle. Return non-zero -# if bytes could be read, zero otherwise. -# - -sub graph_skip(*$;$) -{ - my ($handle, $length, $desc) = @_; - - if (defined(graph_read($handle, $length, $desc))) { - return 1; - } - return 0; -} - -# -# uniq(list) -# -# Return list without duplicate entries. -# - -sub uniq(@) -{ - my (@list) = @_; - my @new_list; - my %known; - - foreach my $item (@list) { - next if ($known{$item}); - $known{$item} = 1; - push(@new_list, $item); - } - - return @new_list; -} - -# -# sort_uniq(list) -# -# Return list in numerically ascending order and without duplicate entries. -# - -sub sort_uniq(@) -{ - my (@list) = @_; - my %hash; - - foreach (@list) { - $hash{$_} = 1; - } - return sort { $a <=> $b } keys(%hash); -} - -# -# sort_uniq_lex(list) -# -# Return list in lexically ascending order and without duplicate entries. -# - -sub sort_uniq_lex(@) -{ - my (@list) = @_; - my %hash; - - foreach (@list) { - $hash{$_} = 1; - } - return sort keys(%hash); -} - -# -# parent_dir(dir) -# -# Return parent directory for DIR. DIR must not contain relative path -# components. -# - -sub parent_dir($) -{ - my ($dir) = @_; - my ($v, $d, $f) = splitpath($dir, 1); - my @dirs = splitdir($d); - - pop(@dirs); - - return catpath($v, catdir(@dirs), $f); -} - -# -# find_base_from_source(base_dir, source_files) -# -# Try to determine the base directory of the object file built from -# SOURCE_FILES. The base directory is the base for all relative filenames in -# the gcov data. It is defined by the current working directory at time -# of compiling the source file. -# -# This function implements a heuristic which relies on the following -# assumptions: -# - all files used for compilation are still present at their location -# - the base directory is either BASE_DIR or one of its parent directories -# - files by the same name are not present in multiple parent directories -# - -sub find_base_from_source($$) -{ - my ($base_dir, $source_files) = @_; - my $old_base; - my $best_miss; - my $best_base; - my %rel_files; - - # Determine list of relative paths - foreach my $filename (@$source_files) { - next if (file_name_is_absolute($filename)); - - $rel_files{$filename} = 1; - } - - # Early exit if there are no relative paths - return $base_dir if (!%rel_files); - - do { - my $miss = 0; - - foreach my $filename (keys(%rel_files)) { - if (!-e solve_relative_path($base_dir, $filename)) { - $miss++; - } - } - - debug("base_dir=$base_dir miss=$miss\n"); - - # Exit if we find an exact match with no misses - return $base_dir if ($miss == 0); - - # No exact match, aim for the one with the least source file - # misses - if (!defined($best_base) || $miss < $best_miss) { - $best_base = $base_dir; - $best_miss = $miss; - } - - # Repeat until there's no more parent directory - $old_base = $base_dir; - $base_dir = parent_dir($base_dir); - } while ($old_base ne $base_dir); - - return $best_base; -} - -# -# adjust_source_filenames(hash, base_dir) -# -# Transform all keys of HASH to absolute form and apply requested -# transformations. -# - -sub adjust_source_filenames($$$) -{ - my ($hash, $base_dir) = @_; - - foreach my $filename (keys(%{$hash})) { - my $old_filename = $filename; - - # Convert to absolute canonical form - $filename = solve_relative_path($base_dir, $filename); - - # Apply adjustment - if (defined($adjust_src_pattern)) { - $filename =~ s/$adjust_src_pattern/$adjust_src_replace/g; - } - - if ($filename ne $old_filename) { - $hash->{$filename} = delete($hash->{$old_filename}); - } - } -} - - -# -# filter_source_files(hash) -# -# Remove unwanted source file data from HASH. -# - -sub filter_source_files($) -{ - my ($hash) = @_; - - foreach my $filename (keys(%{$hash})) { - # Skip external files if requested - goto del if (!$opt_external && is_external($filename)); - - # Apply include patterns - if (@include_patterns) { - my $keep; - - foreach my $pattern (@include_patterns) { - if ($filename =~ (/^$pattern$/)) { - $keep = 1; - last; - } - } - goto del if (!$keep); - } - - # Apply exclude patterns - foreach my $pattern (@exclude_patterns) { - goto del if ($filename =~ (/^$pattern$/)); - } - next; - -del: - # Remove file data - delete($hash->{$filename}); - $excluded_files{$filename} = 1; - } -} - -# -# graph_cleanup(graph) -# -# Remove entries for functions with no lines. Remove duplicate line numbers. -# Sort list of line numbers numerically ascending. -# - -sub graph_cleanup($) -{ - my ($graph) = @_; - my $filename; - - foreach $filename (keys(%{$graph})) { - my $per_file = $graph->{$filename}; - my $function; - - foreach $function (keys(%{$per_file})) { - my $lines = $per_file->{$function}; - - if (scalar(@$lines) == 0) { - # Remove empty function - delete($per_file->{$function}); - next; - } - # Normalize list - $per_file->{$function} = [ uniq(@$lines) ]; - } - if (scalar(keys(%{$per_file})) == 0) { - # Remove empty file - delete($graph->{$filename}); - } - } -} - -# -# graph_find_base(bb) -# -# Try to identify the filename which is the base source file for the -# specified bb data. -# - -sub graph_find_base($) -{ - my ($bb) = @_; - my %file_count; - my $basefile; - my $file; - my $func; - my $filedata; - my $count; - my $num; - - # Identify base name for this bb data. - foreach $func (keys(%{$bb})) { - $filedata = $bb->{$func}; - - foreach $file (keys(%{$filedata})) { - $count = $file_count{$file}; - - # Count file occurrence - $file_count{$file} = defined($count) ? $count + 1 : 1; - } - } - $count = 0; - $num = 0; - foreach $file (keys(%file_count)) { - if ($file_count{$file} > $count) { - # The file that contains code for the most functions - # is likely the base file - $count = $file_count{$file}; - $num = 1; - $basefile = $file; - } elsif ($file_count{$file} == $count) { - # If more than one file could be the basefile, we - # don't have a basefile - $basefile = undef; - } - } - - return $basefile; -} - -# -# graph_from_bb(bb, fileorder, bb_filename, fileorder_first) -# -# Convert data from bb to the graph format and list of instrumented lines. -# -# If FILEORDER_FIRST is set, use fileorder data to determine a functions -# base source file. -# -# Returns (instr, graph). -# -# bb : function name -> file data -# : undef -> file order -# file data : filename -> line data -# line data : [ line1, line2, ... ] -# -# file order : function name -> [ filename1, filename2, ... ] -# -# graph : file name -> function data -# function data : function name -> line data -# line data : [ line1, line2, ... ] -# -# instr : filename -> line data -# line data : [ line1, line2, ... ] -# - -sub graph_from_bb($$$$) -{ - my ($bb, $fileorder, $bb_filename, $fileorder_first) = @_; - my $graph = {}; - my $instr = {}; - my $basefile; - my $file; - my $func; - my $filedata; - my $linedata; - my $order; - - $basefile = graph_find_base($bb); - # Create graph structure - foreach $func (keys(%{$bb})) { - $filedata = $bb->{$func}; - $order = $fileorder->{$func}; - - # Account for lines in functions - if (defined($basefile) && defined($filedata->{$basefile}) && - !$fileorder_first) { - # If the basefile contributes to this function, - # account this function to the basefile. - $graph->{$basefile}->{$func} = $filedata->{$basefile}; - } else { - # If the basefile does not contribute to this function, - # account this function to the first file contributing - # lines. - $graph->{$order->[0]}->{$func} = - $filedata->{$order->[0]}; - } - - foreach $file (keys(%{$filedata})) { - # Account for instrumented lines - $linedata = $filedata->{$file}; - push(@{$instr->{$file}}, @$linedata); - } - } - # Clean up array of instrumented lines - foreach $file (keys(%{$instr})) { - $instr->{$file} = [ sort_uniq(@{$instr->{$file}}) ]; - } - - return ($instr, $graph); -} - -# -# graph_add_order(fileorder, function, filename) -# -# Add an entry for filename to the fileorder data set for function. -# - -sub graph_add_order($$$) -{ - my ($fileorder, $function, $filename) = @_; - my $item; - my $list; - - $list = $fileorder->{$function}; - foreach $item (@$list) { - if ($item eq $filename) { - return; - } - } - push(@$list, $filename); - $fileorder->{$function} = $list; -} - -# -# read_bb_word(handle[, description]) -# -# Read and return a word in .bb format from handle. -# - -sub read_bb_word(*;$) -{ - my ($handle, $desc) = @_; - - return graph_read($handle, 4, $desc); -} - -# -# read_bb_value(handle[, description]) -# -# Read a word in .bb format from handle and return the word and its integer -# value. -# - -sub read_bb_value(*;$) -{ - my ($handle, $desc) = @_; - my $word; - - $word = read_bb_word($handle, $desc); - return undef if (!defined($word)); - - return ($word, unpack("V", $word)); -} - -# -# read_bb_string(handle, delimiter) -# -# Read and return a string in .bb format from handle up to the specified -# delimiter value. -# - -sub read_bb_string(*$) -{ - my ($handle, $delimiter) = @_; - my $word; - my $value; - my $string = ""; - - graph_expect("string"); - do { - ($word, $value) = read_bb_value($handle, "string or delimiter"); - return undef if (!defined($value)); - if ($value != $delimiter) { - $string .= $word; - } - } while ($value != $delimiter); - $string =~ s/\0//g; - - return $string; -} - -# -# read_bb(filename) -# -# Read the contents of the specified .bb file and return (instr, graph), where: -# -# instr : filename -> line data -# line data : [ line1, line2, ... ] -# -# graph : filename -> file_data -# file_data : function name -> line_data -# line_data : [ line1, line2, ... ] -# -# See the gcov info pages of gcc 2.95 for a description of the .bb file format. -# - -sub read_bb($) -{ - my ($bb_filename) = @_; - my $minus_one = 0x80000001; - my $minus_two = 0x80000002; - my $value; - my $filename; - my $function; - my $bb = {}; - my $fileorder = {}; - my $instr; - my $graph; - local *HANDLE; - - open(HANDLE, "<", $bb_filename) or goto open_error; - binmode(HANDLE); - while (!eof(HANDLE)) { - $value = read_bb_value(*HANDLE, "data word"); - goto incomplete if (!defined($value)); - if ($value == $minus_one) { - # Source file name - graph_expect("filename"); - $filename = read_bb_string(*HANDLE, $minus_one); - goto incomplete if (!defined($filename)); - } elsif ($value == $minus_two) { - # Function name - graph_expect("function name"); - $function = read_bb_string(*HANDLE, $minus_two); - goto incomplete if (!defined($function)); - } elsif ($value > 0) { - # Line number - if (!defined($filename) || !defined($function)) { - warn("WARNING: unassigned line number ". - "$value\n"); - next; - } - push(@{$bb->{$function}->{$filename}}, $value); - graph_add_order($fileorder, $function, $filename); - } - } - close(HANDLE); - - ($instr, $graph) = graph_from_bb($bb, $fileorder, $bb_filename, 0); - graph_cleanup($graph); - - return ($instr, $graph); - -open_error: - graph_error($bb_filename, "could not open file"); - return undef; -incomplete: - graph_error($bb_filename, "reached unexpected end of file"); - return undef; -} - -# -# read_bbg_word(handle[, description]) -# -# Read and return a word in .bbg format. -# - -sub read_bbg_word(*;$) -{ - my ($handle, $desc) = @_; - - return graph_read($handle, 4, $desc); -} - -# -# read_bbg_value(handle[, description]) -# -# Read a word in .bbg format from handle and return its integer value. -# - -sub read_bbg_value(*;$) -{ - my ($handle, $desc) = @_; - my $word; - - $word = read_bbg_word($handle, $desc); - return undef if (!defined($word)); - - return unpack("N", $word); -} - -# -# read_bbg_string(handle) -# -# Read and return a string in .bbg format. -# - -sub read_bbg_string(*) -{ - my ($handle, $desc) = @_; - my $length; - my $string; - - graph_expect("string"); - # Read string length - $length = read_bbg_value($handle, "string length"); - return undef if (!defined($length)); - if ($length == 0) { - return ""; - } - # Read string - $string = graph_read($handle, $length, "string"); - return undef if (!defined($string)); - # Skip padding - graph_skip($handle, 4 - $length % 4, "string padding") or return undef; - - return $string; -} - -# -# read_bbg_lines_record(handle, bbg_filename, bb, fileorder, filename, -# function) -# -# Read a bbg format lines record from handle and add the relevant data to -# bb and fileorder. Return filename on success, undef on error. -# - -sub read_bbg_lines_record(*$$$$$) -{ - my ($handle, $bbg_filename, $bb, $fileorder, $filename, $function) = @_; - my $string; - my $lineno; - - graph_expect("lines record"); - # Skip basic block index - graph_skip($handle, 4, "basic block index") or return undef; - while (1) { - # Read line number - $lineno = read_bbg_value($handle, "line number"); - return undef if (!defined($lineno)); - if ($lineno == 0) { - # Got a marker for a new filename - graph_expect("filename"); - $string = read_bbg_string($handle); - return undef if (!defined($string)); - # Check for end of record - if ($string eq "") { - return $filename; - } - $filename = $string; - if (!exists($bb->{$function}->{$filename})) { - $bb->{$function}->{$filename} = []; - } - next; - } - # Got an actual line number - if (!defined($filename)) { - warn("WARNING: unassigned line number in ". - "$bbg_filename\n"); - next; - } - push(@{$bb->{$function}->{$filename}}, $lineno); - graph_add_order($fileorder, $function, $filename); - } -} - -# -# read_bbg(filename) -# -# Read the contents of the specified .bbg file and return the following mapping: -# graph: filename -> file_data -# file_data: function name -> line_data -# line_data: [ line1, line2, ... ] -# -# See the gcov-io.h file in the SLES 9 gcc 3.3.3 source code for a description -# of the .bbg format. -# - -sub read_bbg($) -{ - my ($bbg_filename) = @_; - my $file_magic = 0x67626267; - my $tag_function = 0x01000000; - my $tag_lines = 0x01450000; - my $word; - my $tag; - my $length; - my $function; - my $filename; - my $bb = {}; - my $fileorder = {}; - my $instr; - my $graph; - local *HANDLE; - - open(HANDLE, "<", $bbg_filename) or goto open_error; - binmode(HANDLE); - # Read magic - $word = read_bbg_value(*HANDLE, "file magic"); - goto incomplete if (!defined($word)); - # Check magic - if ($word != $file_magic) { - goto magic_error; - } - # Skip version - graph_skip(*HANDLE, 4, "version") or goto incomplete; - while (!eof(HANDLE)) { - # Read record tag - $tag = read_bbg_value(*HANDLE, "record tag"); - goto incomplete if (!defined($tag)); - # Read record length - $length = read_bbg_value(*HANDLE, "record length"); - goto incomplete if (!defined($tag)); - if ($tag == $tag_function) { - graph_expect("function record"); - # Read function name - graph_expect("function name"); - $function = read_bbg_string(*HANDLE); - goto incomplete if (!defined($function)); - $filename = undef; - # Skip function checksum - graph_skip(*HANDLE, 4, "function checksum") - or goto incomplete; - } elsif ($tag == $tag_lines) { - # Read lines record - $filename = read_bbg_lines_record(HANDLE, $bbg_filename, - $bb, $fileorder, $filename, - $function); - goto incomplete if (!defined($filename)); - } else { - # Skip record contents - graph_skip(*HANDLE, $length, "unhandled record") - or goto incomplete; - } - } - close(HANDLE); - ($instr, $graph) = graph_from_bb($bb, $fileorder, $bbg_filename, 0); - - graph_cleanup($graph); - - return ($instr, $graph); - -open_error: - graph_error($bbg_filename, "could not open file"); - return undef; -incomplete: - graph_error($bbg_filename, "reached unexpected end of file"); - return undef; -magic_error: - graph_error($bbg_filename, "found unrecognized bbg file magic"); - return undef; -} - -# -# read_gcno_word(handle[, description, peek]) -# -# Read and return a word in .gcno format. -# - -sub read_gcno_word(*;$$) -{ - my ($handle, $desc, $peek) = @_; - - return graph_read($handle, 4, $desc, $peek); -} - -# -# read_gcno_value(handle, big_endian[, description, peek]) -# -# Read a word in .gcno format from handle and return its integer value -# according to the specified endianness. If PEEK is non-zero, reset file -# position after read. -# - -sub read_gcno_value(*$;$$) -{ - my ($handle, $big_endian, $desc, $peek) = @_; - my $word; - my $pos; - - $word = read_gcno_word($handle, $desc, $peek); - return undef if (!defined($word)); - if ($big_endian) { - return unpack("N", $word); - } else { - return unpack("V", $word); - } -} - -# -# read_gcno_string(handle, big_endian) -# -# Read and return a string in .gcno format. -# - -sub read_gcno_string(*$) -{ - my ($handle, $big_endian) = @_; - my $length; - my $string; - - graph_expect("string"); - # Read string length - $length = read_gcno_value($handle, $big_endian, "string length"); - return undef if (!defined($length)); - if ($length == 0) { - return ""; - } - $length *= 4; - # Read string - $string = graph_read($handle, $length, "string and padding"); - return undef if (!defined($string)); - $string =~ s/\0//g; - - return $string; -} - -# -# read_gcno_lines_record(handle, gcno_filename, bb, fileorder, filename, -# function, big_endian) -# -# Read a gcno format lines record from handle and add the relevant data to -# bb and fileorder. Return filename on success, undef on error. -# - -sub read_gcno_lines_record(*$$$$$$) -{ - my ($handle, $gcno_filename, $bb, $fileorder, $filename, $function, - $big_endian) = @_; - my $string; - my $lineno; - - graph_expect("lines record"); - # Skip basic block index - graph_skip($handle, 4, "basic block index") or return undef; - while (1) { - # Read line number - $lineno = read_gcno_value($handle, $big_endian, "line number"); - return undef if (!defined($lineno)); - if ($lineno == 0) { - # Got a marker for a new filename - graph_expect("filename"); - $string = read_gcno_string($handle, $big_endian); - return undef if (!defined($string)); - # Check for end of record - if ($string eq "") { - return $filename; - } - $filename = $string; - if (!exists($bb->{$function}->{$filename})) { - $bb->{$function}->{$filename} = []; - } - next; - } - # Got an actual line number - if (!defined($filename)) { - warn("WARNING: unassigned line number in ". - "$gcno_filename\n"); - next; - } - # Add to list - push(@{$bb->{$function}->{$filename}}, $lineno); - graph_add_order($fileorder, $function, $filename); - } -} - -# -# determine_gcno_split_crc(handle, big_endian, rec_length, version) -# -# Determine if HANDLE refers to a .gcno file with a split checksum function -# record format. Return non-zero in case of split checksum format, zero -# otherwise, undef in case of read error. -# - -sub determine_gcno_split_crc($$$$) -{ - my ($handle, $big_endian, $rec_length, $version) = @_; - my $strlen; - my $overlong_string; - - return 1 if ($version >= $GCOV_VERSION_4_7_0); - return 1 if (is_compat($COMPAT_MODE_SPLIT_CRC)); - - # Heuristic: - # Decide format based on contents of next word in record: - # - pre-gcc 4.7 - # This is the function name length / 4 which should be - # less than the remaining record length - # - gcc 4.7 - # This is a checksum, likely with high-order bits set, - # resulting in a large number - $strlen = read_gcno_value($handle, $big_endian, undef, 1); - return undef if (!defined($strlen)); - $overlong_string = 1 if ($strlen * 4 >= $rec_length - 12); - - if ($overlong_string) { - if (is_compat_auto($COMPAT_MODE_SPLIT_CRC)) { - info("Auto-detected compatibility mode for split ". - "checksum .gcno file format\n"); - - return 1; - } else { - # Sanity check - warn("Found overlong string in function record: ". - "try '--compat split_crc'\n"); - } - } - - return 0; -} - -# -# read_gcno_function_record(handle, graph, big_endian, rec_length, version) -# -# Read a gcno format function record from handle and add the relevant data -# to graph. Return (filename, function, artificial) on success, undef on error. -# - -sub read_gcno_function_record(*$$$$$) -{ - my ($handle, $bb, $fileorder, $big_endian, $rec_length, $version) = @_; - my $filename; - my $function; - my $lineno; - my $lines; - my $artificial; - - graph_expect("function record"); - # Skip ident and checksum - graph_skip($handle, 8, "function ident and checksum") or return undef; - # Determine if this is a function record with split checksums - if (!defined($gcno_split_crc)) { - $gcno_split_crc = determine_gcno_split_crc($handle, $big_endian, - $rec_length, - $version); - return undef if (!defined($gcno_split_crc)); - } - # Skip cfg checksum word in case of split checksums - graph_skip($handle, 4, "function cfg checksum") if ($gcno_split_crc); - # Read function name - graph_expect("function name"); - $function = read_gcno_string($handle, $big_endian); - return undef if (!defined($function)); - if ($version >= $GCOV_VERSION_8_0_0) { - $artificial = read_gcno_value($handle, $big_endian, - "compiler-generated entity flag"); - return undef if (!defined($artificial)); - } - # Read filename - graph_expect("filename"); - $filename = read_gcno_string($handle, $big_endian); - return undef if (!defined($filename)); - # Read first line number - $lineno = read_gcno_value($handle, $big_endian, "initial line number"); - return undef if (!defined($lineno)); - # Skip column and ending line number - if ($version >= $GCOV_VERSION_8_0_0) { - graph_skip($handle, 4, "column number") or return undef; - graph_skip($handle, 4, "ending line number") or return undef; - } - # Add to list - push(@{$bb->{$function}->{$filename}}, $lineno); - graph_add_order($fileorder, $function, $filename); - - return ($filename, $function, $artificial); -} - -# -# map_gcno_version -# -# Map version number as found in .gcno files to the format used in geninfo. -# - -sub map_gcno_version($) -{ - my ($version) = @_; - my ($a, $b, $c); - my ($major, $minor); - - $a = $version >> 24; - $b = $version >> 16 & 0xff; - $c = $version >> 8 & 0xff; - - if ($a < ord('A')) { - $major = $a - ord('0'); - $minor = ($b - ord('0')) * 10 + $c - ord('0'); - } else { - $major = ($a - ord('A')) * 10 + $b - ord('0'); - $minor = $c - ord('0'); - } - - return $major << 16 | $minor << 8; -} - -sub remove_fn_from_hash($$) -{ - my ($hash, $fns) = @_; - - foreach my $fn (@$fns) { - delete($hash->{$fn}); - } -} - -# -# read_gcno(filename) -# -# Read the contents of the specified .gcno file and return the following -# mapping: -# graph: filename -> file_data -# file_data: function name -> line_data -# line_data: [ line1, line2, ... ] -# -# See the gcov-io.h file in the gcc 3.3 source code for a description of -# the .gcno format. -# - -sub read_gcno($) -{ - my ($gcno_filename) = @_; - my $file_magic = 0x67636e6f; - my $tag_function = 0x01000000; - my $tag_lines = 0x01450000; - my $big_endian; - my $word; - my $tag; - my $length; - my $filename; - my $function; - my $bb = {}; - my $fileorder = {}; - my $instr; - my $graph; - my $filelength; - my $version; - my $artificial; - my @artificial_fns; - local *HANDLE; - - open(HANDLE, "<", $gcno_filename) or goto open_error; - $filelength = (stat(HANDLE))[7]; - binmode(HANDLE); - # Read magic - $word = read_gcno_word(*HANDLE, "file magic"); - goto incomplete if (!defined($word)); - # Determine file endianness - if (unpack("N", $word) == $file_magic) { - $big_endian = 1; - } elsif (unpack("V", $word) == $file_magic) { - $big_endian = 0; - } else { - goto magic_error; - } - # Read version - $version = read_gcno_value(*HANDLE, $big_endian, "compiler version"); - $version = map_gcno_version($version); - debug(sprintf("found version 0x%08x\n", $version)); - # Skip stamp - graph_skip(*HANDLE, 4, "file timestamp") or goto incomplete; - if ($version >= $GCOV_VERSION_8_0_0) { - graph_skip(*HANDLE, 4, "support unexecuted blocks flag") - or goto incomplete; - } - while (!eof(HANDLE)) { - my $next_pos; - my $curr_pos; - - # Read record tag - $tag = read_gcno_value(*HANDLE, $big_endian, "record tag"); - goto incomplete if (!defined($tag)); - # Read record length - $length = read_gcno_value(*HANDLE, $big_endian, - "record length"); - goto incomplete if (!defined($length)); - # Convert length to bytes - $length *= 4; - # Calculate start of next record - $next_pos = tell(HANDLE); - goto tell_error if ($next_pos == -1); - $next_pos += $length; - # Catch garbage at the end of a gcno file - if ($next_pos > $filelength) { - debug("Overlong record: file_length=$filelength ". - "rec_length=$length\n"); - warn("WARNING: $gcno_filename: Overlong record at end ". - "of file!\n"); - last; - } - # Process record - if ($tag == $tag_function) { - ($filename, $function, $artificial) = - read_gcno_function_record( - *HANDLE, $bb, $fileorder, $big_endian, - $length, $version); - goto incomplete if (!defined($function)); - push(@artificial_fns, $function) if ($artificial); - } elsif ($tag == $tag_lines) { - # Read lines record - $filename = read_gcno_lines_record(*HANDLE, - $gcno_filename, $bb, $fileorder, - $filename, $function, $big_endian); - goto incomplete if (!defined($filename)); - } else { - # Skip record contents - graph_skip(*HANDLE, $length, "unhandled record") - or goto incomplete; - } - # Ensure that we are at the start of the next record - $curr_pos = tell(HANDLE); - goto tell_error if ($curr_pos == -1); - next if ($curr_pos == $next_pos); - goto record_error if ($curr_pos > $next_pos); - graph_skip(*HANDLE, $next_pos - $curr_pos, - "unhandled record content") - or goto incomplete; - } - close(HANDLE); - - # Remove artificial functions from result data - remove_fn_from_hash($bb, \@artificial_fns); - remove_fn_from_hash($fileorder, \@artificial_fns); - - ($instr, $graph) = graph_from_bb($bb, $fileorder, $gcno_filename, 1); - graph_cleanup($graph); - - return ($instr, $graph); - -open_error: - graph_error($gcno_filename, "could not open file"); - return undef; -incomplete: - graph_error($gcno_filename, "reached unexpected end of file"); - return undef; -magic_error: - graph_error($gcno_filename, "found unrecognized gcno file magic"); - return undef; -tell_error: - graph_error($gcno_filename, "could not determine file position"); - return undef; -record_error: - graph_error($gcno_filename, "found unrecognized record format"); - return undef; -} - -sub debug($) -{ - my ($msg) = @_; - - return if (!$debug); - print(STDERR "DEBUG: $msg"); -} - -# -# get_gcov_capabilities -# -# Determine the list of available gcov options. -# - -sub get_gcov_capabilities() -{ - my $help = `$gcov_tool --help`; - my %capabilities; - my %short_option_translations = ( - 'a' => 'all-blocks', - 'b' => 'branch-probabilities', - 'c' => 'branch-counts', - 'f' => 'function-summaries', - 'h' => 'help', - 'i' => 'intermediate-format', - 'l' => 'long-file-names', - 'n' => 'no-output', - 'o' => 'object-directory', - 'p' => 'preserve-paths', - 'u' => 'unconditional-branches', - 'v' => 'version', - 'x' => 'hash-filenames', - ); - - foreach (split(/\n/, $help)) { - my $capability; - if (/--(\S+)/) { - $capability = $1; - } else { - # If the line provides a short option, translate it. - next if (!/^\s*-(\S)\s/); - $capability = $short_option_translations{$1}; - next if not defined($capability); - } - next if ($capability eq 'help'); - next if ($capability eq 'version'); - next if ($capability eq 'object-directory'); - - $capabilities{$capability} = 1; - debug("gcov has capability '$capability'\n"); - } - - return \%capabilities; -} - -# -# parse_ignore_errors(@ignore_errors) -# -# Parse user input about which errors to ignore. -# - -sub parse_ignore_errors(@) -{ - my (@ignore_errors) = @_; - my @items; - my $item; - - return if (!@ignore_errors); - - foreach $item (@ignore_errors) { - $item =~ s/\s//g; - if ($item =~ /,/) { - # Split and add comma-separated parameters - push(@items, split(/,/, $item)); - } else { - # Add single parameter - push(@items, $item); - } - } - foreach $item (@items) { - my $item_id = $ERROR_ID{lc($item)}; - - if (!defined($item_id)) { - die("ERROR: unknown argument for --ignore-errors: ". - "$item\n"); - } - $ignore[$item_id] = 1; - } -} - -# -# is_external(filename) -# -# Determine if a file is located outside of the specified data directories. -# - -sub is_external($) -{ - my ($filename) = @_; - my $dir; - - foreach $dir (@internal_dirs) { - return 0 if ($filename =~ /^\Q$dir\/\E/); - } - return 1; -} - -# -# compat_name(mode) -# -# Return the name of compatibility mode MODE. -# - -sub compat_name($) -{ - my ($mode) = @_; - my $name = $COMPAT_MODE_TO_NAME{$mode}; - - return $name if (defined($name)); - - return ""; -} - -# -# parse_compat_modes(opt) -# -# Determine compatibility mode settings. -# - -sub parse_compat_modes($) -{ - my ($opt) = @_; - my @opt_list; - my %specified; - - # Initialize with defaults - %compat_value = %COMPAT_MODE_DEFAULTS; - - # Add old style specifications - if (defined($opt_compat_libtool)) { - $compat_value{$COMPAT_MODE_LIBTOOL} = - $opt_compat_libtool ? $COMPAT_VALUE_ON - : $COMPAT_VALUE_OFF; - } - - # Parse settings - if (defined($opt)) { - @opt_list = split(/\s*,\s*/, $opt); - } - foreach my $directive (@opt_list) { - my ($mode, $value); - - # Either - # mode=off|on|auto or - # mode (implies on) - if ($directive !~ /^(\w+)=(\w+)$/ && - $directive !~ /^(\w+)$/) { - die("ERROR: Unknown compatibility mode specification: ". - "$directive!\n"); - } - # Determine mode - $mode = $COMPAT_NAME_TO_MODE{lc($1)}; - if (!defined($mode)) { - die("ERROR: Unknown compatibility mode '$1'!\n"); - } - $specified{$mode} = 1; - # Determine value - if (defined($2)) { - $value = $COMPAT_NAME_TO_VALUE{lc($2)}; - if (!defined($value)) { - die("ERROR: Unknown compatibility mode ". - "value '$2'!\n"); - } - } else { - $value = $COMPAT_VALUE_ON; - } - $compat_value{$mode} = $value; - } - # Perform auto-detection - foreach my $mode (sort(keys(%compat_value))) { - my $value = $compat_value{$mode}; - my $is_autodetect = ""; - my $name = compat_name($mode); - - if ($value == $COMPAT_VALUE_AUTO) { - my $autodetect = $COMPAT_MODE_AUTO{$mode}; - - if (!defined($autodetect)) { - die("ERROR: No auto-detection for ". - "mode '$name' available!\n"); - } - - if (ref($autodetect) eq "CODE") { - $value = &$autodetect(); - $compat_value{$mode} = $value; - $is_autodetect = " (auto-detected)"; - } - } - - if ($specified{$mode}) { - if ($value == $COMPAT_VALUE_ON) { - info("Enabling compatibility mode ". - "'$name'$is_autodetect\n"); - } elsif ($value == $COMPAT_VALUE_OFF) { - info("Disabling compatibility mode ". - "'$name'$is_autodetect\n"); - } else { - info("Using delayed auto-detection for ". - "compatibility mode ". - "'$name'\n"); - } - } - } -} - -sub compat_hammer_autodetect() -{ - if ($gcov_version_string =~ /suse/i && $gcov_version == 0x30303 || - $gcov_version_string =~ /mandrake/i && $gcov_version == 0x30302) - { - info("Auto-detected compatibility mode for GCC 3.3 (hammer)\n"); - return $COMPAT_VALUE_ON; - } - return $COMPAT_VALUE_OFF; -} - -# -# is_compat(mode) -# -# Return non-zero if compatibility mode MODE is enabled. -# - -sub is_compat($) -{ - my ($mode) = @_; - - return 1 if ($compat_value{$mode} == $COMPAT_VALUE_ON); - return 0; -} - -# -# is_compat_auto(mode) -# -# Return non-zero if compatibility mode MODE is set to auto-detect. -# - -sub is_compat_auto($) -{ - my ($mode) = @_; - - return 1 if ($compat_value{$mode} == $COMPAT_VALUE_AUTO); - return 0; -} diff --git a/worker/deps/lcov/bin/genpng b/worker/deps/lcov/bin/genpng deleted file mode 100755 index e1d431a348..0000000000 --- a/worker/deps/lcov/bin/genpng +++ /dev/null @@ -1,389 +0,0 @@ -#!/usr/bin/env perl -# -# Copyright (c) International Business Machines Corp., 2002 -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or (at -# your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# -# genpng -# -# This script creates an overview PNG image of a source code file by -# representing each source code character by a single pixel. -# -# Note that the Perl module GD.pm is required for this script to work. -# It may be obtained from http://www.cpan.org -# -# History: -# 2002-08-26: created by Peter Oberparleiter -# - -use strict; -use warnings; -use File::Basename; -use Getopt::Long; -use Cwd qw/abs_path/; - - -# Constants -our $tool_dir = abs_path(dirname($0)); -our $lcov_version = 'LCOV version '.`$tool_dir/get_version.sh --full`; -our $lcov_url = "http://ltp.sourceforge.net/coverage/lcov.php"; -our $tool_name = basename($0); - - -# Prototypes -sub gen_png($$$@); -sub check_and_load_module($); -sub genpng_print_usage(*); -sub genpng_process_file($$$$); -sub genpng_warn_handler($); -sub genpng_die_handler($); - - -# -# Code entry point -# - -# Check whether required module GD.pm is installed -if (check_and_load_module("GD")) -{ - # Note: cannot use die() to print this message because inserting this - # code into another script via do() would not fail as required! - print(STDERR < \$tab_size, - "width=i" => \$width, - "output-filename=s" => \$out_filename, - "help" => \$help, - "version" => \$version)) - { - print(STDERR "Use $tool_name --help to get usage ". - "information\n"); - exit(1); - } - - $filename = $ARGV[0]; - - # Check for help flag - if ($help) - { - genpng_print_usage(*STDOUT); - exit(0); - } - - # Check for version flag - if ($version) - { - print("$tool_name: $lcov_version\n"); - exit(0); - } - - # Check options - if (!$filename) - { - die("No filename specified\n"); - } - - # Check for output filename - if (!$out_filename) - { - $out_filename = "$filename.png"; - } - - genpng_process_file($filename, $out_filename, $width, $tab_size); - exit(0); -} - - -# -# genpng_print_usage(handle) -# -# Write out command line usage information to given filehandle. -# - -sub genpng_print_usage(*) -{ - local *HANDLE = $_[0]; - - print(HANDLE <) - { - if (/^\t\t(.*)$/) - { - # Uninstrumented line - push(@source, ":$1"); - } - elsif (/^ ###### (.*)$/) - { - # Line with zero execution count - push(@source, "0:$1"); - } - elsif (/^( *)(\d*) (.*)$/) - { - # Line with positive execution count - push(@source, "$2:$3"); - } - } - } - else - { - # Plain text file - while () { push(@source, ":$_"); } - } - close(HANDLE); - - gen_png($out_filename, $width, $tab_size, @source); -} - - -# -# gen_png(filename, width, tab_size, source) -# -# Write an overview PNG file to FILENAME. Source code is defined by SOURCE -# which is a list of lines : per source code line. -# The output image will be made up of one pixel per character of source, -# coloring will be done according to execution counts. WIDTH defines the -# image width. TAB_SIZE specifies the number of spaces to use as replacement -# string for tabulator signs in source code text. -# -# Die on error. -# - -sub gen_png($$$@) -{ - my $filename = shift(@_); # Filename for PNG file - my $overview_width = shift(@_); # Imagewidth for image - my $tab_size = shift(@_); # Replacement string for tab signs - my @source = @_; # Source code as passed via argument 2 - my $height; # Height as define by source size - my $overview; # Source code overview image data - my $col_plain_back; # Color for overview background - my $col_plain_text; # Color for uninstrumented text - my $col_cov_back; # Color for background of covered lines - my $col_cov_text; # Color for text of covered lines - my $col_nocov_back; # Color for background of lines which - # were not covered (count == 0) - my $col_nocov_text; # Color for test of lines which were not - # covered (count == 0) - my $col_hi_back; # Color for background of highlighted lines - my $col_hi_text; # Color for text of highlighted lines - my $line; # Current line during iteration - my $row = 0; # Current row number during iteration - my $column; # Current column number during iteration - my $color_text; # Current text color during iteration - my $color_back; # Current background color during iteration - my $last_count; # Count of last processed line - my $count; # Count of current line - my $source; # Source code of current line - my $replacement; # Replacement string for tabulator chars - local *PNG_HANDLE; # Handle for output PNG file - - # Handle empty source files - if (!@source) { - @source = ( "" ); - } - $height = scalar(@source); - # Create image - $overview = new GD::Image($overview_width, $height) - or die("ERROR: cannot allocate overview image!\n"); - - # Define colors - $col_plain_back = $overview->colorAllocate(0xff, 0xff, 0xff); - $col_plain_text = $overview->colorAllocate(0xaa, 0xaa, 0xaa); - $col_cov_back = $overview->colorAllocate(0xaa, 0xa7, 0xef); - $col_cov_text = $overview->colorAllocate(0x5d, 0x5d, 0xea); - $col_nocov_back = $overview->colorAllocate(0xff, 0x00, 0x00); - $col_nocov_text = $overview->colorAllocate(0xaa, 0x00, 0x00); - $col_hi_back = $overview->colorAllocate(0x00, 0xff, 0x00); - $col_hi_text = $overview->colorAllocate(0x00, 0xaa, 0x00); - - # Visualize each line - foreach $line (@source) - { - # Replace tabs with spaces to keep consistent with source - # code view - while ($line =~ /^([^\t]*)(\t)/) - { - $replacement = " "x($tab_size - ((length($1) - 1) % - $tab_size)); - $line =~ s/^([^\t]*)(\t)/$1$replacement/; - } - - # Skip lines which do not follow the : - # specification, otherwise $1 = count, $2 = source code - if (!($line =~ /(\*?)(\d*):(.*)$/)) { next; } - $count = $2; - $source = $3; - - # Decide which color pair to use - - # If this line was not instrumented but the one before was, - # take the color of that line to widen color areas in - # resulting image - if (($count eq "") && defined($last_count) && - ($last_count ne "")) - { - $count = $last_count; - } - - if ($count eq "") - { - # Line was not instrumented - $color_text = $col_plain_text; - $color_back = $col_plain_back; - } - elsif ($count == 0) - { - # Line was instrumented but not executed - $color_text = $col_nocov_text; - $color_back = $col_nocov_back; - } - elsif ($1 eq "*") - { - # Line was highlighted - $color_text = $col_hi_text; - $color_back = $col_hi_back; - } - else - { - # Line was instrumented and executed - $color_text = $col_cov_text; - $color_back = $col_cov_back; - } - - # Write one pixel for each source character - $column = 0; - foreach (split("", $source)) - { - # Check for width - if ($column >= $overview_width) { last; } - - if ($_ eq " ") - { - # Space - $overview->setPixel($column++, $row, - $color_back); - } - else - { - # Text - $overview->setPixel($column++, $row, - $color_text); - } - } - - # Fill rest of line - while ($column < $overview_width) - { - $overview->setPixel($column++, $row, $color_back); - } - - $last_count = $2; - - $row++; - } - - # Write PNG file - open (PNG_HANDLE, ">", $filename) - or die("ERROR: cannot write png file $filename!\n"); - binmode(*PNG_HANDLE); - print(PNG_HANDLE $overview->png()); - close(PNG_HANDLE); -} - -sub genpng_warn_handler($) -{ - my ($msg) = @_; - - warn("$tool_name: $msg"); -} - -sub genpng_die_handler($) -{ - my ($msg) = @_; - - die("$tool_name: $msg"); -} diff --git a/worker/deps/lcov/bin/get_changes.sh b/worker/deps/lcov/bin/get_changes.sh deleted file mode 100755 index ec373b4f4f..0000000000 --- a/worker/deps/lcov/bin/get_changes.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -# -# Usage: get_changes.sh -# -# Print lcov change log information as provided by Git - -TOOLDIR=$(cd $(dirname $0) >/dev/null ; pwd) - -cd $TOOLDIR - -if ! git --no-pager log --no-merges --decorate=short --color=never 2>/dev/null ; then - cat "$TOOLDIR/../CHANGES" 2>/dev/null -fi diff --git a/worker/deps/lcov/bin/get_version.sh b/worker/deps/lcov/bin/get_version.sh deleted file mode 100755 index ac5a363146..0000000000 --- a/worker/deps/lcov/bin/get_version.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -# -# Usage: get_version.sh --version|--release|--full -# -# Print lcov version or release information as provided by Git, .version -# or a fallback. - -TOOLDIR=$(cd $(dirname $0) >/dev/null ; pwd) -GITVER=$(cd $TOOLDIR ; git describe --tags 2>/dev/null) - -if [ -z "$GITVER" ] ; then - # Get version information from file - if [ -e "$TOOLDIR/../.version" ] ; then - source "$TOOLDIR/../.version" - fi -else - # Get version information from git - FULL=${GITVER:1} - VERSION=${GITVER%%-*} - VERSION=${VERSION:1} - if [ "${GITVER#*-}" != "$GITVER" ] ; then - RELEASE=${GITVER#*-} - RELEASE=${RELEASE/-/.} - fi -fi - -# Fallback -[ -z "$VERSION" ] && VERSION="1.0" -[ -z "$RELEASE" ] && RELEASE="1" -[ -z "$FULL" ] && FULL="$VERSION" - -[ "$1" == "--version" ] && echo -n "$VERSION" -[ "$1" == "--release" ] && echo -n "$RELEASE" -[ "$1" == "--full" ] && echo -n "$FULL" diff --git a/worker/deps/lcov/bin/install.sh b/worker/deps/lcov/bin/install.sh deleted file mode 100755 index 2cdef45b6c..0000000000 --- a/worker/deps/lcov/bin/install.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash -# -# install.sh [--uninstall] sourcefile targetfile [install options] -# - - -# Check for uninstall option -if test "x$1" == "x--uninstall" ; then - UNINSTALL=true - SOURCE=$2 - TARGET=$3 - shift 3 -else - UNINSTALL=false - SOURCE=$1 - TARGET=$2 - shift 2 -fi - -# Check usage -if test -z "$SOURCE" || test -z "$TARGET" ; then - echo Usage: install.sh [--uninstall] source target [install options] >&2 - exit 1 -fi - - -# -# do_install(SOURCE_FILE, TARGET_FILE) -# - -do_install() -{ - local SOURCE=$1 - local TARGET=$2 - local PARAMS=$3 - - install -d $(dirname $TARGET) - install -p $PARAMS $SOURCE $TARGET - if [ -n "$LCOV_PERL_PATH" ] ; then - # Replace Perl interpreter specification - sed -e "1 s%^#\!.*perl.*$%#\!$LCOV_PERL_PATH%" -i $TARGET - fi -} - - -# -# do_uninstall(SOURCE_FILE, TARGET_FILE) -# - -do_uninstall() -{ - local SOURCE=$1 - local TARGET=$2 - - # Does target exist? - if test -r $TARGET ; then - # Is target of the same version as this package? - if diff -I '^our \$lcov_version' -I '^\.TH ' -I '^#!' $SOURCE $TARGET >/dev/null; then - rm -f $TARGET - else - echo WARNING: Skipping uninstall for $TARGET - versions differ! >&2 - fi - else - echo WARNING: Skipping uninstall for $TARGET - not installed! >&2 - fi -} - - -# Call sub routine -if $UNINSTALL ; then - do_uninstall $SOURCE $TARGET -else - do_install $SOURCE $TARGET "$*" -fi - -exit 0 diff --git a/worker/deps/lcov/bin/lcov b/worker/deps/lcov/bin/lcov deleted file mode 100755 index e30d991abb..0000000000 --- a/worker/deps/lcov/bin/lcov +++ /dev/null @@ -1,4329 +0,0 @@ -#!/usr/bin/env perl -# -# Copyright (c) International Business Machines Corp., 2002,2012 -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or (at -# your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# -# lcov -# -# This is a wrapper script which provides a single interface for accessing -# LCOV coverage data. -# -# -# History: -# 2002-08-29 created by Peter Oberparleiter -# IBM Lab Boeblingen -# 2002-09-05 / Peter Oberparleiter: implemented --kernel-directory + -# multiple directories -# 2002-10-16 / Peter Oberparleiter: implemented --add-tracefile option -# 2002-10-17 / Peter Oberparleiter: implemented --extract option -# 2002-11-04 / Peter Oberparleiter: implemented --list option -# 2003-03-07 / Paul Larson: Changed to make it work with the latest gcov -# kernel patch. This will break it with older gcov-kernel -# patches unless you change the value of $gcovmod in this script -# 2003-04-07 / Peter Oberparleiter: fixed bug which resulted in an error -# when trying to combine .info files containing data without -# a test name -# 2003-04-10 / Peter Oberparleiter: extended Paul's change so that LCOV -# works both with the new and the old gcov-kernel patch -# 2003-04-10 / Peter Oberparleiter: added $gcov_dir constant in anticipation -# of a possible move of the gcov kernel directory to another -# file system in a future version of the gcov-kernel patch -# 2003-04-15 / Paul Larson: make info write to STDERR, not STDOUT -# 2003-04-15 / Paul Larson: added --remove option -# 2003-04-30 / Peter Oberparleiter: renamed --reset to --zerocounters -# to remove naming ambiguity with --remove -# 2003-04-30 / Peter Oberparleiter: adjusted help text to include --remove -# 2003-06-27 / Peter Oberparleiter: implemented --diff -# 2003-07-03 / Peter Oberparleiter: added line checksum support, added -# --no-checksum -# 2003-12-11 / Laurent Deniel: added --follow option -# 2004-03-29 / Peter Oberparleiter: modified --diff option to better cope with -# ambiguous patch file entries, modified --capture option to use -# modprobe before insmod (needed for 2.6) -# 2004-03-30 / Peter Oberparleiter: added --path option -# 2004-08-09 / Peter Oberparleiter: added configuration file support -# 2008-08-13 / Peter Oberparleiter: added function coverage support -# - -use strict; -use warnings; -use File::Basename; -use File::Path; -use File::Find; -use File::Temp qw /tempdir/; -use File::Spec::Functions qw /abs2rel canonpath catdir catfile catpath - file_name_is_absolute rootdir splitdir splitpath/; -use Getopt::Long; -use Cwd qw /abs_path getcwd/; - - -# Global constants -our $tool_dir = abs_path(dirname($0)); -our $lcov_version = 'LCOV version '.`$tool_dir/get_version.sh --full`; -our $lcov_url = "http://ltp.sourceforge.net/coverage/lcov.php"; -our $tool_name = basename($0); - -# Directory containing gcov kernel files -our $gcov_dir; - -# Where to create temporary directories -our $tmp_dir; - -# Internal constants -our $GKV_PROC = 0; # gcov-kernel data in /proc via external patch -our $GKV_SYS = 1; # gcov-kernel data in /sys via vanilla 2.6.31+ -our @GKV_NAME = ( "external", "upstream" ); -our $pkg_gkv_file = ".gcov_kernel_version"; -our $pkg_build_file = ".build_directory"; - -# Branch data combination types -our $BR_SUB = 0; -our $BR_ADD = 1; - -# Prototypes -sub print_usage(*); -sub check_options(); -sub userspace_reset(); -sub userspace_capture(); -sub kernel_reset(); -sub kernel_capture(); -sub kernel_capture_initial(); -sub package_capture(); -sub add_traces(); -sub read_info_file($); -sub get_info_entry($); -sub set_info_entry($$$$$$$$$;$$$$$$); -sub add_counts($$); -sub merge_checksums($$$); -sub combine_info_entries($$$); -sub combine_info_files($$); -sub write_info_file(*$); -sub extract(); -sub remove(); -sub list(); -sub get_common_filename($$); -sub read_diff($); -sub diff(); -sub system_no_output($@); -sub read_config($); -sub apply_config($); -sub info(@); -sub create_temp_dir(); -sub transform_pattern($); -sub warn_handler($); -sub die_handler($); -sub abort_handler($); -sub temp_cleanup(); -sub setup_gkv(); -sub get_overall_line($$$$); -sub print_overall_rate($$$$$$$$$); -sub lcov_geninfo(@); -sub create_package($$$;$); -sub get_func_found_and_hit($); -sub summary(); -sub rate($$;$$$); - -# Global variables & initialization -our @directory; # Specifies where to get coverage data from -our @kernel_directory; # If set, captures only from specified kernel subdirs -our @add_tracefile; # If set, reads in and combines all files in list -our $list; # If set, list contents of tracefile -our $extract; # If set, extracts parts of tracefile -our $remove; # If set, removes parts of tracefile -our $diff; # If set, modifies tracefile according to diff -our $reset; # If set, reset all coverage data to zero -our $capture; # If set, capture data -our $output_filename; # Name for file to write coverage data to -our $test_name = ""; # Test case name -our $quiet = ""; # If set, suppress information messages -our $help; # Help option flag -our $version; # Version option flag -our $convert_filenames; # If set, convert filenames when applying diff -our $strip; # If set, strip leading directories when applying diff -our $temp_dir_name; # Name of temporary directory -our $cwd = `pwd`; # Current working directory -our $data_stdout; # If set, indicates that data is written to stdout -our $follow; # If set, indicates that find shall follow links -our $diff_path = ""; # Path removed from tracefile when applying diff -our $base_directory; # Base directory (cwd of gcc during compilation) -our $checksum; # If set, calculate a checksum for each line -our $no_checksum; # If set, don't calculate a checksum for each line -our $compat_libtool; # If set, indicates that libtool mode is to be enabled -our $no_compat_libtool; # If set, indicates that libtool mode is to be disabled -our $gcov_tool; -our @opt_ignore_errors; -our $initial; -our @include_patterns; # List of source file patterns to include -our @exclude_patterns; # List of source file patterns to exclude -our $no_recursion = 0; -our $to_package; -our $from_package; -our $maxdepth; -our $no_markers; -our $config; # Configuration file contents -chomp($cwd); -our @temp_dirs; -our $gcov_gkv; # gcov kernel support version found on machine -our $opt_derive_func_data; -our $opt_debug; -our $opt_list_full_path; -our $opt_no_list_full_path; -our $opt_list_width = 80; -our $opt_list_truncate_max = 20; -our $opt_external; -our $opt_no_external; -our $opt_config_file; -our %opt_rc; -our @opt_summary; -our $opt_compat; -our $ln_overall_found; -our $ln_overall_hit; -our $fn_overall_found; -our $fn_overall_hit; -our $br_overall_found; -our $br_overall_hit; -our $func_coverage = 1; -our $br_coverage = 0; - - -# -# Code entry point -# - -$SIG{__WARN__} = \&warn_handler; -$SIG{__DIE__} = \&die_handler; -$SIG{'INT'} = \&abort_handler; -$SIG{'QUIT'} = \&abort_handler; - -# Check command line for a configuration file name -Getopt::Long::Configure("pass_through", "no_auto_abbrev"); -GetOptions("config-file=s" => \$opt_config_file, - "rc=s%" => \%opt_rc); -Getopt::Long::Configure("default"); - -{ - # Remove spaces around rc options - my %new_opt_rc; - - while (my ($key, $value) = each(%opt_rc)) { - $key =~ s/^\s+|\s+$//g; - $value =~ s/^\s+|\s+$//g; - - $new_opt_rc{$key} = $value; - } - %opt_rc = %new_opt_rc; -} - -# Read configuration file if available -if (defined($opt_config_file)) { - $config = read_config($opt_config_file); -} elsif (defined($ENV{"HOME"}) && (-r $ENV{"HOME"}."/.lcovrc")) -{ - $config = read_config($ENV{"HOME"}."/.lcovrc"); -} -elsif (-r "/etc/lcovrc") -{ - $config = read_config("/etc/lcovrc"); -} elsif (-r "/usr/local/etc/lcovrc") -{ - $config = read_config("/usr/local/etc/lcovrc"); -} - -if ($config || %opt_rc) -{ - # Copy configuration file and --rc values to variables - apply_config({ - "lcov_gcov_dir" => \$gcov_dir, - "lcov_tmp_dir" => \$tmp_dir, - "lcov_list_full_path" => \$opt_list_full_path, - "lcov_list_width" => \$opt_list_width, - "lcov_list_truncate_max"=> \$opt_list_truncate_max, - "lcov_branch_coverage" => \$br_coverage, - "lcov_function_coverage"=> \$func_coverage, - }); -} - -# Parse command line options -if (!GetOptions("directory|d|di=s" => \@directory, - "add-tracefile|a=s" => \@add_tracefile, - "list|l=s" => \$list, - "kernel-directory|k=s" => \@kernel_directory, - "extract|e=s" => \$extract, - "remove|r=s" => \$remove, - "diff=s" => \$diff, - "convert-filenames" => \$convert_filenames, - "strip=i" => \$strip, - "capture|c" => \$capture, - "output-file|o=s" => \$output_filename, - "test-name|t=s" => \$test_name, - "zerocounters|z" => \$reset, - "quiet|q" => \$quiet, - "help|h|?" => \$help, - "version|v" => \$version, - "follow|f" => \$follow, - "path=s" => \$diff_path, - "base-directory|b=s" => \$base_directory, - "checksum" => \$checksum, - "no-checksum" => \$no_checksum, - "compat-libtool" => \$compat_libtool, - "no-compat-libtool" => \$no_compat_libtool, - "gcov-tool=s" => \$gcov_tool, - "ignore-errors=s" => \@opt_ignore_errors, - "initial|i" => \$initial, - "include=s" => \@include_patterns, - "exclude=s" => \@exclude_patterns, - "no-recursion" => \$no_recursion, - "to-package=s" => \$to_package, - "from-package=s" => \$from_package, - "no-markers" => \$no_markers, - "derive-func-data" => \$opt_derive_func_data, - "debug" => \$opt_debug, - "list-full-path" => \$opt_list_full_path, - "no-list-full-path" => \$opt_no_list_full_path, - "external" => \$opt_external, - "no-external" => \$opt_no_external, - "summary=s" => \@opt_summary, - "compat=s" => \$opt_compat, - "config-file=s" => \$opt_config_file, - "rc=s%" => \%opt_rc, - )) -{ - print(STDERR "Use $tool_name --help to get usage information\n"); - exit(1); -} -else -{ - # Merge options - if (defined($no_checksum)) - { - $checksum = ($no_checksum ? 0 : 1); - $no_checksum = undef; - } - - if (defined($no_compat_libtool)) - { - $compat_libtool = ($no_compat_libtool ? 0 : 1); - $no_compat_libtool = undef; - } - - if (defined($opt_no_list_full_path)) - { - $opt_list_full_path = ($opt_no_list_full_path ? 0 : 1); - $opt_no_list_full_path = undef; - } - - if (defined($opt_no_external)) { - $opt_external = 0; - $opt_no_external = undef; - } -} - -# Check for help option -if ($help) -{ - print_usage(*STDOUT); - exit(0); -} - -# Check for version option -if ($version) -{ - print("$tool_name: $lcov_version\n"); - exit(0); -} - -# Check list width option -if ($opt_list_width <= 40) { - die("ERROR: lcov_list_width parameter out of range (needs to be ". - "larger than 40)\n"); -} - -# Normalize --path text -$diff_path =~ s/\/$//; - -if ($follow) -{ - $follow = "-follow"; -} -else -{ - $follow = ""; -} - -if ($no_recursion) -{ - $maxdepth = "-maxdepth 1"; -} -else -{ - $maxdepth = ""; -} - -# Check for valid options -check_options(); - -# Only --extract, --remove and --diff allow unnamed parameters -if (@ARGV && !($extract || $remove || $diff || @opt_summary)) -{ - die("Extra parameter found: '".join(" ", @ARGV)."'\n". - "Use $tool_name --help to get usage information\n"); -} - -# Check for output filename -$data_stdout = !($output_filename && ($output_filename ne "-")); - -if ($capture) -{ - if ($data_stdout) - { - # Option that tells geninfo to write to stdout - $output_filename = "-"; - } -} - -# Determine kernel directory for gcov data -if (!$from_package && !@directory && ($capture || $reset)) { - ($gcov_gkv, $gcov_dir) = setup_gkv(); -} - -# Check for requested functionality -if ($reset) -{ - $data_stdout = 0; - # Differentiate between user space and kernel reset - if (@directory) - { - userspace_reset(); - } - else - { - kernel_reset(); - } -} -elsif ($capture) -{ - # Capture source can be user space, kernel or package - if ($from_package) { - package_capture(); - } elsif (@directory) { - userspace_capture(); - } else { - if ($initial) { - if (defined($to_package)) { - die("ERROR: --initial cannot be used together ". - "with --to-package\n"); - } - kernel_capture_initial(); - } else { - kernel_capture(); - } - } -} -elsif (@add_tracefile) -{ - ($ln_overall_found, $ln_overall_hit, - $fn_overall_found, $fn_overall_hit, - $br_overall_found, $br_overall_hit) = add_traces(); -} -elsif ($remove) -{ - ($ln_overall_found, $ln_overall_hit, - $fn_overall_found, $fn_overall_hit, - $br_overall_found, $br_overall_hit) = remove(); -} -elsif ($extract) -{ - ($ln_overall_found, $ln_overall_hit, - $fn_overall_found, $fn_overall_hit, - $br_overall_found, $br_overall_hit) = extract(); -} -elsif ($list) -{ - $data_stdout = 0; - list(); -} -elsif ($diff) -{ - if (scalar(@ARGV) != 1) - { - die("ERROR: option --diff requires one additional argument!\n". - "Use $tool_name --help to get usage information\n"); - } - ($ln_overall_found, $ln_overall_hit, - $fn_overall_found, $fn_overall_hit, - $br_overall_found, $br_overall_hit) = diff(); -} -elsif (@opt_summary) -{ - $data_stdout = 0; - ($ln_overall_found, $ln_overall_hit, - $fn_overall_found, $fn_overall_hit, - $br_overall_found, $br_overall_hit) = summary(); -} - -temp_cleanup(); - -if (defined($ln_overall_found)) { - print_overall_rate(1, $ln_overall_found, $ln_overall_hit, - 1, $fn_overall_found, $fn_overall_hit, - 1, $br_overall_found, $br_overall_hit); -} else { - info("Done.\n") if (!$list && !$capture); -} -exit(0); - -# -# print_usage(handle) -# -# Print usage information. -# - -sub print_usage(*) -{ - local *HANDLE = $_[0]; - - print(HANDLE < 1) - { - die("ERROR: only one of -z, -c, -a, -e, -r, -l, ". - "--diff or --summary allowed!\n". - "Use $tool_name --help to get usage information\n"); - } -} - - -# -# userspace_reset() -# -# Reset coverage data found in DIRECTORY by deleting all contained .da files. -# -# Die on error. -# - -sub userspace_reset() -{ - my $current_dir; - my @file_list; - - foreach $current_dir (@directory) - { - info("Deleting all .da files in $current_dir". - ($no_recursion?"\n":" and subdirectories\n")); - @file_list = `find "$current_dir" $maxdepth $follow -name \\*\\.da -type f -o -name \\*\\.gcda -type f 2>/dev/null`; - chomp(@file_list); - foreach (@file_list) - { - unlink($_) or die("ERROR: cannot remove file $_!\n"); - } - } -} - - -# -# userspace_capture() -# -# Capture coverage data found in DIRECTORY and write it to a package (if -# TO_PACKAGE specified) or to OUTPUT_FILENAME or STDOUT. -# -# Die on error. -# - -sub userspace_capture() -{ - my $dir; - my $build; - - if (!defined($to_package)) { - lcov_geninfo(@directory); - return; - } - if (scalar(@directory) != 1) { - die("ERROR: -d may be specified only once with --to-package\n"); - } - $dir = $directory[0]; - if (defined($base_directory)) { - $build = $base_directory; - } else { - $build = $dir; - } - create_package($to_package, $dir, $build); -} - - -# -# kernel_reset() -# -# Reset kernel coverage. -# -# Die on error. -# - -sub kernel_reset() -{ - local *HANDLE; - my $reset_file; - - info("Resetting kernel execution counters\n"); - if (-e "$gcov_dir/vmlinux") { - $reset_file = "$gcov_dir/vmlinux"; - } elsif (-e "$gcov_dir/reset") { - $reset_file = "$gcov_dir/reset"; - } else { - die("ERROR: no reset control found in $gcov_dir\n"); - } - open(HANDLE, ">", $reset_file) or - die("ERROR: cannot write to $reset_file!\n"); - print(HANDLE "0"); - close(HANDLE); -} - - -# -# lcov_copy_single(from, to) -# -# Copy single regular file FROM to TO without checking its size. This is -# required to work with special files generated by the kernel -# seq_file-interface. -# -# -sub lcov_copy_single($$) -{ - my ($from, $to) = @_; - my $content; - local $/; - local *HANDLE; - - open(HANDLE, "<", $from) or die("ERROR: cannot read $from: $!\n"); - $content = ; - close(HANDLE); - open(HANDLE, ">", $to) or die("ERROR: cannot write $from: $!\n"); - if (defined($content)) { - print(HANDLE $content); - } - close(HANDLE); -} - -# -# lcov_find(dir, function, data[, extension, ...)]) -# -# Search DIR for files and directories whose name matches PATTERN and run -# FUNCTION for each match. If not pattern is specified, match all names. -# -# FUNCTION has the following prototype: -# function(dir, relative_name, data) -# -# Where: -# dir: the base directory for this search -# relative_name: the name relative to the base directory of this entry -# data: the DATA variable passed to lcov_find -# -sub lcov_find($$$;@) -{ - my ($dir, $fn, $data, @pattern) = @_; - my $result; - my $_fn = sub { - my $filename = $File::Find::name; - - if (defined($result)) { - return; - } - $filename = abs2rel($filename, $dir); - foreach (@pattern) { - if ($filename =~ /$_/) { - goto ok; - } - } - return; - ok: - $result = &$fn($dir, $filename, $data); - }; - if (scalar(@pattern) == 0) { - @pattern = ".*"; - } - find( { wanted => $_fn, no_chdir => 1 }, $dir); - - return $result; -} - -# -# lcov_copy_fn(from, rel, to) -# -# Copy directories, files and links from/rel to to/rel. -# - -sub lcov_copy_fn($$$) -{ - my ($from, $rel, $to) = @_; - my $absfrom = canonpath(catfile($from, $rel)); - my $absto = canonpath(catfile($to, $rel)); - - if (-d) { - if (! -d $absto) { - mkpath($absto) or - die("ERROR: cannot create directory $absto\n"); - chmod(0700, $absto); - } - } elsif (-l) { - # Copy symbolic link - my $link = readlink($absfrom); - - if (!defined($link)) { - die("ERROR: cannot read link $absfrom: $!\n"); - } - symlink($link, $absto) or - die("ERROR: cannot create link $absto: $!\n"); - } else { - lcov_copy_single($absfrom, $absto); - chmod(0600, $absto); - } - return undef; -} - -# -# lcov_copy(from, to, subdirs) -# -# Copy all specified SUBDIRS and files from directory FROM to directory TO. For -# regular files, copy file contents without checking its size. This is required -# to work with seq_file-generated files. -# - -sub lcov_copy($$;@) -{ - my ($from, $to, @subdirs) = @_; - my @pattern; - - foreach (@subdirs) { - push(@pattern, "^$_"); - } - lcov_find($from, \&lcov_copy_fn, $to, @pattern); -} - -# -# lcov_geninfo(directory) -# -# Call geninfo for the specified directory and with the parameters specified -# at the command line. -# - -sub lcov_geninfo(@) -{ - my (@dir) = @_; - my @param; - - # Capture data - info("Capturing coverage data from ".join(" ", @dir)."\n"); - @param = ("$tool_dir/geninfo", @dir); - if ($output_filename) - { - @param = (@param, "--output-filename", $output_filename); - } - if ($test_name) - { - @param = (@param, "--test-name", $test_name); - } - if ($follow) - { - @param = (@param, "--follow"); - } - if ($quiet) - { - @param = (@param, "--quiet"); - } - if (defined($checksum)) - { - if ($checksum) - { - @param = (@param, "--checksum"); - } - else - { - @param = (@param, "--no-checksum"); - } - } - if ($base_directory) - { - @param = (@param, "--base-directory", $base_directory); - } - if ($no_compat_libtool) - { - @param = (@param, "--no-compat-libtool"); - } - elsif ($compat_libtool) - { - @param = (@param, "--compat-libtool"); - } - if ($gcov_tool) - { - @param = (@param, "--gcov-tool", $gcov_tool); - } - foreach (@opt_ignore_errors) { - @param = (@param, "--ignore-errors", $_); - } - if ($no_recursion) { - @param = (@param, "--no-recursion"); - } - if ($initial) - { - @param = (@param, "--initial"); - } - if ($no_markers) - { - @param = (@param, "--no-markers"); - } - if ($opt_derive_func_data) - { - @param = (@param, "--derive-func-data"); - } - if ($opt_debug) - { - @param = (@param, "--debug"); - } - if (defined($opt_external) && $opt_external) - { - @param = (@param, "--external"); - } - if (defined($opt_external) && !$opt_external) - { - @param = (@param, "--no-external"); - } - if (defined($opt_compat)) { - @param = (@param, "--compat", $opt_compat); - } - if (%opt_rc) { - foreach my $key (keys(%opt_rc)) { - @param = (@param, "--rc", "$key=".$opt_rc{$key}); - } - } - if (defined($opt_config_file)) { - @param = (@param, "--config-file", $opt_config_file); - } - foreach (@include_patterns) { - @param = (@param, "--include", $_); - } - foreach (@exclude_patterns) { - @param = (@param, "--exclude", $_); - } - - system(@param) and exit($? >> 8); -} - -# -# read_file(filename) -# -# Return the contents of the file defined by filename. -# - -sub read_file($) -{ - my ($filename) = @_; - my $content; - local $\; - local *HANDLE; - - open(HANDLE, "<", $filename) || return undef; - $content = ; - close(HANDLE); - - return $content; -} - -# -# get_package(package_file) -# -# Unpack unprocessed coverage data files from package_file to a temporary -# directory and return directory name, build directory and gcov kernel version -# as found in package. -# - -sub get_package($) -{ - my ($file) = @_; - my $dir = create_temp_dir(); - my $gkv; - my $build; - my $cwd = getcwd(); - my $count; - local *HANDLE; - - info("Reading package $file:\n"); - $file = abs_path($file); - chdir($dir); - open(HANDLE, "-|", "tar xvfz '$file' 2>/dev/null") - or die("ERROR: could not process package $file\n"); - $count = 0; - while () { - if (/\.da$/ || /\.gcda$/) { - $count++; - } - } - close(HANDLE); - if ($count == 0) { - die("ERROR: no data file found in package $file\n"); - } - info(" data directory .......: $dir\n"); - $build = read_file("$dir/$pkg_build_file"); - if (defined($build)) { - info(" build directory ......: $build\n"); - } - $gkv = read_file("$dir/$pkg_gkv_file"); - if (defined($gkv)) { - $gkv = int($gkv); - if ($gkv != $GKV_PROC && $gkv != $GKV_SYS) { - die("ERROR: unsupported gcov kernel version found ". - "($gkv)\n"); - } - info(" content type .........: kernel data\n"); - info(" gcov kernel version ..: %s\n", $GKV_NAME[$gkv]); - } else { - info(" content type .........: application data\n"); - } - info(" data files ...........: $count\n"); - chdir($cwd); - - return ($dir, $build, $gkv); -} - -# -# write_file(filename, $content) -# -# Create a file named filename and write the specified content to it. -# - -sub write_file($$) -{ - my ($filename, $content) = @_; - local *HANDLE; - - open(HANDLE, ">", $filename) || return 0; - print(HANDLE $content); - close(HANDLE) || return 0; - - return 1; -} - -# count_package_data(filename) -# -# Count the number of coverage data files in the specified package file. -# - -sub count_package_data($) -{ - my ($filename) = @_; - local *HANDLE; - my $count = 0; - - open(HANDLE, "-|", "tar tfz '$filename'") or return undef; - while () { - if (/\.da$/ || /\.gcda$/) { - $count++; - } - } - close(HANDLE); - return $count; -} - -# -# create_package(package_file, source_directory, build_directory[, -# kernel_gcov_version]) -# -# Store unprocessed coverage data files from source_directory to package_file. -# - -sub create_package($$$;$) -{ - my ($file, $dir, $build, $gkv) = @_; - my $cwd = getcwd(); - - # Check for availability of tar tool first - system("tar --help > /dev/null") - and die("ERROR: tar command not available\n"); - - # Print information about the package - info("Creating package $file:\n"); - info(" data directory .......: $dir\n"); - - # Handle build directory - if (defined($build)) { - info(" build directory ......: $build\n"); - write_file("$dir/$pkg_build_file", $build) - or die("ERROR: could not write to ". - "$dir/$pkg_build_file\n"); - } - - # Handle gcov kernel version data - if (defined($gkv)) { - info(" content type .........: kernel data\n"); - info(" gcov kernel version ..: %s\n", $GKV_NAME[$gkv]); - write_file("$dir/$pkg_gkv_file", $gkv) - or die("ERROR: could not write to ". - "$dir/$pkg_gkv_file\n"); - } else { - info(" content type .........: application data\n"); - } - - # Create package - $file = abs_path($file); - chdir($dir); - system("tar cfz $file .") - and die("ERROR: could not create package $file\n"); - chdir($cwd); - - # Remove temporary files - unlink("$dir/$pkg_build_file"); - unlink("$dir/$pkg_gkv_file"); - - # Show number of data files - if (!$quiet) { - my $count = count_package_data($file); - - if (defined($count)) { - info(" data files ...........: $count\n"); - } - } -} - -sub find_link_fn($$$) -{ - my ($from, $rel, $filename) = @_; - my $absfile = catfile($from, $rel, $filename); - - if (-l $absfile) { - return $absfile; - } - return undef; -} - -# -# get_base(dir) -# -# Return (BASE, OBJ), where -# - BASE: is the path to the kernel base directory relative to dir -# - OBJ: is the absolute path to the kernel build directory -# - -sub get_base($) -{ - my ($dir) = @_; - my $marker = "kernel/gcov/base.gcno"; - my $markerfile; - my $sys; - my $obj; - my $link; - - $markerfile = lcov_find($dir, \&find_link_fn, $marker); - if (!defined($markerfile)) { - return (undef, undef); - } - - # sys base is parent of parent of markerfile. - $sys = abs2rel(dirname(dirname(dirname($markerfile))), $dir); - - # obj base is parent of parent of markerfile link target. - $link = readlink($markerfile); - if (!defined($link)) { - die("ERROR: could not read $markerfile\n"); - } - $obj = dirname(dirname(dirname($link))); - - return ($sys, $obj); -} - -# -# apply_base_dir(data_dir, base_dir, build_dir, @directories) -# -# Make entries in @directories relative to data_dir. -# - -sub apply_base_dir($$$@) -{ - my ($data, $base, $build, @dirs) = @_; - my $dir; - my @result; - - foreach $dir (@dirs) { - # Is directory path relative to data directory? - if (-d catdir($data, $dir)) { - push(@result, $dir); - next; - } - # Relative to the auto-detected base-directory? - if (defined($base)) { - if (-d catdir($data, $base, $dir)) { - push(@result, catdir($base, $dir)); - next; - } - } - # Relative to the specified base-directory? - if (defined($base_directory)) { - if (file_name_is_absolute($base_directory)) { - $base = abs2rel($base_directory, rootdir()); - } else { - $base = $base_directory; - } - if (-d catdir($data, $base, $dir)) { - push(@result, catdir($base, $dir)); - next; - } - } - # Relative to the build directory? - if (defined($build)) { - if (file_name_is_absolute($build)) { - $base = abs2rel($build, rootdir()); - } else { - $base = $build; - } - if (-d catdir($data, $base, $dir)) { - push(@result, catdir($base, $dir)); - next; - } - } - die("ERROR: subdirectory $dir not found\n". - "Please use -b to specify the correct directory\n"); - } - return @result; -} - -# -# copy_gcov_dir(dir, [@subdirectories]) -# -# Create a temporary directory and copy all or, if specified, only some -# subdirectories from dir to that directory. Return the name of the temporary -# directory. -# - -sub copy_gcov_dir($;@) -{ - my ($data, @dirs) = @_; - my $tempdir = create_temp_dir(); - - info("Copying data to temporary directory $tempdir\n"); - lcov_copy($data, $tempdir, @dirs); - - return $tempdir; -} - -# -# kernel_capture_initial -# -# Capture initial kernel coverage data, i.e. create a coverage data file from -# static graph files which contains zero coverage data for all instrumented -# lines. -# - -sub kernel_capture_initial() -{ - my $build; - my $source; - my @params; - - if (defined($base_directory)) { - $build = $base_directory; - $source = "specified"; - } else { - (undef, $build) = get_base($gcov_dir); - if (!defined($build)) { - die("ERROR: could not auto-detect build directory.\n". - "Please use -b to specify the build directory\n"); - } - $source = "auto-detected"; - } - info("Using $build as kernel build directory ($source)\n"); - # Build directory needs to be passed to geninfo - $base_directory = $build; - if (@kernel_directory) { - foreach my $dir (@kernel_directory) { - push(@params, "$build/$dir"); - } - } else { - push(@params, $build); - } - lcov_geninfo(@params); -} - -# -# kernel_capture_from_dir(directory, gcov_kernel_version, build) -# -# Perform the actual kernel coverage capturing from the specified directory -# assuming that the data was copied from the specified gcov kernel version. -# - -sub kernel_capture_from_dir($$$) -{ - my ($dir, $gkv, $build) = @_; - - # Create package or coverage file - if (defined($to_package)) { - create_package($to_package, $dir, $build, $gkv); - } else { - # Build directory needs to be passed to geninfo - $base_directory = $build; - lcov_geninfo($dir); - } -} - -# -# adjust_kernel_dir(dir, build) -# -# Adjust directories specified with -k so that they point to the directory -# relative to DIR. Return the build directory if specified or the auto- -# detected build-directory. -# - -sub adjust_kernel_dir($$) -{ - my ($dir, $build) = @_; - my ($sys_base, $build_auto) = get_base($dir); - - if (!defined($build)) { - $build = $build_auto; - } - if (!defined($build)) { - die("ERROR: could not auto-detect build directory.\n". - "Please use -b to specify the build directory\n"); - } - # Make @kernel_directory relative to sysfs base - if (@kernel_directory) { - @kernel_directory = apply_base_dir($dir, $sys_base, $build, - @kernel_directory); - } - return $build; -} - -sub kernel_capture() -{ - my $data_dir; - my $build = $base_directory; - - if ($gcov_gkv == $GKV_SYS) { - $build = adjust_kernel_dir($gcov_dir, $build); - } - $data_dir = copy_gcov_dir($gcov_dir, @kernel_directory); - kernel_capture_from_dir($data_dir, $gcov_gkv, $build); -} - -# -# link_data_cb(datadir, rel, graphdir) -# -# Create symbolic link in GRAPDIR/REL pointing to DATADIR/REL. -# - -sub link_data_cb($$$) -{ - my ($datadir, $rel, $graphdir) = @_; - my $absfrom = catfile($datadir, $rel); - my $absto = catfile($graphdir, $rel); - my $base; - my $dir; - - if (-e $absto) { - die("ERROR: could not create symlink at $absto: ". - "File already exists!\n"); - } - if (-l $absto) { - # Broken link - possibly from an interrupted earlier run - unlink($absto); - } - - # Check for graph file - $base = $absto; - $base =~ s/\.(gcda|da)$//; - if (! -e $base.".gcno" && ! -e $base.".bbg" && ! -e $base.".bb") { - die("ERROR: No graph file found for $absfrom in ". - dirname($base)."!\n"); - } - - symlink($absfrom, $absto) or - die("ERROR: could not create symlink at $absto: $!\n"); -} - -# -# unlink_data_cb(datadir, rel, graphdir) -# -# Remove symbolic link from GRAPHDIR/REL to DATADIR/REL. -# - -sub unlink_data_cb($$$) -{ - my ($datadir, $rel, $graphdir) = @_; - my $absfrom = catfile($datadir, $rel); - my $absto = catfile($graphdir, $rel); - my $target; - - return if (!-l $absto); - $target = readlink($absto); - return if (!defined($target) || $target ne $absfrom); - - unlink($absto) or - warn("WARNING: could not remove symlink $absto: $!\n"); -} - -# -# link_data(datadir, graphdir, create) -# -# If CREATE is non-zero, create symbolic links in GRAPHDIR for data files -# found in DATADIR. Otherwise remove link in GRAPHDIR. -# - -sub link_data($$$) -{ - my ($datadir, $graphdir, $create) = @_; - - $datadir = abs_path($datadir); - $graphdir = abs_path($graphdir); - if ($create) { - lcov_find($datadir, \&link_data_cb, $graphdir, '\.gcda$', - '\.da$'); - } else { - lcov_find($datadir, \&unlink_data_cb, $graphdir, '\.gcda$', - '\.da$'); - } -} - -# -# find_graph_cb(datadir, rel, count_ref) -# -# Count number of files found. -# - -sub find_graph_cb($$$) -{ - my ($dir, $rel, $count_ref) = @_; - - ($$count_ref)++; -} - -# -# find_graph(dir) -# -# Search DIR for a graph file. Return non-zero if one was found, zero otherwise. -# - -sub find_graph($) -{ - my ($dir) = @_; - my $count = 0; - - lcov_find($dir, \&find_graph_cb, \$count, '\.gcno$', '\.bb$', '\.bbg$'); - - return $count > 0 ? 1 : 0; -} - -# -# package_capture() -# -# Capture coverage data from a package of unprocessed coverage data files -# as generated by lcov --to-package. -# - -sub package_capture() -{ - my $dir; - my $build; - my $gkv; - - ($dir, $build, $gkv) = get_package($from_package); - - # Check for build directory - if (defined($base_directory)) { - if (defined($build)) { - info("Using build directory specified by -b.\n"); - } - $build = $base_directory; - } - - # Do the actual capture - if (defined($gkv)) { - if ($gkv == $GKV_SYS) { - $build = adjust_kernel_dir($dir, $build); - } - if (@kernel_directory) { - $dir = copy_gcov_dir($dir, @kernel_directory); - } - kernel_capture_from_dir($dir, $gkv, $build); - } else { - # Build directory needs to be passed to geninfo - $base_directory = $build; - if (find_graph($dir)) { - # Package contains graph files - collect from there - lcov_geninfo($dir); - } else { - # No graph files found, link data files next to - # graph files - link_data($dir, $base_directory, 1); - lcov_geninfo($base_directory); - link_data($dir, $base_directory, 0); - } - } -} - - -# -# info(printf_parameter) -# -# Use printf to write PRINTF_PARAMETER to stdout only when the $quiet flag -# is not set. -# - -sub info(@) -{ - if (!$quiet) - { - # Print info string - if (!$data_stdout) - { - printf(@_) - } - else - { - # Don't interfere with the .info output to STDOUT - printf(STDERR @_); - } - } -} - - -# -# create_temp_dir() -# -# Create a temporary directory and return its path. -# -# Die on error. -# - -sub create_temp_dir() -{ - my $dir; - - if (defined($tmp_dir)) { - $dir = tempdir(DIR => $tmp_dir, CLEANUP => 1); - } else { - $dir = tempdir(CLEANUP => 1); - } - if (!defined($dir)) { - die("ERROR: cannot create temporary directory\n"); - } - push(@temp_dirs, $dir); - - return $dir; -} - -sub compress_brcount($) -{ - my ($brcount) = @_; - my $db; - - $db = brcount_to_db($brcount); - return db_to_brcount($db, $brcount); -} - -sub get_br_found_and_hit($) -{ - my ($brcount) = @_; - my $db; - - $db = brcount_to_db($brcount); - - return brcount_db_get_found_and_hit($db); -} - - -# -# read_info_file(info_filename) -# -# Read in the contents of the .info file specified by INFO_FILENAME. Data will -# be returned as a reference to a hash containing the following mappings: -# -# %result: for each filename found in file -> \%data -# -# %data: "test" -> \%testdata -# "sum" -> \%sumcount -# "func" -> \%funcdata -# "found" -> $lines_found (number of instrumented lines found in file) -# "hit" -> $lines_hit (number of executed lines in file) -# "f_found" -> $fn_found (number of instrumented functions found in file) -# "f_hit" -> $fn_hit (number of executed functions in file) -# "b_found" -> $br_found (number of instrumented branches found in file) -# "b_hit" -> $br_hit (number of executed branches in file) -# "check" -> \%checkdata -# "testfnc" -> \%testfncdata -# "sumfnc" -> \%sumfnccount -# "testbr" -> \%testbrdata -# "sumbr" -> \%sumbrcount -# -# %testdata : name of test affecting this file -> \%testcount -# %testfncdata: name of test affecting this file -> \%testfnccount -# %testbrdata: name of test affecting this file -> \%testbrcount -# -# %testcount : line number -> execution count for a single test -# %testfnccount: function name -> execution count for a single test -# %testbrcount : line number -> branch coverage data for a single test -# %sumcount : line number -> execution count for all tests -# %sumfnccount : function name -> execution count for all tests -# %sumbrcount : line number -> branch coverage data for all tests -# %funcdata : function name -> line number -# %checkdata : line number -> checksum of source code line -# $brdata : text "block,branch,taken:..." -# -# Note that .info file sections referring to the same file and test name -# will automatically be combined by adding all execution counts. -# -# Note that if INFO_FILENAME ends with ".gz", it is assumed that the file -# is compressed using GZIP. If available, GUNZIP will be used to decompress -# this file. -# -# Die on error. -# - -sub read_info_file($) -{ - my $tracefile = $_[0]; # Name of tracefile - my %result; # Resulting hash: file -> data - my $data; # Data handle for current entry - my $testdata; # " " - my $testcount; # " " - my $sumcount; # " " - my $funcdata; # " " - my $checkdata; # " " - my $testfncdata; - my $testfnccount; - my $sumfnccount; - my $testbrdata; - my $testbrcount; - my $sumbrcount; - my $line; # Current line read from .info file - my $testname; # Current test name - my $filename; # Current filename - my $hitcount; # Count for lines hit - my $count; # Execution count of current line - my $negative; # If set, warn about negative counts - my $changed_testname; # If set, warn about changed testname - my $line_checksum; # Checksum of current line - local *INFO_HANDLE; # Filehandle for .info file - - info("Reading tracefile $tracefile\n"); - - # Check if file exists and is readable - stat($_[0]); - if (!(-r _)) - { - die("ERROR: cannot read file $_[0]!\n"); - } - - # Check if this is really a plain file - if (!(-f _)) - { - die("ERROR: not a plain file: $_[0]!\n"); - } - - # Check for .gz extension - if ($_[0] =~ /\.gz$/) - { - # Check for availability of GZIP tool - system_no_output(1, "gunzip" ,"-h") - and die("ERROR: gunzip command not available!\n"); - - # Check integrity of compressed file - system_no_output(1, "gunzip", "-t", $_[0]) - and die("ERROR: integrity check failed for ". - "compressed file $_[0]!\n"); - - # Open compressed file - open(INFO_HANDLE, "-|", "gunzip -c '$_[0]'") - or die("ERROR: cannot start gunzip to decompress ". - "file $_[0]!\n"); - } - else - { - # Open decompressed file - open(INFO_HANDLE, "<", $_[0]) - or die("ERROR: cannot read file $_[0]!\n"); - } - - $testname = ""; - while () - { - chomp($_); - $line = $_; - - # Switch statement - foreach ($line) - { - /^TN:([^,]*)(,diff)?/ && do - { - # Test name information found - $testname = defined($1) ? $1 : ""; - if ($testname =~ s/\W/_/g) - { - $changed_testname = 1; - } - $testname .= $2 if (defined($2)); - last; - }; - - /^[SK]F:(.*)/ && do - { - # Filename information found - # Retrieve data for new entry - $filename = $1; - - $data = $result{$filename}; - ($testdata, $sumcount, $funcdata, $checkdata, - $testfncdata, $sumfnccount, $testbrdata, - $sumbrcount) = - get_info_entry($data); - - if (defined($testname)) - { - $testcount = $testdata->{$testname}; - $testfnccount = $testfncdata->{$testname}; - $testbrcount = $testbrdata->{$testname}; - } - else - { - $testcount = {}; - $testfnccount = {}; - $testbrcount = {}; - } - last; - }; - - /^DA:(\d+),(-?\d+)(,[^,\s]+)?/ && do - { - # Fix negative counts - $count = $2 < 0 ? 0 : $2; - if ($2 < 0) - { - $negative = 1; - } - # Execution count found, add to structure - # Add summary counts - $sumcount->{$1} += $count; - - # Add test-specific counts - if (defined($testname)) - { - $testcount->{$1} += $count; - } - - # Store line checksum if available - if (defined($3)) - { - $line_checksum = substr($3, 1); - - # Does it match a previous definition - if (defined($checkdata->{$1}) && - ($checkdata->{$1} ne - $line_checksum)) - { - die("ERROR: checksum mismatch ". - "at $filename:$1\n"); - } - - $checkdata->{$1} = $line_checksum; - } - last; - }; - - /^FN:(\d+),([^,]+)/ && do - { - last if (!$func_coverage); - - # Function data found, add to structure - $funcdata->{$2} = $1; - - # Also initialize function call data - if (!defined($sumfnccount->{$2})) { - $sumfnccount->{$2} = 0; - } - if (defined($testname)) - { - if (!defined($testfnccount->{$2})) { - $testfnccount->{$2} = 0; - } - } - last; - }; - - /^FNDA:(\d+),([^,]+)/ && do - { - last if (!$func_coverage); - - # Function call count found, add to structure - # Add summary counts - $sumfnccount->{$2} += $1; - - # Add test-specific counts - if (defined($testname)) - { - $testfnccount->{$2} += $1; - } - last; - }; - - /^BRDA:(\d+),(\d+),(\d+),(\d+|-)/ && do { - # Branch coverage data found - my ($line, $block, $branch, $taken) = - ($1, $2, $3, $4); - - last if (!$br_coverage); - $sumbrcount->{$line} .= - "$block,$branch,$taken:"; - - # Add test-specific counts - if (defined($testname)) { - $testbrcount->{$line} .= - "$block,$branch,$taken:"; - } - last; - }; - - /^end_of_record/ && do - { - # Found end of section marker - if ($filename) - { - # Store current section data - if (defined($testname)) - { - $testdata->{$testname} = - $testcount; - $testfncdata->{$testname} = - $testfnccount; - $testbrdata->{$testname} = - $testbrcount; - } - - set_info_entry($data, $testdata, - $sumcount, $funcdata, - $checkdata, $testfncdata, - $sumfnccount, - $testbrdata, - $sumbrcount); - $result{$filename} = $data; - last; - } - }; - - # default - last; - } - } - close(INFO_HANDLE); - - # Calculate hit and found values for lines and functions of each file - foreach $filename (keys(%result)) - { - $data = $result{$filename}; - - ($testdata, $sumcount, undef, undef, $testfncdata, - $sumfnccount, $testbrdata, $sumbrcount) = - get_info_entry($data); - - # Filter out empty files - if (scalar(keys(%{$sumcount})) == 0) - { - delete($result{$filename}); - next; - } - # Filter out empty test cases - foreach $testname (keys(%{$testdata})) - { - if (!defined($testdata->{$testname}) || - scalar(keys(%{$testdata->{$testname}})) == 0) - { - delete($testdata->{$testname}); - delete($testfncdata->{$testname}); - } - } - - $data->{"found"} = scalar(keys(%{$sumcount})); - $hitcount = 0; - - foreach (keys(%{$sumcount})) - { - if ($sumcount->{$_} > 0) { $hitcount++; } - } - - $data->{"hit"} = $hitcount; - - # Get found/hit values for function call data - $data->{"f_found"} = scalar(keys(%{$sumfnccount})); - $hitcount = 0; - - foreach (keys(%{$sumfnccount})) { - if ($sumfnccount->{$_} > 0) { - $hitcount++; - } - } - $data->{"f_hit"} = $hitcount; - - # Combine branch data for the same branches - (undef, $data->{"b_found"}, $data->{"b_hit"}) = - compress_brcount($sumbrcount); - foreach $testname (keys(%{$testbrdata})) { - compress_brcount($testbrdata->{$testname}); - } - } - - if (scalar(keys(%result)) == 0) - { - die("ERROR: no valid records found in tracefile $tracefile\n"); - } - if ($negative) - { - warn("WARNING: negative counts found in tracefile ". - "$tracefile\n"); - } - if ($changed_testname) - { - warn("WARNING: invalid characters removed from testname in ". - "tracefile $tracefile\n"); - } - - return(\%result); -} - - -# -# get_info_entry(hash_ref) -# -# Retrieve data from an entry of the structure generated by read_info_file(). -# Return a list of references to hashes: -# (test data hash ref, sum count hash ref, funcdata hash ref, checkdata hash -# ref, testfncdata hash ref, sumfnccount hash ref, testbrdata hash ref, -# sumbrcount hash ref, lines found, lines hit, functions found, -# functions hit, branches found, branches hit) -# - -sub get_info_entry($) -{ - my $testdata_ref = $_[0]->{"test"}; - my $sumcount_ref = $_[0]->{"sum"}; - my $funcdata_ref = $_[0]->{"func"}; - my $checkdata_ref = $_[0]->{"check"}; - my $testfncdata = $_[0]->{"testfnc"}; - my $sumfnccount = $_[0]->{"sumfnc"}; - my $testbrdata = $_[0]->{"testbr"}; - my $sumbrcount = $_[0]->{"sumbr"}; - my $lines_found = $_[0]->{"found"}; - my $lines_hit = $_[0]->{"hit"}; - my $f_found = $_[0]->{"f_found"}; - my $f_hit = $_[0]->{"f_hit"}; - my $br_found = $_[0]->{"b_found"}; - my $br_hit = $_[0]->{"b_hit"}; - - return ($testdata_ref, $sumcount_ref, $funcdata_ref, $checkdata_ref, - $testfncdata, $sumfnccount, $testbrdata, $sumbrcount, - $lines_found, $lines_hit, $f_found, $f_hit, - $br_found, $br_hit); -} - - -# -# set_info_entry(hash_ref, testdata_ref, sumcount_ref, funcdata_ref, -# checkdata_ref, testfncdata_ref, sumfcncount_ref, -# testbrdata_ref, sumbrcount_ref[,lines_found, -# lines_hit, f_found, f_hit, $b_found, $b_hit]) -# -# Update the hash referenced by HASH_REF with the provided data references. -# - -sub set_info_entry($$$$$$$$$;$$$$$$) -{ - my $data_ref = $_[0]; - - $data_ref->{"test"} = $_[1]; - $data_ref->{"sum"} = $_[2]; - $data_ref->{"func"} = $_[3]; - $data_ref->{"check"} = $_[4]; - $data_ref->{"testfnc"} = $_[5]; - $data_ref->{"sumfnc"} = $_[6]; - $data_ref->{"testbr"} = $_[7]; - $data_ref->{"sumbr"} = $_[8]; - - if (defined($_[9])) { $data_ref->{"found"} = $_[9]; } - if (defined($_[10])) { $data_ref->{"hit"} = $_[10]; } - if (defined($_[11])) { $data_ref->{"f_found"} = $_[11]; } - if (defined($_[12])) { $data_ref->{"f_hit"} = $_[12]; } - if (defined($_[13])) { $data_ref->{"b_found"} = $_[13]; } - if (defined($_[14])) { $data_ref->{"b_hit"} = $_[14]; } -} - - -# -# add_counts(data1_ref, data2_ref) -# -# DATA1_REF and DATA2_REF are references to hashes containing a mapping -# -# line number -> execution count -# -# Return a list (RESULT_REF, LINES_FOUND, LINES_HIT) where RESULT_REF -# is a reference to a hash containing the combined mapping in which -# execution counts are added. -# - -sub add_counts($$) -{ - my $data1_ref = $_[0]; # Hash 1 - my $data2_ref = $_[1]; # Hash 2 - my %result; # Resulting hash - my $line; # Current line iteration scalar - my $data1_count; # Count of line in hash1 - my $data2_count; # Count of line in hash2 - my $found = 0; # Total number of lines found - my $hit = 0; # Number of lines with a count > 0 - - foreach $line (keys(%$data1_ref)) - { - $data1_count = $data1_ref->{$line}; - $data2_count = $data2_ref->{$line}; - - # Add counts if present in both hashes - if (defined($data2_count)) { $data1_count += $data2_count; } - - # Store sum in %result - $result{$line} = $data1_count; - - $found++; - if ($data1_count > 0) { $hit++; } - } - - # Add lines unique to data2_ref - foreach $line (keys(%$data2_ref)) - { - # Skip lines already in data1_ref - if (defined($data1_ref->{$line})) { next; } - - # Copy count from data2_ref - $result{$line} = $data2_ref->{$line}; - - $found++; - if ($result{$line} > 0) { $hit++; } - } - - return (\%result, $found, $hit); -} - - -# -# merge_checksums(ref1, ref2, filename) -# -# REF1 and REF2 are references to hashes containing a mapping -# -# line number -> checksum -# -# Merge checksum lists defined in REF1 and REF2 and return reference to -# resulting hash. Die if a checksum for a line is defined in both hashes -# but does not match. -# - -sub merge_checksums($$$) -{ - my $ref1 = $_[0]; - my $ref2 = $_[1]; - my $filename = $_[2]; - my %result; - my $line; - - foreach $line (keys(%{$ref1})) - { - if (defined($ref2->{$line}) && - ($ref1->{$line} ne $ref2->{$line})) - { - die("ERROR: checksum mismatch at $filename:$line\n"); - } - $result{$line} = $ref1->{$line}; - } - - foreach $line (keys(%{$ref2})) - { - $result{$line} = $ref2->{$line}; - } - - return \%result; -} - - -# -# merge_func_data(funcdata1, funcdata2, filename) -# - -sub merge_func_data($$$) -{ - my ($funcdata1, $funcdata2, $filename) = @_; - my %result; - my $func; - - if (defined($funcdata1)) { - %result = %{$funcdata1}; - } - - foreach $func (keys(%{$funcdata2})) { - my $line1 = $result{$func}; - my $line2 = $funcdata2->{$func}; - - if (defined($line1) && ($line1 != $line2)) { - warn("WARNING: function data mismatch at ". - "$filename:$line2\n"); - next; - } - $result{$func} = $line2; - } - - return \%result; -} - - -# -# add_fnccount(fnccount1, fnccount2) -# -# Add function call count data. Return list (fnccount_added, f_found, f_hit) -# - -sub add_fnccount($$) -{ - my ($fnccount1, $fnccount2) = @_; - my %result; - my $f_found; - my $f_hit; - my $function; - - if (defined($fnccount1)) { - %result = %{$fnccount1}; - } - foreach $function (keys(%{$fnccount2})) { - $result{$function} += $fnccount2->{$function}; - } - $f_found = scalar(keys(%result)); - $f_hit = 0; - foreach $function (keys(%result)) { - if ($result{$function} > 0) { - $f_hit++; - } - } - - return (\%result, $f_found, $f_hit); -} - -# -# add_testfncdata(testfncdata1, testfncdata2) -# -# Add function call count data for several tests. Return reference to -# added_testfncdata. -# - -sub add_testfncdata($$) -{ - my ($testfncdata1, $testfncdata2) = @_; - my %result; - my $testname; - - foreach $testname (keys(%{$testfncdata1})) { - if (defined($testfncdata2->{$testname})) { - my $fnccount; - - # Function call count data for this testname exists - # in both data sets: merge - ($fnccount) = add_fnccount( - $testfncdata1->{$testname}, - $testfncdata2->{$testname}); - $result{$testname} = $fnccount; - next; - } - # Function call count data for this testname is unique to - # data set 1: copy - $result{$testname} = $testfncdata1->{$testname}; - } - - # Add count data for testnames unique to data set 2 - foreach $testname (keys(%{$testfncdata2})) { - if (!defined($result{$testname})) { - $result{$testname} = $testfncdata2->{$testname}; - } - } - return \%result; -} - - -# -# brcount_to_db(brcount) -# -# Convert brcount data to the following format: -# -# db: line number -> block hash -# block hash: block number -> branch hash -# branch hash: branch number -> taken value -# - -sub brcount_to_db($) -{ - my ($brcount) = @_; - my $line; - my $db = {}; - - # Add branches to database - foreach $line (keys(%{$brcount})) { - my $brdata = $brcount->{$line}; - - foreach my $entry (split(/:/, $brdata)) { - my ($block, $branch, $taken) = split(/,/, $entry); - my $old = $db->{$line}->{$block}->{$branch}; - - if (!defined($old) || $old eq "-") { - $old = $taken; - } elsif ($taken ne "-") { - $old += $taken; - } - - $db->{$line}->{$block}->{$branch} = $old; - } - } - - return $db; -} - - -# -# db_to_brcount(db[, brcount]) -# -# Convert branch coverage data back to brcount format. If brcount is specified, -# the converted data is directly inserted in brcount. -# - -sub db_to_brcount($;$) -{ - my ($db, $brcount) = @_; - my $line; - my $br_found = 0; - my $br_hit = 0; - - # Convert database back to brcount format - foreach $line (sort({$a <=> $b} keys(%{$db}))) { - my $ldata = $db->{$line}; - my $brdata; - my $block; - - foreach $block (sort({$a <=> $b} keys(%{$ldata}))) { - my $bdata = $ldata->{$block}; - my $branch; - - foreach $branch (sort({$a <=> $b} keys(%{$bdata}))) { - my $taken = $bdata->{$branch}; - - $br_found++; - $br_hit++ if ($taken ne "-" && $taken > 0); - $brdata .= "$block,$branch,$taken:"; - } - } - $brcount->{$line} = $brdata; - } - - return ($brcount, $br_found, $br_hit); -} - - -# -# brcount_db_combine(db1, db2, op) -# -# db1 := db1 op db2, where -# db1, db2: brcount data as returned by brcount_to_db -# op: one of $BR_ADD and BR_SUB -# -sub brcount_db_combine($$$) -{ - my ($db1, $db2, $op) = @_; - - foreach my $line (keys(%{$db2})) { - my $ldata = $db2->{$line}; - - foreach my $block (keys(%{$ldata})) { - my $bdata = $ldata->{$block}; - - foreach my $branch (keys(%{$bdata})) { - my $taken = $bdata->{$branch}; - my $new = $db1->{$line}->{$block}->{$branch}; - - if (!defined($new) || $new eq "-") { - $new = $taken; - } elsif ($taken ne "-") { - if ($op == $BR_ADD) { - $new += $taken; - } elsif ($op == $BR_SUB) { - $new -= $taken; - $new = 0 if ($new < 0); - } - } - - $db1->{$line}->{$block}->{$branch} = $new; - } - } - } -} - - -# -# brcount_db_get_found_and_hit(db) -# -# Return (br_found, br_hit) for db. -# - -sub brcount_db_get_found_and_hit($) -{ - my ($db) = @_; - my ($br_found , $br_hit) = (0, 0); - - foreach my $line (keys(%{$db})) { - my $ldata = $db->{$line}; - - foreach my $block (keys(%{$ldata})) { - my $bdata = $ldata->{$block}; - - foreach my $branch (keys(%{$bdata})) { - my $taken = $bdata->{$branch}; - - $br_found++; - $br_hit++ if ($taken ne "-" && $taken > 0); - } - } - } - - return ($br_found, $br_hit); -} - - -# combine_brcount(brcount1, brcount2, type, inplace) -# -# If add is BR_ADD, add branch coverage data and return list brcount_added. -# If add is BR_SUB, subtract the taken values of brcount2 from brcount1 and -# return brcount_sub. If inplace is set, the result is inserted into brcount1. -# - -sub combine_brcount($$$;$) -{ - my ($brcount1, $brcount2, $type, $inplace) = @_; - my ($db1, $db2); - - $db1 = brcount_to_db($brcount1); - $db2 = brcount_to_db($brcount2); - brcount_db_combine($db1, $db2, $type); - - return db_to_brcount($db1, $inplace ? $brcount1 : undef); -} - - -# -# add_testbrdata(testbrdata1, testbrdata2) -# -# Add branch coverage data for several tests. Return reference to -# added_testbrdata. -# - -sub add_testbrdata($$) -{ - my ($testbrdata1, $testbrdata2) = @_; - my %result; - my $testname; - - foreach $testname (keys(%{$testbrdata1})) { - if (defined($testbrdata2->{$testname})) { - my $brcount; - - # Branch coverage data for this testname exists - # in both data sets: add - ($brcount) = combine_brcount( - $testbrdata1->{$testname}, - $testbrdata2->{$testname}, $BR_ADD); - $result{$testname} = $brcount; - next; - } - # Branch coverage data for this testname is unique to - # data set 1: copy - $result{$testname} = $testbrdata1->{$testname}; - } - - # Add count data for testnames unique to data set 2 - foreach $testname (keys(%{$testbrdata2})) { - if (!defined($result{$testname})) { - $result{$testname} = $testbrdata2->{$testname}; - } - } - return \%result; -} - - -# -# combine_info_entries(entry_ref1, entry_ref2, filename) -# -# Combine .info data entry hashes referenced by ENTRY_REF1 and ENTRY_REF2. -# Return reference to resulting hash. -# - -sub combine_info_entries($$$) -{ - my $entry1 = $_[0]; # Reference to hash containing first entry - my $testdata1; - my $sumcount1; - my $funcdata1; - my $checkdata1; - my $testfncdata1; - my $sumfnccount1; - my $testbrdata1; - my $sumbrcount1; - - my $entry2 = $_[1]; # Reference to hash containing second entry - my $testdata2; - my $sumcount2; - my $funcdata2; - my $checkdata2; - my $testfncdata2; - my $sumfnccount2; - my $testbrdata2; - my $sumbrcount2; - - my %result; # Hash containing combined entry - my %result_testdata; - my $result_sumcount = {}; - my $result_funcdata; - my $result_testfncdata; - my $result_sumfnccount; - my $result_testbrdata; - my $result_sumbrcount; - my $lines_found; - my $lines_hit; - my $f_found; - my $f_hit; - my $br_found; - my $br_hit; - - my $testname; - my $filename = $_[2]; - - # Retrieve data - ($testdata1, $sumcount1, $funcdata1, $checkdata1, $testfncdata1, - $sumfnccount1, $testbrdata1, $sumbrcount1) = get_info_entry($entry1); - ($testdata2, $sumcount2, $funcdata2, $checkdata2, $testfncdata2, - $sumfnccount2, $testbrdata2, $sumbrcount2) = get_info_entry($entry2); - - # Merge checksums - $checkdata1 = merge_checksums($checkdata1, $checkdata2, $filename); - - # Combine funcdata - $result_funcdata = merge_func_data($funcdata1, $funcdata2, $filename); - - # Combine function call count data - $result_testfncdata = add_testfncdata($testfncdata1, $testfncdata2); - ($result_sumfnccount, $f_found, $f_hit) = - add_fnccount($sumfnccount1, $sumfnccount2); - - # Combine branch coverage data - $result_testbrdata = add_testbrdata($testbrdata1, $testbrdata2); - ($result_sumbrcount, $br_found, $br_hit) = - combine_brcount($sumbrcount1, $sumbrcount2, $BR_ADD); - - # Combine testdata - foreach $testname (keys(%{$testdata1})) - { - if (defined($testdata2->{$testname})) - { - # testname is present in both entries, requires - # combination - ($result_testdata{$testname}) = - add_counts($testdata1->{$testname}, - $testdata2->{$testname}); - } - else - { - # testname only present in entry1, add to result - $result_testdata{$testname} = $testdata1->{$testname}; - } - - # update sum count hash - ($result_sumcount, $lines_found, $lines_hit) = - add_counts($result_sumcount, - $result_testdata{$testname}); - } - - foreach $testname (keys(%{$testdata2})) - { - # Skip testnames already covered by previous iteration - if (defined($testdata1->{$testname})) { next; } - - # testname only present in entry2, add to result hash - $result_testdata{$testname} = $testdata2->{$testname}; - - # update sum count hash - ($result_sumcount, $lines_found, $lines_hit) = - add_counts($result_sumcount, - $result_testdata{$testname}); - } - - # Calculate resulting sumcount - - # Store result - set_info_entry(\%result, \%result_testdata, $result_sumcount, - $result_funcdata, $checkdata1, $result_testfncdata, - $result_sumfnccount, $result_testbrdata, - $result_sumbrcount, $lines_found, $lines_hit, - $f_found, $f_hit, $br_found, $br_hit); - - return(\%result); -} - - -# -# combine_info_files(info_ref1, info_ref2) -# -# Combine .info data in hashes referenced by INFO_REF1 and INFO_REF2. Return -# reference to resulting hash. -# - -sub combine_info_files($$) -{ - my %hash1 = %{$_[0]}; - my %hash2 = %{$_[1]}; - my $filename; - - foreach $filename (keys(%hash2)) - { - if ($hash1{$filename}) - { - # Entry already exists in hash1, combine them - $hash1{$filename} = - combine_info_entries($hash1{$filename}, - $hash2{$filename}, - $filename); - } - else - { - # Entry is unique in both hashes, simply add to - # resulting hash - $hash1{$filename} = $hash2{$filename}; - } - } - - return(\%hash1); -} - - -# -# add_traces() -# - -sub add_traces() -{ - my $total_trace; - my $current_trace; - my $tracefile; - my @result; - local *INFO_HANDLE; - - info("Combining tracefiles.\n"); - - foreach $tracefile (@add_tracefile) - { - $current_trace = read_info_file($tracefile); - if ($total_trace) - { - $total_trace = combine_info_files($total_trace, - $current_trace); - } - else - { - $total_trace = $current_trace; - } - } - - # Write combined data - if (!$data_stdout) - { - info("Writing data to $output_filename\n"); - open(INFO_HANDLE, ">", $output_filename) - or die("ERROR: cannot write to $output_filename!\n"); - @result = write_info_file(*INFO_HANDLE, $total_trace); - close(*INFO_HANDLE); - } - else - { - @result = write_info_file(*STDOUT, $total_trace); - } - - return @result; -} - - -# -# write_info_file(filehandle, data) -# - -sub write_info_file(*$) -{ - local *INFO_HANDLE = $_[0]; - my %data = %{$_[1]}; - my $source_file; - my $entry; - my $testdata; - my $sumcount; - my $funcdata; - my $checkdata; - my $testfncdata; - my $sumfnccount; - my $testbrdata; - my $sumbrcount; - my $testname; - my $line; - my $func; - my $testcount; - my $testfnccount; - my $testbrcount; - my $found; - my $hit; - my $f_found; - my $f_hit; - my $br_found; - my $br_hit; - my $ln_total_found = 0; - my $ln_total_hit = 0; - my $fn_total_found = 0; - my $fn_total_hit = 0; - my $br_total_found = 0; - my $br_total_hit = 0; - - foreach $source_file (sort(keys(%data))) - { - $entry = $data{$source_file}; - ($testdata, $sumcount, $funcdata, $checkdata, $testfncdata, - $sumfnccount, $testbrdata, $sumbrcount, $found, $hit, - $f_found, $f_hit, $br_found, $br_hit) = - get_info_entry($entry); - - # Add to totals - $ln_total_found += $found; - $ln_total_hit += $hit; - $fn_total_found += $f_found; - $fn_total_hit += $f_hit; - $br_total_found += $br_found; - $br_total_hit += $br_hit; - - foreach $testname (sort(keys(%{$testdata}))) - { - $testcount = $testdata->{$testname}; - $testfnccount = $testfncdata->{$testname}; - $testbrcount = $testbrdata->{$testname}; - $found = 0; - $hit = 0; - - print(INFO_HANDLE "TN:$testname\n"); - print(INFO_HANDLE "SF:$source_file\n"); - - # Write function related data - foreach $func ( - sort({$funcdata->{$a} <=> $funcdata->{$b}} - keys(%{$funcdata}))) - { - print(INFO_HANDLE "FN:".$funcdata->{$func}. - ",$func\n"); - } - foreach $func (keys(%{$testfnccount})) { - print(INFO_HANDLE "FNDA:". - $testfnccount->{$func}. - ",$func\n"); - } - ($f_found, $f_hit) = - get_func_found_and_hit($testfnccount); - print(INFO_HANDLE "FNF:$f_found\n"); - print(INFO_HANDLE "FNH:$f_hit\n"); - - # Write branch related data - $br_found = 0; - $br_hit = 0; - foreach $line (sort({$a <=> $b} - keys(%{$testbrcount}))) { - my $brdata = $testbrcount->{$line}; - - foreach my $brentry (split(/:/, $brdata)) { - my ($block, $branch, $taken) = - split(/,/, $brentry); - - print(INFO_HANDLE "BRDA:$line,$block,". - "$branch,$taken\n"); - $br_found++; - $br_hit++ if ($taken ne '-' && - $taken > 0); - } - } - if ($br_found > 0) { - print(INFO_HANDLE "BRF:$br_found\n"); - print(INFO_HANDLE "BRH:$br_hit\n"); - } - - # Write line related data - foreach $line (sort({$a <=> $b} keys(%{$testcount}))) - { - print(INFO_HANDLE "DA:$line,". - $testcount->{$line}. - (defined($checkdata->{$line}) && - $checksum ? - ",".$checkdata->{$line} : "")."\n"); - $found++; - if ($testcount->{$line} > 0) - { - $hit++; - } - - } - print(INFO_HANDLE "LF:$found\n"); - print(INFO_HANDLE "LH:$hit\n"); - print(INFO_HANDLE "end_of_record\n"); - } - } - - return ($ln_total_found, $ln_total_hit, $fn_total_found, $fn_total_hit, - $br_total_found, $br_total_hit); -} - - -# -# transform_pattern(pattern) -# -# Transform shell wildcard expression to equivalent Perl regular expression. -# Return transformed pattern. -# - -sub transform_pattern($) -{ - my $pattern = $_[0]; - - # Escape special chars - - $pattern =~ s/\\/\\\\/g; - $pattern =~ s/\//\\\//g; - $pattern =~ s/\^/\\\^/g; - $pattern =~ s/\$/\\\$/g; - $pattern =~ s/\(/\\\(/g; - $pattern =~ s/\)/\\\)/g; - $pattern =~ s/\[/\\\[/g; - $pattern =~ s/\]/\\\]/g; - $pattern =~ s/\{/\\\{/g; - $pattern =~ s/\}/\\\}/g; - $pattern =~ s/\./\\\./g; - $pattern =~ s/\,/\\\,/g; - $pattern =~ s/\|/\\\|/g; - $pattern =~ s/\+/\\\+/g; - $pattern =~ s/\!/\\\!/g; - - # Transform ? => (.) and * => (.*) - - $pattern =~ s/\*/\(\.\*\)/g; - $pattern =~ s/\?/\(\.\)/g; - - return $pattern; -} - - -# -# extract() -# - -sub extract() -{ - my $data = read_info_file($extract); - my $filename; - my $keep; - my $pattern; - my @pattern_list; - my $extracted = 0; - my @result; - local *INFO_HANDLE; - - # Need perlreg expressions instead of shell pattern - @pattern_list = map({ transform_pattern($_); } @ARGV); - - # Filter out files which do not match any pattern - foreach $filename (sort(keys(%{$data}))) - { - $keep = 0; - - foreach $pattern (@pattern_list) - { - $keep ||= ($filename =~ (/^$pattern$/)); - } - - - if (!$keep) - { - delete($data->{$filename}); - } - else - { - info("Extracting $filename\n"), - $extracted++; - } - } - - # Write extracted data - if (!$data_stdout) - { - info("Extracted $extracted files\n"); - info("Writing data to $output_filename\n"); - open(INFO_HANDLE, ">", $output_filename) - or die("ERROR: cannot write to $output_filename!\n"); - @result = write_info_file(*INFO_HANDLE, $data); - close(*INFO_HANDLE); - } - else - { - @result = write_info_file(*STDOUT, $data); - } - - return @result; -} - - -# -# remove() -# - -sub remove() -{ - my $data = read_info_file($remove); - my $filename; - my $match_found; - my $pattern; - my @pattern_list; - my $removed = 0; - my @result; - local *INFO_HANDLE; - - # Need perlreg expressions instead of shell pattern - @pattern_list = map({ transform_pattern($_); } @ARGV); - - # Filter out files that match the pattern - foreach $filename (sort(keys(%{$data}))) - { - $match_found = 0; - - foreach $pattern (@pattern_list) - { - $match_found ||= ($filename =~ (/^$pattern$/)); - } - - - if ($match_found) - { - delete($data->{$filename}); - info("Removing $filename\n"), - $removed++; - } - } - - # Write data - if (!$data_stdout) - { - info("Deleted $removed files\n"); - info("Writing data to $output_filename\n"); - open(INFO_HANDLE, ">", $output_filename) - or die("ERROR: cannot write to $output_filename!\n"); - @result = write_info_file(*INFO_HANDLE, $data); - close(*INFO_HANDLE); - } - else - { - @result = write_info_file(*STDOUT, $data); - } - - return @result; -} - - -# get_prefix(max_width, max_percentage_too_long, path_list) -# -# Return a path prefix that satisfies the following requirements: -# - is shared by more paths in path_list than any other prefix -# - the percentage of paths which would exceed the given max_width length -# after applying the prefix does not exceed max_percentage_too_long -# -# If multiple prefixes satisfy all requirements, the longest prefix is -# returned. Return an empty string if no prefix could be found. - -sub get_prefix($$@) -{ - my ($max_width, $max_long, @path_list) = @_; - my $path; - my $ENTRY_NUM = 0; - my $ENTRY_LONG = 1; - my %prefix; - - # Build prefix hash - foreach $path (@path_list) { - my ($v, $d, $f) = splitpath($path); - my @dirs = splitdir($d); - my $p_len = length($path); - my $i; - - # Remove trailing '/' - pop(@dirs) if ($dirs[scalar(@dirs) - 1] eq ''); - for ($i = 0; $i < scalar(@dirs); $i++) { - my $subpath = catpath($v, catdir(@dirs[0..$i]), ''); - my $entry = $prefix{$subpath}; - - $entry = [ 0, 0 ] if (!defined($entry)); - $entry->[$ENTRY_NUM]++; - if (($p_len - length($subpath) - 1) > $max_width) { - $entry->[$ENTRY_LONG]++; - } - $prefix{$subpath} = $entry; - } - } - # Find suitable prefix (sort descending by two keys: 1. number of - # entries covered by a prefix, 2. length of prefix) - foreach $path (sort {($prefix{$a}->[$ENTRY_NUM] == - $prefix{$b}->[$ENTRY_NUM]) ? - length($b) <=> length($a) : - $prefix{$b}->[$ENTRY_NUM] <=> - $prefix{$a}->[$ENTRY_NUM]} - keys(%prefix)) { - my ($num, $long) = @{$prefix{$path}}; - - # Check for additional requirement: number of filenames - # that would be too long may not exceed a certain percentage - if ($long <= $num * $max_long / 100) { - return $path; - } - } - - return ""; -} - - -# -# shorten_filename(filename, width) -# -# Truncate filename if it is longer than width characters. -# - -sub shorten_filename($$) -{ - my ($filename, $width) = @_; - my $l = length($filename); - my $s; - my $e; - - return $filename if ($l <= $width); - $e = int(($width - 3) / 2); - $s = $width - 3 - $e; - - return substr($filename, 0, $s).'...'.substr($filename, $l - $e); -} - - -sub shorten_number($$) -{ - my ($number, $width) = @_; - my $result = sprintf("%*d", $width, $number); - - return $result if (length($result) <= $width); - $number = $number / 1000; - return $result if (length($result) <= $width); - $result = sprintf("%*dk", $width - 1, $number); - return $result if (length($result) <= $width); - $number = $number / 1000; - $result = sprintf("%*dM", $width - 1, $number); - return $result if (length($result) <= $width); - return '#'; -} - -sub shorten_rate($$$) -{ - my ($hit, $found, $width) = @_; - my $result = rate($hit, $found, "%", 1, $width); - - return $result if (length($result) <= $width); - $result = rate($hit, $found, "%", 0, $width); - return $result if (length($result) <= $width); - return "#"; -} - -# -# list() -# - -sub list() -{ - my $data = read_info_file($list); - my $filename; - my $found; - my $hit; - my $entry; - my $fn_found; - my $fn_hit; - my $br_found; - my $br_hit; - my $total_found = 0; - my $total_hit = 0; - my $fn_total_found = 0; - my $fn_total_hit = 0; - my $br_total_found = 0; - my $br_total_hit = 0; - my $prefix; - my $strlen = length("Filename"); - my $format; - my $heading1; - my $heading2; - my @footer; - my $barlen; - my $rate; - my $fnrate; - my $brrate; - my $lastpath; - my $F_LN_NUM = 0; - my $F_LN_RATE = 1; - my $F_FN_NUM = 2; - my $F_FN_RATE = 3; - my $F_BR_NUM = 4; - my $F_BR_RATE = 5; - my @fwidth_narrow = (5, 5, 3, 5, 4, 5); - my @fwidth_wide = (6, 5, 5, 5, 6, 5); - my @fwidth = @fwidth_wide; - my $w; - my $max_width = $opt_list_width; - my $max_long = $opt_list_truncate_max; - my $fwidth_narrow_length; - my $fwidth_wide_length; - my $got_prefix = 0; - my $root_prefix = 0; - - # Calculate total width of narrow fields - $fwidth_narrow_length = 0; - foreach $w (@fwidth_narrow) { - $fwidth_narrow_length += $w + 1; - } - # Calculate total width of wide fields - $fwidth_wide_length = 0; - foreach $w (@fwidth_wide) { - $fwidth_wide_length += $w + 1; - } - # Get common file path prefix - $prefix = get_prefix($max_width - $fwidth_narrow_length, $max_long, - keys(%{$data})); - $root_prefix = 1 if ($prefix eq rootdir()); - $got_prefix = 1 if (length($prefix) > 0); - $prefix =~ s/\/$//; - # Get longest filename length - foreach $filename (keys(%{$data})) { - if (!$opt_list_full_path) { - if (!$got_prefix || !$root_prefix && - !($filename =~ s/^\Q$prefix\/\E//)) { - my ($v, $d, $f) = splitpath($filename); - - $filename = $f; - } - } - # Determine maximum length of entries - if (length($filename) > $strlen) { - $strlen = length($filename) - } - } - if (!$opt_list_full_path) { - my $blanks; - - $w = $fwidth_wide_length; - # Check if all columns fit into max_width characters - if ($strlen + $fwidth_wide_length > $max_width) { - # Use narrow fields - @fwidth = @fwidth_narrow; - $w = $fwidth_narrow_length; - if (($strlen + $fwidth_narrow_length) > $max_width) { - # Truncate filenames at max width - $strlen = $max_width - $fwidth_narrow_length; - } - } - # Add some blanks between filename and fields if possible - $blanks = int($strlen * 0.5); - $blanks = 4 if ($blanks < 4); - $blanks = 8 if ($blanks > 8); - if (($strlen + $w + $blanks) < $max_width) { - $strlen += $blanks; - } else { - $strlen = $max_width - $w; - } - } - # Filename - $w = $strlen; - $format = "%-${w}s|"; - $heading1 = sprintf("%*s|", $w, ""); - $heading2 = sprintf("%-*s|", $w, "Filename"); - $barlen = $w + 1; - # Line coverage rate - $w = $fwidth[$F_LN_RATE]; - $format .= "%${w}s "; - $heading1 .= sprintf("%-*s |", $w + $fwidth[$F_LN_NUM], - "Lines"); - $heading2 .= sprintf("%-*s ", $w, "Rate"); - $barlen += $w + 1; - # Number of lines - $w = $fwidth[$F_LN_NUM]; - $format .= "%${w}s|"; - $heading2 .= sprintf("%*s|", $w, "Num"); - $barlen += $w + 1; - # Function coverage rate - $w = $fwidth[$F_FN_RATE]; - $format .= "%${w}s "; - $heading1 .= sprintf("%-*s|", $w + $fwidth[$F_FN_NUM] + 1, - "Functions"); - $heading2 .= sprintf("%-*s ", $w, "Rate"); - $barlen += $w + 1; - # Number of functions - $w = $fwidth[$F_FN_NUM]; - $format .= "%${w}s|"; - $heading2 .= sprintf("%*s|", $w, "Num"); - $barlen += $w + 1; - # Branch coverage rate - $w = $fwidth[$F_BR_RATE]; - $format .= "%${w}s "; - $heading1 .= sprintf("%-*s", $w + $fwidth[$F_BR_NUM] + 1, - "Branches"); - $heading2 .= sprintf("%-*s ", $w, "Rate"); - $barlen += $w + 1; - # Number of branches - $w = $fwidth[$F_BR_NUM]; - $format .= "%${w}s"; - $heading2 .= sprintf("%*s", $w, "Num"); - $barlen += $w; - # Line end - $format .= "\n"; - $heading1 .= "\n"; - $heading2 .= "\n"; - - # Print heading - print($heading1); - print($heading2); - print(("="x$barlen)."\n"); - - # Print per file information - foreach $filename (sort(keys(%{$data}))) - { - my @file_data; - my $print_filename = $filename; - - $entry = $data->{$filename}; - if (!$opt_list_full_path) { - my $p; - - $print_filename = $filename; - if (!$got_prefix || !$root_prefix && - !($print_filename =~ s/^\Q$prefix\/\E//)) { - my ($v, $d, $f) = splitpath($filename); - - $p = catpath($v, $d, ""); - $p =~ s/\/$//; - $print_filename = $f; - } else { - $p = $prefix; - } - - if (!defined($lastpath) || $lastpath ne $p) { - print("\n") if (defined($lastpath)); - $lastpath = $p; - print("[$lastpath/]\n") if (!$root_prefix); - } - $print_filename = shorten_filename($print_filename, - $strlen); - } - - (undef, undef, undef, undef, undef, undef, undef, undef, - $found, $hit, $fn_found, $fn_hit, $br_found, $br_hit) = - get_info_entry($entry); - - # Assume zero count if there is no function data for this file - if (!defined($fn_found) || !defined($fn_hit)) { - $fn_found = 0; - $fn_hit = 0; - } - # Assume zero count if there is no branch data for this file - if (!defined($br_found) || !defined($br_hit)) { - $br_found = 0; - $br_hit = 0; - } - - # Add line coverage totals - $total_found += $found; - $total_hit += $hit; - # Add function coverage totals - $fn_total_found += $fn_found; - $fn_total_hit += $fn_hit; - # Add branch coverage totals - $br_total_found += $br_found; - $br_total_hit += $br_hit; - - # Determine line coverage rate for this file - $rate = shorten_rate($hit, $found, $fwidth[$F_LN_RATE]); - # Determine function coverage rate for this file - $fnrate = shorten_rate($fn_hit, $fn_found, $fwidth[$F_FN_RATE]); - # Determine branch coverage rate for this file - $brrate = shorten_rate($br_hit, $br_found, $fwidth[$F_BR_RATE]); - - # Assemble line parameters - push(@file_data, $print_filename); - push(@file_data, $rate); - push(@file_data, shorten_number($found, $fwidth[$F_LN_NUM])); - push(@file_data, $fnrate); - push(@file_data, shorten_number($fn_found, $fwidth[$F_FN_NUM])); - push(@file_data, $brrate); - push(@file_data, shorten_number($br_found, $fwidth[$F_BR_NUM])); - - # Print assembled line - printf($format, @file_data); - } - - # Determine total line coverage rate - $rate = shorten_rate($total_hit, $total_found, $fwidth[$F_LN_RATE]); - # Determine total function coverage rate - $fnrate = shorten_rate($fn_total_hit, $fn_total_found, - $fwidth[$F_FN_RATE]); - # Determine total branch coverage rate - $brrate = shorten_rate($br_total_hit, $br_total_found, - $fwidth[$F_BR_RATE]); - - # Print separator - print(("="x$barlen)."\n"); - - # Assemble line parameters - push(@footer, sprintf("%*s", $strlen, "Total:")); - push(@footer, $rate); - push(@footer, shorten_number($total_found, $fwidth[$F_LN_NUM])); - push(@footer, $fnrate); - push(@footer, shorten_number($fn_total_found, $fwidth[$F_FN_NUM])); - push(@footer, $brrate); - push(@footer, shorten_number($br_total_found, $fwidth[$F_BR_NUM])); - - # Print assembled line - printf($format, @footer); -} - - -# -# get_common_filename(filename1, filename2) -# -# Check for filename components which are common to FILENAME1 and FILENAME2. -# Upon success, return -# -# (common, path1, path2) -# -# or 'undef' in case there are no such parts. -# - -sub get_common_filename($$) -{ - my @list1 = split("/", $_[0]); - my @list2 = split("/", $_[1]); - my @result; - - # Work in reverse order, i.e. beginning with the filename itself - while (@list1 && @list2 && ($list1[$#list1] eq $list2[$#list2])) - { - unshift(@result, pop(@list1)); - pop(@list2); - } - - # Did we find any similarities? - if (scalar(@result) > 0) - { - return (join("/", @result), join("/", @list1), - join("/", @list2)); - } - else - { - return undef; - } -} - - -# -# strip_directories($path, $depth) -# -# Remove DEPTH leading directory levels from PATH. -# - -sub strip_directories($$) -{ - my $filename = $_[0]; - my $depth = $_[1]; - my $i; - - if (!defined($depth) || ($depth < 1)) - { - return $filename; - } - for ($i = 0; $i < $depth; $i++) - { - $filename =~ s/^[^\/]*\/+(.*)$/$1/; - } - return $filename; -} - - -# -# read_diff(filename) -# -# Read diff output from FILENAME to memory. The diff file has to follow the -# format generated by 'diff -u'. Returns a list of hash references: -# -# (mapping, path mapping) -# -# mapping: filename -> reference to line hash -# line hash: line number in new file -> corresponding line number in old file -# -# path mapping: filename -> old filename -# -# Die in case of error. -# - -sub read_diff($) -{ - my $diff_file = $_[0]; # Name of diff file - my %diff; # Resulting mapping filename -> line hash - my %paths; # Resulting mapping old path -> new path - my $mapping; # Reference to current line hash - my $line; # Contents of current line - my $num_old; # Current line number in old file - my $num_new; # Current line number in new file - my $file_old; # Name of old file in diff section - my $file_new; # Name of new file in diff section - my $filename; # Name of common filename of diff section - my $in_block = 0; # Non-zero while we are inside a diff block - local *HANDLE; # File handle for reading the diff file - - info("Reading diff $diff_file\n"); - - # Check if file exists and is readable - stat($diff_file); - if (!(-r _)) - { - die("ERROR: cannot read file $diff_file!\n"); - } - - # Check if this is really a plain file - if (!(-f _)) - { - die("ERROR: not a plain file: $diff_file!\n"); - } - - # Check for .gz extension - if ($diff_file =~ /\.gz$/) - { - # Check for availability of GZIP tool - system_no_output(1, "gunzip", "-h") - and die("ERROR: gunzip command not available!\n"); - - # Check integrity of compressed file - system_no_output(1, "gunzip", "-t", $diff_file) - and die("ERROR: integrity check failed for ". - "compressed file $diff_file!\n"); - - # Open compressed file - open(HANDLE, "-|", "gunzip -c '$diff_file'") - or die("ERROR: cannot start gunzip to decompress ". - "file $_[0]!\n"); - } - else - { - # Open decompressed file - open(HANDLE, "<", $diff_file) - or die("ERROR: cannot read file $_[0]!\n"); - } - - # Parse diff file line by line - while () - { - chomp($_); - $line = $_; - - foreach ($line) - { - # Filename of old file: - # --- - /^--- (\S+)/ && do - { - $file_old = strip_directories($1, $strip); - last; - }; - # Filename of new file: - # +++ - /^\+\+\+ (\S+)/ && do - { - # Add last file to resulting hash - if ($filename) - { - my %new_hash; - $diff{$filename} = $mapping; - $mapping = \%new_hash; - } - $file_new = strip_directories($1, $strip); - $filename = $file_old; - $paths{$filename} = $file_new; - $num_old = 1; - $num_new = 1; - last; - }; - # Start of diff block: - # @@ -old_start,old_num, +new_start,new_num @@ - /^\@\@\s+-(\d+),(\d+)\s+\+(\d+),(\d+)\s+\@\@$/ && do - { - $in_block = 1; - while ($num_old < $1) - { - $mapping->{$num_new} = $num_old; - $num_old++; - $num_new++; - } - last; - }; - # Unchanged line - # - /^ / && do - { - if ($in_block == 0) - { - last; - } - $mapping->{$num_new} = $num_old; - $num_old++; - $num_new++; - last; - }; - # Line as seen in old file - # - /^-/ && do - { - if ($in_block == 0) - { - last; - } - $num_old++; - last; - }; - # Line as seen in new file - # - /^\+/ && do - { - if ($in_block == 0) - { - last; - } - $num_new++; - last; - }; - # Empty line - /^$/ && do - { - if ($in_block == 0) - { - last; - } - $mapping->{$num_new} = $num_old; - $num_old++; - $num_new++; - last; - }; - } - } - - close(HANDLE); - - # Add final diff file section to resulting hash - if ($filename) - { - $diff{$filename} = $mapping; - } - - if (!%diff) - { - die("ERROR: no valid diff data found in $diff_file!\n". - "Make sure to use 'diff -u' when generating the diff ". - "file.\n"); - } - return (\%diff, \%paths); -} - - -# -# apply_diff($count_data, $line_hash) -# -# Transform count data using a mapping of lines: -# -# $count_data: reference to hash: line number -> data -# $line_hash: reference to hash: line number new -> line number old -# -# Return a reference to transformed count data. -# - -sub apply_diff($$) -{ - my $count_data = $_[0]; # Reference to data hash: line -> hash - my $line_hash = $_[1]; # Reference to line hash: new line -> old line - my %result; # Resulting hash - my $last_new = 0; # Last new line number found in line hash - my $last_old = 0; # Last old line number found in line hash - - # Iterate all new line numbers found in the diff - foreach (sort({$a <=> $b} keys(%{$line_hash}))) - { - $last_new = $_; - $last_old = $line_hash->{$last_new}; - - # Is there data associated with the corresponding old line? - if (defined($count_data->{$line_hash->{$_}})) - { - # Copy data to new hash with a new line number - $result{$_} = $count_data->{$line_hash->{$_}}; - } - } - # Transform all other lines which come after the last diff entry - foreach (sort({$a <=> $b} keys(%{$count_data}))) - { - if ($_ <= $last_old) - { - # Skip lines which were covered by line hash - next; - } - # Copy data to new hash with an offset - $result{$_ + ($last_new - $last_old)} = $count_data->{$_}; - } - - return \%result; -} - - -# -# apply_diff_to_brcount(brcount, linedata) -# -# Adjust line numbers of branch coverage data according to linedata. -# - -sub apply_diff_to_brcount($$) -{ - my ($brcount, $linedata) = @_; - my $db; - - # Convert brcount to db format - $db = brcount_to_db($brcount); - # Apply diff to db format - $db = apply_diff($db, $linedata); - # Convert db format back to brcount format - ($brcount) = db_to_brcount($db); - - return $brcount; -} - - -# -# get_hash_max(hash_ref) -# -# Return the highest integer key from hash. -# - -sub get_hash_max($) -{ - my ($hash) = @_; - my $max; - - foreach (keys(%{$hash})) { - if (!defined($max)) { - $max = $_; - } elsif ($hash->{$_} > $max) { - $max = $_; - } - } - return $max; -} - -sub get_hash_reverse($) -{ - my ($hash) = @_; - my %result; - - foreach (keys(%{$hash})) { - $result{$hash->{$_}} = $_; - } - - return \%result; -} - -# -# apply_diff_to_funcdata(funcdata, line_hash) -# - -sub apply_diff_to_funcdata($$) -{ - my ($funcdata, $linedata) = @_; - my $last_new = get_hash_max($linedata); - my $last_old = $linedata->{$last_new}; - my $func; - my %result; - my $line_diff = get_hash_reverse($linedata); - - foreach $func (keys(%{$funcdata})) { - my $line = $funcdata->{$func}; - - if (defined($line_diff->{$line})) { - $result{$func} = $line_diff->{$line}; - } elsif ($line > $last_old) { - $result{$func} = $line + $last_new - $last_old; - } - } - - return \%result; -} - - -# -# get_line_hash($filename, $diff_data, $path_data) -# -# Find line hash in DIFF_DATA which matches FILENAME. On success, return list -# line hash. or undef in case of no match. Die if more than one line hashes in -# DIFF_DATA match. -# - -sub get_line_hash($$$) -{ - my $filename = $_[0]; - my $diff_data = $_[1]; - my $path_data = $_[2]; - my $conversion; - my $old_path; - my $new_path; - my $diff_name; - my $common; - my $old_depth; - my $new_depth; - - # Remove trailing slash from diff path - $diff_path =~ s/\/$//; - foreach (keys(%{$diff_data})) - { - my $sep = ""; - - $sep = '/' if (!/^\//); - - # Try to match diff filename with filename - if ($filename =~ /^\Q$diff_path$sep$_\E$/) - { - if ($diff_name) - { - # Two files match, choose the more specific one - # (the one with more path components) - $old_depth = ($diff_name =~ tr/\///); - $new_depth = (tr/\///); - if ($old_depth == $new_depth) - { - die("ERROR: diff file contains ". - "ambiguous entries for ". - "$filename\n"); - } - elsif ($new_depth > $old_depth) - { - $diff_name = $_; - } - } - else - { - $diff_name = $_; - } - }; - } - if ($diff_name) - { - # Get converted path - if ($filename =~ /^(.*)$diff_name$/) - { - ($common, $old_path, $new_path) = - get_common_filename($filename, - $1.$path_data->{$diff_name}); - } - return ($diff_data->{$diff_name}, $old_path, $new_path); - } - else - { - return undef; - } -} - - -# -# convert_paths(trace_data, path_conversion_data) -# -# Rename all paths in TRACE_DATA which show up in PATH_CONVERSION_DATA. -# - -sub convert_paths($$) -{ - my $trace_data = $_[0]; - my $path_conversion_data = $_[1]; - my $filename; - my $new_path; - - if (scalar(keys(%{$path_conversion_data})) == 0) - { - info("No path conversion data available.\n"); - return; - } - - # Expand path conversion list - foreach $filename (keys(%{$path_conversion_data})) - { - $new_path = $path_conversion_data->{$filename}; - while (($filename =~ s/^(.*)\/[^\/]+$/$1/) && - ($new_path =~ s/^(.*)\/[^\/]+$/$1/) && - ($filename ne $new_path)) - { - $path_conversion_data->{$filename} = $new_path; - } - } - - # Adjust paths - FILENAME: foreach $filename (keys(%{$trace_data})) - { - # Find a path in our conversion table that matches, starting - # with the longest path - foreach (sort({length($b) <=> length($a)} - keys(%{$path_conversion_data}))) - { - # Is this path a prefix of our filename? - if (!($filename =~ /^$_(.*)$/)) - { - next; - } - $new_path = $path_conversion_data->{$_}.$1; - - # Make sure not to overwrite an existing entry under - # that path name - if ($trace_data->{$new_path}) - { - # Need to combine entries - $trace_data->{$new_path} = - combine_info_entries( - $trace_data->{$filename}, - $trace_data->{$new_path}, - $filename); - } - else - { - # Simply rename entry - $trace_data->{$new_path} = - $trace_data->{$filename}; - } - delete($trace_data->{$filename}); - next FILENAME; - } - info("No conversion available for filename $filename\n"); - } -} - -# -# sub adjust_fncdata(funcdata, testfncdata, sumfnccount) -# -# Remove function call count data from testfncdata and sumfnccount which -# is no longer present in funcdata. -# - -sub adjust_fncdata($$$) -{ - my ($funcdata, $testfncdata, $sumfnccount) = @_; - my $testname; - my $func; - my $f_found; - my $f_hit; - - # Remove count data in testfncdata for functions which are no longer - # in funcdata - foreach $testname (keys(%{$testfncdata})) { - my $fnccount = $testfncdata->{$testname}; - - foreach $func (keys(%{$fnccount})) { - if (!defined($funcdata->{$func})) { - delete($fnccount->{$func}); - } - } - } - # Remove count data in sumfnccount for functions which are no longer - # in funcdata - foreach $func (keys(%{$sumfnccount})) { - if (!defined($funcdata->{$func})) { - delete($sumfnccount->{$func}); - } - } -} - -# -# get_func_found_and_hit(sumfnccount) -# -# Return (f_found, f_hit) for sumfnccount -# - -sub get_func_found_and_hit($) -{ - my ($sumfnccount) = @_; - my $function; - my $f_found; - my $f_hit; - - $f_found = scalar(keys(%{$sumfnccount})); - $f_hit = 0; - foreach $function (keys(%{$sumfnccount})) { - if ($sumfnccount->{$function} > 0) { - $f_hit++; - } - } - return ($f_found, $f_hit); -} - -# -# diff() -# - -sub diff() -{ - my $trace_data = read_info_file($diff); - my $diff_data; - my $path_data; - my $old_path; - my $new_path; - my %path_conversion_data; - my $filename; - my $line_hash; - my $new_name; - my $entry; - my $testdata; - my $testname; - my $sumcount; - my $funcdata; - my $checkdata; - my $testfncdata; - my $sumfnccount; - my $testbrdata; - my $sumbrcount; - my $found; - my $hit; - my $f_found; - my $f_hit; - my $br_found; - my $br_hit; - my $converted = 0; - my $unchanged = 0; - my @result; - local *INFO_HANDLE; - - ($diff_data, $path_data) = read_diff($ARGV[0]); - - foreach $filename (sort(keys(%{$trace_data}))) - { - # Find a diff section corresponding to this file - ($line_hash, $old_path, $new_path) = - get_line_hash($filename, $diff_data, $path_data); - if (!$line_hash) - { - # There's no diff section for this file - $unchanged++; - next; - } - $converted++; - if ($old_path && $new_path && ($old_path ne $new_path)) - { - $path_conversion_data{$old_path} = $new_path; - } - # Check for deleted files - if (scalar(keys(%{$line_hash})) == 0) - { - info("Removing $filename\n"); - delete($trace_data->{$filename}); - next; - } - info("Converting $filename\n"); - $entry = $trace_data->{$filename}; - ($testdata, $sumcount, $funcdata, $checkdata, $testfncdata, - $sumfnccount, $testbrdata, $sumbrcount) = - get_info_entry($entry); - # Convert test data - foreach $testname (keys(%{$testdata})) - { - # Adjust line numbers of line coverage data - $testdata->{$testname} = - apply_diff($testdata->{$testname}, $line_hash); - # Adjust line numbers of branch coverage data - $testbrdata->{$testname} = - apply_diff_to_brcount($testbrdata->{$testname}, - $line_hash); - # Remove empty sets of test data - if (scalar(keys(%{$testdata->{$testname}})) == 0) - { - delete($testdata->{$testname}); - delete($testfncdata->{$testname}); - delete($testbrdata->{$testname}); - } - } - # Rename test data to indicate conversion - foreach $testname (keys(%{$testdata})) - { - # Skip testnames which already contain an extension - if ($testname =~ /,[^,]+$/) - { - next; - } - # Check for name conflict - if (defined($testdata->{$testname.",diff"})) - { - # Add counts - ($testdata->{$testname}) = add_counts( - $testdata->{$testname}, - $testdata->{$testname.",diff"}); - delete($testdata->{$testname.",diff"}); - # Add function call counts - ($testfncdata->{$testname}) = add_fnccount( - $testfncdata->{$testname}, - $testfncdata->{$testname.",diff"}); - delete($testfncdata->{$testname.",diff"}); - # Add branch counts - combine_brcount( - $testbrdata->{$testname}, - $testbrdata->{$testname.",diff"}, - $BR_ADD, 1); - delete($testbrdata->{$testname.",diff"}); - } - # Move test data to new testname - $testdata->{$testname.",diff"} = $testdata->{$testname}; - delete($testdata->{$testname}); - # Move function call count data to new testname - $testfncdata->{$testname.",diff"} = - $testfncdata->{$testname}; - delete($testfncdata->{$testname}); - # Move branch count data to new testname - $testbrdata->{$testname.",diff"} = - $testbrdata->{$testname}; - delete($testbrdata->{$testname}); - } - # Convert summary of test data - $sumcount = apply_diff($sumcount, $line_hash); - # Convert function data - $funcdata = apply_diff_to_funcdata($funcdata, $line_hash); - # Convert branch coverage data - $sumbrcount = apply_diff_to_brcount($sumbrcount, $line_hash); - # Update found/hit numbers - # Convert checksum data - $checkdata = apply_diff($checkdata, $line_hash); - # Convert function call count data - adjust_fncdata($funcdata, $testfncdata, $sumfnccount); - ($f_found, $f_hit) = get_func_found_and_hit($sumfnccount); - ($br_found, $br_hit) = get_br_found_and_hit($sumbrcount); - # Update found/hit numbers - $found = 0; - $hit = 0; - foreach (keys(%{$sumcount})) - { - $found++; - if ($sumcount->{$_} > 0) - { - $hit++; - } - } - if ($found > 0) - { - # Store converted entry - set_info_entry($entry, $testdata, $sumcount, $funcdata, - $checkdata, $testfncdata, $sumfnccount, - $testbrdata, $sumbrcount, $found, $hit, - $f_found, $f_hit, $br_found, $br_hit); - } - else - { - # Remove empty data set - delete($trace_data->{$filename}); - } - } - - # Convert filenames as well if requested - if ($convert_filenames) - { - convert_paths($trace_data, \%path_conversion_data); - } - - info("$converted entr".($converted != 1 ? "ies" : "y")." converted, ". - "$unchanged entr".($unchanged != 1 ? "ies" : "y")." left ". - "unchanged.\n"); - - # Write data - if (!$data_stdout) - { - info("Writing data to $output_filename\n"); - open(INFO_HANDLE, ">", $output_filename) - or die("ERROR: cannot write to $output_filename!\n"); - @result = write_info_file(*INFO_HANDLE, $trace_data); - close(*INFO_HANDLE); - } - else - { - @result = write_info_file(*STDOUT, $trace_data); - } - - return @result; -} - -# -# summary() -# - -sub summary() -{ - my $filename; - my $current; - my $total; - my $ln_total_found; - my $ln_total_hit; - my $fn_total_found; - my $fn_total_hit; - my $br_total_found; - my $br_total_hit; - - # Read and combine trace files - foreach $filename (@opt_summary) { - $current = read_info_file($filename); - if (!defined($total)) { - $total = $current; - } else { - $total = combine_info_files($total, $current); - } - } - # Calculate coverage data - foreach $filename (keys(%{$total})) - { - my $entry = $total->{$filename}; - my $ln_found; - my $ln_hit; - my $fn_found; - my $fn_hit; - my $br_found; - my $br_hit; - - (undef, undef, undef, undef, undef, undef, undef, undef, - $ln_found, $ln_hit, $fn_found, $fn_hit, $br_found, - $br_hit) = get_info_entry($entry); - - # Add to totals - $ln_total_found += $ln_found; - $ln_total_hit += $ln_hit; - $fn_total_found += $fn_found; - $fn_total_hit += $fn_hit; - $br_total_found += $br_found; - $br_total_hit += $br_hit; - } - - - return ($ln_total_found, $ln_total_hit, $fn_total_found, $fn_total_hit, - $br_total_found, $br_total_hit); -} - -# -# system_no_output(mode, parameters) -# -# Call an external program using PARAMETERS while suppressing depending on -# the value of MODE: -# -# MODE & 1: suppress STDOUT -# MODE & 2: suppress STDERR -# -# Return 0 on success, non-zero otherwise. -# - -sub system_no_output($@) -{ - my $mode = shift; - my $result; - local *OLD_STDERR; - local *OLD_STDOUT; - - # Save old stdout and stderr handles - ($mode & 1) && open(OLD_STDOUT, ">>&", "STDOUT"); - ($mode & 2) && open(OLD_STDERR, ">>&", "STDERR"); - - # Redirect to /dev/null - ($mode & 1) && open(STDOUT, ">", "/dev/null"); - ($mode & 2) && open(STDERR, ">", "/dev/null"); - - system(@_); - $result = $?; - - # Close redirected handles - ($mode & 1) && close(STDOUT); - ($mode & 2) && close(STDERR); - - # Restore old handles - ($mode & 1) && open(STDOUT, ">>&", "OLD_STDOUT"); - ($mode & 2) && open(STDERR, ">>&", "OLD_STDERR"); - - return $result; -} - - -# -# read_config(filename) -# -# Read configuration file FILENAME and return a reference to a hash containing -# all valid key=value pairs found. -# - -sub read_config($) -{ - my $filename = $_[0]; - my %result; - my $key; - my $value; - local *HANDLE; - - if (!open(HANDLE, "<", $filename)) - { - warn("WARNING: cannot read configuration file $filename\n"); - return undef; - } - while () - { - chomp; - # Skip comments - s/#.*//; - # Remove leading blanks - s/^\s+//; - # Remove trailing blanks - s/\s+$//; - next unless length; - ($key, $value) = split(/\s*=\s*/, $_, 2); - if (defined($key) && defined($value)) - { - $result{$key} = $value; - } - else - { - warn("WARNING: malformed statement in line $. ". - "of configuration file $filename\n"); - } - } - close(HANDLE); - return \%result; -} - - -# -# apply_config(REF) -# -# REF is a reference to a hash containing the following mapping: -# -# key_string => var_ref -# -# where KEY_STRING is a keyword and VAR_REF is a reference to an associated -# variable. If the global configuration hashes CONFIG or OPT_RC contain a value -# for keyword KEY_STRING, VAR_REF will be assigned the value for that keyword. -# - -sub apply_config($) -{ - my $ref = $_[0]; - - foreach (keys(%{$ref})) - { - if (defined($opt_rc{$_})) { - ${$ref->{$_}} = $opt_rc{$_}; - } elsif (defined($config->{$_})) { - ${$ref->{$_}} = $config->{$_}; - } - } -} - -sub warn_handler($) -{ - my ($msg) = @_; - - warn("$tool_name: $msg"); -} - -sub die_handler($) -{ - my ($msg) = @_; - - temp_cleanup(); - die("$tool_name: $msg"); -} - -sub abort_handler($) -{ - temp_cleanup(); - exit(1); -} - -sub temp_cleanup() -{ - # Ensure temp directory is not in use by current process - chdir("/"); - - if (@temp_dirs) { - info("Removing temporary directories.\n"); - foreach (@temp_dirs) { - rmtree($_); - } - @temp_dirs = (); - } -} - -sub setup_gkv_sys() -{ - system_no_output(3, "mount", "-t", "debugfs", "nodev", - "/sys/kernel/debug"); -} - -sub setup_gkv_proc() -{ - if (system_no_output(3, "modprobe", "gcov_proc")) { - system_no_output(3, "modprobe", "gcov_prof"); - } -} - -sub check_gkv_sys($) -{ - my ($dir) = @_; - - if (-e "$dir/reset") { - return 1; - } - return 0; -} - -sub check_gkv_proc($) -{ - my ($dir) = @_; - - if (-e "$dir/vmlinux") { - return 1; - } - return 0; -} - -sub setup_gkv() -{ - my $dir; - my $sys_dir = "/sys/kernel/debug/gcov"; - my $proc_dir = "/proc/gcov"; - my @todo; - - if (!defined($gcov_dir)) { - info("Auto-detecting gcov kernel support.\n"); - @todo = ( "cs", "cp", "ss", "cs", "sp", "cp" ); - } elsif ($gcov_dir =~ /proc/) { - info("Checking gcov kernel support at $gcov_dir ". - "(user-specified).\n"); - @todo = ( "cp", "sp", "cp", "cs", "ss", "cs"); - } else { - info("Checking gcov kernel support at $gcov_dir ". - "(user-specified).\n"); - @todo = ( "cs", "ss", "cs", "cp", "sp", "cp", ); - } - foreach (@todo) { - if ($_ eq "cs") { - # Check /sys - $dir = defined($gcov_dir) ? $gcov_dir : $sys_dir; - if (check_gkv_sys($dir)) { - info("Found ".$GKV_NAME[$GKV_SYS]." gcov ". - "kernel support at $dir\n"); - return ($GKV_SYS, $dir); - } - } elsif ($_ eq "cp") { - # Check /proc - $dir = defined($gcov_dir) ? $gcov_dir : $proc_dir; - if (check_gkv_proc($dir)) { - info("Found ".$GKV_NAME[$GKV_PROC]." gcov ". - "kernel support at $dir\n"); - return ($GKV_PROC, $dir); - } - } elsif ($_ eq "ss") { - # Setup /sys - setup_gkv_sys(); - } elsif ($_ eq "sp") { - # Setup /proc - setup_gkv_proc(); - } - } - if (defined($gcov_dir)) { - die("ERROR: could not find gcov kernel data at $gcov_dir\n"); - } else { - die("ERROR: no gcov kernel data found\n"); - } -} - - -# -# get_overall_line(found, hit, name_singular, name_plural) -# -# Return a string containing overall information for the specified -# found/hit data. -# - -sub get_overall_line($$$$) -{ - my ($found, $hit, $name_sn, $name_pl) = @_; - my $name; - - return "no data found" if (!defined($found) || $found == 0); - $name = ($found == 1) ? $name_sn : $name_pl; - - return rate($hit, $found, "% ($hit of $found $name)"); -} - - -# -# print_overall_rate(ln_do, ln_found, ln_hit, fn_do, fn_found, fn_hit, br_do -# br_found, br_hit) -# -# Print overall coverage rates for the specified coverage types. -# - -sub print_overall_rate($$$$$$$$$) -{ - my ($ln_do, $ln_found, $ln_hit, $fn_do, $fn_found, $fn_hit, - $br_do, $br_found, $br_hit) = @_; - - info("Summary coverage rate:\n"); - info(" lines......: %s\n", - get_overall_line($ln_found, $ln_hit, "line", "lines")) - if ($ln_do); - info(" functions..: %s\n", - get_overall_line($fn_found, $fn_hit, "function", "functions")) - if ($fn_do); - info(" branches...: %s\n", - get_overall_line($br_found, $br_hit, "branch", "branches")) - if ($br_do); -} - - -# -# rate(hit, found[, suffix, precision, width]) -# -# Return the coverage rate [0..100] for HIT and FOUND values. 0 is only -# returned when HIT is 0. 100 is only returned when HIT equals FOUND. -# PRECISION specifies the precision of the result. SUFFIX defines a -# string that is appended to the result if FOUND is non-zero. Spaces -# are added to the start of the resulting string until it is at least WIDTH -# characters wide. -# - -sub rate($$;$$$) -{ - my ($hit, $found, $suffix, $precision, $width) = @_; - my $rate; - - # Assign defaults if necessary - $precision = 1 if (!defined($precision)); - $suffix = "" if (!defined($suffix)); - $width = 0 if (!defined($width)); - - return sprintf("%*s", $width, "-") if (!defined($found) || $found == 0); - $rate = sprintf("%.*f", $precision, $hit * 100 / $found); - - # Adjust rates if necessary - if ($rate == 0 && $hit > 0) { - $rate = sprintf("%.*f", $precision, 1 / 10 ** $precision); - } elsif ($rate == 100 && $hit != $found) { - $rate = sprintf("%.*f", $precision, 100 - 1 / 10 ** $precision); - } - - return sprintf("%*s", $width, $rate.$suffix); -} diff --git a/worker/deps/lcov/bin/updateversion.pl b/worker/deps/lcov/bin/updateversion.pl deleted file mode 100755 index 19db81ecd3..0000000000 --- a/worker/deps/lcov/bin/updateversion.pl +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env perl - -use strict; -use warnings; - -use File::Basename; - -sub update_man_page($); -sub update_bin_tool($); -sub update_txt_file($); -sub update_spec_file($); -sub write_version_file($); -sub get_file_info($); - -our $directory = $ARGV[0]; -our $version = $ARGV[1]; -our $release = $ARGV[2]; -our $full = $ARGV[3]; - -our @man_pages = ("man/gendesc.1", "man/genhtml.1", "man/geninfo.1", - "man/genpng.1", "man/lcov.1", "man/lcovrc.5"); -our @bin_tools = ("bin/gendesc", "bin/genhtml", "bin/geninfo", - "bin/genpng", "bin/lcov"); -our @txt_files = ("README"); -our @spec_files = ("rpm/lcov.spec"); - -if (!defined($directory) || !defined($version) || !defined($release)) { - die("Usage: $0 DIRECTORY|FILE VERSION RELEASE FULL_VERSION\n"); -} - -# Determine mode of operation -if (-f $directory) { - my $file = $directory; - my $base = basename($file); - - if (grep(/^$base$/, map({ basename($_) } @man_pages))) { - print("Updating man page $file\n"); - update_man_page($file); - } elsif (grep(/^$base$/, map({ basename($_) } @bin_tools))) { - print("Updating bin tool $file\n"); - update_bin_tool($file); - } elsif (grep(/^$base$/, map({ basename($_) } @txt_files))) { - print("Updating text file $file\n"); - update_txt_file($file); - } elsif (grep(/^$base$/, map({ basename($_) } @spec_files))) { - print("Updating spec file $file\n"); - update_spec_file($file); - } elsif ($base eq ".version") { - print("Updating version file $file\n"); - write_version_file($file); - } else { - print("WARNING: Skipping unknown file $file\n"); - } - print("Done.\n"); - exit(0); -} - -foreach (@man_pages) { - print("Updating man page $_\n"); - update_man_page($directory."/".$_); -} -foreach (@bin_tools) { - print("Updating bin tool $_\n"); - update_bin_tool($directory."/".$_); -} -foreach (@txt_files) { - print("Updating text file $_\n"); - update_txt_file($directory."/".$_); -} -foreach (@spec_files) { - print("Updating spec file $_\n"); - update_spec_file($directory."/".$_); -} -print("Updating version file $directory/.version\n"); -write_version_file("$directory/.version"); -print("Done.\n"); - -sub get_file_info($) -{ - my ($filename) = @_; - my ($sec, $min, $hour, $year, $month, $day); - my @stat; - my $gittime; - - return (0, 0, 0) if (!-e $filename); - @stat = stat($filename); - ($sec, $min, $hour, $day, $month, $year) = gmtime($stat[9]); - $year += 1900; - $month += 1; - - return (sprintf("%04d-%02d-%02d", $year, $month, $day), - sprintf("%04d%02d%02d%02d%02d.%02d", $year, $month, $day, - $hour, $min, $sec), - sprintf("%o", $stat[2] & 07777)); -} - -sub update_man_page($) -{ - my ($filename) = @_; - my @date = get_file_info($filename); - my $date_string = $date[0]; - local *IN; - local *OUT; - - $date_string =~ s/-/\\-/g; - open(IN, "<$filename") || die ("Error: cannot open $filename\n"); - open(OUT, ">$filename.new") || - die("Error: cannot create $filename.new\n"); - while () { - s/\"LCOV\s+\d+\.\d+\"/\"LCOV $version\"/g; - s/\d\d\d\d\\\-\d\d\\\-\d\d/$date_string/g; - print(OUT $_); - } - close(OUT); - close(IN); - chmod(oct($date[2]), "$filename.new"); - system("mv", "-f", "$filename.new", "$filename"); - system("touch", "$filename", "-t", $date[1]); -} - -sub update_bin_tool($) -{ - my ($filename) = @_; - my @date = get_file_info($filename); - local *IN; - local *OUT; - - open(IN, "<$filename") || die ("Error: cannot open $filename\n"); - open(OUT, ">$filename.new") || - die("Error: cannot create $filename.new\n"); - while () { - s/^(our\s+\$lcov_version\s*=).*$/$1 "LCOV version $full";/g; - print(OUT $_); - } - close(OUT); - close(IN); - chmod(oct($date[2]), "$filename.new"); - system("mv", "-f", "$filename.new", "$filename"); - system("touch", "$filename", "-t", $date[1]); -} - -sub update_txt_file($) -{ - my ($filename) = @_; - my @date = get_file_info($filename); - local *IN; - local *OUT; - - open(IN, "<$filename") || die ("Error: cannot open $filename\n"); - open(OUT, ">$filename.new") || - die("Error: cannot create $filename.new\n"); - while () { - s/(Last\s+changes:\s+)\d\d\d\d-\d\d-\d\d/$1$date[0]/g; - print(OUT $_); - } - close(OUT); - close(IN); - chmod(oct($date[2]), "$filename.new"); - system("mv", "-f", "$filename.new", "$filename"); - system("touch", "$filename", "-t", $date[1]); -} - -sub update_spec_file($) -{ - my ($filename) = @_; - my @date = get_file_info($filename); - local *IN; - local *OUT; - - open(IN, "<$filename") || die ("Error: cannot open $filename\n"); - open(OUT, ">$filename.new") || - die("Error: cannot create $filename.new\n"); - while () { - s/^(Version:\s*)\d+\.\d+.*$/$1$version/; - s/^(Release:\s*).*$/$1$release/; - print(OUT $_); - } - close(OUT); - close(IN); - system("mv", "-f", "$filename.new", "$filename"); - system("touch", "$filename", "-t", $date[1]); -} - -sub write_version_file($) -{ - my ($filename) = @_; - my $fd; - - open($fd, ">", $filename) or die("Error: cannot write $filename: $!\n"); - print($fd "VERSION=$version\n"); - print($fd "RELEASE=$release\n"); - print($fd "FULL=$full\n"); - close($fd); -} diff --git a/worker/deps/lcov/example/Makefile b/worker/deps/lcov/example/Makefile deleted file mode 100644 index 2f698a1b32..0000000000 --- a/worker/deps/lcov/example/Makefile +++ /dev/null @@ -1,98 +0,0 @@ -# -# Makefile for the LCOV example program. -# -# Make targets: -# - example: compile the example program -# - output: run test cases on example program and create HTML output -# - clean: clean up directory -# - -CC := gcc -CFLAGS := -Wall -I. -fprofile-arcs -ftest-coverage - -LCOV := ../bin/lcov -GENHTML := ../bin/genhtml -GENDESC := ../bin/gendesc -GENPNG := ../bin/genpng - -# Depending on the presence of the GD.pm perl module, we can use the -# special option '--frames' for genhtml -USE_GENPNG := $(shell $(GENPNG) --help >/dev/null 2>/dev/null; echo $$?) - -ifeq ($(USE_GENPNG),0) - FRAMES := --frames -else - FRAMES := -endif - -.PHONY: clean output test_noargs test_2_to_2000 test_overflow - -all: output - -example: example.o iterate.o gauss.o - $(CC) example.o iterate.o gauss.o -o example -lgcov - -example.o: example.c iterate.h gauss.h - $(CC) $(CFLAGS) -c example.c -o example.o - -iterate.o: methods/iterate.c iterate.h - $(CC) $(CFLAGS) -c methods/iterate.c -o iterate.o - -gauss.o: methods/gauss.c gauss.h - $(CC) $(CFLAGS) -c methods/gauss.c -o gauss.o - -output: example descriptions test_noargs test_2_to_2000 test_overflow - @echo - @echo '*' - @echo '* Generating HTML output' - @echo '*' - @echo - $(GENHTML) trace_noargs.info trace_args.info trace_overflow.info \ - --output-directory output --title "Basic example" \ - --show-details --description-file descriptions $(FRAMES) \ - --legend - @echo - @echo '*' - @echo '* See '`pwd`/output/index.html - @echo '*' - @echo - -descriptions: descriptions.txt - $(GENDESC) descriptions.txt -o descriptions - -all_tests: example test_noargs test_2_to_2000 test_overflow - -test_noargs: - @echo - @echo '*' - @echo '* Test case 1: running ./example without parameters' - @echo '*' - @echo - $(LCOV) --zerocounters --directory . - ./example - $(LCOV) --capture --directory . --output-file trace_noargs.info --test-name test_noargs --no-external - -test_2_to_2000: - @echo - @echo '*' - @echo '* Test case 2: running ./example 2 2000' - @echo '*' - @echo - $(LCOV) --zerocounters --directory . - ./example 2 2000 - $(LCOV) --capture --directory . --output-file trace_args.info --test-name test_2_to_2000 --no-external - -test_overflow: - @echo - @echo '*' - @echo '* Test case 3: running ./example 0 100000 (causes an overflow)' - @echo '*' - @echo - $(LCOV) --zerocounters --directory . - ./example 0 100000 || true - $(LCOV) --capture --directory . --output-file trace_overflow.info --test-name "test_overflow" --no-external - -clean: - rm -rf *.o *.bb *.bbg *.da *.gcno *.gcda *.info output example \ - descriptions - diff --git a/worker/deps/lcov/example/README b/worker/deps/lcov/example/README deleted file mode 100644 index cf6cf2e4c6..0000000000 --- a/worker/deps/lcov/example/README +++ /dev/null @@ -1,6 +0,0 @@ - -To get an example of how the LCOV generated HTML output looks like, -type 'make output' and point a web browser to the resulting file - - output/index.html - diff --git a/worker/deps/lcov/example/descriptions.txt b/worker/deps/lcov/example/descriptions.txt deleted file mode 100644 index 47e6021310..0000000000 --- a/worker/deps/lcov/example/descriptions.txt +++ /dev/null @@ -1,10 +0,0 @@ -test_noargs - Example program is called without arguments so that default range - [0..9] is used. - -test_2_to_2000 - Example program is called with "2" and "2000" as arguments. - -test_overflow - Example program is called with "0" and "100000" as arguments. The - resulting sum is too large to be stored as an int variable. diff --git a/worker/deps/lcov/example/example.c b/worker/deps/lcov/example/example.c deleted file mode 100644 index f9049aa64b..0000000000 --- a/worker/deps/lcov/example/example.c +++ /dev/null @@ -1,60 +0,0 @@ -/* - * example.c - * - * Calculate the sum of a given range of integer numbers. The range is - * specified by providing two integer numbers as command line argument. - * If no arguments are specified, assume the predefined range [0..9]. - * Abort with an error message if the resulting number is too big to be - * stored as int variable. - * - * This program example is similar to the one found in the GCOV documentation. - * It is used to demonstrate the HTML output generated by LCOV. - * - * The program is split into 3 modules to better demonstrate the 'directory - * overview' function. There are also a lot of bloated comments inserted to - * artificially increase the source code size so that the 'source code - * overview' function makes at least a minimum of sense. - * - */ - -#include -#include -#include "iterate.h" -#include "gauss.h" - -static int start = 0; -static int end = 9; - - -int main (int argc, char* argv[]) -{ - int total1, total2; - - /* Accept a pair of numbers as command line arguments. */ - - if (argc == 3) - { - start = atoi(argv[1]); - end = atoi(argv[2]); - } - - - /* Use both methods to calculate the result. */ - - total1 = iterate_get_sum (start, end); - total2 = gauss_get_sum (start, end); - - - /* Make sure both results are the same. */ - - if (total1 != total2) - { - printf ("Failure (%d != %d)!\n", total1, total2); - } - else - { - printf ("Success, sum[%d..%d] = %d\n", start, end, total1); - } - - return 0; -} diff --git a/worker/deps/lcov/example/gauss.h b/worker/deps/lcov/example/gauss.h deleted file mode 100644 index 302a4a9803..0000000000 --- a/worker/deps/lcov/example/gauss.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef GAUSS_H -#define GAUSS_H GAUSS_h - -extern int gauss_get_sum (int min, int max); - -#endif /* GAUSS_H */ diff --git a/worker/deps/lcov/example/iterate.h b/worker/deps/lcov/example/iterate.h deleted file mode 100644 index 471327951c..0000000000 --- a/worker/deps/lcov/example/iterate.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef ITERATE_H -#define ITERATE_H ITERATE_H - -extern int iterate_get_sum (int min, int max); - -#endif /* ITERATE_H */ diff --git a/worker/deps/lcov/example/methods/gauss.c b/worker/deps/lcov/example/methods/gauss.c deleted file mode 100644 index 9da3ce5083..0000000000 --- a/worker/deps/lcov/example/methods/gauss.c +++ /dev/null @@ -1,48 +0,0 @@ -/* - * methods/gauss.c - * - * Calculate the sum of a given range of integer numbers. - * - * Somewhat of a more subtle way of calculation - and it even has a story - * behind it: - * - * Supposedly during math classes in elementary school, the teacher of - * young mathematician Gauss gave the class an assignment to calculate the - * sum of all natural numbers between 1 and 100, hoping that this task would - * keep the kids occupied for some time. The story goes that Gauss had the - * result ready after only a few minutes. What he had written on his black - * board was something like this: - * - * 1 + 100 = 101 - * 2 + 99 = 101 - * 3 + 98 = 101 - * . - * . - * 100 + 1 = 101 - * - * s = (1/2) * 100 * 101 = 5050 - * - * A more general form of this formula would be - * - * s = (1/2) * (max + min) * (max - min + 1) - * - * which is used in the piece of code below to implement the requested - * function in constant time, i.e. without dependencies on the size of the - * input parameters. - * - */ - -#include "gauss.h" - - -int gauss_get_sum (int min, int max) -{ - /* This algorithm doesn't work well with invalid range specifications - so we're intercepting them here. */ - if (max < min) - { - return 0; - } - - return (int) ((max + min) * (double) (max - min + 1) / 2); -} diff --git a/worker/deps/lcov/example/methods/iterate.c b/worker/deps/lcov/example/methods/iterate.c deleted file mode 100644 index 023d1801c9..0000000000 --- a/worker/deps/lcov/example/methods/iterate.c +++ /dev/null @@ -1,45 +0,0 @@ -/* - * methods/iterate.c - * - * Calculate the sum of a given range of integer numbers. - * - * This particular method of implementation works by way of brute force, - * i.e. it iterates over the entire range while adding the numbers to finally - * get the total sum. As a positive side effect, we're able to easily detect - * overflows, i.e. situations in which the sum would exceed the capacity - * of an integer variable. - * - */ - -#include -#include -#include "iterate.h" - - -int iterate_get_sum (int min, int max) -{ - int i, total; - - total = 0; - - /* This is where we loop over each number in the range, including - both the minimum and the maximum number. */ - - for (i = min; i <= max; i++) - { - /* We can detect an overflow by checking whether the new - sum would become negative. */ - - if (total + i < total) - { - printf ("Error: sum too large!\n"); - exit (1); - } - - /* Everything seems to fit into an int, so continue adding. */ - - total += i; - } - - return total; -} diff --git a/worker/deps/lcov/lcovrc b/worker/deps/lcov/lcovrc deleted file mode 100644 index bd4bc3b73a..0000000000 --- a/worker/deps/lcov/lcovrc +++ /dev/null @@ -1,172 +0,0 @@ -# -# /etc/lcovrc - system-wide defaults for LCOV -# -# To change settings for a single user, place a customized copy of this file -# at location ~/.lcovrc -# - -# Specify an external style sheet file (same as --css-file option of genhtml) -#genhtml_css_file = gcov.css - -# Specify coverage rate limits (in %) for classifying file entries -# HI: hi_limit <= rate <= 100 graph color: green -# MED: med_limit <= rate < hi_limit graph color: orange -# LO: 0 <= rate < med_limit graph color: red -genhtml_hi_limit = 90 -genhtml_med_limit = 75 - -# Width of line coverage field in source code view -genhtml_line_field_width = 12 - -# Width of branch coverage field in source code view -genhtml_branch_field_width = 16 - -# Width of overview image (used by --frames option of genhtml) -genhtml_overview_width = 80 - -# Resolution of overview navigation: this number specifies the maximum -# difference in lines between the position a user selected from the overview -# and the position the source code window is scrolled to (used by --frames -# option of genhtml) -genhtml_nav_resolution = 4 - -# Clicking a line in the overview image should show the source code view at -# a position a bit further up so that the requested line is not the first -# line in the window. This number specifies that offset in lines (used by -# --frames option of genhtml) -genhtml_nav_offset = 10 - -# Do not remove unused test descriptions if non-zero (same as -# --keep-descriptions option of genhtml) -genhtml_keep_descriptions = 0 - -# Do not remove prefix from directory names if non-zero (same as --no-prefix -# option of genhtml) -genhtml_no_prefix = 0 - -# Do not create source code view if non-zero (same as --no-source option of -# genhtml) -genhtml_no_source = 0 - -# Replace tabs with number of spaces in source view (same as --num-spaces -# option of genhtml) -genhtml_num_spaces = 8 - -# Highlight lines with converted-only data if non-zero (same as --highlight -# option of genhtml) -genhtml_highlight = 0 - -# Include color legend in HTML output if non-zero (same as --legend option of -# genhtml) -genhtml_legend = 0 - -# Use FILE as HTML prolog for generated pages (same as --html-prolog option of -# genhtml) -#genhtml_html_prolog = FILE - -# Use FILE as HTML epilog for generated pages (same as --html-epilog option of -# genhtml) -#genhtml_html_epilog = FILE - -# Use custom filename extension for pages (same as --html-extension option of -# genhtml) -#genhtml_html_extension = html - -# Compress all generated html files with gzip. -#genhtml_html_gzip = 1 - -# Include sorted overview pages (can be disabled by the --no-sort option of -# genhtml) -genhtml_sort = 1 - -# Include function coverage data display (can be disabled by the -# --no-func-coverage option of genhtml) -#genhtml_function_coverage = 1 - -# Include branch coverage data display (can be disabled by the -# --no-branch-coverage option of genhtml) -#genhtml_branch_coverage = 1 - -# Specify the character set of all generated HTML pages -genhtml_charset=UTF-8 - -# Allow HTML markup in test case description text if non-zero -genhtml_desc_html=0 - -# Specify the precision for coverage rates -#genhtml_precision=1 - -# Show missed counts instead of hit counts -#genhtml_missed=1 - -# Demangle C++ symbols -#genhtml_demangle_cpp=1 - -# Location of the gcov tool (same as --gcov-info option of geninfo) -#geninfo_gcov_tool = gcov - -# Adjust test names to include operating system information if non-zero -#geninfo_adjust_testname = 0 - -# Calculate checksum for each source code line if non-zero (same as --checksum -# option of geninfo if non-zero, same as --no-checksum if zero) -#geninfo_checksum = 1 - -# Specify whether to capture coverage data for external source files (can -# be overridden by the --external and --no-external options of geninfo/lcov) -#geninfo_external = 1 - -# Enable libtool compatibility mode if non-zero (same as --compat-libtool option -# of geninfo if non-zero, same as --no-compat-libtool if zero) -#geninfo_compat_libtool = 0 - -# Use gcov's --all-blocks option if non-zero -#geninfo_gcov_all_blocks = 1 - -# Specify compatiblity modes (same as --compat option of geninfo). -#geninfo_compat = libtool=on, hammer=auto, split_crc=auto - -# Adjust path to source files by removing or changing path components that -# match the specified pattern (Perl regular expression format) -#geninfo_adjust_src_path = /tmp/build => /usr/src - -# Specify if geninfo should try to automatically determine the base-directory -# when collecting coverage data. -geninfo_auto_base = 1 - -# Use gcov intermediate format? Valid values are 0, 1, auto -geninfo_intermediate = auto - -# Directory containing gcov kernel files -# lcov_gcov_dir = /proc/gcov - -# Location of the insmod tool -lcov_insmod_tool = /sbin/insmod - -# Location of the modprobe tool -lcov_modprobe_tool = /sbin/modprobe - -# Location of the rmmod tool -lcov_rmmod_tool = /sbin/rmmod - -# Location for temporary directories -lcov_tmp_dir = /tmp - -# Show full paths during list operation if non-zero (same as --list-full-path -# option of lcov) -lcov_list_full_path = 0 - -# Specify the maximum width for list output. This value is ignored when -# lcov_list_full_path is non-zero. -lcov_list_width = 80 - -# Specify the maximum percentage of file names which may be truncated when -# choosing a directory prefix in list output. This value is ignored when -# lcov_list_full_path is non-zero. -lcov_list_truncate_max = 20 - -# Specify if function coverage data should be collected and processed. -lcov_function_coverage = 1 - -# Specify if branch coverage data should be collected and processed. -lcov_branch_coverage = 0 diff --git a/worker/deps/lcov/man/gendesc.1 b/worker/deps/lcov/man/gendesc.1 deleted file mode 100644 index 806d1f5e45..0000000000 --- a/worker/deps/lcov/man/gendesc.1 +++ /dev/null @@ -1,78 +0,0 @@ -.TH gendesc 1 "LCOV 1.14" 2016\-12\-19 "User Manuals" -.SH NAME -gendesc \- Generate a test case description file -.SH SYNOPSIS -.B gendesc -.RB [ \-h | \-\-help ] -.RB [ \-v | \-\-version ] -.RS 8 -.br -.RB [ \-o | \-\-output\-filename -.IR filename ] -.br -.I inputfile -.SH DESCRIPTION -Convert plain text test case descriptions into a format as understood by -.BR genhtml . -.I inputfile -needs to observe the following format: - -For each test case: -.IP " \-" -one line containing the test case name beginning at the start of the line -.RE -.IP " \-" -one or more lines containing the test case description indented with at -least one whitespace character (tab or space) -.RE - -.B Example input file: - -test01 -.RS -An example test case description. -.br -Description continued -.RE - -test42 -.RS -Supposedly the answer to most of your questions -.RE - -Note: valid test names can consist of letters, decimal digits and the -underscore character ('_'). -.SH OPTIONS -.B \-h -.br -.B \-\-help -.RS -Print a short help text, then exit. -.RE - -.B \-v -.br -.B \-\-version -.RS -Print version number, then exit. -.RE - - -.BI "\-o " filename -.br -.BI "\-\-output\-filename " filename -.RS -Write description data to -.IR filename . - -By default, output is written to STDOUT. -.RE -.SH AUTHOR -Peter Oberparleiter - -.SH SEE ALSO -.BR lcov (1), -.BR genhtml (1), -.BR geninfo (1), -.BR genpng (1), -.BR gcov (1) diff --git a/worker/deps/lcov/man/genhtml.1 b/worker/deps/lcov/man/genhtml.1 deleted file mode 100644 index 6f13be302e..0000000000 --- a/worker/deps/lcov/man/genhtml.1 +++ /dev/null @@ -1,601 +0,0 @@ -.TH genhtml 1 "LCOV 1.14" 2018\-01\-30 "User Manuals" -.SH NAME -genhtml \- Generate HTML view from LCOV coverage data files -.SH SYNOPSIS -.B genhtml -.RB [ \-h | \-\-help ] -.RB [ \-v | \-\-version ] -.RS 8 -.br -.RB [ \-q | \-\-quiet ] -.RB [ \-s | \-\-show\-details ] -.RB [ \-f | \-\-frames ] -.br -.RB [ \-b | \-\-baseline\-file ] -.IR baseline\-file -.br -.RB [ \-o | \-\-output\-directory -.IR output\-directory ] -.br -.RB [ \-t | \-\-title -.IR title ] -.br -.RB [ \-d | \-\-description\-file -.IR description\-file ] -.br -.RB [ \-k | \-\-keep\-descriptions ] -.RB [ \-c | \-\-css\-file -.IR css\-file ] -.br -.RB [ \-p | \-\-prefix -.IR prefix ] -.RB [ \-\-no\-prefix ] -.br -.RB [ \-\-no\-source ] -.RB [ \-\-num\-spaces -.IR num ] -.RB [ \-\-highlight ] -.br -.RB [ \-\-legend ] -.RB [ \-\-html\-prolog -.IR prolog\-file ] -.br -.RB [ \-\-html\-epilog -.IR epilog\-file ] -.RB [ \-\-html\-extension -.IR extension ] -.br -.RB [ \-\-html\-gzip ] -.RB [ \-\-sort ] -.RB [ \-\-no\-sort ] -.br -.RB [ \-\-function\-coverage ] -.RB [ \-\-no\-function\-coverage ] -.br -.RB [ \-\-branch\-coverage ] -.RB [ \-\-no\-branch\-coverage ] -.br -.RB [ \-\-demangle\-cpp ] -.RB [ \-\-ignore\-errors -.IR errors ] -.br -.RB [ \-\-config\-file -.IR config\-file ] -.RB [ \-\-rc -.IR keyword = value ] -.br -.RB [ \-\-precision -.IR num ] -.RB [ \-\-missed ] -.br -.IR tracefile(s) -.RE -.SH DESCRIPTION -Create an HTML view of coverage data found in -.IR tracefile . -Note that -.I tracefile -may also be a list of filenames. - -HTML output files are created in the current working directory unless the -\-\-output\-directory option is used. If -.I tracefile -ends with ".gz", it is assumed to be GZIP\-compressed and the gunzip tool -will be used to decompress it transparently. - -Note that all source code files have to be present and readable at the -exact file system location they were compiled. - -Use option -.I \--css\-file -to modify layout and colors of the generated HTML output. Files are -marked in different colors depending on the associated coverage rate. By -default, the coverage limits for low, medium and high coverage are set to -0\-75%, 75\-90% and 90\-100% percent respectively. To change these -values, use configuration file options -.IR genhtml_hi_limit " and " genhtml_med_limit . - -Also note that when displaying percentages, 0% and 100% are only printed when -the values are exactly 0% and 100% respectively. Other values which would -conventionally be rounded to 0% or 100% are instead printed as nearest -non-boundary value. This behavior is in accordance with that of the -.BR gcov (1) -tool. - -.SH OPTIONS -.B \-h -.br -.B \-\-help -.RS -Print a short help text, then exit. - -.RE -.B \-v -.br -.B \-\-version -.RS -Print version number, then exit. - -.RE -.B \-q -.br -.B \-\-quiet -.RS -Do not print progress messages. - -Suppresses all informational progress output. When this switch is enabled, -only error or warning messages are printed. - -.RE -.B \-f -.br -.B \-\-frames -.RS -Use HTML frames for source code view. - -If enabled, a frameset is created for each source code file, providing -an overview of the source code as a "clickable" image. Note that this -option will slow down output creation noticeably because each source -code character has to be inspected once. Note also that the GD.pm Perl -module has to be installed for this option to work (it may be obtained -from http://www.cpan.org). - -.RE -.B \-s -.br -.B \-\-show\-details -.RS -Generate detailed directory view. - -When this option is enabled, -.B genhtml -generates two versions of each -file view. One containing the standard information plus a link to a -"detailed" version. The latter additionally contains information about -which test case covered how many lines of each source file. - -.RE -.BI "\-b " baseline\-file -.br -.BI "\-\-baseline\-file " baseline\-file -.RS -Use data in -.I baseline\-file -as coverage baseline. - -The tracefile specified by -.I baseline\-file -is read and all counts found in the original -.I tracefile -are decremented by the corresponding counts in -.I baseline\-file -before creating any output. - -Note that when a count for a particular line in -.I baseline\-file -is greater than the count in the -.IR tracefile , -the result is zero. - -.RE -.BI "\-o " output\-directory -.br -.BI "\-\-output\-directory " output\-directory -.RS -Create files in -.I output\-directory. - -Use this option to tell -.B genhtml -to write the resulting files to a directory other than -the current one. If -.I output\-directory -does not exist, it will be created. - -It is advisable to use this option since depending on the -project size, a lot of files and subdirectories may be created. - -.RE -.BI "\-t " title -.br -.BI "\-\-title " title -.RS -Display -.I title -in header of all pages. - -.I title -is written to the header portion of each generated HTML page to -identify the context in which a particular output -was created. By default this is the name of the tracefile. - -.RE -.BI "\-d " description\-file -.br -.BI "\-\-description\-file " description\-file -.RS -Read test case descriptions from -.IR description\-file . - -All test case descriptions found in -.I description\-file -and referenced in the input data file are read and written to an extra page -which is then incorporated into the HTML output. - -The file format of -.IR "description\-file " is: - -for each test case: -.RS -TN: -.br -TD: - -.RE - -Valid test case names can consist of letters, numbers and the underscore -character ('_'). -.RE -.B \-k -.br -.B \-\-keep\-descriptions -.RS -Do not remove unused test descriptions. - -Keep descriptions found in the description file even if the coverage data -indicates that the associated test case did not cover any lines of code. - -This option can also be configured permanently using the configuration file -option -.IR genhtml_keep_descriptions . - -.RE -.BI "\-c " css\-file -.br -.BI "\-\-css\-file " css\-file -.RS -Use external style sheet file -.IR css\-file . - -Using this option, an extra .css file may be specified which will replace -the default one. This may be helpful if the default colors make your eyes want -to jump out of their sockets :) - -This option can also be configured permanently using the configuration file -option -.IR genhtml_css_file . - -.RE -.BI "\-p " prefix -.br -.BI "\-\-prefix " prefix -.RS -Remove -.I prefix -from all directory names. - -Because lists containing long filenames are difficult to read, there is a -mechanism implemented that will automatically try to shorten all directory -names on the overview page beginning with a common prefix. By default, -this is done using an algorithm that tries to find the prefix which, when -applied, will minimize the resulting sum of characters of all directory -names. - -Use this option to specify the prefix to be removed by yourself. - -.RE -.B \-\-no\-prefix -.RS -Do not remove prefix from directory names. - -This switch will completely disable the prefix mechanism described in the -previous section. - -This option can also be configured permanently using the configuration file -option -.IR genhtml_no_prefix . - -.RE -.B \-\-no\-source -.RS -Do not create source code view. - -Use this switch if you don't want to get a source code view for each file. - -This option can also be configured permanently using the configuration file -option -.IR genhtml_no_source . - -.RE -.BI "\-\-num\-spaces " spaces -.RS -Replace tabs in source view with -.I num -spaces. - -Default value is 8. - -This option can also be configured permanently using the configuration file -option -.IR genhtml_num_spaces . - -.RE -.B \-\-highlight -.RS -Highlight lines with converted\-only coverage data. - -Use this option in conjunction with the \-\-diff option of -.B lcov -to highlight those lines which were only covered in data sets which were -converted from previous source code versions. - -This option can also be configured permanently using the configuration file -option -.IR genhtml_highlight . - -.RE -.B \-\-legend -.RS -Include color legend in HTML output. - -Use this option to include a legend explaining the meaning of color coding -in the resulting HTML output. - -This option can also be configured permanently using the configuration file -option -.IR genhtml_legend . - -.RE -.BI "\-\-html\-prolog " prolog\-file -.RS -Read customized HTML prolog from -.IR prolog\-file . - -Use this option to replace the default HTML prolog (the initial part of the -HTML source code leading up to and including the tag) with the contents -of -.IR prolog\-file . -Within the prolog text, the following words will be replaced when a page is generated: - -.B "@pagetitle@" -.br -The title of the page. - -.B "@basedir@" -.br -A relative path leading to the base directory (e.g. for locating css\-files). - -This option can also be configured permanently using the configuration file -option -.IR genhtml_html_prolog . - -.RE -.BI "\-\-html\-epilog " epilog\-file -.RS -Read customized HTML epilog from -.IR epilog\-file . - -Use this option to replace the default HTML epilog (the final part of the HTML -source including ) with the contents of -.IR epilog\-file . - -Within the epilog text, the following words will be replaced when a page is generated: - -.B "@basedir@" -.br -A relative path leading to the base directory (e.g. for locating css\-files). - -This option can also be configured permanently using the configuration file -option -.IR genhtml_html_epilog . - -.RE -.BI "\-\-html\-extension " extension -.RS -Use customized filename extension for generated HTML pages. - -This option is useful in situations where different filename extensions -are required to render the resulting pages correctly (e.g. php). Note that -a '.' will be inserted between the filename and the extension specified by -this option. - -This option can also be configured permanently using the configuration file -option -.IR genhtml_html_extension . -.RE - -.B \-\-html\-gzip -.RS -Compress all generated html files with gzip and add a .htaccess file specifying -gzip\-encoding in the root output directory. - -Use this option if you want to save space on your webserver. Requires a -webserver with .htaccess support and a browser with support for gzip -compressed html. - -This option can also be configured permanently using the configuration file -option -.IR genhtml_html_gzip . - -.RE -.B \-\-sort -.br -.B \-\-no\-sort -.RS -Specify whether to include sorted views of file and directory overviews. - -Use \-\-sort to include sorted views or \-\-no\-sort to not include them. -Sorted views are -.B enabled -by default. - -When sorted views are enabled, each overview page will contain links to -views of that page sorted by coverage rate. - -This option can also be configured permanently using the configuration file -option -.IR genhtml_sort . - -.RE -.B \-\-function\-coverage -.br -.B \-\-no\-function\-coverage -.RS -Specify whether to display function coverage summaries in HTML output. - -Use \-\-function\-coverage to enable function coverage summaries or -\-\-no\-function\-coverage to disable it. Function coverage summaries are -.B enabled -by default - -When function coverage summaries are enabled, each overview page will contain -the number of functions found and hit per file or directory, together with -the resulting coverage rate. In addition, each source code view will contain -a link to a page which lists all functions found in that file plus the -respective call count for those functions. - -This option can also be configured permanently using the configuration file -option -.IR genhtml_function_coverage . - -.RE -.B \-\-branch\-coverage -.br -.B \-\-no\-branch\-coverage -.RS -Specify whether to display branch coverage data in HTML output. - -Use \-\-branch\-coverage to enable branch coverage display or -\-\-no\-branch\-coverage to disable it. Branch coverage data display is -.B enabled -by default - -When branch coverage display is enabled, each overview page will contain -the number of branches found and hit per file or directory, together with -the resulting coverage rate. In addition, each source code view will contain -an extra column which lists all branches of a line with indications of -whether the branch was taken or not. Branches are shown in the following format: - - ' + ': Branch was taken at least once -.br - ' - ': Branch was not taken -.br - ' # ': The basic block containing the branch was never executed -.br - -Note that it might not always be possible to relate branches to the -corresponding source code statements: during compilation, GCC might shuffle -branches around or eliminate some of them to generate better code. - -This option can also be configured permanently using the configuration file -option -.IR genhtml_branch_coverage . - -.RE -.B \-\-demangle\-cpp -.RS -Specify whether to demangle C++ function names. - -Use this option if you want to convert C++ internal function names to -human readable format for display on the HTML function overview page. -This option requires that the c++filt tool is installed (see -.BR c++filt (1)). - -.RE -.B \-\-ignore\-errors -.I errors -.br -.RS -Specify a list of errors after which to continue processing. - -Use this option to specify a list of one or more classes of errors after which -geninfo should continue processing instead of aborting. - -.I errors -can be a comma\-separated list of the following keywords: - -.B source: -the source code file for a data set could not be found. -.RE - -.B \-\-config\-file -.I config\-file -.br -.RS -Specify a configuration file to use. - -When this option is specified, neither the system\-wide configuration file -/etc/lcovrc, nor the per\-user configuration file ~/.lcovrc is read. - -This option may be useful when there is a need to run several -instances of -.B genhtml -with different configuration file options in parallel. -.RE - -.B \-\-rc -.IR keyword = value -.br -.RS -Override a configuration directive. - -Use this option to specify a -.IR keyword = value -statement which overrides the corresponding configuration statement in -the lcovrc configuration file. You can specify this option more than once -to override multiple configuration statements. -See -.BR lcovrc (5) -for a list of available keywords and their meaning. -.RE - -.BI "\-\-precision " num -.RS -Show coverage rates with -.I num -number of digits after the decimal-point. - -Default value is 1. - -This option can also be configured permanently using the configuration file -option -.IR genhtml_precision . -.RE - -.B \-\-missed -.RS -Show counts of missed lines, functions, or branches - -Use this option to change overview pages to show the count of lines, functions, -or branches that were not hit. These counts are represented by negative numbers. - -When specified together with \-\-sort, file and directory views will be sorted -by missed counts. - -This option can also be configured permanently using the configuration file -option -.IR genhtml_missed . -.RE - -.SH FILES - -.I /etc/lcovrc -.RS -The system\-wide configuration file. -.RE - -.I ~/.lcovrc -.RS -The per\-user configuration file. -.RE - -.SH AUTHOR -Peter Oberparleiter - -.SH SEE ALSO -.BR lcov (1), -.BR lcovrc (5), -.BR geninfo (1), -.BR genpng (1), -.BR gendesc (1), -.BR gcov (1) diff --git a/worker/deps/lcov/man/geninfo.1 b/worker/deps/lcov/man/geninfo.1 deleted file mode 100644 index 1878575b3b..0000000000 --- a/worker/deps/lcov/man/geninfo.1 +++ /dev/null @@ -1,578 +0,0 @@ -.TH geninfo 1 "LCOV 1.14" 2017\-10\-10 "User Manuals" -.SH NAME -geninfo \- Generate tracefiles from .da files -.SH SYNOPSIS -.B geninfo -.RB [ \-h | \-\-help ] -.RB [ \-v | \-\-version ] -.RB [ \-q | \-\-quiet ] -.br -.RS 8 -.RB [ \-i | \-\-initial ] -.RB [ \-t | \-\-test\-name -.IR test\-name ] -.br -.RB [ \-o | \-\-output\-filename -.IR filename ] -.RB [ \-f | \-\-follow ] -.br -.RB [ \-b | \-\-base\-directory -.IR directory ] -.br -.RB [ \-\-checksum ] -.RB [ \-\-no\-checksum ] -.br -.RB [ \-\-compat\-libtool ] -.RB [ \-\-no\-compat\-libtool ] -.br -.RB [ \-\-gcov\-tool -.IR tool ] -.RB [ \-\-ignore\-errors -.IR errors ] -.br -.RB [ \-\-no\-recursion ] -.I directory -.RB [ \-\-external ] -.RB [ \-\-no\-external ] -.br -.RB [ \-\-config\-file -.IR config\-file ] -.RB [ \-\-no\-markers ] -.br -.RB [ \-\-derive\-func\-data ] -.RB [ \-\-compat -.IR mode =on|off|auto] -.br -.RB [ \-\-rc -.IR keyword = value ] -.br -.RB [ \-\-include -.IR pattern ] -.RB [ \-\-exclude -.IR pattern ] -.RE -.SH DESCRIPTION -.B geninfo -converts all GCOV coverage data files found in -.I directory -into tracefiles, which the -.B genhtml -tool can convert to HTML output. - -Unless the \-\-output\-filename option is specified, -.B geninfo -writes its -output to one file per .da file, the name of which is generated by simply -appending ".info" to the respective .da file name. - -Note that the current user needs write access to both -.I directory -as well as to the original source code location. This is necessary because -some temporary files have to be created there during the conversion process. - -Note also that -.B geninfo -is called from within -.BR lcov , -so that there is usually no need to call it directly. - -.B Exclusion markers - -To exclude specific lines of code from a tracefile, you can add exclusion -markers to the source code. Additionally you can exclude specific branches from -branch coverage without excluding the involved lines from line and function -coverage. Exclusion markers are keywords which can for example be added in the -form of a comment. -See -.BR lcovrc (5) -how to override some of them. - -The following markers are recognized by geninfo: - -LCOV_EXCL_LINE -.RS -Lines containing this marker will be excluded. -.br -.RE -LCOV_EXCL_START -.RS -Marks the beginning of an excluded section. The current line is part of this -section. -.br -.RE -LCOV_EXCL_STOP -.RS -Marks the end of an excluded section. The current line not part of this -section. -.RE -.br -LCOV_EXCL_BR_LINE -.RS -Lines containing this marker will be excluded from branch coverage. -.br -.RE -LCOV_EXCL_BR_START -.RS -Marks the beginning of a section which is excluded from branch coverage. The -current line is part of this section. -.br -.RE -LCOV_EXCL_BR_STOP -.RS -Marks the end of a section which is excluded from branch coverage. The current -line not part of this section. -.RE -.br - -.SH OPTIONS - -.B \-b -.I directory -.br -.B \-\-base\-directory -.I directory -.br -.RS -.RI "Use " directory -as base directory for relative paths. - -Use this option to specify the base directory of a build\-environment -when geninfo produces error messages like: - -.RS -ERROR: could not read source file /home/user/project/subdir1/subdir2/subdir1/subdir2/file.c -.RE - -In this example, use /home/user/project as base directory. - -This option is required when using geninfo on projects built with libtool or -similar build environments that work with a base directory, i.e. environments, -where the current working directory when invoking the compiler is not the same -directory in which the source code file is located. - -Note that this option will not work in environments where multiple base -directories are used. In that case use configuration file setting -.B geninfo_auto_base=1 -(see -.BR lcovrc (5)). -.RE - -.B \-\-checksum -.br -.B \-\-no\-checksum -.br -.RS -Specify whether to generate checksum data when writing tracefiles. - -Use \-\-checksum to enable checksum generation or \-\-no\-checksum to -disable it. Checksum generation is -.B disabled -by default. - -When checksum generation is enabled, a checksum will be generated for each -source code line and stored along with the coverage data. This checksum will -be used to prevent attempts to combine coverage data from different source -code versions. - -If you don't work with different source code versions, disable this option -to speed up coverage data processing and to reduce the size of tracefiles. -.RE - -.B \-\-compat -.IR mode = value [, mode = value ,...] -.br -.RS -Set compatibility mode. - -Use \-\-compat to specify that geninfo should enable one or more compatibility -modes when capturing coverage data. You can provide a comma-separated list -of mode=value pairs to specify the values for multiple modes. - -Valid -.I values -are: - -.B on -.RS -Enable compatibility mode. -.RE -.B off -.RS -Disable compatibility mode. -.RE -.B auto -.RS -Apply auto-detection to determine if compatibility mode is required. Note that -auto-detection is not available for all compatibility modes. -.RE - -If no value is specified, 'on' is assumed as default value. - -Valid -.I modes -are: - -.B libtool -.RS -Enable this mode if you are capturing coverage data for a project that -was built using the libtool mechanism. See also -\-\-compat\-libtool. - -The default value for this setting is 'on'. - -.RE -.B hammer -.RS -Enable this mode if you are capturing coverage data for a project that -was built using a version of GCC 3.3 that contains a modification -(hammer patch) of later GCC versions. You can identify a modified GCC 3.3 -by checking the build directory of your project for files ending in the -extension '.bbg'. Unmodified versions of GCC 3.3 name these files '.bb'. - -The default value for this setting is 'auto'. - -.RE -.B split_crc -.RS -Enable this mode if you are capturing coverage data for a project that -was built using a version of GCC 4.6 that contains a modification -(split function checksums) of later GCC versions. Typical error messages -when running geninfo on coverage data produced by such GCC versions are -\'out of memory' and 'reached unexpected end of file'. - -The default value for this setting is 'auto' -.RE - -.RE - -.B \-\-compat\-libtool -.br -.B \-\-no\-compat\-libtool -.br -.RS -Specify whether to enable libtool compatibility mode. - -Use \-\-compat\-libtool to enable libtool compatibility mode or \-\-no\-compat\-libtool -to disable it. The libtool compatibility mode is -.B enabled -by default. - -When libtool compatibility mode is enabled, geninfo will assume that the source -code relating to a .da file located in a directory named ".libs" can be -found in its parent directory. - -If you have directories named ".libs" in your build environment but don't use -libtool, disable this option to prevent problems when capturing coverage data. -.RE - -.B \-\-config\-file -.I config\-file -.br -.RS -Specify a configuration file to use. - -When this option is specified, neither the system\-wide configuration file -/etc/lcovrc, nor the per\-user configuration file ~/.lcovrc is read. - -This option may be useful when there is a need to run several -instances of -.B geninfo -with different configuration file options in parallel. -.RE - -.B \-\-derive\-func\-data -.br -.RS -Calculate function coverage data from line coverage data. - -Use this option to collect function coverage data, even if the version of the -gcov tool installed on the test system does not provide this data. lcov will -instead derive function coverage data from line coverage data and -information about which lines belong to a function. -.RE - -.B \-\-exclude -.I pattern -.br -.RS -Exclude source files matching -.IR pattern . - -Use this switch if you want to exclude coverage data for a particular set -of source files matching any of the given patterns. Multiple patterns can be -specified by using multiple -.B --exclude -command line switches. The -.I patterns -will be interpreted as shell wildcard patterns (note that they may need to be -escaped accordingly to prevent the shell from expanding them first). - -Can be combined with the -.B --include -command line switch. If a given file matches both the include pattern and the -exclude pattern, the exclude pattern will take precedence. -.RE - -.B \-\-external -.br -.B \-\-no\-external -.br -.RS -Specify whether to capture coverage data for external source files. - -External source files are files which are not located in one of the directories -specified by \-\-directory or \-\-base\-directory. Use \-\-external to include -external source files while capturing coverage data or \-\-no\-external to -ignore this data. - -Data for external source files is -.B included -by default. -.RE - -.B \-f -.br -.B \-\-follow -.RS -Follow links when searching .da files. -.RE - -.B \-\-gcov\-tool -.I tool -.br -.RS -Specify the location of the gcov tool. -.RE - -.B \-h -.br -.B \-\-help -.RS -Print a short help text, then exit. -.RE - -.B \-\-include -.I pattern -.br -.RS -Include source files matching -.IR pattern . - -Use this switch if you want to include coverage data for only a particular set -of source files matching any of the given patterns. Multiple patterns can be -specified by using multiple -.B --include -command line switches. The -.I patterns -will be interpreted as shell wildcard patterns (note that they may need to be -escaped accordingly to prevent the shell from expanding them first). -.RE - -.B \-\-ignore\-errors -.I errors -.br -.RS -Specify a list of errors after which to continue processing. - -Use this option to specify a list of one or more classes of errors after which -geninfo should continue processing instead of aborting. - -.I errors -can be a comma\-separated list of the following keywords: - -.B gcov: -the gcov tool returned with a non\-zero return code. - -.B source: -the source code file for a data set could not be found. -.RE - -.B \-i -.br -.B \-\-initial -.RS -Capture initial zero coverage data. - -Run geninfo with this option on the directories containing .bb, .bbg or .gcno -files before running any test case. The result is a "baseline" coverage data -file that contains zero coverage for every instrumented line and function. -Combine this data file (using lcov \-a) with coverage data files captured -after a test run to ensure that the percentage of total lines covered is -correct even when not all object code files were loaded during the test. - -Note: currently, the \-\-initial option does not generate branch coverage -information. -.RE - -.B \-\-no\-markers -.br -.RS -Use this option if you want to get coverage data without regard to exclusion -markers in the source code file. -.RE - -.B \-\-no\-recursion -.br -.RS -Use this option if you want to get coverage data for the specified directory -only without processing subdirectories. -.RE - -.BI "\-o " output\-filename -.br -.BI "\-\-output\-filename " output\-filename -.RS -Write all data to -.IR output\-filename . - -If you want to have all data written to a single file (for easier -handling), use this option to specify the respective filename. By default, -one tracefile will be created for each processed .da file. -.RE - -.B \-q -.br -.B \-\-quiet -.RS -Do not print progress messages. - -Suppresses all informational progress output. When this switch is enabled, -only error or warning messages are printed. -.RE - -.B \-\-rc -.IR keyword = value -.br -.RS -Override a configuration directive. - -Use this option to specify a -.IR keyword = value -statement which overrides the corresponding configuration statement in -the lcovrc configuration file. You can specify this option more than once -to override multiple configuration statements. -See -.BR lcovrc (5) -for a list of available keywords and their meaning. -.RE - -.BI "\-t " testname -.br -.BI "\-\-test\-name " testname -.RS -Use test case name -.I testname -for resulting data. Valid test case names can consist of letters, decimal -digits and the underscore character ('_'). - -This proves useful when data from several test cases is merged (i.e. by -simply concatenating the respective tracefiles) in which case a test -name can be used to differentiate between data from each test case. -.RE - -.B \-v -.br -.B \-\-version -.RS -Print version number, then exit. -.RE - - -.SH FILES - -.I /etc/lcovrc -.RS -The system\-wide configuration file. -.RE - -.I ~/.lcovrc -.RS -The per\-user configuration file. -.RE - -Following is a quick description of the tracefile format as used by -.BR genhtml ", " geninfo " and " lcov . - -A tracefile is made up of several human\-readable lines of text, -divided into sections. If available, a tracefile begins with the -.I testname -which is stored in the following format: - - TN: - -For each source file referenced in the .da file, there is a section containing -filename and coverage data: - - SF: - -Following is a list of line numbers for each function name found in the -source file: - - FN:, - -Next, there is a list of execution counts for each instrumented function: - - FNDA:, - -This list is followed by two lines containing the number of functions found -and hit: - - FNF: - FNH: - -Branch coverage information is stored which one line per branch: - - BRDA:,,, - -Block number and branch number are gcc internal IDs for the branch. Taken is -either '-' if the basic block containing the branch was never executed or -a number indicating how often that branch was taken. - -Branch coverage summaries are stored in two lines: - - BRF: - BRH: - -Then there is a list of execution counts for each instrumented line -(i.e. a line which resulted in executable code): - - DA:,[,] - -Note that there may be an optional checksum present for each instrumented -line. The current -.B geninfo -implementation uses an MD5 hash as checksumming algorithm. - -At the end of a section, there is a summary about how many lines -were found and how many were actually instrumented: - - LH: - LF: - -Each sections ends with: - - end_of_record - -In addition to the main source code file there are sections for all -#included files which also contain executable code. - -Note that the absolute path of a source file is generated by interpreting -the contents of the respective .bb file (see -.BR "gcov " (1) -for more information on this file type). Relative filenames are prefixed -with the directory in which the .bb file is found. - -Note also that symbolic links to the .bb file will be resolved so that the -actual file path is used instead of the path to a link. This approach is -necessary for the mechanism to work with the /proc/gcov files. - -.SH AUTHOR -Peter Oberparleiter - -.SH SEE ALSO -.BR lcov (1), -.BR lcovrc (5), -.BR genhtml (1), -.BR genpng (1), -.BR gendesc (1), -.BR gcov (1) diff --git a/worker/deps/lcov/man/genpng.1 b/worker/deps/lcov/man/genpng.1 deleted file mode 100644 index 376f2646aa..0000000000 --- a/worker/deps/lcov/man/genpng.1 +++ /dev/null @@ -1,101 +0,0 @@ -.TH genpng 1 "LCOV 1.14" 2016\-12\-19 "User Manuals" -.SH NAME -genpng \- Generate an overview image from a source file -.SH SYNOPSIS -.B genpng -.RB [ \-h | \-\-help ] -.RB [ \-v | \-\-version ] -.RS 7 -.br -.RB [ \-t | \-\-tab\-size -.IR tabsize ] -.RB [ \-w | \-\-width -.IR width ] -.br -.RB [ \-o | \-\-output\-filename -.IR output\-filename ] -.br -.IR source\-file -.SH DESCRIPTION -.B genpng -creates an overview image for a given source code file of either -plain text or .gcov file format. - -Note that the -.I GD.pm -Perl module has to be installed for this script to work -(it may be obtained from -.IR http://www.cpan.org ). - -Note also that -.B genpng -is called from within -.B genhtml -so that there is usually no need to call it directly. - -.SH OPTIONS -.B \-h -.br -.B \-\-help -.RS -Print a short help text, then exit. -.RE - -.B \-v -.br -.B \-\-version -.RS -Print version number, then exit. -.RE - -.BI "\-t " tab\-size -.br -.BI "\-\-tab\-size " tab\-size -.RS -Use -.I tab\-size -spaces in place of tab. - -All occurrences of tabulator signs in the source code file will be replaced -by the number of spaces defined by -.I tab\-size -(default is 4). -.RE - -.BI "\-w " width -.br -.BI "\-\-width " width -.RS -Set width of output image to -.I width -pixel. - -The resulting image will be exactly -.I width -pixel wide (default is 80). - -Note that source code lines which are longer than -.I width -will be truncated. -.RE - - -.BI "\-o " filename -.br -.BI "\-\-output\-filename " filename -.RS -Write image to -.IR filename . - -Specify a name for the resulting image file (default is -.IR source\-file .png). -.RE -.SH AUTHOR -Peter Oberparleiter - -.SH SEE ALSO -.BR lcov (1), -.BR genhtml (1), -.BR geninfo (1), -.BR gendesc (1), -.BR gcov (1) diff --git a/worker/deps/lcov/man/lcov.1 b/worker/deps/lcov/man/lcov.1 deleted file mode 100644 index 2bf91bbf83..0000000000 --- a/worker/deps/lcov/man/lcov.1 +++ /dev/null @@ -1,930 +0,0 @@ -.TH lcov 1 "LCOV 1.14" 2017\-10\-10 "User Manuals" -.SH NAME -lcov \- a graphical GCOV front\-end -.SH SYNOPSIS -.B lcov -.BR \-c | \-\-capture -.RS 5 -.br -.RB [ \-d | \-\-directory -.IR directory ] -.RB [ \-k | \-\-kernel\-directory -.IR directory ] -.br -.RB [ \-o | \-\-output\-file -.IR tracefile ] -.RB [ \-t | \-\-test\-name -.IR testname ] -.br -.RB [ \-b | \-\-base\-directory -.IR directory ] -.RB [ \-i | \-\-initial ] -.RB [ \-\-gcov\-tool -.IR tool ] -.br -.RB [ \-\-checksum ] -.RB [ \-\-no\-checksum ] -.RB [ \-\-no\-recursion ] -.RB [ \-f | \-\-follow ] -.br -.RB [ \-\-compat\-libtool ] -.RB [ \-\-no\-compat\-libtool ] -.RB [ \-\-ignore\-errors -.IR errors ] -.br -.RB [ \-\-to\-package -.IR package ] -.RB [ \-\-from\-package -.IR package ] -.RB [ \-q | \-\-quiet ] -.br -.RB [ \-\-no\-markers ] -.RB [ \-\-external ] -.RB [ \-\-no\-external ] -.br -.RB [ \-\-config\-file -.IR config\-file ] -.RB [ \-\-rc -.IR keyword = value ] -.br -.RB [ \-\-compat -.IR mode =on|off|auto] -.br -.RB [ \-\-include -.IR pattern ] -.RB [ \-\-exclude -.IR pattern ] -.br -.RE - -.B lcov -.BR \-z | \-\-zerocounters -.RS 5 -.br -.RB [ \-d | \-\-directory -.IR directory ] -.RB [ \-\-no\-recursion ] -.RB [ \-f | \-\-follow ] -.br -.RB [ \-q | \-\-quiet ] -.br -.RE - -.B lcov -.BR \-l | \-\-list -.I tracefile -.RS 5 -.br -.RB [ \-q | \-\-quiet ] -.RB [ \-\-list\-full\-path ] -.RB [ \-\-no\-list\-full\-path ] -.br -.RB [ \-\-config\-file -.IR config\-file ] -.RB [ \-\-rc -.IR keyword = value ] -.br -.RE - -.B lcov -.BR \-a | \-\-add\-tracefile -.I tracefile -.RS 5 -.br -.RB [ \-o | \-\-output\-file -.IR tracefile ] -.RB [ \-\-checksum ] -.RB [ \-\-no\-checksum ] -.br -.RB [ \-q | \-\-quiet ] -.RB [ \-\-config\-file -.IR config\-file ] -.RB [ \-\-rc -.IR keyword = value ] -.br -.RE - -.B lcov -.BR \-e | \-\-extract -.I tracefile pattern -.RS 5 -.br -.RB [ \-o | \-\-output\-file -.IR tracefile ] -.RB [ \-\-checksum ] -.RB [ \-\-no\-checksum ] -.br -.RB [ \-q | \-\-quiet ] -.RB [ \-\-config\-file -.IR config\-file ] -.RB [ \-\-rc -.IR keyword = value ] -.br -.RE - -.B lcov -.BR \-r | \-\-remove -.I tracefile pattern -.RS 5 -.br -.RB [ \-o | \-\-output\-file -.IR tracefile ] -.RB [ \-\-checksum ] -.RB [ \-\-no\-checksum ] -.br -.RB [ \-q | \-\-quiet ] -.RB [ \-\-config\-file -.IR config\-file ] -.RB [ \-\-rc -.IR keyword = value ] -.br -.RE - -.B lcov -.BR \-\-diff -.IR "tracefile diff" -.RS 5 -.br -.RB [ \-o | \-\-output\-file -.IR tracefile ] -.RB [ \-\-checksum ] -.RB [ \-\-no\-checksum ] -.br -.RB [ \-\-convert\-filenames ] -.RB [ \-\-strip -.IR depth ] -.RB [ \-\-path -.IR path ] -.RB [ \-q | \-\-quiet ] -.br -.RB [ \-\-config\-file -.IR config\-file ] -.RB [ \-\-rc -.IR keyword = value ] -.br -.RE - -.B lcov -.BR \-\-summary -.I tracefile -.RS 5 -.br -.RB [ \-q | \-\-quiet ] -.br -.RE - -.B lcov -.RB [ \-h | \-\-help ] -.RB [ \-v | \-\-version ] -.RS 5 -.br -.RE - -.SH DESCRIPTION -.B lcov -is a graphical front\-end for GCC's coverage testing tool gcov. It collects -line, function and branch coverage data for multiple source files and creates -HTML pages containing the source code annotated with coverage information. -It also adds overview pages for easy navigation within the file structure. - -Use -.B lcov -to collect coverage data and -.B genhtml -to create HTML pages. Coverage data can either be collected from the -currently running Linux kernel or from a user space application. To do this, -you have to complete the following preparation steps: - -For Linux kernel coverage: -.RS -Follow the setup instructions for the gcov\-kernel infrastructure: -.I http://ltp.sourceforge.net/coverage/gcov.php -.br - - -.RE -For user space application coverage: -.RS -Compile the application with GCC using the options -"\-fprofile\-arcs" and "\-ftest\-coverage". -.RE - -Please note that this man page refers to the output format of -.B lcov -as ".info file" or "tracefile" and that the output of GCOV -is called ".da file". - -Also note that when printing percentages, 0% and 100% are only printed when -the values are exactly 0% and 100% respectively. Other values which would -conventionally be rounded to 0% or 100% are instead printed as nearest -non-boundary value. This behavior is in accordance with that of the -.BR gcov (1) -tool. - -.SH OPTIONS - - -.B \-a -.I tracefile -.br -.B \-\-add\-tracefile -.I tracefile -.br -.RS -Add contents of -.IR tracefile . - -Specify several tracefiles using the \-a switch to combine the coverage data -contained in these files by adding up execution counts for matching test and -filename combinations. - -The result of the add operation will be written to stdout or the tracefile -specified with \-o. - -Only one of \-z, \-c, \-a, \-e, \-r, \-l, \-\-diff or \-\-summary may be -specified at a time. - -.RE - -.B \-b -.I directory -.br -.B \-\-base\-directory -.I directory -.br -.RS -.RI "Use " directory -as base directory for relative paths. - -Use this option to specify the base directory of a build\-environment -when lcov produces error messages like: - -.RS -ERROR: could not read source file /home/user/project/subdir1/subdir2/subdir1/subdir2/file.c -.RE - -In this example, use /home/user/project as base directory. - -This option is required when using lcov on projects built with libtool or -similar build environments that work with a base directory, i.e. environments, -where the current working directory when invoking the compiler is not the same -directory in which the source code file is located. - -Note that this option will not work in environments where multiple base -directories are used. In that case use configuration file setting -.B geninfo_auto_base=1 -(see -.BR lcovrc (5)). -.RE - -.B \-c -.br -.B \-\-capture -.br -.RS -Capture coverage data. - -By default captures the current kernel execution counts and writes the -resulting coverage data to the standard output. Use the \-\-directory -option to capture counts for a user space program. - -The result of the capture operation will be written to stdout or the tracefile -specified with \-o. - -Only one of \-z, \-c, \-a, \-e, \-r, \-l, \-\-diff or \-\-summary may be -specified at a time. -.RE - -.B \-\-checksum -.br -.B \-\-no\-checksum -.br -.RS -Specify whether to generate checksum data when writing tracefiles. - -Use \-\-checksum to enable checksum generation or \-\-no\-checksum to -disable it. Checksum generation is -.B disabled -by default. - -When checksum generation is enabled, a checksum will be generated for each -source code line and stored along with the coverage data. This checksum will -be used to prevent attempts to combine coverage data from different source -code versions. - -If you don't work with different source code versions, disable this option -to speed up coverage data processing and to reduce the size of tracefiles. -.RE - -.B \-\-compat -.IR mode = value [, mode = value ,...] -.br -.RS -Set compatibility mode. - -Use \-\-compat to specify that lcov should enable one or more compatibility -modes when capturing coverage data. You can provide a comma-separated list -of mode=value pairs to specify the values for multiple modes. - -Valid -.I values -are: - -.B on -.RS -Enable compatibility mode. -.RE -.B off -.RS -Disable compatibility mode. -.RE -.B auto -.RS -Apply auto-detection to determine if compatibility mode is required. Note that -auto-detection is not available for all compatibility modes. -.RE - -If no value is specified, 'on' is assumed as default value. - -Valid -.I modes -are: - -.B libtool -.RS -Enable this mode if you are capturing coverage data for a project that -was built using the libtool mechanism. See also -\-\-compat\-libtool. - -The default value for this setting is 'on'. - -.RE -.B hammer -.RS -Enable this mode if you are capturing coverage data for a project that -was built using a version of GCC 3.3 that contains a modification -(hammer patch) of later GCC versions. You can identify a modified GCC 3.3 -by checking the build directory of your project for files ending in the -extension '.bbg'. Unmodified versions of GCC 3.3 name these files '.bb'. - -The default value for this setting is 'auto'. - -.RE -.B split_crc -.RS -Enable this mode if you are capturing coverage data for a project that -was built using a version of GCC 4.6 that contains a modification -(split function checksums) of later GCC versions. Typical error messages -when running lcov on coverage data produced by such GCC versions are -\'out of memory' and 'reached unexpected end of file'. - -The default value for this setting is 'auto' -.RE - -.RE - -.B \-\-compat\-libtool -.br -.B \-\-no\-compat\-libtool -.br -.RS -Specify whether to enable libtool compatibility mode. - -Use \-\-compat\-libtool to enable libtool compatibility mode or \-\-no\-compat\-libtool -to disable it. The libtool compatibility mode is -.B enabled -by default. - -When libtool compatibility mode is enabled, lcov will assume that the source -code relating to a .da file located in a directory named ".libs" can be -found in its parent directory. - -If you have directories named ".libs" in your build environment but don't use -libtool, disable this option to prevent problems when capturing coverage data. -.RE - -.B \-\-config\-file -.I config\-file -.br -.RS -Specify a configuration file to use. - -When this option is specified, neither the system\-wide configuration file -/etc/lcovrc, nor the per\-user configuration file ~/.lcovrc is read. - -This option may be useful when there is a need to run several -instances of -.B lcov -with different configuration file options in parallel. -.RE - -.B \-\-convert\-filenames -.br -.RS -Convert filenames when applying diff. - -Use this option together with \-\-diff to rename the file names of processed -data sets according to the data provided by the diff. -.RE - -.B \-\-diff -.I tracefile -.I difffile -.br -.RS -Convert coverage data in -.I tracefile -using source code diff file -.IR difffile . - -Use this option if you want to merge coverage data from different source code -levels of a program, e.g. when you have data taken from an older version -and want to combine it with data from a more current version. -.B lcov -will try to map source code lines between those versions and adjust the coverage -data respectively. -.I difffile -needs to be in unified format, i.e. it has to be created using the "\-u" option -of the -.B diff -tool. - -Note that lines which are not present in the old version will not be counted -as instrumented, therefore tracefiles resulting from this operation should -not be interpreted individually but together with other tracefiles taken -from the newer version. Also keep in mind that converted coverage data should -only be used for overview purposes as the process itself introduces a loss -of accuracy. - -The result of the diff operation will be written to stdout or the tracefile -specified with \-o. - -Only one of \-z, \-c, \-a, \-e, \-r, \-l, \-\-diff or \-\-summary may be -specified at a time. -.RE - -.B \-d -.I directory -.br -.B \-\-directory -.I directory -.br -.RS -Use .da files in -.I directory -instead of kernel. - -If you want to work on coverage data for a user space program, use this -option to specify the location where the program was compiled (that's -where the counter files ending with .da will be stored). - -Note that you may specify this option more than once. -.RE - -.B \-\-exclude -.I pattern -.br -.RS -Exclude source files matching -.IR pattern . - -Use this switch if you want to exclude coverage data for a particular set -of source files matching any of the given patterns. Multiple patterns can be -specified by using multiple -.B --exclude -command line switches. The -.I patterns -will be interpreted as shell wildcard patterns (note that they may need to be -escaped accordingly to prevent the shell from expanding them first). - -Can be combined with the -.B --include -command line switch. If a given file matches both the include pattern and the -exclude pattern, the exclude pattern will take precedence. -.RE - -.B \-\-external -.br -.B \-\-no\-external -.br -.RS -Specify whether to capture coverage data for external source files. - -External source files are files which are not located in one of the directories -specified by \-\-directory or \-\-base\-directory. Use \-\-external to include -external source files while capturing coverage data or \-\-no\-external to -ignore this data. - -Data for external source files is -.B included -by default. -.RE - -.B \-e -.I tracefile -.I pattern -.br -.B \-\-extract -.I tracefile -.I pattern -.br -.RS -Extract data from -.IR tracefile . - -Use this switch if you want to extract coverage data for only a particular -set of files from a tracefile. Additional command line parameters will be -interpreted as shell wildcard patterns (note that they may need to be -escaped accordingly to prevent the shell from expanding them first). -Every file entry in -.I tracefile -which matches at least one of those patterns will be extracted. - -The result of the extract operation will be written to stdout or the tracefile -specified with \-o. - -Only one of \-z, \-c, \-a, \-e, \-r, \-l, \-\-diff or \-\-summary may be -specified at a time. -.RE - -.B \-f -.br -.B \-\-follow -.br -.RS -Follow links when searching for .da files. -.RE - -.B \-\-from\-package -.I package -.br -.RS -Use .da files in -.I package -instead of kernel or directory. - -Use this option if you have separate machines for build and test and -want to perform the .info file creation on the build machine. See -\-\-to\-package for more information. -.RE - -.B \-\-gcov\-tool -.I tool -.br -.RS -Specify the location of the gcov tool. -.RE - -.B \-h -.br -.B \-\-help -.br -.RS -Print a short help text, then exit. -.RE - -.B \-\-include -.I pattern -.br -.RS -Include source files matching -.IR pattern . - -Use this switch if you want to include coverage data for only a particular set -of source files matching any of the given patterns. Multiple patterns can be -specified by using multiple -.B --include -command line switches. The -.I patterns -will be interpreted as shell wildcard patterns (note that they may need to be -escaped accordingly to prevent the shell from expanding them first). -.RE - -.B \-\-ignore\-errors -.I errors -.br -.RS -Specify a list of errors after which to continue processing. - -Use this option to specify a list of one or more classes of errors after which -lcov should continue processing instead of aborting. - -.I errors -can be a comma\-separated list of the following keywords: - -.B gcov: -the gcov tool returned with a non\-zero return code. - -.B source: -the source code file for a data set could not be found. - -.B graph: -the graph file could not be found or is corrupted. -.RE - -.B \-i -.br -.B \-\-initial -.RS -Capture initial zero coverage data. - -Run lcov with \-c and this option on the directories containing .bb, .bbg -or .gcno files before running any test case. The result is a "baseline" -coverage data file that contains zero coverage for every instrumented line. -Combine this data file (using lcov \-a) with coverage data files captured -after a test run to ensure that the percentage of total lines covered is -correct even when not all source code files were loaded during the test. - -Recommended procedure when capturing data for a test case: - -1. create baseline coverage data file -.RS -# lcov \-c \-i \-d appdir \-o app_base.info -.br - -.RE -2. perform test -.RS -# appdir/test -.br - -.RE -3. create test coverage data file -.RS -# lcov \-c \-d appdir \-o app_test.info -.br - -.RE -4. combine baseline and test coverage data -.RS -# lcov \-a app_base.info \-a app_test.info \-o app_total.info -.br - -.RE -.RE - -.B \-k -.I subdirectory -.br -.B \-\-kernel\-directory -.I subdirectory -.br -.RS -Capture kernel coverage data only from -.IR subdirectory . - -Use this option if you don't want to get coverage data for all of the -kernel, but only for specific subdirectories. This option may be specified -more than once. - -Note that you may need to specify the full path to the kernel subdirectory -depending on the version of the kernel gcov support. -.RE - -.B \-l -.I tracefile -.br -.B \-\-list -.I tracefile -.br -.RS -List the contents of the -.IR tracefile . - -Only one of \-z, \-c, \-a, \-e, \-r, \-l, \-\-diff or \-\-summary may be -specified at a time. -.RE - -.B \-\-list\-full\-path -.br -.B \-\-no\-list\-full\-path -.br -.RS -Specify whether to show full paths during list operation. - -Use \-\-list\-full\-path to show full paths during list operation -or \-\-no\-list\-full\-path to show shortened paths. Paths are -.B shortened -by default. -.RE - -.B \-\-no\-markers -.br -.RS -Use this option if you want to get coverage data without regard to exclusion -markers in the source code file. See -.BR "geninfo " (1) -for details on exclusion markers. -.RE - -.B \-\-no\-recursion -.br -.RS -Use this option if you want to get coverage data for the specified directory -only without processing subdirectories. -.RE - -.B \-o -.I tracefile -.br -.B \-\-output\-file -.I tracefile -.br -.RS -Write data to -.I tracefile -instead of stdout. - -Specify "\-" as a filename to use the standard output. - -By convention, lcov\-generated coverage data files are called "tracefiles" and -should have the filename extension ".info". -.RE - -.B \-\-path -.I path -.br -.RS -Strip path from filenames when applying diff. - -Use this option together with \-\-diff to tell lcov to disregard the specified -initial path component when matching between tracefile and diff filenames. -.RE - -.B \-q -.br -.B \-\-quiet -.br -.RS -Do not print progress messages. - -This option is implied when no output filename is specified to prevent -progress messages to mess with coverage data which is also printed to -the standard output. -.RE - -.B \-\-rc -.IR keyword = value -.br -.RS -Override a configuration directive. - -Use this option to specify a -.IR keyword = value -statement which overrides the corresponding configuration statement in -the lcovrc configuration file. You can specify this option more than once -to override multiple configuration statements. -See -.BR lcovrc (5) -for a list of available keywords and their meaning. -.RE - -.B \-r -.I tracefile -.I pattern -.br -.B \-\-remove -.I tracefile -.I pattern -.br -.RS -Remove data from -.IR tracefile . - -Use this switch if you want to remove coverage data for a particular -set of files from a tracefile. Additional command line parameters will be -interpreted as shell wildcard patterns (note that they may need to be -escaped accordingly to prevent the shell from expanding them first). -Every file entry in -.I tracefile -which matches at least one of those patterns will be removed. - -The result of the remove operation will be written to stdout or the tracefile -specified with \-o. - -Only one of \-z, \-c, \-a, \-e, \-r, \-l, \-\-diff or \-\-summary may be -specified at a time. -.RE - -.B \-\-strip -.I depth -.br -.RS -Strip path components when applying diff. - -Use this option together with \-\-diff to tell lcov to disregard the specified -number of initial directories when matching tracefile and diff filenames. -.RE - -.B \-\-summary -.I tracefile -.br -.RS -Show summary coverage information for the specified tracefile. - -Note that you may specify this option more than once. - -Only one of \-z, \-c, \-a, \-e, \-r, \-l, \-\-diff or \-\-summary may be -specified at a time. -.RE - -.B \-t -.I testname -.br -.B \-\-test\-name -.I testname -.br -.RS -Specify test name to be stored in the tracefile. - -This name identifies a coverage data set when more than one data set is merged -into a combined tracefile (see option \-a). - -Valid test names can consist of letters, decimal digits and the underscore -character ("_"). -.RE - -.B \-\-to\-package -.I package -.br -.RS -Store .da files for later processing. - -Use this option if you have separate machines for build and test and -want to perform the .info file creation on the build machine. To do this, -follow these steps: - -On the test machine: -.RS -.br -\- run the test -.br -\- run lcov \-c [\-d directory] \-\-to-package -.I file -.br -\- copy -.I file -to the build machine -.RE -.br - -On the build machine: -.RS -.br -\- run lcov \-c \-\-from-package -.I file -[\-o and other options] -.RE -.br - -This works for both kernel and user space coverage data. Note that you might -have to specify the path to the build directory using \-b with -either \-\-to\-package or \-\-from-package. Note also that the package data -must be converted to a .info file before recompiling the program or it will -become invalid. -.RE - -.B \-v -.br -.B \-\-version -.br -.RS -Print version number, then exit. -.RE - -.B \-z -.br -.B \-\-zerocounters -.br -.RS -Reset all execution counts to zero. - -By default tries to reset kernel execution counts. Use the \-\-directory -option to reset all counters of a user space program. - -Only one of \-z, \-c, \-a, \-e, \-r, \-l, \-\-diff or \-\-summary may be -specified at a time. -.RE - -.SH FILES - -.I /etc/lcovrc -.RS -The system\-wide configuration file. -.RE - -.I ~/.lcovrc -.RS -The per\-user configuration file. -.RE - -.SH AUTHOR -Peter Oberparleiter - -.SH SEE ALSO -.BR lcovrc (5), -.BR genhtml (1), -.BR geninfo (1), -.BR genpng (1), -.BR gendesc (1), -.BR gcov (1) diff --git a/worker/deps/lcov/man/lcovrc.5 b/worker/deps/lcov/man/lcovrc.5 deleted file mode 100644 index 07d35ff3c5..0000000000 --- a/worker/deps/lcov/man/lcovrc.5 +++ /dev/null @@ -1,937 +0,0 @@ -.TH lcovrc 5 "LCOV 1.14" 2018\-01\-30 "User Manuals" - -.SH NAME -lcovrc \- lcov configuration file - -.SH DESCRIPTION -The -.I lcovrc -file contains configuration information for the -.B lcov -code coverage tool (see -.BR lcov (1)). -.br - -The system\-wide configuration file is located at -.IR /etc/lcovrc . -To change settings for a single user, place a customized copy of this file at -location -.IR ~/.lcovrc . -Where available, command\-line options override configuration file settings. - -Lines in a configuration file can either be: -.IP " *" -empty lines or lines consisting only of white space characters. These lines are -ignored. -.IP " *" -comment lines which start with a hash sign ('#'). These are treated like empty -lines and will be ignored. -.IP " *" -statements in the form -.RI ' key " = " value '. -A list of valid statements and their description can be found in -section 'OPTIONS' below. -.PP - -.B Example configuration: -.IP -# -.br -# Example LCOV configuration file -.br -# -.br - -# External style sheet file -.br -#genhtml_css_file = gcov.css -.br - -# Coverage rate limits -.br -genhtml_hi_limit = 90 -.br -genhtml_med_limit = 75 -.br - -# Width of line coverage field in source code view -.br -genhtml_line_field_width = 12 -.br - -# Width of branch coverage field in source code view -.br -genhtml_branch_field_width = 16 -.br - -# Width of overview image -.br -genhtml_overview_width = 80 -.br - -# Resolution of overview navigation -.br -genhtml_nav_resolution = 4 -.br - -# Offset for source code navigation -.br -genhtml_nav_offset = 10 -.br - -# Do not remove unused test descriptions if non\-zero -.br -genhtml_keep_descriptions = 0 -.br - -# Do not remove prefix from directory names if non\-zero -.br -genhtml_no_prefix = 0 -.br - -# Do not create source code view if non\-zero -.br -genhtml_no_source = 0 -.br - -# Specify size of tabs -.br -genhtml_num_spaces = 8 -.br - -# Highlight lines with converted\-only data if non\-zero -.br -genhtml_highlight = 0 -.br - -# Include color legend in HTML output if non\-zero -.br -genhtml_legend = 0 -.br - -# Include HTML file at start of HTML output -.br -#genhtml_html_prolog = prolog.html -.br - -# Include HTML file at end of HTML output -.br -#genhtml_html_epilog = epilog.html -.br - -# Use custom HTML file extension -.br -#genhtml_html_extension = html -.br - -# Compress all generated html files with gzip. -.br -#genhtml_html_gzip = 1 -.br - -# Include sorted overview pages -.br -genhtml_sort = 1 -.br - -# Include function coverage data display -.br -#genhtml_function_coverage = 1 -.br - -# Include branch coverage data display -.br -#genhtml_branch_coverage = 1 -.br - -# Specify the character set of all generated HTML pages -.br -genhtml_charset=UTF\-8 -.br - -# Allow HTML markup in test case description text if non\-zero -.br -genhtml_desc_html=0 -.br - -# Specify the precision for coverage rates -.br -#genhtml_precision=1 -.br - -# Show missed counts instead of hit counts -.br -#genhtml_missed=1 -.br - -# Demangle C++ symbols -.br -#genhtml_demangle_cpp=1 -.br - -# Location of the gcov tool -.br -#geninfo_gcov_tool = gcov -.br - -# Adjust test names if non\-zero -.br -#geninfo_adjust_testname = 0 -.br - -# Calculate a checksum for each line if non\-zero -.br -geninfo_checksum = 0 -.br - -# Enable libtool compatibility mode if non\-zero -.br -geninfo_compat_libtool = 0 -.br - -# Specify whether to capture coverage data for external source -.br -# files -.br -#geninfo_external = 1 -.br - -# Use gcov's --all-blocks option if non-zero -.br -#geninfo_gcov_all_blocks = 1 -.br - -# Specify compatiblity modes (same as \-\-compat option -.br -# of geninfo) -.br -#geninfo_compat = libtool=on, hammer=auto, split_crc=auto -.br - -# Adjust path to source files by removing or changing path -.br -# components that match the specified pattern (Perl regular -.br -# expression format) -.br -#geninfo_adjust_src_path = /tmp/build => /usr/src - -# Specify if geninfo should try to automatically determine -.br -# the base-directory when collecting coverage data. -.br -geninfo_auto_base = 1 -.br - -# Use gcov intermediate format? Valid values are 0, 1, auto -.br -geninfo_intermediate = auto -.br - -# Directory containing gcov kernel files -.br -lcov_gcov_dir = /proc/gcov -.br - -# Location for temporary directories -.br -lcov_tmp_dir = /tmp -.br - -# Show full paths during list operation if non\-zero -.br -lcov_list_full_path = 0 -.br - -# Specify the maximum width for list output. This value is -.br -# ignored when lcov_list_full_path is non\-zero. -.br -lcov_list_width = 80 -.br - -# Specify the maximum percentage of file names which may be -.br -# truncated when choosing a directory prefix in list output. -.br -# This value is ignored when lcov_list_full_path is non\-zero. -.br -lcov_list_truncate_max = 20 - -# Specify if function coverage data should be collected and -.br -# processed. -.br -lcov_function_coverage = 1 -.br - -# Specify if branch coverage data should be collected and -.br -# processed. -.br -lcov_branch_coverage = 0 -.br -.PP - -.SH OPTIONS - -.BR genhtml_css_file " =" -.I filename -.IP -Specify an external style sheet file. Use this option to modify the appearance of the HTML output as generated by -.BR genhtml . -During output generation, a copy of this file will be placed in the output -directory. -.br - -This option corresponds to the \-\-css\-file command line option of -.BR genhtml . -.br - -By default, a standard CSS file is generated. -.PP - -.BR genhtml_hi_limit " =" -.I hi_limit -.br -.BR genhtml_med_limit " =" -.I med_limit -.br -.IP -Specify coverage rate limits for classifying file entries. Use this option to -modify the coverage rates (in percent) for line, function and branch coverage at -which a result is classified as high, medium or low coverage. This -classification affects the color of the corresponding entries on the overview -pages of the HTML output: -.br - -High: hi_limit <= rate <= 100 default color: green -.br -Medium: med_limit <= rate < hi_limit default color: orange -.br -Low: 0 <= rate < med_limit default color: red -.br - -Defaults are 90 and 75 percent. -.PP - -.BR genhtml_line_field_width " =" -.I number_of_characters -.IP -Specify the width (in characters) of the source code view column containing -line coverage information. -.br - -Default is 12. -.PP - -.BR genhtml_branch_field_width " =" -.I number_of_characters -.IP -Specify the width (in characters) of the source code view column containing -branch coverage information. -.br - -Default is 16. -.PP - -.BR genhtml_overview_width " =" -.I pixel_size -.IP -Specify the width (in pixel) of the overview image created when generating HTML -output using the \-\-frames option of -.BR genhtml . -.br - -Default is 80. -.PP - -.BR genhtml_nav_resolution " =" -.I lines -.IP -Specify the resolution of overview navigation when generating HTML output using -the \-\-frames option of -.BR genhtml . -This number specifies the maximum difference in lines between the position a -user selected from the overview and the position the source code window is -scrolled to. -.br - -Default is 4. -.PP - - -.BR genhtml_nav_offset " =" -.I lines -.IP -Specify the overview navigation line offset as applied when generating HTML -output using the \-\-frames option of -.BR genhtml. -.br - -Clicking a line in the overview image should show the source code view at -a position a bit further up, so that the requested line is not the first -line in the window. This number specifies that offset. -.br - -Default is 10. -.PP - - -.BR genhtml_keep_descriptions " =" -.IR 0 | 1 -.IP -If non\-zero, keep unused test descriptions when generating HTML output using -.BR genhtml . -.br - -This option corresponds to the \-\-keep\-descriptions option of -.BR genhtml . -.br - -Default is 0. -.PP - -.BR genhtml_no_prefix " =" -.IR 0 | 1 -.IP -If non\-zero, do not try to find and remove a common prefix from directory names. -.br - -This option corresponds to the \-\-no\-prefix option of -.BR genhtml . -.br - -Default is 0. -.PP - -.BR genhtml_no_source " =" -.IR 0 | 1 -.IP -If non\-zero, do not create a source code view when generating HTML output using -.BR genhtml . -.br - -This option corresponds to the \-\-no\-source option of -.BR genhtml . -.br - -Default is 0. -.PP - -.BR genhtml_num_spaces " =" -.I num -.IP -Specify the number of spaces to use as replacement for tab characters in the -HTML source code view as generated by -.BR genhtml . -.br - -This option corresponds to the \-\-num\-spaces option of -.BR genthml . -.br - -Default is 8. - -.PP - -.BR genhtml_highlight " =" -.IR 0 | 1 -.IP -If non\-zero, highlight lines with converted\-only data in -HTML output as generated by -.BR genhtml . -.br - -This option corresponds to the \-\-highlight option of -.BR genhtml . -.br - -Default is 0. -.PP - -.BR genhtml_legend " =" -.IR 0 | 1 -.IP -If non\-zero, include a legend explaining the meaning of color coding in the HTML -output as generated by -.BR genhtml . -.br - -This option corresponds to the \-\-legend option of -.BR genhtml . -.br - -Default is 0. -.PP - -.BR genhtml_html_prolog " =" -.I filename -.IP -If set, include the contents of the specified file at the beginning of HTML -output. - -This option corresponds to the \-\-html\-prolog option of -.BR genhtml . -.br - -Default is to use no extra prolog. -.PP - -.BR genhtml_html_epilog " =" -.I filename -.IP -If set, include the contents of the specified file at the end of HTML output. - -This option corresponds to the \-\-html\-epilog option of -.BR genhtml . -.br - -Default is to use no extra epilog. -.PP - -.BR genhtml_html_extension " =" -.I extension -.IP -If set, use the specified string as filename extension for generated HTML files. - -This option corresponds to the \-\-html\-extension option of -.BR genhtml . -.br - -Default extension is "html". -.PP - -.BR genhtml_html_gzip " =" -.IR 0 | 1 -.IP -If set, compress all html files using gzip. - -This option corresponds to the \-\-html\-gzip option of -.BR genhtml . -.br - -Default extension is 0. -.PP - -.BR genhtml_sort " =" -.IR 0 | 1 -.IP -If non\-zero, create overview pages sorted by coverage rates when generating -HTML output using -.BR genhtml . -.br - -This option can be set to 0 by using the \-\-no\-sort option of -.BR genhtml . -.br - -Default is 1. -.PP - -.BR genhtml_function_coverage " =" -.IR 0 | 1 -.IP -If non\-zero, include function coverage data when generating HTML output using -.BR genhtml . -.br - -This option can be set to 0 by using the \-\-no\-function\-coverage option of -.BR genhtml . -.br - -Default is 1. -.PP - -.BR genhtml_branch_coverage " =" -.IR 0 | 1 -.IP -If non\-zero, include branch coverage data when generating HTML output using -.BR genhtml . -.br - -This option can be set to 0 by using the \-\-no\-branch\-coverage option of -.BR genhtml . -.br - -Default is 1. -.PP - -.BR genhtml_charset " =" -.I charset -.IP -Specify the character set of all generated HTML pages. -.br - -Use this option if the source code contains characters which are not -part of the default character set. Note that this option is ignored -when a custom HTML prolog is specified (see also -.BR genhtml_html_prolog ). -.br - -Default is UTF-8. -.PP - -.BR genhtml_demangle_cpp " =" -.IR 0 | 1 -.IP -If non-zero, demangle C++ function names in function overviews. - -Set this option to one if you want to convert C++ internal function -names to human readable format for display on the HTML function overview -page. This option requires that the c++filt tool is installed (see -.BR c++filt(1) -). -.br - -This option corresponds to the \-\-demangle\-cpp command line option of -.BR genhtml . -.br - -Default is 0. -.PP - -.BR genhtml_desc_html " =" -.IR 0 | 1 -.IP -If non-zero, test case descriptions may contain HTML markup. - -Set this option to one if you want to embed HTML markup (for example to -include links) in test case descriptions. When set to zero, HTML markup -characters will be escaped to show up as plain text on the test case -description page. -.br - -Default is 0. -.PP - -.BR genhtml_precision " =" -.IR 1 | 2 | 3 | 4 -.IP -Specify how many digits after the decimal-point should be used for -displaying coverage rates. -.br - -Default is 1. -.PP -.BR genhtml_missed " =" -.IR 0 | 1 -.IP -If non-zero, the count of missed lines, functions, or branches is shown -as negative numbers in overview pages. -.br - -Default is 0. -.PP - -. -.BR geninfo_gcov_tool " =" -.I path_to_gcov -.IP -Specify the location of the gcov tool (see -.BR gcov (1)) -which is used to generate coverage information from data files. -.br - -Default is 'gcov'. -.PP - -.BR geninfo_adjust_testname " =" -.IR 0 | 1 -.IP -If non\-zero, adjust test names to include operating system information -when capturing coverage data. -.br - -Default is 0. -.PP - -.BR geninfo_checksum " =" -.IR 0 | 1 -.IP -If non\-zero, generate source code checksums when capturing coverage data. -Checksums are useful to prevent merging coverage data from incompatible -source code versions but checksum generation increases the size of coverage -files and the time used to generate those files. -.br - -This option corresponds to the \-\-checksum and \-\-no\-checksum command line -option of -.BR geninfo . -.br - -Default is 0. -.PP - -.BR geninfo_compat_libtool " =" -.IR 0 | 1 -.IP -If non\-zero, enable libtool compatibility mode. When libtool compatibility -mode is enabled, lcov will assume that the source code relating to a .da file -located in a directory named ".libs" can be found in its parent directory. -.br - -This option corresponds to the \-\-compat\-libtool and \-\-no\-compat\-libtool -command line option of -.BR geninfo . -.br - -Default is 1. -.PP - -.BR geninfo_external " =" -.IR 0 | 1 -.IP -If non\-zero, capture coverage data for external source files. - -External source files are files which are not located in one of the directories -(including sub-directories) -specified by the \-\-directory or \-\-base\-directory options of -.BR lcov / geninfo . - -Default is 1. -.PP - -.BR geninfo_gcov_all_blocks " =" -.IR 0 | 1 -.IP -If non\-zero, call the gcov tool with option --all-blocks. - -Using --all-blocks will produce more detailed branch coverage information for -each line. Set this option to zero if you do not need detailed branch coverage -information to speed up the process of capturing code coverage or to work -around a bug in some versions of gcov which will cause it to endlessly loop -when analysing some files. - -Default is 1. -.PP - -.BR geninfo_compat " =" -.IR mode = value [, mode = value ,...] -.IP -Specify that geninfo should enable one or more compatibility modes -when capturing coverage data. - -This option corresponds to the \-\-compat command line option of -.BR geninfo . - -Default is 'libtool=on, hammer=auto, split_crc=auto'. -.PP - -.BR geninfo_adjust_src_path " =" -.IR pattern " => " replacement -.br -.BR geninfo_adjust_src_path " =" -.I pattern -.IP -Adjust source paths when capturing coverage data. - -Use this option in situations where geninfo cannot find the correct -path to source code files of a project. By providing a -.I pattern -in Perl regular expression format (see -.BR perlre (1)) -and an optional replacement string, you can instruct geninfo to -remove or change parts of the incorrect source path. - -.B Example: -.br - -1. When geninfo reports that it cannot find source file -.br - - /path/to/src/.libs/file.c -.br - -while the file is actually located in -.br - - /path/to/src/file.c -.br - -use the following parameter: -.br - - geninfo_adjust_src_path = /.libs - -This will remove all "/.libs" strings from the path. - -2. When geninfo reports that it cannot find source file -.br - - /tmp/build/file.c -.br - -while the file is actually located in -.br - - /usr/src/file.c -.br - -use the following parameter: -.br - - geninfo_adjust_src_path = /tmp/build => /usr/src -.br - -This will change all "/tmp/build" strings in the path to "/usr/src". -.PP - -.BR geninfo_auto_base " =" -.IR 0 | 1 -.IP -If non\-zero, apply a heuristic to determine the base directory when -collecting coverage data. -.br - -Use this option when using geninfo on projects built with libtool or -similar build environments that work with multiple base directories, -i.e. environments, where the current working directory when invoking the -compiler ist not the same directory in which the source code file is -located, and in addition, is different between files of the same project. -.br - -Default is 1. -.PP - -.BR geninfo_intermediate " =" -.IR 0 | 1 | auto -.IP -Specify whether to use gcov intermediate format -.br - -Use this option to control whether geninfo should use the gcov intermediate -format while collecting coverage data. The use of the gcov intermediate format -should increase processing speed. It also provides branch coverage data when -using the \-\-initial command line option. -.br - -Valid values are 0 for off, 1 for on, and "auto" to let geninfo automatically -use immediate format when supported by gcov. -.br - -Default is "auto". -.PP - -.BR lcov_gcov_dir " =" -.I path_to_kernel_coverage_data -.IP -Specify the path to the directory where kernel coverage data can be found -or leave undefined for auto-detection. -.br - -Default is auto-detection. -.PP - -.BR lcov_tmp_dir " =" -.I temp -.IP -Specify the location of a directory used for temporary files. -.br - -Default is '/tmp'. -.PP - -.BR lcov_list_full_path " =" -.IR 0 | 1 -.IP -If non-zero, print the full path to source code files during a list operation. -.br - -This option corresponds to the \-\-list\-full\-path option of -.BR lcov . -.br - -Default is 0. -.PP - -.BR lcov_list_max_width " =" -.IR width -.IP -Specify the maximum width for list output. This value is ignored when -lcov_list_full_path is non\-zero. -.br - -Default is 80. -.PP - -.BR lcov_list_truncate_max -.B " =" -.IR percentage -.IP -Specify the maximum percentage of file names which may be truncated when -choosing a directory prefix in list output. This value is ignored when -lcov_list_full_path is non\-zero. -.br - -Default is 20. -.PP - -.BR lcov_function_coverage " =" -.IR 0 | 1 -.IP -Specify whether lcov should handle function coverage data. -.br - -Setting this option to 0 can reduce memory and CPU time consumption -when lcov is collecting and processing coverage data, as well as -reduce the size of the resulting data files. Note that setting -.B genhtml_function_coverage -will override this option for HTML generation. -.br - -Default is 1. -.PP - -.BR lcov_branch_coverage " =" -.IR 0 | 1 -.IP -Specify whether lcov should handle branch coverage data. -.br - -Setting this option to 0 can reduce memory and CPU time consumption -when lcov is collecting and processing coverage data, as well as -reduce the size of the resulting data files. Note that setting -.B genhtml_branch_coverage -will override this option for HTML generation. -.br - -Default is 0. -.PP - -.BR lcov_excl_line " =" -.I expression -.IP -Specify the regular expression of lines to exclude. -.br - -Default is 'LCOV_EXCL_LINE'. -.PP - -.BR lcov_excl_br_line " =" -.I expression -.IP -Specify the regular expression of lines to exclude from branch coverage. -.br - -Default is 'LCOV_EXCL_BR_LINE'. -.PP - -.SH FILES - -.TP -.I /etc/lcovrc -The system\-wide -.B lcov -configuration file. - -.TP -.I ~/.lcovrc -The individual per\-user configuration file. -.PP - -.SH SEE ALSO -.BR lcov (1), -.BR genhtml (1), -.BR geninfo (1), -.BR gcov (1) diff --git a/worker/deps/lcov/rpm/lcov.spec b/worker/deps/lcov/rpm/lcov.spec deleted file mode 100644 index e96c8d47bd..0000000000 --- a/worker/deps/lcov/rpm/lcov.spec +++ /dev/null @@ -1,59 +0,0 @@ -Summary: A graphical GCOV front-end -Name: lcov -Version: 1.14 -Release: 1 -License: GPLv2+ -Group: Development/Tools -URL: http://ltp.sourceforge.net/coverage/lcov.php -Source0: http://downloads.sourceforge.net/ltp/%{name}-%{version}.tar.gz -BuildRoot: %{_tmppath}/%{name}-%{version}-root -BuildArch: noarch -Requires: perl >= 5.8.8 - -%description -LCOV is a graphical front-end for GCC's coverage testing tool gcov. It collects -gcov data for multiple source files and creates HTML pages containing the -source code annotated with coverage information. It also adds overview pages -for easy navigation within the file structure. - -%prep -%setup -q -n %{name}-%{version} - -%build -exit 0 - -%install -rm -rf $RPM_BUILD_ROOT -make install DESTDIR=$RPM_BUILD_ROOT PREFIX=/usr CFG_DIR=/etc - -%clean -rm -rf $RPM_BUILD_ROOT - -%files -%defattr(-,root,root) -/usr/bin/* -/usr/share/man/man*/* -%config /etc/* - -%changelog -* Mon Aug 22 2016 Peter Oberparleiter (Peter.Oberparleiter@de.ibm.com) -- updated "make install" call to work with PREFIX Makefile changes - -* Mon May 07 2012 Peter Oberparleiter (Peter.Oberparleiter@de.ibm.com) -- added dependency on perl 5.8.8 for >>& open mode support - -* Wed Aug 13 2008 Peter Oberparleiter (Peter.Oberparleiter@de.ibm.com) -- changed description + summary text - -* Mon Aug 20 2007 Peter Oberparleiter (Peter.Oberparleiter@de.ibm.com) -- fixed "Copyright" tag - -* Mon Jul 14 2003 Peter Oberparleiter (Peter.Oberparleiter@de.ibm.com) -- removed variables for version/release to support source rpm building -- added initial rm command in install section - -* Mon Apr 7 2003 Peter Oberparleiter (Peter.Oberparleiter@de.ibm.com) -- implemented variables for version/release - -* Fri Oct 18 2002 Peter Oberparleiter (Peter.Oberparleiter@de.ibm.com) -- created initial spec file diff --git a/worker/deps/lcov/test/Makefile b/worker/deps/lcov/test/Makefile deleted file mode 100644 index ecb96042aa..0000000000 --- a/worker/deps/lcov/test/Makefile +++ /dev/null @@ -1,27 +0,0 @@ -include common.mak - -TESTDIRS := $(sort $(patsubst %/,%,$(dir $(wildcard */Makefile)))) - -help: info - -info: - echo "Available make targets:" - echo " test : perform self-tests" - echo " clean : remove all temporary files" - echo "" - echo "Available make variables:" - echo " SIZE : specify size of test data (small, medium, large)" - echo " V : specify level of verbosity (0, 1, 2)" - -test: - for TEST in $(TESTDIRS) ; do \ - make -C $$TEST test ; \ - done - -clean: - rm -rf *.info *.counts test.log src/ - for TEST in $(TESTDIRS) ; do \ - make -C $$TEST clean ; \ - done - -.PHONY: help info test clean diff --git a/worker/deps/lcov/test/bin/common b/worker/deps/lcov/test/bin/common deleted file mode 100644 index a8b527deda..0000000000 --- a/worker/deps/lcov/test/bin/common +++ /dev/null @@ -1,103 +0,0 @@ -function elapsed_to_ms() -{ - local ELAPSED=$1 - local IFS=:. - local MS - - set -- $ELAPSED - if [ $# -eq 3 ] ; then - let MS=${3#0}*10+${2#0}*1000+$1*60000 - else - let MS=${4#0}*10+${3#0}*1000+${2#0}*60000+$1*3600000 - fi - - echo $MS -} - -function t_timestamp() -{ - date +"%Y-%m-%d %H:%M:%S %z" -} - -function t_marker() -{ - echo - echo "======================================================================" -} - -function t_detail() -{ - local KEY=$1 - local VALUE=$2 - local DOTS=" ............" - - printf "%-.12s: %s\n" "$KEY$DOTS" "$VALUE" -} - -function t_announce() -{ - local TESTNAME="$1" - - printf "$BOLD%-.30s$RESET " "$TESTNAME .............................." - t_marker >> "$LOGFILE" - t_detail "DATE" "$(t_timestamp)" >> "$LOGFILE" - t_detail "TESTNAME" "$TESTNAME" >> "$LOGFILE" -} - -function t_result() -{ - local COLOR="$1" - local TEXT="$2" - - printf "[$COLOR$TEXT$RESET]" -} - -function t_pass() -{ - local TESTNAME="$1" - - t_result "$GREEN" "pass" - echo "pass $TESTNAME" >> "$COUNTFILE" -} - -function t_fail() -{ - local TESTNAME="$1" - - t_result "$RED" "fail" - echo "fail $TESTNAME" >> "$COUNTFILE" -} - -function t_kill() -{ - local TESTNAME="$1" - - t_result "$RED" "kill" - echo "fail $TESTNAME" >> "$COUNTFILE" -} - -function t_skip() -{ - local TESTNAME="$1" - - t_result "$BLUE" "skip" - echo "skip $TESTNAME" >> "$COUNTFILE" -} - -function t_indent() -{ - sed -e 's/^/ /' -} - -LOGFILE="$TOPDIR/test.log" -COUNTFILE="$TOPDIR/test.counts" -TIMEFILE="$TOPDIR/test.time" - -if [ -t 1 ] ; then - RED="\e[31m" - GREEN="\e[32m" - BLUE="\e[34m" - BOLD="\e[1m" - DEFAULT="\e[39m" - RESET="\e[0m" -fi diff --git a/worker/deps/lcov/test/bin/mkinfo b/worker/deps/lcov/test/bin/mkinfo deleted file mode 100755 index 5231aeac5d..0000000000 --- a/worker/deps/lcov/test/bin/mkinfo +++ /dev/null @@ -1,952 +0,0 @@ -#!/usr/bin/env perl -# -# Copyright IBM Corp. 2017 -# -# Usage: mkinfo [-o ] [--seed ] -# [=...] -# -# Create a fake lcov code coverage data file and optionally the corresponding -# source tree. DATA_FILE contains all specifications for creating the data -# file. Directives can be overridden using KEY=VALUE specifications with KEY -# being in the form SECTION.KEY. SEED specifies the number used to initialize -# the pseudo random number generator. -# -# Example: -# mkinfo profiles/small -o src files.numfiles=12 -# - -use strict; -use warnings; - -use Getopt::Long; -use Cwd qw(abs_path getcwd); -use File::Path qw(make_path); -use File::Basename; -use Data::Dumper; - -my $MAX_TAKEN = 1000; -my $use_colors = -t STDIN; -my $BOLD = $use_colors ? "\033[1m" : ""; -my $RESET = $use_colors ? "\033[0m" : ""; - -sub usage() -{ - print(< [-o ] [--seed ] [=...] - -Create a fake lcov code coverage data file and optionally the corresponding -source tree. DATA_FILE contains all specifications for creating the data -file. Directives can be overridden using KEY=VALUE specifications with KEY -being in the form SECTION.KEY. SEED specifies the number used to initialize -the pseudo random number generator. - -Example: -$0 profiles/small -o src files.numfiles=12 -EOF -} - -sub read_config($) -{ - my ($filename) = @_; - my $fd; - my %config; - my $section; - - open($fd, "<", $filename) or die("Could not open $filename: $!\n"); - while (my $line = <$fd>) { - my ($key, $value); - - $line =~ s/(^\s*|\s*$)//g; - next if ($line eq "" || $line =~ /^#/); - if ($line =~ /^\[\s*(\S+)\s*]$/) { - $section = $1; - next; - } - if ($line !~ /^(\S+)\s*=\s*(.*)$/) { - die("$filename:$.: Unknown line format: $line\n"); - } - ($key, $value) = ($1, $2); - if (!defined($section)) { - die("$filename:$.: Directive outside of section\n"); - } - $config{$section}->{$1} = $2; - } - close($fd); - - return \%config; -} - -sub apply_config($$) -{ - my ($config, $directive) = @_; - - for my $dir (@$directive) { - if ($dir !~ /^([^\.]+)\.([^=]+)=(.*)$/) { - die("Unknown directive format: $dir\n"); - } - $config->{$1}->{$2} = $3; - } -} - -sub get_value($$;$) -{ - my ($config, $dir, $default) = @_; - my ($section, $key, $value); - - if ($dir !~ /^([^\.]+)\.([^=]+)$/) { - die("$0: Internal error: Unknown key format: $key\n"); - } - ($section, $key) = ($1, $2); - - $value = $config->{$section}->{$key}; - - if (!defined($value)) { - if (!defined($default)) { - die("$0: Missing config value for $dir\n"); - } - $value = $default; - } - - return $value; -} - -sub get_int($$;$$$) -{ - my ($config, $dir, $default, $min, $max) = @_; - my $value = get_value($config, $dir, $default); - - if ($value !~ /^\d+$/) { - die("$0: Config value $dir must be an integer: $value\n"); - } - $value = int($value); - if (defined($min) && $value < $min) { - die("$0: Config value $dir is too low (min $min): $value\n"); - } - if (defined($max) && $value > $max) { - die("$0: Config value $dir is too high (max $max): $value\n"); - } - - return int($value); -} - -sub get_list($$;$) -{ - my ($config, $dir, $default) = @_; - my $value = get_value($config, $dir, $default); - my @list = split(/\s+/, $value); - - return \@list; -} - -sub randlist($) -{ - my ($list) = @_; - - return "" if (!@$list); - return $list->[int(rand(scalar(@$list)))]; -} - -sub randbool() -{ - return int(rand(2)); -} - -# Reduce LIST to PERCENTAGE of its former size. -sub reduce_list_per($$) -{ - my ($list, $percentage) = @_; - my $remove; - - $remove = int((100 - $percentage) * scalar(@$list) / 100); - - for (my $i = 0; $i < $remove; $i++) { - splice(@$list, int(rand(scalar(@$list))), 1); - } -} - -# Reduce LIST to NUM items. -sub reduce_list_num($$) -{ - my ($list, $num) = @_; - my $remove; - - $remove = scalar(@$list) - $num; - - for (my $i = 0; $i < $remove; $i++) { - splice(@$list, int(rand(scalar(@$list))), 1); - } -} - -sub _gen_filename($$) -{ - my ($c, $root) = @_; - my $ltop = get_list($c, "files.top", ""); - my $lsub = get_list($c, "files.sub", ""); - my $lsubsub = get_list($c, "files.subsub", ""); - my $lprefix = get_list($c, "files.prefix"); - my $lsuffix = get_list($c, "files.suffix", ""); - my $lext = get_list($c, "files.ext"); - my ($top, $sub, $subsub, $prefix, $suffix, $ext) = - ("", "", "", "", "", ""); - my $filename = ""; - - $top = randlist($ltop) if (randbool()); - $sub = randlist($lsub) if (randbool()); - $subsub = randlist($lsubsub) if (randbool()); - $prefix = randlist($lprefix); - $suffix = randlist($lsuffix) if (randbool()); - $ext = randlist($lext); - - $filename = $root; - $filename .= "/".$top if ($top ne ""); - $filename .= "/".$sub if ($sub ne ""); - $filename .= "/".$subsub if ($subsub ne ""); - $filename .= "/".$prefix; - $filename .= "_".$suffix if ($suffix ne ""); - $filename .= $ext; - $filename =~ s#^//#/#; - - return $filename; -} - -sub gen_filename($$$) -{ - my ($c, $root, $filenames) = @_; - my $filename; - - do { - $filename = _gen_filename($c, $root); - } while ($filenames->{$filename}); - $filenames->{$filename} = 1; - - return $filename; -} - -sub gen_lines($$) -{ - my ($c, $length) = @_; - my @lines = 1 .. $length; - my $percent = get_int($c, "lines.instrumented", undef, 0, 100); - - reduce_list_per(\@lines, $percent); - - return \@lines; -} - -sub gen_fnname($$) -{ - my ($c, $hash) = @_; - my $lverb = get_list($c, "functions.verb"); - my $ladj = get_list($c, "functions.adj", ""); - my $lnoun = get_list($c, "functions.noun", ""); - my ($verb, $adj, $noun) = ("", "", ""); - my $fnname; - - $verb = randlist($lverb); - $adj = randlist($ladj) if (randbool()); - $noun = randlist($lnoun) if (randbool()); - - $fnname = $verb; - $fnname .= "_".$adj if ($adj ne ""); - $fnname .= "_".$noun if ($noun ne ""); - - if (exists($hash->{$fnname})) { - my $i = 2; - - while (exists($hash->{$fnname.$i})) { - $i++; - } - $fnname .= $i; - } - $hash->{$fnname} = 1; - - return $fnname; -} - -sub gen_functions($$) -{ - my ($c, $lines) = @_; - my @fnlines; - my @functions; - my %names; - my $percent = get_int($c, "functions.perinstrumented", undef, 0, 100); - - @fnlines = @$lines; - reduce_list_per(\@fnlines, $percent); - - foreach my $fnline (@fnlines) { - push(@functions, [ $fnline, gen_fnname($c, \%names) ]); - } - - return \@functions; -} - - -# Returns a value distribution object. This object can be used to randomly -# choose one element from a list of elements with a given relative distribution. -# -# dist: [ sumprob, probs] -# sumprob: Sum of all probabilities -# probs: [ prob1, prob2, ... ] -# prob: [ num, x ] -# num: Value -sub get_dist($$;$) -{ - my ($c, $dir, $default) = @_; - my $list = get_list($c, $dir, $default); - my $sumprob = 0; - my @probs; - - foreach my $spec (@$list) { - my ($n, $p); - - if ($spec =~ /^(\d+):(\d+)$/) { - ($n, $p) = ($1, $2); - } elsif ($spec =~ /^(\d+)$/) { - $n = $1; - $p = 1; - } else { - die("$0: Config value $dir must be a distribution ". - "list (a:p1 b:p2 ...)\n"); - } - $sumprob += $p; - push(@probs, [ $n, $sumprob ]); - } - - return [ $sumprob, \@probs ]; -} - -sub rand_dist($) -{ - my ($dist) = @_; - my ($sumprob, $probs) = @$dist; - my $r = int(rand($sumprob)); - - foreach my $prob (@$probs) { - my ($num, $x) = @$prob; - return $num if ($r < $x); - } - - die("Internal error: Incomplete distribution list\n"); -} - -sub gen_branches($$) -{ - my ($c, $lines) = @_; - my $percent = get_int($c, "branches.perinstrumented", undef, 0, 100); - my @allblocks = @{get_list($c, "branches.blocks", "0")}; - my $branchdist = get_dist($c, "branches.branchdist", "2"); - my @brlines; - my @branches; - - @brlines = @$lines; - reduce_list_per(\@brlines, $percent); - - foreach my $brline (@brlines) { - my @blocks = @allblocks; - my $numblocks = int(rand(scalar(@blocks))) + 1; - - reduce_list_num(\@blocks, $numblocks); - - foreach my $block (@blocks) { - my $numbranch = rand_dist($branchdist); - - for (my $branch = 0; $branch < $numbranch; $branch++) { - push(@branches, [ $brline, $block, $branch]); - } - } - } - - return \@branches; -} - -sub gen_filesrc($) -{ - my ($c) = @_; - my ($length, $lines, $functions, $branches); - my $do_ln = get_int($c, "lines.enabled"); - my $do_fn = get_int($c, "functions.enabled"); - my $do_br = get_int($c, "branches.enabled"); - - $length = 1 + int(rand(get_int($c, "lines.maxlines"))); - $lines = gen_lines($c, $length); - $functions = gen_functions($c, $lines) if ($do_fn); - $branches = gen_branches($c, $lines) if ($do_br); - - return [ $length, $lines, $functions, $branches ]; -} - -# Generate fake source tree. -# -# returns: [ files, numlns, numfns, numbrs ] -# files: filename -> filesrc -# filesrc: [ length, lines, functions, branches ] -# length: Total number of lines in file -# -# lines: [ line1, line2, ... ] -# -# functions: [ fn1, fn2, ... ] -# fn: [ fnline, fnname ] -# fnline: Starting line of function -# fnname: Function name -# -# branches: [ brdata1, brdata2, ...] -# brdata: [ brline, block, branch ] -# brline: Line number containing branches -# block: Block ID -# branch: Branch ID -# -sub gen_src($$) -{ - my ($c, $root) = @_; - my %files; - my $numfiles = get_int($c, "files.numfiles"); - my %filenames; - my ($numlns, $numfns, $numbrs) = (0, 0, 0); - - for (my $i = 0; $i < $numfiles; $i++) { - my $filename = gen_filename($c, $root, \%filenames); - my $filesrc = gen_filesrc($c); - - $files{$filename} = $filesrc; - $numlns += scalar(@{$filesrc->[1]}) if (defined($filesrc->[1])); - $numfns += scalar(@{$filesrc->[2]}) if (defined($filesrc->[2])); - $numbrs += scalar(@{$filesrc->[3]}) if (defined($filesrc->[3])); - } - - return [ \%files, $numlns, $numfns, $numbrs ]; -} - -sub write_src($) -{ - my ($src) = @_; - my ($files, $numlns, $numfns, $numbrs) = @$src; - - foreach my $filename (sort(keys(%{$files}))) { - my $filesrc = $files->{$filename}; - my $length = $filesrc->[0]; - my $dir = dirname($filename); - my $fd; - - if (!-d $dir) { - make_path($dir) or - die("Could not create directory $dir\n"); - } - - open($fd, ">", $filename) or - die("Could not create file $filename: $!\n"); - for (my $i = 0; $i < $length; $i++) { - print($fd "\n"); - } - close($fd); - } -} - -sub write_branches($$$$) -{ - my ($fd, $branches, $brhits, $iref) = @_; - my ($found, $hit) = (0, 0); - - # Line coverage data - foreach my $brdata (@$branches) { - my $brhit = $brhits->[$$iref++]; - my ($brline, $block, $branch) = @$brdata; - - $found++; - $hit++ if ($brhit ne "-" && $brhit > 0); - print($fd "BRDA:$brline,$block,$branch,$brhit\n"); - } - if ($found > 0) { - print($fd "BRF:$found\n"); - print($fd "BRH:$hit\n"); - } -} - -sub write_lines($$$$) -{ - my ($fd, $lines, $lnhist, $iref) = @_; - my ($found, $hit) = (0, 0); - - # Line coverage data - foreach my $line (@$lines) { - my $lnhit = $lnhist->[$$iref++]; - - $found++; - $hit++ if ($lnhit > 0); - print($fd "DA:$line,$lnhit\n"); - } - print($fd "LF:$found\n"); - print($fd "LH:$hit\n"); -} - -sub write_functions($$$$) -{ - my ($fd, $functions, $fnhits, $iref) = @_; - my ($found, $hit) = (0, 0); - - # Function coverage data - foreach my $fn (@$functions) { - my ($fnline, $fnname) = @$fn; - - print($fd "FN:$fnline,$fnname\n"); - } - foreach my $fn (@$functions) { - my ($fnline, $fnname) = @$fn; - my $fnhit = $fnhits->[$$iref++]; - - $found++; - $hit++ if ($fnhit > 0); - print($fd "FNDA:$fnhit,$fnname\n"); - } - print($fd "FNF:$found\n"); - print($fd "FNH:$hit\n"); -} - -sub write_filesrc($$$$$) -{ - my ($c, $fd, $filesrc, $hits, $iter) = @_; - my ($length, $lines, $functions, $branches) = @$filesrc; - my $do_ln = get_int($c, "lines.enabled"); - my $do_fn = get_int($c, "functions.enabled"); - my $do_br = get_int($c, "branches.enabled"); - - write_functions($fd, $functions, $hits->[1], \$iter->[1]) if ($do_fn); - write_branches($fd, $branches, $hits->[2], \$iter->[2]) if ($do_br); - write_lines($fd, $lines, $hits->[0], \$iter->[0]) if ($do_ln); -} - -sub write_info($$$$) -{ - my ($c, $filename, $src, $hits) = @_; - my $files = $src->[0]; - my $fd; - my %iters; - - foreach my $testname (keys(%{$hits})) { - $iters{$testname} = [ 0, 0, 0 ]; - } - - open($fd, ">", $filename) or die("Could not create $filename: $!\n"); - - foreach my $filename (sort(keys(%{$files}))) { - my $filesrc = $files->{$filename}; - - foreach my $testname (sort(keys(%{$hits}))) { - my $testhits = $hits->{$testname}; - my $iter = $iters{$testname}; - - print($fd "TN:$testname\n"); - print($fd "SF:$filename\n"); - - write_filesrc($c, $fd, $filesrc, $testhits, $iter); - - print($fd "end_of_record\n"); - } - } - - close($fd); -} - -sub get_hit_found($) -{ - my ($list) = @_; - my ($hit, $found) = (0, 0); - - foreach my $e (@$list) { - $hit++ if ($e ne "-" && $e > 0); - $found++; - } - return ($hit, $found); -} - -sub write_counts($$) -{ - my ($filename, $hits) = @_; - my $fd; - my (@tlnhits, @tfnhits, @tbrhits); - - foreach my $testname (keys(%{$hits})) { - my $testhits = $hits->{$testname}; - my ($lnhits, $fnhits, $brhits) = @$testhits; - - for (my $i = 0; $i < scalar(@$lnhits); $i++) { - $tlnhits[$i] += $lnhits->[$i]; - } - for (my $i = 0; $i < scalar(@$fnhits); $i++) { - $tfnhits[$i] += $fnhits->[$i]; - } - for (my $i = 0; $i < scalar(@$brhits); $i++) { - my $h = $brhits->[$i]; - - $h = 0 if ($h eq "-"); - $tbrhits[$i] += $h; - } - } - - open($fd, ">", $filename) or die("Could not create $filename: $!\n"); - print($fd join(" ", get_hit_found(\@tlnhits), get_hit_found(\@tfnhits), - get_hit_found(\@tbrhits))."\n"); - close($fd); -} - -# A branch hit value for a block that was not hit must be "-". A branch hit -# value for a block that was hit cannot be "-", but must be "0" if not hit. -sub sanitize_brhits($) -{ - my ($brhits) = @_; - my $block_hit = 0; - - foreach my $brhit_ref (@$brhits) { - if ($$brhit_ref ne "-" && $$brhit_ref > 0) { - $block_hit = 1; - last; - } - } - foreach my $brhit_ref (@$brhits) { - if (!$block_hit) { - $$brhit_ref = "-"; - } elsif ($$brhit_ref eq "-") { - $$brhit_ref = 0; - } - } -} - -# Ensure coverage rate interdependencies are met -sub sanitize_hits($$) -{ - my ($src, $hits) = @_; - my $files = $src->[0]; - - foreach my $hits (values(%{$hits})) { - my $brhits = $hits->[2]; - my $i = 0; - - foreach my $filename (sort(keys(%{$files}))) { - my $filesrc = $files->{$filename}; - my $branches = $filesrc->[3]; - my $lastblock; - my $lastline; - my @blist; - - foreach my $brdata (@$branches) { - my ($brline, $block, $branch) = @$brdata; - - if (!defined($lastblock) || - $block != $lastblock || - $brline != $lastline) { - sanitize_brhits(\@blist); - @blist = (); - $lastblock = $block; - $lastline = $brline; - } - push(@blist, \$brhits->[$i++]); - } - sanitize_brhits(\@blist); - } - } -} - -# Generate random coverage data -# -# returns: testname -> testhits -# testhits: [ lnhits, fnhits, brhits ] -# lnhits: [ ln1hit, ln2hit, ... ] -# lnhit: Number of times a line was hit by a specific test -# fnhits: [ fn1hit, fn2hit, ... ] -# fnhit: Number of times a function was hit by a specific test -# brhits: [ br1hit, br2hit, ... ] -# brhit: Number of times a branch was hit by a specific test -sub gen_hits($$) -{ - my ($c, $src) = @_; - my (@lnhits, @fnhits, @brhits); - my ($files, $numlns, $numfns, $numbrs) = @$src; - my $testnames = get_list($c, "tests.names", ""); - my %hits; - - $testnames = [ "" ] if (!@$testnames); - - foreach my $testname (@$testnames) { - my (@lnhits, @fnhits, @brhits); - - for (my $i = 0; $i < $numlns; $i++) { - push(@lnhits, 1 + int(rand($MAX_TAKEN))); - } - - for (my $i = 0; $i < $numfns; $i++) { - push(@fnhits, 1 + int(rand($MAX_TAKEN))); - } - - for (my $i = 0; $i < $numbrs; $i++) { - push(@brhits, 1 + int(rand($MAX_TAKEN))); - } - - $hits{$testname} = [ \@lnhits, \@fnhits, \@brhits ]; - } - - sanitize_hits($src, \%hits); - - return \%hits; -} - -# Return a hash containing RATE percent of indices [0..NUM-1]. -sub gen_filter($$) -{ - my ($num, $rate) = @_; - my @list = (0 .. ($num - 1)); - my %hash; - - reduce_list_per(\@list, $rate); - foreach my $i (@list) { - $hash{$i} = 1; - } - - return \%hash; -} - -# Zero all entries in LIST identified by the indices in FILTER. -sub zero_by_filter($$) -{ - my ($list, $filter) = @_; - - foreach my $i (keys(%{$filter})) { - $list->[$i] = 0; - } -} - -# Add a random number of indices between [0..NUM-1] to FILTER. -sub widen_filter($$) -{ - my ($filter, $num) = @_; - my @list; - - for (my $i = 0; $i < $num; $i++) { - push(@list, $i) if (!exists($filter->{$i})); - } - reduce_list_per(\@list, int(rand(101))); - - foreach my $i (@list) { - $filter->{$i} = 1; - } -} - -# Zero coverage data in HITS until the combined coverage rates reach the -# specified RATEs. -sub reduce_hits($$$$$) -{ - my ($src, $hits, $lnrate, $fnrate, $brrate) = @_; - my ($files, $numlns, $numfns, $numbrs) = @$src; - my ($lnfilter, $fnfilter, $brfilter); - - $lnfilter = gen_filter($numlns, 100 - $lnrate); - $fnfilter = gen_filter($numfns, 100 - $fnrate); - $brfilter = gen_filter($numbrs, 100 - $brrate); - - foreach my $testhits (values(%{$hits})) { - my ($lnhits, $fnhits, $brhits) = @$testhits; - - zero_by_filter($lnhits, $lnfilter); - zero_by_filter($fnhits, $fnfilter); - zero_by_filter($brhits, $brfilter); - - # Provide some variation between tests - widen_filter($lnfilter, $numlns); - widen_filter($fnfilter, $numfns); - widen_filter($brfilter, $numbrs); - } - - sanitize_hits($src, $hits); -} - -sub zero_list($) -{ - my ($list) = @_; - - foreach my $i (@$list) { - $i = 0; - } -} - -# Zero all coverage in HITS. -sub zero_hits($$) -{ - my ($src, $hits) = @_; - - foreach my $testhits (values(%{$hits})) { - my ($lnhits, $fnhits, $brhits) = @$testhits; - - zero_list($lnhits); - zero_list($fnhits); - zero_list($brhits); - } - - sanitize_hits($src, $hits); -} - -# Distribute items from LIST to A and B depending on whether the index for -# an item is found in FILTER. -sub split_by_filter($$$$) -{ - my ($list, $filter, $a, $b) = @_; - - for (my $i = 0; $i < scalar(@$list); $i++) { - if (exists($filter->{$i})) { - push(@$a, $list->[$i]); - push(@$b, 0); - } else { - push(@$a, 0); - push(@$b, $list->[$i]); - } - } -} - -sub split_hits($$$) -{ - my ($c, $src, $hits) = @_; - my ($files, $numlns, $numfns, $numbrs) = @$src; - my ($lnsplit, $fnsplit, $brsplit); - my (%a, %b); - - $lnsplit = gen_filter($numlns, int(rand(101))); - $fnsplit = gen_filter($numfns, int(rand(101))); - $brsplit = gen_filter($numbrs, int(rand(101))); - - foreach my $testname (keys(%{$hits})) { - my $testhits = $hits->{$testname}; - my ($lnhits, $fnhits, $brhits) = @$testhits; - my (@lnhitsa, @fnhitsa, @brhitsa); - my (@lnhitsb, @fnhitsb, @brhitsb); - - split_by_filter($lnhits, $lnsplit, \@lnhitsa, \@lnhitsb); - split_by_filter($fnhits, $fnsplit, \@fnhitsa, \@fnhitsb); - split_by_filter($brhits, $brsplit, \@brhitsa, \@brhitsb); - - $a{$testname} = [ \@lnhitsa, \@fnhitsa, \@brhitsa ]; - $b{$testname} = [ \@lnhitsb, \@fnhitsb, \@brhitsb ]; - } - - sanitize_hits($src, \%a); - sanitize_hits($src, \%b); - - return (\%a, \%b); -} - -sub plural($$$) -{ - my ($num, $sing, $plur) = @_; - - return $num <= 1 ? $sing : $plur; -} - -sub print_intro($) -{ - my ($c) = @_; - my $numtests = scalar(@{get_list($c, "tests.names")}); - my $numfiles = get_int($c, "files.numfiles"); - - $numtests = 1 if ($numtests < 1); - - print($BOLD."Creating coverage files ($numtests ". - plural($numtests, "test", "tests").", $numfiles ". - plural($numfiles, "source file", "source files").")\n".$RESET); -} - -sub main() -{ - my $opt_help; - my $opt_output; - my $opt_configfile; - my $opt_seed = 0; - my $c; - my $src; - my $hits; - my $root; - my $enum; - my ($a, $b); - - # Parse options - if (!GetOptions("output|o=s" => \$opt_output, - "seed=s" => \$opt_seed, - "help|h" => \$opt_help, - )) { - print(STDERR "Use $0 --help to get usage information\n"); - exit(2); - } - - if ($opt_help) { - usage(); - exit(0); - } - - $opt_configfile = shift(@ARGV); - if (!defined($opt_configfile)) { - print(STDERR "Please specify a config file\n"); - exit(2); - } - - if (defined($opt_output)) { - if (! -d $opt_output) { - mkdir($opt_output) or - die("$0: Could not create directory ". - "$opt_output: $!\n"); - } - $root = abs_path($opt_output) - } else { - $root = "/"; - } - - srand($opt_seed); - - # Get config - $c = read_config($opt_configfile); - apply_config($c, \@ARGV) if (@ARGV); - - print_intro($c); - # Show lines on STDOUT without newline - $| = 1; - - # Create source tree - print(" Source tree ......... "); - $src = gen_src($c, $root); - # Write out source code if requested - write_src($src) if (defined($opt_output)); - print("done ("); - print($src->[1]." lines, "); - print($src->[2]." functions, "); - print($src->[3]." branches)\n"); - - # Write out full-coverage data files - print(" Full coverage ....... "); - $hits = gen_hits($c, $src); - write_info($c, "full.info", $src, $hits); - write_counts("full.counts", $hits); - print("done\n"); - - # Write out data files with target coverage rates - print(" Target coverage ..... "); - reduce_hits($src, $hits, get_int($c, "lines.covered"), - get_int($c, "functions.covered"), - get_int($c, "branches.covered")); - write_info($c, "target.info", $src, $hits); - write_counts("target.counts", $hits); - print("done\n"); - - # Write out partial data files - print(" Partial coverage .... "); - ($a, $b) = split_hits($c, $src, $hits); - write_info($c, "part1.info", $src, $a); - write_counts("part1.counts", $a); - write_info($c, "part2.info", $src, $b); - write_counts("part2.counts", $b); - print("done\n"); - - # Write out zero-coverage data files - print(" Zero coverage ....... "); - zero_hits($src, $hits); - write_info($c, "zero.info", $src, $hits); - write_counts("zero.counts", $hits); - print("done\n"); -} - -main(); -exit(0); diff --git a/worker/deps/lcov/test/bin/norminfo b/worker/deps/lcov/test/bin/norminfo deleted file mode 100755 index 9fe0ef2f00..0000000000 --- a/worker/deps/lcov/test/bin/norminfo +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env perl -# -# Copyright IBM Corp. 2017 -# -# Usage: norminfo [] -# -# Normalize coverage data file (ensure stable order), perform some sanity -# checks, and apply optional multiplier to execution counts. -# - -use strict; -use warnings; - -sub ferr($$$) -{ - my ($pos, $filename, $msg) = @_; - - if (defined($pos)) { - $pos .= ":"; - } else { - $pos = ""; - } - - die("$0:$filename:$pos $msg"); -} - -sub print_sorted($$$) -{ - my ($fd, $info, $multi) = @_; - my (%fn, %fns, %fnda, %brda, %da); - my ($fnf, $fnh, $brf, $brh, $lf, $lh); - - while (my $line = <$fd>) { - $line =~ s/(^\s*|\s*$)//g; - - if ($line =~ /^end_of_record$/) { - last; - } elsif ($line =~ /^FN:(\d+),(.*)$/) { - my ($lineno, $fnname) = ($1, $2); - - if (exists($fn{$lineno})) { - ferr($., $info, "Duplicate FN: entry\n"); - } - $fn{$lineno} = $fnname; - if (exists($fns{$fnname})) { - ferr($., $info, "Duplicate function name\n"); - } - $fns{$fnname} = $lineno; - } elsif ($line =~ /^FNDA:(\d+),(.*)$/) { - my ($count, $fnname) = ($1, $2); - - if (exists($fnda{$fnname})) { - ferr($., $info, "Duplicate FNDA: entry\n"); - } - $fnda{$fnname} = int($count * $multi); - } elsif ($line =~ /^FNF:(\d+)$/) { - if (defined($fnf)) { - ferr($., $info, "Duplicate FNF: entry\n"); - } - $fnf = $1; - } elsif ($line =~ /^FNH:(\d+)$/) { - if (defined($fnh)) { - ferr($., $info, "Duplicate FNH: entry\n"); - } - $fnh = $1; - } elsif ($line =~ /^BRDA:(\d+),(\d+),(\d+),(\d+|-)$/) { - my ($lineno, $block, $branch, $count) = ($1, $2, $3, $4); - - if (exists($brda{$lineno}->{$block}->{$branch})) { - ferr($., $info, "Duplicate BRDA: entry\n"); - } - $count = int($count * $multi) if ($count ne "-"); - $brda{$lineno}->{$block}->{$branch} = $count; - - } elsif ($line =~ /^BRF:(\d+)$/) { - if (defined($brf)) { - ferr($., $info, "Duplicate BRF: entry\n"); - } - $brf = $1; - } elsif ($line =~ /^BRH:(\d+)$/) { - if (defined($brh)) { - ferr($., $info, "Duplicate BRH: entry\n"); - } - $brh = $1; - } elsif ($line =~ /^DA:(\d+),(\d+)$/) { - my ($lineno, $count) = ($1, $2); - - if (exists($da{$lineno})) { - ferr($., $info, "Duplicate FNDA: entry\n"); - } - $da{$lineno} = int($count * $multi); - } elsif ($line =~ /^LF:(\d+)$/) { - if (defined($lf)) { - ferr($., $info, "Duplicate LF: entry\n"); - } - $lf = $1; - } elsif ($line =~ /^LH:(\d+)$/) { - if (defined($lh)) { - ferr($., $info, "Duplicate LH: entry\n"); - } - $lh = $1; - } else { - ferr($., $info, "Unknown line: $line\n"); - } - } - - # FN:, - foreach my $lineno (sort({ $a <=> $b } keys(%fn))) { - my $fnname = $fn{$lineno}; - print("FN:$lineno,$fnname\n"); - } - - # FNDA:, - foreach my $fnname (keys(%fnda)) { - if (!exists($fns{$fnname})) { - ferr(undef, $info, "FNDA entry without FN: $fnname\n"); - } - } - foreach my $fnname (sort({ $fns{$a} <=> $fns{$b} } keys(%fnda))) { - my $count = $fnda{$fnname}; - print("FNDA:$count,$fnname\n"); - } - # FNF: - print("FNF:$fnf\n") if (defined($fnf)); - # FNH: - if (defined($fnh)) { - $fnh = 0 if ($multi == 0); - print("FNH:$fnh\n"); - } - # BRDA:,,, - foreach my $lineno (sort({ $a <=> $b } keys(%brda))) { - my $blocks = $brda{$lineno}; - - foreach my $block (sort({ $a <=> $b } keys(%{$blocks}))) { - my $branches = $blocks->{$block}; - - foreach my $branch (sort({ $a <=> $b } - keys(%{$branches}))) { - my $count = $branches->{$branch}; - - $count = "-" if ($multi == 0); - print("BRDA:$lineno,$block,$branch,$count\n"); - } - } - - } - # BRF: - print("BRF:$brf\n") if (defined($brf)); - # BRH: - if (defined($brh)) { - $brh = 0 if ($multi == 0); - print("BRH:$brh\n"); - } - # DA:, - foreach my $lineno (sort({ $a <=> $b } keys(%da))) { - my $count = $da{$lineno}; - - print("DA:$lineno,$count\n"); - } - # LF: - print("LF:$lf\n") if (defined($lf)); - # LH: - if (defined($lh)) { - $lh = 0 if ($multi == 0); - print("LH:$lh\n"); - } -} - -sub main() -{ - my $infofile = $ARGV[0]; - my $multi = $ARGV[1]; - # info: testname -> files - # files: infofile -> data - # data: [ starting offset, starting line ] - my %info; - my $fd; - my $tn = ""; - my %allfiles; - - $multi = 1 if (!defined($multi)); - if (!defined($infofile)) { - $infofile = "standard input"; - warn("$0: Reading data from standard input\n"); - open($fd, "<&STDIN") or - die("$0: Could not duplicated stdin: $!\n"); - } else { - open($fd, "<", $infofile) or - die("$0: Could not open $infofile: $!\n"); - } - - # Register starting positions of data sets - while (my $line = <$fd>) { - if ($line =~ /^TN:(.*)$/) { - $tn = $1; - } elsif ($line =~ /^SF:(.*)$/) { - my $sf = $1; - my $pos = tell($fd); - - die("$0: Could not get file position: $!\n") - if ($pos == -1); - if (exists($info{$tn}->{$sf})) { - ferr($., $infofile, - "Duplicate entry for $tn:$sf\n"); - } - $info{$tn}->{$sf} = [ $pos, $. ]; - $allfiles{$sf} = 1; - } - } - - # Print data sets in normalized order - foreach my $filename (sort(keys(%allfiles))) { - foreach my $testname (sort(keys(%info))) { - my $pos = $info{$testname}->{$filename}; - my ($cpos, $lpos) = @$pos; - - next if (!defined($pos)); - - if (seek($fd, $cpos, 0) != 1) { - die("$0: Could not seek in $infofile: $!\n"); - } - printf("TN:$testname\n"); - printf("SF:$filename\n"); - - $. = $lpos; - print_sorted($fd, $infofile, $multi); - - printf("end_of_record\n"); - - } - } - foreach my $testname (sort(keys(%info))) { - my $files = $info{$testname}; - - foreach my $filename (sort(keys(%{$files}))) { - } - } - - close($fd); -} - -main(); -exit(0); diff --git a/worker/deps/lcov/test/bin/test_run b/worker/deps/lcov/test/bin/test_run deleted file mode 100755 index 23e69d0f4e..0000000000 --- a/worker/deps/lcov/test/bin/test_run +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright IBM Corp. 2017 -# -# Usage: test_run -# -# Announce a test case, run it, and record the resulting output in the -# test log file. Must be run after testsuite_init. -# - -TOPDIR=$(realpath $(dirname $0)/..) && source "$TOPDIR/bin/common" -EXCERPTLEN=10 -TESTNAME="$1" -shift - -TIME=$(which time 2>/dev/null) -if [ ! -z "$TIME" ] ; then - TIME="$TIME -v -o $TIMEFILE" - if ! $TIME true 2>/dev/null ; then - TIME="" - fi -fi - -t_announce "$TESTNAME" - -let POS=$(stat -c %s "$LOGFILE")+1 - -t_detail "COMMAND" "\"$*\"" >>"$LOGFILE" -t_detail "OUTPUT" "" >>"$LOGFILE" - -# Run command -$TIME bash -c "$*" 2>&1 | t_indent >>"$LOGFILE" -RC=$? - -# Evaluate output of time command -ELAPSED= -RESIDENT= -SIGNAL= -if [ ! -z "$TIME" ] ; then - while read LINE ; do - case "$LINE" in - "Command terminated by signal"*) SIGNAL=${LINE##* } ;; - "Elapsed"*) ELAPSED=$(elapsed_to_ms ${LINE##* }) ;; - "Maximum resident"*) RESIDENT=${LINE##* } ;; - "Exit status"*) RC=${LINE##* } ;; - esac - done < "$TIMEFILE" - rm -f "$TIMEFILE" -fi - -t_detail "EXITCODE" "$RC" >>"$LOGFILE" - -# Show result -if [ $RC -eq 0 -a -z "$SIGNAL" ] ; then - RESULT="pass" - t_pass "$TESTNAME" -else - if [ -z "$SIGNAL" ] ; then - RESULT="fail" - t_fail "$TESTNAME" - else - RESULT="kill" - t_kill "$TESTNAME" - fi -fi - -if [ ! -z "$SIGNAL" ] ; then - t_detail "SIGNAL" "$SIGNAL" >>"$LOGFILE" -fi - -if [ ! -z "$ELAPSED" ] ; then - echo -n " (time $(($ELAPSED/1000)).$(($ELAPSED%1000/100))s, " - echo "elapsed $TESTNAME $ELAPSED" >> "$COUNTFILE" -fi - -if [ ! -z "$RESIDENT" ] ; then - echo -n "mem $(($RESIDENT/1024)).$((($RESIDENT%1024)/100))MB)" - echo "resident $TESTNAME $RESIDENT" >> "$COUNTFILE" -fi - -echo - -# Show log excerpt on failure or if requested -if [ $RC -ne 0 -o "$V" == "1" ] ; then - LEN=$(tail -c "+$POS" "$LOGFILE" | wc -l) - if [ "$LEN" -gt "$EXCERPTLEN" -a "$V" != "1" ] ; then - tail -c "+$POS" "$LOGFILE" | head -n $EXCERPTLEN | t_indent - let LEN=$LEN-$EXCERPTLEN - echo " ..." - echo " Skipping $LEN more lines (see $LOGFILE)" - else - tail -c "+$POS" "$LOGFILE" | t_indent - fi -fi - -# Log more details -[ ! -z "$ELAPSED" ] && t_detail "TIME" "${ELAPSED}ms" >>"$LOGFILE" -[ ! -z "$RESIDENT" ] && t_detail "MEM" "${RESIDENT}kB" >>"$LOGFILE" -t_detail "RESULT" "$RESULT" >> "$LOGFILE" diff --git a/worker/deps/lcov/test/bin/test_skip b/worker/deps/lcov/test/bin/test_skip deleted file mode 100755 index 202606f4f9..0000000000 --- a/worker/deps/lcov/test/bin/test_skip +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright IBM Corp. 2017 -# -# Usage: test_skip -# -# Announce and record that a single test case was skipped, including an -# optional reason text. Must be run after testsuite_init. -# - -TOPDIR=$(realpath $(dirname $0)/..) && source "$TOPDIR/bin/common" -TESTNAME="$1" -REASON="${*:2}" ; [ -z "$REASON" ] && REASON="" - -t_announce "$TESTNAME" -t_skip "$TESTNAME" -echo -t_detail "REASON" "$REASON" >>"$LOGFILE" -t_detail "REASON" "$REASON" | t_indent diff --git a/worker/deps/lcov/test/bin/testsuite_exit b/worker/deps/lcov/test/bin/testsuite_exit deleted file mode 100755 index 6720df99f2..0000000000 --- a/worker/deps/lcov/test/bin/testsuite_exit +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright IBM Corp. 2017 -# -# Usage: testsuite_exit -# -# Announce end of test suite and show aggregate results. -# - -TOPDIR=$(realpath $(dirname $0)/..) && source "$TOPDIR/bin/common" - -echo "end_time $(date +%s.%N)" >>"$COUNTFILE" - -SUCCESS=0 -FAILED=0 -SKIPPED=0 -TOTAL_TIME=0 -TOTAL_MEM=0 -HAVE_EXT=0 - -# Get results -while read LINE ; do - set -- $LINE - case "$1" in - start_time) START_TIME=$2 ;; - end_time) END_TIME=$2 ;; - pass) let SUCCESS=$SUCCESS+1 ;; - fail) let FAILED=$FAILED+1 ;; - skip) let SKIPPED=$SKIPPED+1 ;; - elapsed) let TOTAL_TIME=$TOTAL_TIME+$3 ; HAVE_EXT=1 ;; - resident) let TOTAL_MEM=$TOTAL_MEM+$3 ; HAVE_EXT=1 ;; - esac -done < "$COUNTFILE" - -exec 3>&1 -exec >>"$LOGFILE" 2>&1 - -t_marker -t_detail "DATE" "$(t_timestamp)" - -let TOTAL=$SUCCESS+$SKIPPED+$FAILED -t_detail "EXECUTED" "$TOTAL" -t_detail "PASSED" "$SUCCESS" -t_detail "FAILED" "$FAILED" -t_detail "SKIPPED" "$SKIPPED" -[ $HAVE_EXT -eq 1 ] && t_detail "TIME" "${TOTAL_TIME}ms" -[ $HAVE_EXT -eq 1 ] && t_detail "MEM" "${TOTAL_MEM}kB" - -TOTAL_TIME=$(($TOTAL_TIME/1000)).$(($TOTAL_TIME%1000/100)) -TOTAL_MEM=$(($TOTAL_MEM/1024)).$((($TOTAL_MEM%1024)/100)) -TOTAL="$BOLD$TOTAL tests executed$RESET" -PASS="$SUCCESS passed" -FAIL="$FAILED failed" -SKIP="$SKIPPED skipped" -TIME="time ${TOTAL_TIME}s" -MEM="mem ${TOTAL_MEM}MB" - -[ "$SUCCESS" -gt 0 ] && PASS="$GREEN$PASS$DEFAULT" -[ "$FAILED" -gt 0 ] && FAIL="$RED$FAIL$DEFAULT" -[ "$SKIPPED" -gt 0 ] && SKIP="$BLUE$SKIP$DEFAULT" - -echo -en "$TOTAL, $PASS, $FAIL, $SKIP$RESET" >&3 -[ $HAVE_EXT -eq 1 ] && echo -n " ($TIME, $MEM)" >&3 -echo >&3 -echo "Result log stored in $LOGFILE" >&3 - -if [ "$FAILED" -gt 0 ] ; then - exit 1 -fi - -exit 0 diff --git a/worker/deps/lcov/test/bin/testsuite_init b/worker/deps/lcov/test/bin/testsuite_init deleted file mode 100755 index f901e35f13..0000000000 --- a/worker/deps/lcov/test/bin/testsuite_init +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright IBM Corp. 2017 -# -# Usage: testsuite_init -# -# Announce start of test suite and prepare log files. -# - -TOPDIR=$(realpath $(dirname $0)/..) && source "$TOPDIR/bin/common" - -echo -e $BOLD"Starting tests"$RESET -echo "start_time $(date +%s.%N)" >"$COUNTFILE" -exec >"$LOGFILE" 2>&1 - -t_detail "DATE" "$(t_timestamp)" - -t_detail "LCOV" "" -lcov --version 2>&1 | t_indent - -t_detail "GCOV" "" -gcov --version 2>&1 | t_indent - -t_detail "CPUINFO" "" -t_indent < /proc/cpuinfo - -t_detail "MEMINFO" "" -t_indent < /proc/meminfo diff --git a/worker/deps/lcov/test/common.mak b/worker/deps/lcov/test/common.mak deleted file mode 100644 index 55f31eb99c..0000000000 --- a/worker/deps/lcov/test/common.mak +++ /dev/null @@ -1,50 +0,0 @@ -TOPDIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST)))) -TESTDIR := $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) -PARENTDIR := $(dir $(patsubst %/,%,$(TOPDIR))) -RELDIR := $(TESTDIR:$(PARENTDIR)%=%) -ZEROINFO := $(TOPDIR)zero.info -ZEROCOUNTS := $(TOPDIR)zero.counts -FULLINFO := $(TOPDIR)full.info -FULLCOUNTS := $(TOPDIR)full.counts -TARGETINFO := $(TOPDIR)target.info -TARGETCOUNTS := $(TOPDIR)target.counts -PART1INFO := $(TOPDIR)part1.info -PART1COUNTS := $(TOPDIR)part1.counts -PART2INFO := $(TOPDIR)part2.info -PART2COUNTS := $(TOPDIR)part2.counts -INFOFILES := $(ZEROINFO) $(FULLINFO) $(TARGETINFO) $(PART1INFO) $(PART2INFO) -COUNTFILES := $(ZEROCOUNTS) $(FULLCOUNTS) $(TARGETCOUNTS) $(PART1COUNTS) \ - $(PART2COUNTS) -LCOVRC := $(TOPDIR)lcovrc -LCOVFLAGS := --config-file $(LCOVRC) -SIZE := small -CC := gcc - -export LCOV := lcov $(LCOVFLAGS) -export GENHTML := genhtml $(LCOVFLAGS) -export PATH := $(TOPDIR)/../bin:$(TOPDIR)/bin:$(PATH) -export LANG := C - -all: prepare init test exit - -init: - testsuite_init - -exit: - testsuite_exit - -prepare: $(INFOFILES) $(COUNTFILES) - -clean: clean_common - -clean_common: - echo " CLEAN $(patsubst %/,%,$(RELDIR))" - -$(INFOFILES) $(COUNTFILES): - cd $(TOPDIR) && mkinfo profiles/$(SIZE) -o src/ - -ifneq ($(V),2) -.SILENT: -endif - -.PHONY: all init exit prepare clean clean_common diff --git a/worker/deps/lcov/test/genhtml_output/Makefile b/worker/deps/lcov/test/genhtml_output/Makefile deleted file mode 100644 index 0fbd882670..0000000000 --- a/worker/deps/lcov/test/genhtml_output/Makefile +++ /dev/null @@ -1,31 +0,0 @@ -include ../common.mak - -GENHTML_TEST := ./genhtml_test - -TESTS := genhtml_output_zero genhtml_output_full genhtml_output_target \ - genhtml_output_part1 genhtml_output_part2 genhtml_output_combined - -test: $(TESTS) - -genhtml_output_zero: - @test_run genhtml_output_zero $(GENHTML) $(ZEROINFO) -o out_zero/ - -genhtml_output_full: - @test_run genhtml_output_full $(GENHTML) $(FULLINFO) -o out_full/ - -genhtml_output_target: - @test_run genhtml_output_target $(GENHTML) $(TARGETINFO) -o out_target/ - -genhtml_output_part1: - @test_run genhtml_output_part1 $(GENHTML) $(PART1INFO) -o out_part1/ - -genhtml_output_part2: - @test_run genhtml_output_part2 $(GENHTML) $(PART2INFO) -o out_part2/ - -genhtml_output_combined: genhtml_output_target - @test_run genhtml_output_combined $(GENHTML_TEST) $(TARGETINFO) $(PART1INFO) $(PART2INFO) - -clean: - rm -rf out_*/ - -.PHONY: test $(TESTS) clean diff --git a/worker/deps/lcov/test/genhtml_output/genhtml_test b/worker/deps/lcov/test/genhtml_output/genhtml_test deleted file mode 100755 index 0b0f834918..0000000000 --- a/worker/deps/lcov/test/genhtml_output/genhtml_test +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright IBM Corp. 2017 -# -# Usage: genhtml_test [...] -# -# Compare genhtml output of a reference coverage data file with that of -# a combination of multiple files. -# - -function die() -{ - echo "Error: $@" >&2 - exit 1 -} - -GENHTMLFLAGS="-t title" -REFFILE=$1 -shift - -if [ -z "$REFFILE" -o -z "$*" ] ; then - echo "Usage: $0 [...]" >&2 - exit 2 -fi - -OUTREF="out_$(basename $REFFILE .info)" -OUTCOMBINED="out_combined" - -$GENHTML $GENHTMLFLAGS "$REFFILE" -o "$OUTREF" || \ - die "Could not generate HTML for reference file" - -$GENHTML $GENHTMLFLAGS "$@" -o "$OUTCOMBINED" || \ - die "Could not generate HTML for combined files" - -diff -ur "$OUTREF" "$OUTCOMBINED" -I "headerValue" || \ - die "Mismatch in generated output" diff --git a/worker/deps/lcov/test/lcov_add_files/Makefile b/worker/deps/lcov/test/lcov_add_files/Makefile deleted file mode 100644 index 87937a1558..0000000000 --- a/worker/deps/lcov/test/lcov_add_files/Makefile +++ /dev/null @@ -1,47 +0,0 @@ -include ../common.mak - -ADDTEST := ./add_test - -TESTS := lcov_add_zero lcov_add_zero2 lcov_add_full lcov_add_full2 \ - lcov_add_part lcov_add_part2 lcov_add_concatenated4 - - -test: $(TESTS) - -lcov_add_zero: - # Add single zero coverage file - output should be same as input - test_run lcov_add_zero $(ADDTEST) 1 "$(ZEROINFO)" "$(ZEROINFO)" - -lcov_add_zero2: - # Add two zero coverage files - output should be same as input - test_run lcov_add_zero2 $(ADDTEST) 1 "$(ZEROINFO)" "$(ZEROINFO)" "$(ZEROINFO)" - -lcov_add_full: - # Add single 100% coverage file - output should be same as input - test_run lcov_add_full $(ADDTEST) 1 "$(FULLINFO)" "$(FULLINFO)" - -lcov_add_full2: - # Add two 100% coverage file and reduce counts to 1/2 - output should - # be same as input - test_run lcov_add_full2 $(ADDTEST) 0.5 "$(FULLINFO)" "$(FULLINFO)" "$(FULLINFO)" - -lcov_add_part: - # Add single coverage file with random coverage rate - output should - # be same as input - test_run lcov_add_part $(ADDTEST) 1 "$(PART1INFO)" "$(PART1INFO)" - -lcov_add_part2: - # Add two coverage files that were split from target file - output - # should be same as target file - test_run lcov_add_part2 $(ADDTEST) 1 "$(TARGETINFO)" "$(PART1INFO)" "$(PART2INFO)" - -lcov_add_concatenated4: - # Add coverage file that consists of 4 concatenation of target files - # and reduce counts to 1/4 - output should be the same as input - cat $(TARGETINFO) $(TARGETINFO) $(TARGETINFO) $(TARGETINFO) >concatenated.info - test_run lcov_add_concatenated4 $(ADDTEST) 0.25 $(TARGETINFO) concatenated.info - -clean: - rm -f *.info - -.PHONY: test $(TESTS) clean diff --git a/worker/deps/lcov/test/lcov_add_files/add_test b/worker/deps/lcov/test/lcov_add_files/add_test deleted file mode 100755 index 4ff5ffeb6c..0000000000 --- a/worker/deps/lcov/test/lcov_add_files/add_test +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright IBM Corp. 2017 -# -# Usage: add_test [...] -# -# Add multiple coverage data files, normalize the output and multiply counts -# with multiplier. Compare against reference file. Report deviations. -# - -MULTI=$1 -REFFILE=$2 -shift 2 - -ADD= -for INFO in $* ; do - ADD="$ADD -a $INFO" -done - -if [ -z "$MULTI" -o -z "$REFFILE" -o -z "$ADD" ] ; then - echo "Usage: $0 [...]" >&2 - exit 1 -fi - -OUTFILE="add_"$(basename "$REFFILE") -SORTFILE="norm_$OUTFILE" - -set -x - -echo "Adding files..." -if ! $LCOV $ADD -o "$OUTFILE" ; then - echo "Error: lcov returned with non-zero exit code $?" >&2 - exit 1 -fi - -echo "Normalizing result..." -if ! norminfo "$OUTFILE" "$MULTI" > "$SORTFILE" ; then - echo "Error: Normalization of lcov result file failed" >&2 - exit 1 -fi - -echo "Comparing with reference..." -if ! diff -u "$REFFILE" "$SORTFILE" ; then - echo "Error: Result of combination differs from reference file" >&2 - exit 1 -fi diff --git a/worker/deps/lcov/test/lcov_diff/Makefile b/worker/deps/lcov/test/lcov_diff/Makefile deleted file mode 100644 index d2d4dd6621..0000000000 --- a/worker/deps/lcov/test/lcov_diff/Makefile +++ /dev/null @@ -1,9 +0,0 @@ -include ../common.mak - -test: - test_run lcov_diff_apply ./diff_test - -clean: - make -C old clean - make -C new clean - rm -f *.info diff diff --git a/worker/deps/lcov/test/lcov_diff/diff_test b/worker/deps/lcov/test/lcov_diff/diff_test deleted file mode 100755 index e0f8c0b308..0000000000 --- a/worker/deps/lcov/test/lcov_diff/diff_test +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright IBM Corp. 2017 -# -# Usage: diff_test -# -# Check lcov's diff function: -# - Compile two slightly different test programs -# - Run the programs and collect coverage data -# - Generate a patch containing the difference between the source code -# - Apply the patch to the coverage data -# - Compare the resulting patched coverage data file with the data from the -# patched source file -# - -function die() -{ - echo "Error: $@" >&2 - exit 1 -} - -make -C old || die "Failed to compile old source" -make -C new || die "Failed to compile new source" -diff -u $PWD/old/prog.c $PWD/new/prog.c > diff - -$LCOV --diff old/prog.info diff --convert-filenames -o patched.info -t bla || \ - die "Failed to apply patch to coverage data file" -norminfo new/prog.info > new_normalized.info -norminfo patched.info > patched_normalized.info -sed -i -e 's/^TN:.*$/TN:/' patched_normalized.info - -diff -u patched_normalized.info new_normalized.info || \ - die "Mismatch in patched coverage data file" - -echo "Patched coverage data file matches expected file" diff --git a/worker/deps/lcov/test/lcov_diff/new/Makefile b/worker/deps/lcov/test/lcov_diff/new/Makefile deleted file mode 100644 index 51005c71dd..0000000000 --- a/worker/deps/lcov/test/lcov_diff/new/Makefile +++ /dev/null @@ -1,17 +0,0 @@ -prog.info: - -include ../../common.mak - -prog.info: prog.gcda - $(LCOV) -c -d . -o prog.info - -prog.gcda: prog - ./prog || true - -prog: prog.c - $(CC) prog.c -o prog --coverage - -clean: - rm -f prog prog.gcda prog.gcno prog.info - -.PHONY: all clean diff --git a/worker/deps/lcov/test/lcov_diff/new/prog.c b/worker/deps/lcov/test/lcov_diff/new/prog.c deleted file mode 100644 index 6f4607cc4f..0000000000 --- a/worker/deps/lcov/test/lcov_diff/new/prog.c +++ /dev/null @@ -1,41 +0,0 @@ - - - -int fn(int x) -{ - switch (x) { - case -1: return 0; - - - case 0: return 2; - case 2: return 3; - - - case 12: return 7; - default: return 255; - } - - - -} - -int fn2() -{ - - - return 7; -} - - - -int main(int argc, char *argv[]) -{ - - - if (argc > 1) - return fn(argc); - - return fn2(); - - -} diff --git a/worker/deps/lcov/test/lcov_diff/old/Makefile b/worker/deps/lcov/test/lcov_diff/old/Makefile deleted file mode 100644 index 51005c71dd..0000000000 --- a/worker/deps/lcov/test/lcov_diff/old/Makefile +++ /dev/null @@ -1,17 +0,0 @@ -prog.info: - -include ../../common.mak - -prog.info: prog.gcda - $(LCOV) -c -d . -o prog.info - -prog.gcda: prog - ./prog || true - -prog: prog.c - $(CC) prog.c -o prog --coverage - -clean: - rm -f prog prog.gcda prog.gcno prog.info - -.PHONY: all clean diff --git a/worker/deps/lcov/test/lcov_diff/old/prog.c b/worker/deps/lcov/test/lcov_diff/old/prog.c deleted file mode 100644 index a4eda25557..0000000000 --- a/worker/deps/lcov/test/lcov_diff/old/prog.c +++ /dev/null @@ -1,22 +0,0 @@ -int fn(int x) -{ - switch (x) { - case -1: return 0; - case 0: return 2; - case 2: return 3; - case 12: return 7; - default: return 255; - } -} - -int fn2() -{ - return 7; -} - -int main(int argc, char *argv[]) -{ - if (argc > 1) - return fn(argc); - return fn2(); -} diff --git a/worker/deps/lcov/test/lcov_misc/Makefile b/worker/deps/lcov/test/lcov_misc/Makefile deleted file mode 100644 index d3bcc4ab28..0000000000 --- a/worker/deps/lcov/test/lcov_misc/Makefile +++ /dev/null @@ -1,5 +0,0 @@ -include ../common.mak - -test: - @test_run lcov_version lcov --version - @test_run lcov_help lcov --help diff --git a/worker/deps/lcov/test/lcov_summary/Makefile b/worker/deps/lcov/test/lcov_summary/Makefile deleted file mode 100644 index f48d0bc766..0000000000 --- a/worker/deps/lcov/test/lcov_summary/Makefile +++ /dev/null @@ -1,41 +0,0 @@ -include ../common.mak - -CHECK := ./check_counts -TESTS := lcov_summary_zero lcov_summary_full lcov_summary_target \ - lcov_summary_part1 lcov_summary_part2 lcov_summary_concatenated \ - lcov_summary_concatenated2 - -test: $(TESTS) - -lcov_summary_zero: - # Compare output of lcov --summary with generated counts - test_run lcov_summary_zero $(CHECK) $(ZEROCOUNTS) $(ZEROINFO) -lcov_summary_full: - # Compare output of lcov --summary with generated counts - test_run lcov_summary_full $(CHECK) $(FULLCOUNTS) $(FULLINFO) -lcov_summary_target: - # Compare output of lcov --summary with generated counts - test_run lcov_summary_target $(CHECK) $(TARGETCOUNTS) $(TARGETINFO) -lcov_summary_part1: - # Compare output of lcov --summary with generated counts - test_run lcov_summary_part1 $(CHECK) $(PART1COUNTS) $(PART1INFO) -lcov_summary_part2: - # Compare output of lcov --summary with generated counts - test_run lcov_summary_part2 $(CHECK) $(PART2COUNTS) $(PART2INFO) -lcov_summary_concatenated: - # Compare output of lcov --summary with generated counts for a - # concatenated coverage data file - cat $(TARGETINFO) $(TARGETINFO) > concatenated.info - test_run lcov_summary_concatenated $(CHECK) $(TARGETCOUNTS) concatenated.info -lcov_summary_concatenated2: - # Compare output of lcov --summary with generated counts for a - # concatenated coverage data file (part1+part2=target) - cat $(PART1INFO) $(PART2INFO) > concatenated2.info - test_run lcov_summary_concatenated2 $(CHECK) $(TARGETCOUNTS) concatenated2.info - - - -clean: - rm -f *.info - -.PHONY: test $(TESTS) clean diff --git a/worker/deps/lcov/test/lcov_summary/check_counts b/worker/deps/lcov/test/lcov_summary/check_counts deleted file mode 100755 index 32d454230c..0000000000 --- a/worker/deps/lcov/test/lcov_summary/check_counts +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env perl -# -# Copyright IBM Corp. 2017 -# -# Usage: check_counts -# -# Compare the output of "lcov --summary" for with the -# coverage data counts specified in . This file has the following -# format (all in a single line): -# -# lnhit lnfound fnhit fnfound brhit brfound2 -# - -use strict; -use warnings; - -sub do_cmp($$$) -{ - my ($title, $a, $b) = @_; - - if ($a == $b) { - print("$title: $a == $b\n"); - return 0; - } else { - print("$title: $a != $b => mismatch!\n"); - return 1; - } -} - -my $lcov = $ENV{"LCOV"}; -my ($counts, $info) = @ARGV; -my $fd; -my $cmdline; -my ($lnhit, $lnfound, $fnhit, $fnfound, $brhit, $brfound) = (0, 0, 0, 0, 0, 0); -my ($lnhit2, $lnfound2, $fnhit2, $fnfound2, $brhit2, $brfound2); -my $rc = 0; - -die("$0: LCOV environment variable not defined\n") if (!defined($lcov)); -if (!defined($counts) || !defined($info)) { - die("Usage: $0 \n"); -} - -$cmdline = "$lcov --summary $info"; -open($fd, "-|", $cmdline) or die("$0: Could not run $cmdline: $!\n"); -while (<$fd>) { - ($lnhit, $lnfound) = ($1, $2) if (/(\d+) of (\d+) lines/); - ($fnhit, $fnfound) = ($1, $2) if (/(\d+) of (\d+) functions/); - ($brhit, $brfound) = ($1, $2) if (/(\d+) of (\d+) branches/); -} -close($fd); - -die("$0: Non-zero result code ($?) of command: $cmdline\n") if ($? != 0); - -open($fd, "<", $counts) or die("$0: Could not open $counts: $!\n"); -if (<$fd> !~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/) { - die("$0: Invalid count file: $counts\n"); -} -($lnhit2, $lnfound2, $fnhit2, $fnfound2, $brhit2, $brfound2) = - ($1, $2, $3, $4, $5, $6); -close($fd); - -print("Comparing --summary output for $info and $counts:\n"); -$rc |= do_cmp("line hit", $lnhit, $lnhit2); -$rc |= do_cmp("line found", $lnfound, $lnfound2); -$rc |= do_cmp("functions hit", $fnhit, $fnhit2); -$rc |= do_cmp("functions found", $fnfound, $fnfound2); -$rc |= do_cmp("branches hit", $brhit, $brhit2); -$rc |= do_cmp("branches found", $brfound, $brfound2); - -exit($rc); diff --git a/worker/deps/lcov/test/lcovrc b/worker/deps/lcov/test/lcovrc deleted file mode 100644 index 5005f637d5..0000000000 --- a/worker/deps/lcov/test/lcovrc +++ /dev/null @@ -1,4 +0,0 @@ -# lcovrc file used during tests - -lcov_function_coverage = 1 -lcov_branch_coverage = 1 diff --git a/worker/deps/lcov/test/profiles/large b/worker/deps/lcov/test/profiles/large deleted file mode 100644 index 31788b0409..0000000000 --- a/worker/deps/lcov/test/profiles/large +++ /dev/null @@ -1,51 +0,0 @@ -# Profile of a large source code project. Use with mkinfo to generate fake test -# source code and coverage data. - -[tests] -# List of test names -names = test1 test2 - -[files] -# Create this many files -numfiles = 500 -# Generate paths from these components (top/sub/subsub/prefix_suffix.ext) -top = lib tools test bin img scripts -sub = build debug release include target sys config -subsub = work www utils gui info log basic -prefix = main misc report tune mem list -suffix = a b c top work proto final fast -ext = .c .h - -[lines] -# Generate line coverage data -enabled = 1 -# Line coverage rate -covered = 80 -# Percentage of lines covered -instrumented = 80 -# Maximum number of lines per file -maxlines = 2000 - -[functions] -# Generate function coverage data -enabled = 1 -# Function coverage rate -covered = 60 -# Percent of instrumented lines containing function definitions -perinstrumented = 10 -# Generate function names from these components (verb_adj_noun) -verb = get set find read write stat add sub combine -adj = first last best min max avg -noun = bit byte file str num obj data - -[branches] -# Generate branch coverage data -enabled = 1 -# Branch coverage rate -covered = 20 -# Percent of instrumented lines containing branches -perinstrumented = 5 -# List of blocks to use -blocks = 0 4294967295 -# Distribution of number of branches per block (num:probability) -branchdist = 2:50 3:25 5:20 100:5 diff --git a/worker/deps/lcov/test/profiles/medium b/worker/deps/lcov/test/profiles/medium deleted file mode 100644 index 56598e8682..0000000000 --- a/worker/deps/lcov/test/profiles/medium +++ /dev/null @@ -1,51 +0,0 @@ -# Profile of a medium-sized source code project. Use with mkinfo to generate -# fake test source code and coverage data. - -[tests] -# List of test names -names = test1 test2 test3 - -[files] -# Create this many files -numfiles = 50 -# Generate paths from these components (top/sub/subsub/prefix_suffix.ext) -top = lib tools test bin img scripts -sub = build debug release include target sys config -subsub = work www utils gui info log basic -prefix = main misc report tune mem list -suffix = a b c top work proto final fast -ext = .c .h - -[lines] -# Generate line coverage data -enabled = 1 -# Line coverage rate -covered = 80 -# Percentage of lines covered -instrumented = 50 -# Maximum number of lines per file -maxlines = 1000 - -[functions] -# Generate function coverage data -enabled = 1 -# Function coverage rate -covered = 60 -# Percent of instrumented lines containing function definitions -perinstrumented = 5 -# Generate function names from these components (verb_adj_noun) -verb = get set find read write stat add sub combine -adj = first last best min max avg -noun = bit byte file str num obj data - -[branches] -# Generate branch coverage data -enabled = 1 -# Branch coverage rate -covered = 20 -# Percent of instrumented lines containing branches -perinstrumented = 50 -# List of blocks to use -blocks = 0 4294967295 -# Distribution of number of branches per block (num:probability) -branchdist = 2:50 3:50 diff --git a/worker/deps/lcov/test/profiles/small b/worker/deps/lcov/test/profiles/small deleted file mode 100644 index 388d2a3bb5..0000000000 --- a/worker/deps/lcov/test/profiles/small +++ /dev/null @@ -1,51 +0,0 @@ -# Profile of a small source code project. Use with mkinfo to generate fake test -# source code and coverage data. - -[tests] -# List of test names -names = test1 test2 - -[files] -# Create this many files -numfiles = 5 -# Generate paths from these components (top/sub/subsub/prefix_suffix.ext) -top = lib tools test bin img scripts -sub = build debug release include target sys config -subsub = work www utils gui info log basic -prefix = main misc report tune mem list -suffix = a b c top work proto final fast -ext = .c .h - -[lines] -# Generate line coverage data -enabled = 1 -# Line coverage rate -covered = 80 -# Percentage of lines covered -instrumented = 50 -# Maximum number of lines per file -maxlines = 500 - -[functions] -# Generate function coverage data -enabled = 1 -# Function coverage rate -covered = 60 -# Percent of instrumented lines containing function definitions -perinstrumented = 5 -# Generate function names from these components (verb_adj_noun) -verb = get set find read write stat add sub combine -adj = first last best min max avg -noun = bit byte file str num obj data - -[branches] -# Generate branch coverage data -enabled = 1 -# Branch coverage rate -covered = 20 -# Percent of instrumented lines containing branches -perinstrumented = 50 -# List of blocks to use -blocks = 0 4294967295 -# Distribution of number of branches per block (num:probability) -branchdist = 2:50 3:45 50:5 diff --git a/worker/deps/libwebrtc/libwebrtc/api/transport/network_types.cc b/worker/deps/libwebrtc/libwebrtc/api/transport/network_types.cc index d0a0c4a05f..88b67b3a47 100644 --- a/worker/deps/libwebrtc/libwebrtc/api/transport/network_types.cc +++ b/worker/deps/libwebrtc/libwebrtc/api/transport/network_types.cc @@ -13,8 +13,7 @@ #include namespace webrtc { -// TODO(srte): Revert to using default after removing union member. -StreamsConfig::StreamsConfig() {} +StreamsConfig::StreamsConfig() = default; StreamsConfig::StreamsConfig(const StreamsConfig&) = default; StreamsConfig::~StreamsConfig() = default; diff --git a/worker/deps/libwebrtc/libwebrtc/api/transport/network_types.h b/worker/deps/libwebrtc/libwebrtc/api/transport/network_types.h index 20ab6aaba7..0604b6d383 100644 --- a/worker/deps/libwebrtc/libwebrtc/api/transport/network_types.h +++ b/worker/deps/libwebrtc/libwebrtc/api/transport/network_types.h @@ -34,9 +34,7 @@ struct StreamsConfig { Timestamp at_time = Timestamp::PlusInfinity(); absl::optional requests_alr_probing; absl::optional pacing_factor; - union { - absl::optional min_total_allocated_bitrate = absl::nullopt; - }; + absl::optional min_total_allocated_bitrate = absl::nullopt; absl::optional max_padding_rate; absl::optional max_total_allocated_bitrate; }; diff --git a/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/alr_detector.cc b/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/alr_detector.cc index 95a000d346..eb03fae118 100644 --- a/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/alr_detector.cc +++ b/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/alr_detector.cc @@ -62,6 +62,11 @@ AlrDetector::AlrDetector( experiment_settings ? experiment_settings->alr_stop_budget_level_percent / 100.0 : kDefaultStopBudgetLevelRatio), + alr_timeout_( + "alr_timeout", + experiment_settings + ? experiment_settings->alr_timeout + : kDefaultAlrTimeout), alr_budget_(0, true) { ParseFieldTrial({&bandwidth_usage_ratio_, &start_budget_level_ratio_, &stop_budget_level_ratio_}, @@ -73,7 +78,7 @@ AlrDetector::~AlrDetector() {} void AlrDetector::OnBytesSent(size_t bytes_sent, int64_t send_time_ms) { if (!last_send_time_ms_.has_value()) { last_send_time_ms_ = send_time_ms; - // Since the duration for sending the bytes is unknwon, return without + // Since the duration for sending the bytes is unknown, return without // updating alr state. return; } @@ -109,4 +114,18 @@ absl::optional AlrDetector::GetApplicationLimitedRegionStartTime() return alr_started_time_ms_; } +absl::optional AlrDetector::GetApplicationLimitedRegionStartTime( + int64_t at_time_ms) { + if (!alr_started_time_ms_ && last_send_time_ms_.has_value()) { + int64_t delta_time_ms = at_time_ms - *last_send_time_ms_; + // If ALR is stopped and we haven't sent any packets for a while, force start. + if (delta_time_ms > alr_timeout_) { + MS_WARN_TAG(bwe, "large delta_time_ms: %" PRIi64 ", forcing alr state change", + delta_time_ms); + alr_started_time_ms_.emplace(at_time_ms); + } + } + return alr_started_time_ms_; +} + } // namespace webrtc diff --git a/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/alr_detector.h b/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/alr_detector.h index e1fbb74525..e1cf8de92b 100644 --- a/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/alr_detector.h +++ b/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/alr_detector.h @@ -43,6 +43,7 @@ class AlrDetector { // Returns time in milliseconds when the current application-limited region // started or empty result if the sender is currently not application-limited. absl::optional GetApplicationLimitedRegionStartTime() const; + absl::optional GetApplicationLimitedRegionStartTime(int64_t at_time_ms); void UpdateBudgetWithElapsedTime(int64_t delta_time_ms); void UpdateBudgetWithBytesSent(size_t bytes_sent); @@ -56,6 +57,7 @@ class AlrDetector { static constexpr double kDefaultBandwidthUsageRatio = 0.65; static constexpr double kDefaultStartBudgetLevelRatio = 0.80; static constexpr double kDefaultStopBudgetLevelRatio = 0.50; + static constexpr int kDefaultAlrTimeout = 3000; AlrDetector(const WebRtcKeyValueConfig* key_value_config, absl::optional experiment_settings); @@ -64,6 +66,7 @@ class AlrDetector { FieldTrialParameter bandwidth_usage_ratio_; FieldTrialParameter start_budget_level_ratio_; FieldTrialParameter stop_budget_level_ratio_; + FieldTrialParameter alr_timeout_; absl::optional last_send_time_ms_; diff --git a/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/delay_based_bwe.cc b/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/delay_based_bwe.cc index 57f4afa146..6871afae0e 100644 --- a/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/delay_based_bwe.cc +++ b/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/delay_based_bwe.cc @@ -61,7 +61,7 @@ DelayBasedBwe::DelayBasedBwe(const WebRtcKeyValueConfig* key_value_config, network_state_predictor_(network_state_predictor), inter_arrival_(), delay_detector_( - new TrendlineEstimator(key_value_config_, network_state_predictor_)), + new TrendlineEstimator(network_state_predictor_)), last_seen_packet_(Timestamp::MinusInfinity()), uma_recorded_(false), rate_control_(key_value_config, /*send_side=*/true), @@ -127,7 +127,7 @@ void DelayBasedBwe::IncomingPacketFeedback(const PacketResult& packet_feedback, new InterArrival((kTimestampGroupLengthMs << kInterArrivalShift) / 1000, kTimestampToMs, true)); delay_detector_.reset( - new TrendlineEstimator(key_value_config_, network_state_predictor_)); + new TrendlineEstimator(network_state_predictor_)); } last_seen_packet_ = at_time; diff --git a/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/goog_cc_network_control.cc b/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/goog_cc_network_control.cc index aced2db317..df64112a41 100644 --- a/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/goog_cc_network_control.cc +++ b/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/goog_cc_network_control.cc @@ -195,7 +195,7 @@ NetworkControlUpdate GoogCcNetworkController::OnProcessInterval( } bandwidth_estimation_->UpdateEstimate(msg.at_time); absl::optional start_time_ms = - alr_detector_->GetApplicationLimitedRegionStartTime(); + alr_detector_->GetApplicationLimitedRegionStartTime(msg.at_time.ms()); probe_controller_->SetAlrStartTimeMs(start_time_ms); auto probes = probe_controller_->Process(msg.at_time.ms()); diff --git a/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/trendline_estimator.cc b/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/trendline_estimator.cc index 885c6776cb..90b80c97ef 100644 --- a/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/trendline_estimator.cc +++ b/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/trendline_estimator.cc @@ -13,96 +13,92 @@ #include "modules/congestion_controller/goog_cc/trendline_estimator.h" -#include "modules/remote_bitrate_estimator/include/bwe_defines.h" -#include "rtc_base/numerics/safe_minmax.h" - -#include "Logger.hpp" - -#include #include + #include #include +#include "absl/strings/match.h" +#include "absl/types/optional.h" +#include "api/network_state_predictor.h" +#include "rtc_base/numerics/safe_minmax.h" +#include "api/transport/webrtc_key_value_config.h" + +#include "Logger.hpp" + namespace webrtc { namespace { // Parameters for linear least squares fit of regression line to noisy data. -constexpr size_t kDefaultTrendlineWindowSize = 20; -constexpr double kDefaultTrendlineSmoothingCoeff = 0.6; +constexpr double kDefaultTrendlineSmoothingCoeff = 0.9; constexpr double kDefaultTrendlineThresholdGain = 4.0; -const char kBweWindowSizeInPacketsExperiment[] = - "WebRTC-BweWindowSizeInPackets"; - -size_t ReadTrendlineFilterWindowSize( - const WebRtcKeyValueConfig* key_value_config) { - std::string experiment_string = - key_value_config->Lookup(kBweWindowSizeInPacketsExperiment); - size_t window_size; - int parsed_values = - sscanf(experiment_string.c_str(), "Enabled-%zu", &window_size); - if (parsed_values == 1) { - if (window_size > 1) - return window_size; - MS_WARN_DEV("window size must be greater than 1"); - } - MS_WARN_DEV( - "failed to parse parameters for BweWindowSizeInPackets" - " experiment from field trial string, using default"); - return kDefaultTrendlineWindowSize; -} absl::optional LinearFitSlope( - const std::deque>& points) { - //RTC_DCHECK(points.size() >= 2); + const std::deque& packets) { + // RTC_DCHECK(packets.size() >= 2); // Compute the "center of mass". double sum_x = 0; double sum_y = 0; - for (const auto& point : points) { - sum_x += point.first; - sum_y += point.second; + for (const auto& packet : packets) { + sum_x += packet.arrival_time_ms; + sum_y += packet.smoothed_delay_ms; } - double x_avg = sum_x / points.size(); - double y_avg = sum_y / points.size(); + double x_avg = sum_x / packets.size(); + double y_avg = sum_y / packets.size(); // Compute the slope k = \sum (x_i-x_avg)(y_i-y_avg) / \sum (x_i-x_avg)^2 double numerator = 0; double denominator = 0; - for (const auto& point : points) { - numerator += (point.first - x_avg) * (point.second - y_avg); - denominator += (point.first - x_avg) * (point.first - x_avg); + for (const auto& packet : packets) { + double x = packet.arrival_time_ms; + double y = packet.smoothed_delay_ms; + numerator += (x - x_avg) * (y - y_avg); + denominator += (x - x_avg) * (x - x_avg); } if (denominator == 0) return absl::nullopt; return numerator / denominator; } +absl::optional ComputeSlopeCap( + const std::deque& packets, + const TrendlineEstimatorSettings& settings) { + // RTC_DCHECK(1 <= settings.beginning_packets && + // settings.beginning_packets < packets.size()); + // RTC_DCHECK(1 <= settings.end_packets && + // settings.end_packets < packets.size()); + // RTC_DCHECK(settings.beginning_packets + settings.end_packets <= + // packets.size()); + TrendlineEstimator::PacketTiming early = packets[0]; + for (size_t i = 1; i < settings.beginning_packets; ++i) { + if (packets[i].raw_delay_ms < early.raw_delay_ms) + early = packets[i]; + } + size_t late_start = packets.size() - settings.end_packets; + TrendlineEstimator::PacketTiming late = packets[late_start]; + for (size_t i = late_start + 1; i < packets.size(); ++i) { + if (packets[i].raw_delay_ms < late.raw_delay_ms) + late = packets[i]; + } + if (late.arrival_time_ms - early.arrival_time_ms < 1) { + return absl::nullopt; + } + return (late.raw_delay_ms - early.raw_delay_ms) / + (late.arrival_time_ms - early.arrival_time_ms) + + settings.cap_uncertainty; +} + constexpr double kMaxAdaptOffsetMs = 15.0; -constexpr double kOverUsingTimeThreshold = 30; +constexpr double kOverUsingTimeThreshold = 10; constexpr int kMinNumDeltas = 60; constexpr int kDeltaCounterMax = 1000; } // namespace TrendlineEstimator::TrendlineEstimator( - const WebRtcKeyValueConfig* key_value_config, NetworkStatePredictor* network_state_predictor) - : TrendlineEstimator( - key_value_config->Lookup(kBweWindowSizeInPacketsExperiment) - .find("Enabled") == 0 - ? ReadTrendlineFilterWindowSize(key_value_config) - : kDefaultTrendlineWindowSize, - kDefaultTrendlineSmoothingCoeff, - kDefaultTrendlineThresholdGain, - network_state_predictor) {} - -TrendlineEstimator::TrendlineEstimator( - size_t window_size, - double smoothing_coef, - double threshold_gain, - NetworkStatePredictor* network_state_predictor) - : window_size_(window_size), - smoothing_coef_(smoothing_coef), - threshold_gain_(threshold_gain), + : smoothing_coef_(kDefaultTrendlineSmoothingCoeff), + threshold_gain_(kDefaultTrendlineThresholdGain), num_of_deltas_(0), first_arrival_time_ms_(-1), accumulated_delay_(0), @@ -120,63 +116,75 @@ TrendlineEstimator::TrendlineEstimator( hypothesis_(BandwidthUsage::kBwNormal), hypothesis_predicted_(BandwidthUsage::kBwNormal), network_state_predictor_(network_state_predictor) { - MS_DEBUG_DEV( - "using Trendline filter for delay change estimation with window size: %zu", - window_size_); } TrendlineEstimator::~TrendlineEstimator() {} +void TrendlineEstimator::UpdateTrendline(double recv_delta_ms, + double send_delta_ms, + int64_t send_time_ms, + int64_t arrival_time_ms) { + const double delta_ms = recv_delta_ms - send_delta_ms; + ++num_of_deltas_; + num_of_deltas_ = std::min(num_of_deltas_, kDeltaCounterMax); + if (first_arrival_time_ms_ == -1) + first_arrival_time_ms_ = arrival_time_ms; + + // Exponential backoff filter. + accumulated_delay_ += delta_ms; + // BWE_TEST_LOGGING_PLOT(1, "accumulated_delay_ms", arrival_time_ms, + // accumulated_delay_); + smoothed_delay_ = smoothing_coef_ * smoothed_delay_ + + (1 - smoothing_coef_) * accumulated_delay_; + // BWE_TEST_LOGGING_PLOT(1, "smoothed_delay_ms", arrival_time_ms, + // smoothed_delay_); + + // Maintain packet window + delay_hist_.emplace_back( + static_cast(arrival_time_ms - first_arrival_time_ms_), + smoothed_delay_, accumulated_delay_); + if (settings_.enable_sort) { + for (size_t i = delay_hist_.size() - 1; + i > 0 && + delay_hist_[i].arrival_time_ms < delay_hist_[i - 1].arrival_time_ms; + --i) { + std::swap(delay_hist_[i], delay_hist_[i - 1]); + } + } + if (delay_hist_.size() > settings_.window_size) + delay_hist_.pop_front(); + + // Simple linear regression. + double trend = prev_trend_; + if (delay_hist_.size() == settings_.window_size) { + // Update trend_ if it is possible to fit a line to the data. The delay + // trend can be seen as an estimate of (send_rate - capacity)/capacity. + // 0 < trend < 1 -> the delay increases, queues are filling up + // trend == 0 -> the delay does not change + // trend < 0 -> the delay decreases, queues are being emptied + trend = LinearFitSlope(delay_hist_).value_or(trend); + if (settings_.enable_cap) { + absl::optional cap = ComputeSlopeCap(delay_hist_, settings_); + // We only use the cap to filter out overuse detections, not + // to detect additional underuses. + if (trend >= 0 && cap.has_value() && trend > cap.value()) { + trend = cap.value(); + } + } + } + // BWE_TEST_LOGGING_PLOT(1, "trendline_slope", arrival_time_ms, trend); + + Detect(trend, send_delta_ms, arrival_time_ms); +} + void TrendlineEstimator::Update(double recv_delta_ms, double send_delta_ms, int64_t send_time_ms, int64_t arrival_time_ms, bool calculated_deltas) { if (calculated_deltas) { - const double delta_ms = recv_delta_ms - send_delta_ms; - ++num_of_deltas_; - num_of_deltas_ = std::min(num_of_deltas_, kDeltaCounterMax); - if (first_arrival_time_ms_ == -1) - first_arrival_time_ms_ = arrival_time_ms; - - // Exponential backoff filter. - accumulated_delay_ += delta_ms; - // BWE_TEST_LOGGING_PLOT(1, "accumulated_delay_ms", arrival_time_ms, - // accumulated_delay_); - // smoothed_delay_ = smoothing_coef_ * smoothed_delay_ + - // (1 - smoothing_coef_) * accumulated_delay_; - // MS_NOTE: Apply WEMA to the current delta_ms. Don't consider the - // accumulated delay. Tests show it behaves more robustly upon delta peaks. - smoothed_delay_ = smoothing_coef_ * delta_ms + - (1 - smoothing_coef_) * smoothed_delay_; - // BWE_TEST_LOGGING_PLOT(1, "smoothed_delay_ms", arrival_time_ms, - // smoothed_delay_); - - // Simple linear regression. - delay_hist_.push_back(std::make_pair( - static_cast(arrival_time_ms - first_arrival_time_ms_), - smoothed_delay_)); - if (delay_hist_.size() > window_size_) - delay_hist_.pop_front(); - double trend = prev_trend_; - if (delay_hist_.size() == window_size_) { - // Update trend_ if it is possible to fit a line to the data. The delay - // trend can be seen as an estimate of (send_rate - capacity)/capacity. - // 0 < trend < 1 -> the delay increases, queues are filling up - // trend == 0 -> the delay does not change - // trend < 0 -> the delay decreases, queues are being emptied - trend = LinearFitSlope(delay_hist_).value_or(trend); - } - - // BWE_TEST_LOGGING_PLOT(1, "trendline_slope", arrival_time_ms, trend); - - MS_DEBUG_DEV("trend:%f, send_delta_ms:%f, recv_delta_ms:%f, delta_ms:%f arrival_time_ms:%" PRIi64 ", accumulated_delay_:%f, smoothed_delay_:%f", trend, send_delta_ms, recv_delta_ms, delta_ms, arrival_time_ms, accumulated_delay_, smoothed_delay_); - Detect(trend, send_delta_ms, arrival_time_ms); - } - else { - MS_DEBUG_DEV("no calculated deltas"); + UpdateTrendline(recv_delta_ms, send_delta_ms, send_time_ms, arrival_time_ms); } - if (network_state_predictor_) { hypothesis_predicted_ = network_state_predictor_->Update( send_time_ms, arrival_time_ms, hypothesis_); @@ -212,15 +220,17 @@ void TrendlineEstimator::Detect(double trend, double ts_delta, int64_t now_ms) { if (trend >= prev_trend_) { time_over_using_ = 0; overuse_counter_ = 0; + hypothesis_ = BandwidthUsage::kBwOverusing; MS_DEBUG_DEV("hypothesis_: BandwidthUsage::kBwOverusing"); #if MS_LOG_DEV_LEVEL == 3 - for (auto& kv : delay_hist_) { - MS_DEBUG_DEV("arrival_time_ms - first_arrival_time_ms_:%f, smoothed_delay_:%f", kv.first, kv.second); + for (auto& packetTiming : delay_hist_) { + MS_DEBUG_DEV( + "packetTiming [arrival_time_ms:%f, smoothed_delay_ms:%f, raw_delay_ms:%f", + packetTiming.arrival_time_ms, packetTiming.smoothed_delay_ms, packetTiming.raw_delay_ms + ); } #endif - - hypothesis_ = BandwidthUsage::kBwOverusing; } } } else if (modified_trend < -threshold_) { @@ -231,8 +241,8 @@ void TrendlineEstimator::Detect(double trend, double ts_delta, int64_t now_ms) { } else { time_over_using_ = -1; overuse_counter_ = 0; - MS_DEBUG_DEV("---- BandwidthUsage::kBwNormal ---"); hypothesis_ = BandwidthUsage::kBwNormal; + MS_DEBUG_DEV("---- BandwidthUsage::kBwNormal ---"); } prev_trend_ = trend; UpdateThreshold(modified_trend, now_ms); diff --git a/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/trendline_estimator.h b/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/trendline_estimator.h index f4c23d706d..7eeb101d02 100644 --- a/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/trendline_estimator.h +++ b/worker/deps/libwebrtc/libwebrtc/modules/congestion_controller/goog_cc/trendline_estimator.h @@ -10,37 +10,48 @@ #ifndef MODULES_CONGESTION_CONTROLLER_GOOG_CC_TRENDLINE_ESTIMATOR_H_ #define MODULES_CONGESTION_CONTROLLER_GOOG_CC_TRENDLINE_ESTIMATOR_H_ +#include +#include + +#include +#include +#include + #include "api/network_state_predictor.h" #include "api/transport/webrtc_key_value_config.h" #include "modules/congestion_controller/goog_cc/delay_increase_detector_interface.h" #include "modules/remote_bitrate_estimator/include/bwe_defines.h" #include "rtc_base/constructor_magic.h" -#include -#include -#include -#include - namespace webrtc { +struct TrendlineEstimatorSettings { + static constexpr unsigned kDefaultTrendlineWindowSize = 20; + + // Sort the packets in the window. Should be redundant, + // but then almost no cost. + bool enable_sort = false; + + // Cap the trendline slope based on the minimum delay seen + // in the beginning_packets and end_packets respectively. + bool enable_cap = false; + unsigned beginning_packets = 7; + unsigned end_packets = 7; + double cap_uncertainty = 0.0; + + // Size (in packets) of the window. + unsigned window_size = kDefaultTrendlineWindowSize; +}; + class TrendlineEstimator : public DelayIncreaseDetectorInterface { public: - TrendlineEstimator(const WebRtcKeyValueConfig* key_value_config, - NetworkStatePredictor* network_state_predictor); - // |window_size| is the number of points required to compute a trend line. - // |smoothing_coef| controls how much we smooth out the delay before fitting - // the trend line. |threshold_gain| is used to scale the trendline slope for - // comparison to the old threshold. Once the old estimator has been removed - // (or the thresholds been merged into the estimators), we can just set the - // threshold instead of setting a gain.|network_state_predictor| is used to - // bettter predict network state. - TrendlineEstimator(size_t window_size, - double smoothing_coef, - double threshold_gain, - NetworkStatePredictor* network_state_predictor); + TrendlineEstimator(NetworkStatePredictor* network_state_predictor); ~TrendlineEstimator() override; + TrendlineEstimator(const TrendlineEstimator&) = delete; + TrendlineEstimator& operator=(const TrendlineEstimator&) = delete; + // Update the estimator with a new sample. The deltas should represent deltas // between timestamp groups as defined by the InterArrival class. void Update(double recv_delta_ms, @@ -49,21 +60,33 @@ class TrendlineEstimator : public DelayIncreaseDetectorInterface { int64_t arrival_time_ms, bool calculated_deltas) override; + void UpdateTrendline(double recv_delta_ms, + double send_delta_ms, + int64_t send_time_ms, + int64_t arrival_time_ms); + BandwidthUsage State() const override; - protected: - // Used in unit tests. - double modified_trend() const { return prev_trend_ * threshold_gain_; } + struct PacketTiming { + PacketTiming(double arrival_time_ms, + double smoothed_delay_ms, + double raw_delay_ms) + : arrival_time_ms(arrival_time_ms), + smoothed_delay_ms(smoothed_delay_ms), + raw_delay_ms(raw_delay_ms) {} + double arrival_time_ms; + double smoothed_delay_ms; + double raw_delay_ms; + }; private: friend class GoogCcStatePrinter; - void Detect(double trend, double ts_delta, int64_t now_ms); void UpdateThreshold(double modified_offset, int64_t now_ms); // Parameters. - const size_t window_size_; + TrendlineEstimatorSettings settings_; const double smoothing_coef_; const double threshold_gain_; // Used by the existing threshold. @@ -74,7 +97,7 @@ class TrendlineEstimator : public DelayIncreaseDetectorInterface { double accumulated_delay_; double smoothed_delay_; // Linear least squares regression. - std::deque> delay_hist_; + std::deque delay_hist_; const double k_up_; const double k_down_; @@ -88,8 +111,6 @@ class TrendlineEstimator : public DelayIncreaseDetectorInterface { BandwidthUsage hypothesis_; BandwidthUsage hypothesis_predicted_; NetworkStatePredictor* network_state_predictor_; - - RTC_DISALLOW_COPY_AND_ASSIGN(TrendlineEstimator); }; } // namespace webrtc diff --git a/worker/deps/libwebrtc/libwebrtc/modules/remote_bitrate_estimator/remote_bitrate_estimator_abs_send_time.cc b/worker/deps/libwebrtc/libwebrtc/modules/remote_bitrate_estimator/remote_bitrate_estimator_abs_send_time.cc index 30da04df75..80b2a2835d 100644 --- a/worker/deps/libwebrtc/libwebrtc/modules/remote_bitrate_estimator/remote_bitrate_estimator_abs_send_time.cc +++ b/worker/deps/libwebrtc/libwebrtc/modules/remote_bitrate_estimator/remote_bitrate_estimator_abs_send_time.cc @@ -289,6 +289,8 @@ void RemoteBitrateEstimatorAbsSendTime::IncomingPacketInfo( if (payload_size > kMinProbePacketSize && (!remote_rate_.ValidEstimate() || now_ms - first_packet_time_ms_ < kInitialProbingIntervalMs)) { + +#if MS_LOG_DEV_LEVEL == 3 // TODO(holmer): Use a map instead to get correct order? if (total_probes_received_ < kMaxProbePackets) { int send_delta_ms = -1; @@ -306,6 +308,8 @@ void RemoteBitrateEstimatorAbsSendTime::IncomingPacketInfo( send_delta_ms, recv_delta_ms); } +#endif + probes_.push_back(Probe(send_time_ms, arrival_time_ms, payload_size)); ++total_probes_received_; // Make sure that a probe which updated the bitrate immediately has an diff --git a/worker/deps/libwebrtc/libwebrtc/rtc_base/experiments/alr_experiment.cc b/worker/deps/libwebrtc/libwebrtc/rtc_base/experiments/alr_experiment.cc index 43f36889a8..dd5b5c5957 100644 --- a/worker/deps/libwebrtc/libwebrtc/rtc_base/experiments/alr_experiment.cc +++ b/worker/deps/libwebrtc/libwebrtc/rtc_base/experiments/alr_experiment.cc @@ -26,7 +26,7 @@ const char AlrExperimentSettings::kScreenshareProbingBweExperimentName[] = "WebRTC-ProbingScreenshareBwe"; const char AlrExperimentSettings::kStrictPacingAndProbingExperimentName[] = "WebRTC-StrictPacingAndProbing"; -const char kDefaultProbingScreenshareBweSettings[] = "1.0,2875,80,40,-60,3"; +const char kDefaultProbingScreenshareBweSettings[] = "1.0,2875,80,40,-60,3,3000"; bool AlrExperimentSettings::MaxOneFieldTrialEnabled() { return AlrExperimentSettings::MaxOneFieldTrialEnabled( @@ -71,12 +71,13 @@ AlrExperimentSettings::CreateFromFieldTrial( } AlrExperimentSettings settings; - if (sscanf(group_name.c_str(), "%f,%" PRId64 ",%d,%d,%d,%d", + if (sscanf(group_name.c_str(), "%f,%" PRId64 ",%d,%d,%d,%d,%d", &settings.pacing_factor, &settings.max_paced_queue_time, &settings.alr_bandwidth_usage_percent, &settings.alr_start_budget_level_percent, &settings.alr_stop_budget_level_percent, - &settings.group_id) == 6) { + &settings.group_id, + &settings.alr_timeout) == 7) { ret.emplace(settings); MS_DEBUG_TAG(bwe, "Using ALR experiment settings: " "pacing factor: %f" @@ -84,13 +85,15 @@ AlrExperimentSettings::CreateFromFieldTrial( ", ALR bandwidth usage percent: %d" ", ALR start budget level percent: %d" ", ALR end budget level percent: %d" - ", ALR experiment group ID: %d", + ", ALR experiment group ID: %d" + ", ALR timeout: %d", settings.pacing_factor, settings.max_paced_queue_time, settings.alr_bandwidth_usage_percent, settings.alr_start_budget_level_percent, settings.alr_stop_budget_level_percent, - settings.group_id); + settings.group_id, + settings.alr_timeout); } else { MS_DEBUG_TAG(bwe, "Failed to parse ALR experiment: %s", experiment_name); } diff --git a/worker/deps/libwebrtc/libwebrtc/rtc_base/experiments/alr_experiment.h b/worker/deps/libwebrtc/libwebrtc/rtc_base/experiments/alr_experiment.h index 4d4c38337b..64ab57b85f 100644 --- a/worker/deps/libwebrtc/libwebrtc/rtc_base/experiments/alr_experiment.h +++ b/worker/deps/libwebrtc/libwebrtc/rtc_base/experiments/alr_experiment.h @@ -24,6 +24,7 @@ struct AlrExperimentSettings { int alr_bandwidth_usage_percent; int alr_start_budget_level_percent; int alr_stop_budget_level_percent; + int alr_timeout; // Will be sent to the receive side for stats slicing. // Can be 0..6, because it's sent as a 3 bits value and there's also // reserved value to indicate absence of experiment. diff --git a/worker/deps/libwebrtc/meson.build b/worker/deps/libwebrtc/meson.build index 7d2c54e219..5e91710aa5 100644 --- a/worker/deps/libwebrtc/meson.build +++ b/worker/deps/libwebrtc/meson.build @@ -51,7 +51,6 @@ abseil_cpp_proj = subproject( 'warning_level=0', ], ) - local_include_directories = declare_dependency( include_directories: include_directories('libwebrtc') ) @@ -64,7 +63,7 @@ libwebrtc = library( openssl_proj.get_variable('openssl_dep'), abseil_cpp_proj.get_variable('absl_strings_dep'), abseil_cpp_proj.get_variable('absl_types_dep'), - nlohmann_json_proj.get_variable('nlohmann_json_dep'), + flatbuffers_proj.get_variable('flatbuffers_dep'), libuv_proj.get_variable('libuv_dep'), ], include_directories: libwebrtc_include_directories, diff --git a/worker/fbs/activeSpeakerObserver.fbs b/worker/fbs/activeSpeakerObserver.fbs new file mode 100644 index 0000000000..e2de103fd4 --- /dev/null +++ b/worker/fbs/activeSpeakerObserver.fbs @@ -0,0 +1,12 @@ +namespace FBS.ActiveSpeakerObserver; + +table ActiveSpeakerObserverOptions { + interval: uint16; +} + +// Notifications from Worker. + +table DominantSpeakerNotification { + producer_id: string (required); +} + diff --git a/worker/fbs/audioLevelObserver.fbs b/worker/fbs/audioLevelObserver.fbs new file mode 100644 index 0000000000..1ce66e402b --- /dev/null +++ b/worker/fbs/audioLevelObserver.fbs @@ -0,0 +1,19 @@ +namespace FBS.AudioLevelObserver; + +table AudioLevelObserverOptions { + max_entries: uint16; + threshold: int8; + interval: uint16; +} + +// Notifications from Worker. + +table Volume { + producer_id: string (required); + volume: int8; +} + +table VolumesNotification { + volumes: [Volume] (required); +} + diff --git a/worker/fbs/common.fbs b/worker/fbs/common.fbs new file mode 100644 index 0000000000..85093fc747 --- /dev/null +++ b/worker/fbs/common.fbs @@ -0,0 +1,33 @@ +namespace FBS.Common; + +table StringString { + key: string (required); + value: string (required); +} + +table StringUint8 { + key: string (required); + value: uint8; +} + +table Uint16String { + key: uint16; + value: string (required); +} + +table Uint32String { + key: uint32; + value: string (required); +} + +table StringStringArray { + key: string (required); + values: [string] (required); +} + +// NOTE (windows): IN|OUT are macros defined in windef.h. +enum TraceDirection: uint8 { + DIRECTION_IN = 0, + DIRECTION_OUT +} + diff --git a/worker/fbs/consumer.fbs b/worker/fbs/consumer.fbs new file mode 100644 index 0000000000..715ce2bd53 --- /dev/null +++ b/worker/fbs/consumer.fbs @@ -0,0 +1,125 @@ +include "common.fbs"; +include "rtpPacket.fbs"; +include "rtpParameters.fbs"; +include "rtpStream.fbs"; + +namespace FBS.Consumer; + +table ConsumerLayers { + spatial_layer: uint8; + temporal_layer: uint8 = null; +} + +table ConsumerScore { + score: uint8; + producer_score: uint8; + producer_scores: [uint8] (required); +} + +table SetPreferredLayersRequest { + preferred_layers: ConsumerLayers (required); +} + +table SetPreferredLayersResponse { + preferred_layers: ConsumerLayers; +} + +table SetPriorityRequest { + priority: uint8; +} + +table SetPriorityResponse { + priority: uint8; +} + +enum TraceEventType: uint8 { + KEYFRAME = 0, + FIR, + NACK, + PLI, + RTP, +} + +table EnableTraceEventRequest { + events: [TraceEventType] (required); +} + +table DumpResponse { + data: ConsumerDump (required); +} + +table BaseConsumerDump { + id: string (required); + type: FBS.RtpParameters.Type; + producer_id: string (required); + kind: FBS.RtpParameters.MediaKind; + rtp_parameters: FBS.RtpParameters.RtpParameters (required); + consumable_rtp_encodings: [FBS.RtpParameters.RtpEncodingParameters] (required); + supported_codec_payload_types: [uint8] (required); + trace_event_types: [TraceEventType] (required); + paused: bool; + producer_paused: bool; + priority: uint8; +} + +table ConsumerDump { + base: BaseConsumerDump (required); + rtp_streams: [FBS.RtpStream.Dump] (required); + preferred_spatial_layer: int16 = null; + target_spatial_layer: int16 = null; + current_spatial_layer: int16 = null; + preferred_temporal_layer: int16 = null; + target_temporal_layer: int16 = null; + current_temporal_layer: int16 = null; +} + +table GetStatsResponse { + stats: [FBS.RtpStream.Stats] (required); +} + +// Notifications from Worker. + +table LayersChangeNotification { + layers: ConsumerLayers; +} + +table RtpNotification { + data: [ubyte] (required); +} + +table ScoreNotification { + score: ConsumerScore (required); +} + +union TraceInfo { + KeyFrameTraceInfo, + FirTraceInfo, + PliTraceInfo, + RtpTraceInfo, +} + +table KeyFrameTraceInfo { + rtp_packet: FBS.RtpPacket.Dump (required); + is_rtx: bool; +} + +table FirTraceInfo { + ssrc: uint32; +} + +table PliTraceInfo { + ssrc: uint32; +} + +table RtpTraceInfo { + rtp_packet: FBS.RtpPacket.Dump (required); + is_rtx: bool; +} + +table TraceNotification { + type: TraceEventType; + timestamp: uint64; + direction: FBS.Common.TraceDirection; + info: TraceInfo; +} + diff --git a/worker/fbs/dataConsumer.fbs b/worker/fbs/dataConsumer.fbs new file mode 100644 index 0000000000..2670288a58 --- /dev/null +++ b/worker/fbs/dataConsumer.fbs @@ -0,0 +1,76 @@ +include "common.fbs"; +include "dataProducer.fbs"; +include "sctpParameters.fbs"; + +namespace FBS.DataConsumer; + +table GetBufferedAmountResponse { + buffered_amount: uint32; +} + +table SetBufferedAmountLowThresholdRequest { + threshold: uint32; +} + +table DumpResponse { + id: string (required); + data_producer_id: string (required); + type: FBS.DataProducer.Type; + sctp_stream_parameters: FBS.SctpParameters.SctpStreamParameters; + label: string (required); + protocol: string (required); + buffered_amount_low_threshold: uint32; + paused: bool; + data_producer_paused: bool; + subchannels: [uint16] (required); +} + +table GetStatsResponse { + timestamp: uint64; + label: string (required); + protocol: string (required); + messages_sent: uint64; + bytes_sent: uint64; + buffered_amount: uint32; +} + +table SendRequest { + ppid: uint32; + data: [uint8] (required); +} + +table SetSubchannelsRequest { + subchannels: [uint16] (required); +} + +table SetSubchannelsResponse { + subchannels: [uint16] (required); +} + +table AddSubchannelRequest { + subchannel: uint16; +} + +table AddSubchannelResponse { + subchannels: [uint16] (required); +} + +table RemoveSubchannelRequest { + subchannel: uint16; +} + +table RemoveSubchannelResponse { + subchannels: [uint16] (required); +} + +// Notifications from Worker. + +table BufferedAmountLowNotification { + buffered_amount: uint32; +} + +table MessageNotification { + ppid: uint32; + data: [uint8] (required); +} + diff --git a/worker/fbs/dataProducer.fbs b/worker/fbs/dataProducer.fbs new file mode 100644 index 0000000000..3d8dbd367b --- /dev/null +++ b/worker/fbs/dataProducer.fbs @@ -0,0 +1,34 @@ +include "sctpParameters.fbs"; + +namespace FBS.DataProducer; + +enum Type: uint8 { + SCTP, + DIRECT +} + +table DumpResponse { + id: string (required); + type: Type; + sctp_stream_parameters: FBS.SctpParameters.SctpStreamParameters; + label: string (required); + protocol: string (required); + paused: bool; +} + +table GetStatsResponse { + timestamp: uint64; + label: string (required); + protocol: string (required); + messages_received: uint64; + bytes_received: uint64; + buffered_amount: uint32; +} + +table SendNotification { + ppid: uint32; + data: [uint8] (required); + subchannels: [uint16]; + required_subchannel: uint16 = null; +} + diff --git a/worker/fbs/directTransport.fbs b/worker/fbs/directTransport.fbs new file mode 100644 index 0000000000..28dd372281 --- /dev/null +++ b/worker/fbs/directTransport.fbs @@ -0,0 +1,22 @@ +include "transport.fbs"; + +namespace FBS.DirectTransport; + +table DirectTransportOptions { + base: FBS.Transport.Options (required); +} + +table DumpResponse { + base: FBS.Transport.Dump (required); +} + +table GetStatsResponse { + base: FBS.Transport.Stats (required); +} + +// Notifications from Worker. + +table RtcpNotification { + data: [uint8] (required); +} + diff --git a/worker/fbs/liburing.fbs b/worker/fbs/liburing.fbs new file mode 100644 index 0000000000..68ce1ae074 --- /dev/null +++ b/worker/fbs/liburing.fbs @@ -0,0 +1,8 @@ +namespace FBS.LibUring; + +table Dump { + sqe_process_count: uint64; + sqe_miss_count: uint64; + user_data_miss_count: uint64; +} + diff --git a/worker/fbs/log.fbs b/worker/fbs/log.fbs new file mode 100644 index 0000000000..562fa58d35 --- /dev/null +++ b/worker/fbs/log.fbs @@ -0,0 +1,6 @@ +namespace FBS.Log; + +table Log { + data: string (required); +} + diff --git a/worker/fbs/meson.build b/worker/fbs/meson.build new file mode 100644 index 0000000000..8288279ff3 --- /dev/null +++ b/worker/fbs/meson.build @@ -0,0 +1,55 @@ +flatbuffers_schemas = [ + 'activeSpeakerObserver.fbs', + 'audioLevelObserver.fbs', + 'common.fbs', + 'consumer.fbs', + 'dataConsumer.fbs', + 'dataProducer.fbs', + 'directTransport.fbs', + 'liburing.fbs', + 'log.fbs', + 'message.fbs', + 'notification.fbs', + 'pipeTransport.fbs', + 'plainTransport.fbs', + 'producer.fbs', + 'request.fbs', + 'response.fbs', + 'router.fbs', + 'rtpObserver.fbs', + 'rtpPacket.fbs', + 'rtpParameters.fbs', + 'rtpStream.fbs', + 'rtxStream.fbs', + 'sctpAssociation.fbs', + 'sctpParameters.fbs', + 'srtpParameters.fbs', + 'transport.fbs', + 'webRtcServer.fbs', + 'webRtcTransport.fbs', + 'worker.fbs', +] + +# Directory from which worker code will include the header files. +flatbuffers_cpp_out_dir = 'FBS' + +flatc = find_program('flatc') +flatbuffers_generator = custom_target('flatbuffers-generator', + output: flatbuffers_cpp_out_dir, + input: flatbuffers_schemas, + command : [ + flatc, + '--cpp', + '--cpp-field-case-style', 'lower', + '--reflect-names', + '--scoped-enums', + '--filename-suffix', '', + '-o', '@OUTPUT@', + '@INPUT@' + ], + build_by_default: true, +) + +flatbuffers_generator_dep = declare_dependency( + include_directories: '.', +) diff --git a/worker/fbs/message.fbs b/worker/fbs/message.fbs new file mode 100644 index 0000000000..233ff28b08 --- /dev/null +++ b/worker/fbs/message.fbs @@ -0,0 +1,20 @@ +include "log.fbs"; +include "notification.fbs"; +include "request.fbs"; +include "response.fbs"; + +namespace FBS.Message; + +union Body { + Request: FBS.Request.Request, + Response: FBS.Response.Response, + Notification: FBS.Notification.Notification, + Log: FBS.Log.Log, +} + +table Message { + data: Body (required); +} + +root_type Message; + diff --git a/worker/fbs/notification.fbs b/worker/fbs/notification.fbs new file mode 100644 index 0000000000..10725c1843 --- /dev/null +++ b/worker/fbs/notification.fbs @@ -0,0 +1,83 @@ +include "transport.fbs"; +include "webRtcTransport.fbs"; +include "plainTransport.fbs"; +include "directTransport.fbs"; +include "producer.fbs"; +include "dataProducer.fbs"; +include "dataConsumer.fbs"; +include "activeSpeakerObserver.fbs"; +include "audioLevelObserver.fbs"; + +namespace FBS.Notification; + +enum Event: uint8 { + // Notifications to worker. + TRANSPORT_SEND_RTCP = 0, + PRODUCER_SEND, + DATAPRODUCER_SEND, + + // Notifications from worker. + WORKER_RUNNING, + TRANSPORT_SCTP_STATE_CHANGE, + TRANSPORT_TRACE, + WEBRTCTRANSPORT_ICE_SELECTED_TUPLE_CHANGE, + WEBRTCTRANSPORT_ICE_STATE_CHANGE, + WEBRTCTRANSPORT_DTLS_STATE_CHANGE, + PLAINTRANSPORT_TUPLE, + PLAINTRANSPORT_RTCP_TUPLE, + DIRECTTRANSPORT_RTCP, + PRODUCER_SCORE, + PRODUCER_TRACE, + PRODUCER_VIDEO_ORIENTATION_CHANGE, + CONSUMER_PRODUCER_PAUSE, + CONSUMER_PRODUCER_RESUME, + CONSUMER_PRODUCER_CLOSE, + CONSUMER_LAYERS_CHANGE, + CONSUMER_RTP, + CONSUMER_SCORE, + CONSUMER_TRACE, + DATACONSUMER_BUFFERED_AMOUNT_LOW, + DATACONSUMER_SCTP_SENDBUFFER_FULL, + DATACONSUMER_DATAPRODUCER_PAUSE, + DATACONSUMER_DATAPRODUCER_RESUME, + DATACONSUMER_DATAPRODUCER_CLOSE, + DATACONSUMER_MESSAGE, + ACTIVESPEAKEROBSERVER_DOMINANT_SPEAKER, + AUDIOLEVELOBSERVER_SILENCE, + AUDIOLEVELOBSERVER_VOLUMES, +} + +union Body { + // Notifications to worker. + Transport_SendRtcpNotification: FBS.Transport.SendRtcpNotification, + Transport_SctpStateChangeNotification: FBS.Transport.SctpStateChangeNotification, + Producer_SendNotification: FBS.Producer.SendNotification, + DataProducer_SendNotification: FBS.DataProducer.SendNotification, + + // Notifications from worker. + Transport_TraceNotification: FBS.Transport.TraceNotification, + WebRtcTransport_IceSelectedTupleChangeNotification: FBS.WebRtcTransport.IceSelectedTupleChangeNotification, + WebRtcTransport_IceStateChangeNotification: FBS.WebRtcTransport.IceStateChangeNotification, + WebRtcTransport_DtlsStateChangeNotification: FBS.WebRtcTransport.DtlsStateChangeNotification, + PlainTransport_TupleNotification: FBS.PlainTransport.TupleNotification, + PlainTransport_RtcpTupleNotification: FBS.PlainTransport.RtcpTupleNotification, + DirectTransport_RtcpNotification: FBS.DirectTransport.RtcpNotification, + Producer_ScoreNotification: FBS.Producer.ScoreNotification, + Producer_TraceNotification: FBS.Producer.TraceNotification, + Producer_VideoOrientationChangeNotification: FBS.Producer.VideoOrientationChangeNotification, + Consumer_LayersChangeNotification: FBS.Consumer.LayersChangeNotification, + Consumer_RtpNotification: FBS.Consumer.RtpNotification, + Consumer_ScoreNotification: FBS.Consumer.ScoreNotification, + Consumer_TraceNotification: FBS.Consumer.TraceNotification, + DataConsumer_MessageNotification: FBS.DataConsumer.MessageNotification, + DataConsumer_BufferedAmountLowNotification: FBS.DataConsumer.BufferedAmountLowNotification, + ActiveSpeakerObserver_DominantSpeakerNotification: FBS.ActiveSpeakerObserver.DominantSpeakerNotification, + AudioLevelObserver_VolumesNotification: FBS.AudioLevelObserver.VolumesNotification, +} + +table Notification { + handler_id: string (required); + event: Event; + body: Body; +} + diff --git a/worker/fbs/pipeTransport.fbs b/worker/fbs/pipeTransport.fbs new file mode 100644 index 0000000000..6d86b9dce6 --- /dev/null +++ b/worker/fbs/pipeTransport.fbs @@ -0,0 +1,34 @@ +include "transport.fbs"; +include "srtpParameters.fbs"; + +namespace FBS.PipeTransport; + +table PipeTransportOptions { + base: FBS.Transport.Options (required); + listen_info: FBS.Transport.ListenInfo (required); + enable_rtx: bool; + enable_srtp: bool; +} + +table ConnectRequest { + ip: string (required); + port: uint16 = null; + srtp_parameters: FBS.SrtpParameters.SrtpParameters; +} + +table ConnectResponse { + tuple: FBS.Transport.Tuple (required); +} + +table DumpResponse { + base: FBS.Transport.Dump (required); + tuple: FBS.Transport.Tuple (required); + rtx: bool; + srtp_parameters: FBS.SrtpParameters.SrtpParameters; +} + +table GetStatsResponse { + base: FBS.Transport.Stats (required); + tuple: FBS.Transport.Tuple (required); +} + diff --git a/worker/fbs/plainTransport.fbs b/worker/fbs/plainTransport.fbs new file mode 100644 index 0000000000..670443e721 --- /dev/null +++ b/worker/fbs/plainTransport.fbs @@ -0,0 +1,56 @@ +include "transport.fbs"; +include "sctpParameters.fbs"; +include "srtpParameters.fbs"; + +namespace FBS.PlainTransport; + +table PlainTransportOptions { + base: FBS.Transport.Options (required); + listen_info: FBS.Transport.ListenInfo (required); + rtcp_listen_info: FBS.Transport.ListenInfo; + rtcp_mux: bool; + comedia: bool; + enable_srtp: bool; + srtp_crypto_suite: FBS.SrtpParameters.SrtpCryptoSuite = null; +} + +table ConnectRequest { + ip: string; + port: uint16 = null; + rtcp_port: uint16 = null; + srtp_parameters: FBS.SrtpParameters.SrtpParameters; +} + +table ConnectResponse { + tuple: FBS.Transport.Tuple (required); + rtcp_tuple: FBS.Transport.Tuple; + srtp_parameters: FBS.SrtpParameters.SrtpParameters; +} + +table DumpResponse { + base: FBS.Transport.Dump (required); + rtcp_mux: bool; + comedia: bool; + tuple: FBS.Transport.Tuple (required); + rtcp_tuple: FBS.Transport.Tuple; + srtp_parameters: FBS.SrtpParameters.SrtpParameters; +} + +table GetStatsResponse { + base: FBS.Transport.Stats (required); + rtcp_mux: bool; + comedia: bool; + tuple: FBS.Transport.Tuple (required); + rtcp_tuple: FBS.Transport.Tuple; +} + +// Notifications from Worker. + +table TupleNotification { + tuple: FBS.Transport.Tuple (required); +} + +table RtcpTupleNotification { + tuple: FBS.Transport.Tuple (required); +} + diff --git a/worker/fbs/producer.fbs b/worker/fbs/producer.fbs new file mode 100644 index 0000000000..8913cb9222 --- /dev/null +++ b/worker/fbs/producer.fbs @@ -0,0 +1,100 @@ +include "common.fbs"; +include "rtpPacket.fbs"; +include "rtpParameters.fbs"; +include "rtpStream.fbs"; + +namespace FBS.Producer; + +enum TraceEventType: uint8 { + KEYFRAME = 0, + FIR, + NACK, + PLI, + RTP, + SR, +} + +table EnableTraceEventRequest { + events: [TraceEventType] (required); +} + +table DumpResponse { + id: string (required); + kind: FBS.RtpParameters.MediaKind; + type: FBS.RtpParameters.Type; + rtp_parameters: FBS.RtpParameters.RtpParameters (required); + rtp_mapping: FBS.RtpParameters.RtpMapping (required); + rtp_streams: [FBS.RtpStream.Dump] (required); + trace_event_types: [TraceEventType] (required); + paused: bool; +} + +table GetStatsResponse { + stats: [FBS.RtpStream.Stats] (required); +} + +table SendNotification { + data: [uint8] (required); +} + +// Notifications from Worker. + +table Score { + encoding_idx: uint32; + ssrc: uint32; + rid: string; + score: uint8; +} + +table ScoreNotification { + scores: [Score] (required); +} + +table VideoOrientationChangeNotification { + camera: bool; + flip: bool; + rotation: uint16; +} + +union TraceInfo { + KeyFrameTraceInfo, + FirTraceInfo, + PliTraceInfo, + RtpTraceInfo, + SrTraceInfo, +} + +table KeyFrameTraceInfo { + rtp_packet: FBS.RtpPacket.Dump (required); + is_rtx: bool; +} + +table FirTraceInfo { + ssrc: uint32; +} + +table PliTraceInfo { + ssrc: uint32; +} + +table RtpTraceInfo { + rtp_packet: FBS.RtpPacket.Dump (required); + is_rtx: bool; +} + +table SrTraceInfo { + ssrc: uint32; + ntp_sec: uint32; + ntp_frac: uint32; + rtp_ts: uint32; + packet_count: uint32; + octet_count: uint32; +} + +table TraceNotification { + type: TraceEventType; + timestamp: uint64; + direction: FBS.Common.TraceDirection; + info: TraceInfo; +} + diff --git a/worker/fbs/request.fbs b/worker/fbs/request.fbs new file mode 100644 index 0000000000..d9f869e878 --- /dev/null +++ b/worker/fbs/request.fbs @@ -0,0 +1,133 @@ +include "worker.fbs"; +include "router.fbs"; +include "transport.fbs"; +include "producer.fbs"; +include "consumer.fbs"; +include "dataConsumer.fbs"; +include "rtpObserver.fbs"; + +namespace FBS.Request; + +enum Method: uint8 { + WORKER_CLOSE = 0, + WORKER_DUMP, + WORKER_GET_RESOURCE_USAGE, + WORKER_UPDATE_SETTINGS, + WORKER_CREATE_WEBRTCSERVER, + WORKER_CREATE_ROUTER, + WORKER_WEBRTCSERVER_CLOSE, + WORKER_CLOSE_ROUTER, + WEBRTCSERVER_DUMP, + ROUTER_DUMP, + ROUTER_CREATE_WEBRTCTRANSPORT, + ROUTER_CREATE_WEBRTCTRANSPORT_WITH_SERVER, + ROUTER_CREATE_PLAINTRANSPORT, + ROUTER_CREATE_PIPETRANSPORT, + ROUTER_CREATE_DIRECTTRANSPORT, + ROUTER_CLOSE_TRANSPORT, + ROUTER_CREATE_ACTIVESPEAKEROBSERVER, + ROUTER_CREATE_AUDIOLEVELOBSERVER, + ROUTER_CLOSE_RTPOBSERVER, + TRANSPORT_DUMP, + TRANSPORT_GET_STATS, + TRANSPORT_CONNECT, + TRANSPORT_SET_MAX_INCOMING_BITRATE, + TRANSPORT_SET_MAX_OUTGOING_BITRATE, + TRANSPORT_SET_MIN_OUTGOING_BITRATE, + TRANSPORT_RESTART_ICE, + TRANSPORT_PRODUCE, + TRANSPORT_PRODUCE_DATA, + TRANSPORT_CONSUME, + TRANSPORT_CONSUME_DATA, + TRANSPORT_ENABLE_TRACE_EVENT, + TRANSPORT_CLOSE_PRODUCER, + TRANSPORT_CLOSE_CONSUMER, + TRANSPORT_CLOSE_DATAPRODUCER, + TRANSPORT_CLOSE_DATACONSUMER, + PLAINTRANSPORT_CONNECT, + PIPETRANSPORT_CONNECT, + WEBRTCTRANSPORT_CONNECT, + PRODUCER_DUMP, + PRODUCER_GET_STATS, + PRODUCER_PAUSE, + PRODUCER_RESUME, + PRODUCER_ENABLE_TRACE_EVENT, + CONSUMER_DUMP, + CONSUMER_GET_STATS, + CONSUMER_PAUSE, + CONSUMER_RESUME, + CONSUMER_SET_PREFERRED_LAYERS, + CONSUMER_SET_PRIORITY, + CONSUMER_REQUEST_KEY_FRAME, + CONSUMER_ENABLE_TRACE_EVENT, + DATAPRODUCER_DUMP, + DATAPRODUCER_GET_STATS, + DATAPRODUCER_PAUSE, + DATAPRODUCER_RESUME, + DATACONSUMER_DUMP, + DATACONSUMER_GET_STATS, + DATACONSUMER_PAUSE, + DATACONSUMER_RESUME, + DATACONSUMER_GET_BUFFERED_AMOUNT, + DATACONSUMER_SET_BUFFERED_AMOUNT_LOW_THRESHOLD, + DATACONSUMER_SEND, + DATACONSUMER_SET_SUBCHANNELS, + DATACONSUMER_ADD_SUBCHANNEL, + DATACONSUMER_REMOVE_SUBCHANNEL, + RTPOBSERVER_PAUSE, + RTPOBSERVER_RESUME, + RTPOBSERVER_ADD_PRODUCER, + RTPOBSERVER_REMOVE_PRODUCER, +} + +union Body { + Worker_UpdateSettingsRequest: FBS.Worker.UpdateSettingsRequest, + Worker_CreateWebRtcServerRequest: FBS.Worker.CreateWebRtcServerRequest, + Worker_CloseWebRtcServerRequest: FBS.Worker.CloseWebRtcServerRequest, + Worker_CreateRouterRequest: FBS.Worker.CreateRouterRequest, + Worker_CloseRouterRequest: FBS.Worker.CloseRouterRequest, + Router_CreateWebRtcTransportRequest: FBS.Router.CreateWebRtcTransportRequest, + Router_CreatePlainTransportRequest: FBS.Router.CreatePlainTransportRequest, + Router_CreatePipeTransportRequest: FBS.Router.CreatePipeTransportRequest, + Router_CreateDirectTransportRequest: FBS.Router.CreateDirectTransportRequest, + Router_CreateActiveSpeakerObserverRequest: FBS.Router.CreateActiveSpeakerObserverRequest, + Router_CreateAudioLevelObserverRequest: FBS.Router.CreateAudioLevelObserverRequest, + Router_CloseTransportRequest: FBS.Router.CloseTransportRequest, + Router_CloseRtpObserverRequest: FBS.Router.CloseRtpObserverRequest, + Transport_SetMaxIncomingBitrateRequest: FBS.Transport.SetMaxIncomingBitrateRequest, + Transport_SetMaxOutgoingBitrateRequest: FBS.Transport.SetMaxOutgoingBitrateRequest, + Transport_SetMinOutgoingBitrateRequest: FBS.Transport.SetMinOutgoingBitrateRequest, + Transport_ProduceRequest: FBS.Transport.ProduceRequest, + Transport_ConsumeRequest: FBS.Transport.ConsumeRequest, + Transport_ProduceDataRequest: FBS.Transport.ProduceDataRequest, + Transport_ConsumeDataRequest: FBS.Transport.ConsumeDataRequest, + Transport_EnableTraceEventRequest: FBS.Transport.EnableTraceEventRequest, + Transport_CloseProducerRequest: FBS.Transport.CloseProducerRequest, + Transport_CloseConsumerRequest: FBS.Transport.CloseConsumerRequest, + Transport_CloseDataProducerRequest: FBS.Transport.CloseDataProducerRequest, + Transport_CloseDataConsumerRequest: FBS.Transport.CloseDataConsumerRequest, + PlainTransport_ConnectRequest: FBS.PlainTransport.ConnectRequest, + PipeTransport_ConnectRequest: FBS.PipeTransport.ConnectRequest, + WebRtcTransport_ConnectRequest: FBS.WebRtcTransport.ConnectRequest, + Producer_EnableTraceEventRequest: FBS.Producer.EnableTraceEventRequest, + Consumer_SetPreferredLayersRequest: FBS.Consumer.SetPreferredLayersRequest, + Consumer_SetPriorityRequest: FBS.Consumer.SetPriorityRequest, + Consumer_EnableTraceEventRequest: FBS.Consumer.EnableTraceEventRequest, + DataConsumer_SetBufferedAmountLowThresholdRequest: FBS.DataConsumer.SetBufferedAmountLowThresholdRequest, + DataConsumer_SendRequest: FBS.DataConsumer.SendRequest, + DataConsumer_SetSubchannelsRequest: FBS.DataConsumer.SetSubchannelsRequest, + DataConsumer_AddSubchannelRequest: FBS.DataConsumer.AddSubchannelRequest, + DataConsumer_RemoveSubchannelRequest: FBS.DataConsumer.RemoveSubchannelRequest, + RtpObserver_AddProducerRequest: FBS.RtpObserver.AddProducerRequest, + RtpObserver_RemoveProducerRequest: FBS.RtpObserver.RemoveProducerRequest, +} + +table Request { + id: uint32; + method: Method; + handler_id: string (required); + body: Body; +} + +root_type Request; + diff --git a/worker/fbs/response.fbs b/worker/fbs/response.fbs new file mode 100644 index 0000000000..707e5750ef --- /dev/null +++ b/worker/fbs/response.fbs @@ -0,0 +1,54 @@ +include "worker.fbs"; +include "router.fbs"; +include "webRtcServer.fbs"; +include "transport.fbs"; +include "producer.fbs"; +include "consumer.fbs"; +include "dataProducer.fbs"; +include "dataConsumer.fbs"; + +namespace FBS.Response; + +union Body { + Worker_DumpResponse: FBS.Worker.DumpResponse, + Worker_ResourceUsageResponse: FBS.Worker.ResourceUsageResponse, + WebRtcServer_DumpResponse: FBS.WebRtcServer.DumpResponse, + Router_DumpResponse: FBS.Router.DumpResponse, + Transport_ProduceResponse: FBS.Transport.ProduceResponse, + Transport_ConsumeResponse: FBS.Transport.ConsumeResponse, + Transport_RestartIceResponse: FBS.Transport.RestartIceResponse, + PlainTransport_ConnectResponse: FBS.PlainTransport.ConnectResponse, + PlainTransport_DumpResponse: FBS.PlainTransport.DumpResponse, + PlainTransport_GetStatsResponse: FBS.PlainTransport.GetStatsResponse, + PipeTransport_ConnectResponse: FBS.PipeTransport.ConnectResponse, + PipeTransport_DumpResponse: FBS.PipeTransport.DumpResponse, + PipeTransport_GetStatsResponse: FBS.PipeTransport.GetStatsResponse, + DirectTransport_DumpResponse: FBS.DirectTransport.DumpResponse, + DirectTransport_GetStatsResponse: FBS.DirectTransport.GetStatsResponse, + WebRtcTransport_ConnectResponse: FBS.WebRtcTransport.ConnectResponse, + WebRtcTransport_DumpResponse: FBS.WebRtcTransport.DumpResponse, + WebRtcTransport_GetStatsResponse: FBS.WebRtcTransport.GetStatsResponse, + Producer_DumpResponse: FBS.Producer.DumpResponse, + Producer_GetStatsResponse: FBS.Producer.GetStatsResponse, + Consumer_DumpResponse: FBS.Consumer.DumpResponse, + Consumer_GetStatsResponse: FBS.Consumer.GetStatsResponse, + Consumer_SetPreferredLayersResponse: FBS.Consumer.SetPreferredLayersResponse, + Consumer_SetPriorityResponse: FBS.Consumer.SetPriorityResponse, + DataProducer_DumpResponse: FBS.DataProducer.DumpResponse, + DataProducer_GetStatsResponse: FBS.DataProducer.GetStatsResponse, + DataConsumer_GetBufferedAmountResponse: FBS.DataConsumer.GetBufferedAmountResponse, + DataConsumer_DumpResponse: FBS.DataConsumer.DumpResponse, + DataConsumer_GetStatsResponse: FBS.DataConsumer.GetStatsResponse, + DataConsumer_SetSubchannelsResponse: FBS.DataConsumer.SetSubchannelsResponse, + DataConsumer_AddSubchannelResponse: FBS.DataConsumer.AddSubchannelResponse, + DataConsumer_RemoveSubchannelResponse: FBS.DataConsumer.RemoveSubchannelResponse +} + +table Response { + id: uint32; + accepted: bool; + body: Body; + error: string; + reason: string; +} + diff --git a/worker/fbs/router.fbs b/worker/fbs/router.fbs new file mode 100644 index 0000000000..6c0a34b256 --- /dev/null +++ b/worker/fbs/router.fbs @@ -0,0 +1,60 @@ +include "common.fbs"; +include "activeSpeakerObserver.fbs"; +include "audioLevelObserver.fbs"; +include "transport.fbs"; +include "pipeTransport.fbs"; +include "plainTransport.fbs"; +include "webRtcTransport.fbs"; +include "directTransport.fbs"; + +namespace FBS.Router; + +table DumpResponse { + id: string (required); + transport_ids: [string] (required); + rtp_observer_ids: [string] (required); + map_producer_id_consumer_ids: [FBS.Common.StringStringArray] (required); + map_consumer_id_producer_id: [FBS.Common.StringString] (required); + map_producer_id_observer_ids: [FBS.Common.StringStringArray] (required); + map_data_producer_id_data_consumer_ids: [FBS.Common.StringStringArray] (required); + map_data_consumer_id_data_producer_id: [FBS.Common.StringString] (required); +} + +table CreatePipeTransportRequest { + transport_id: string (required); + options: FBS.PipeTransport.PipeTransportOptions (required); +} + +table CreatePlainTransportRequest { + transport_id: string (required); + options: FBS.PlainTransport.PlainTransportOptions (required); +} + +table CreateWebRtcTransportRequest { + transport_id: string (required); + options: FBS.WebRtcTransport.WebRtcTransportOptions (required); +} + +table CreateDirectTransportRequest { + transport_id: string (required); + options: FBS.DirectTransport.DirectTransportOptions (required); +} + +table CreateAudioLevelObserverRequest { + rtp_observer_id: string (required); + options: FBS.AudioLevelObserver.AudioLevelObserverOptions (required); +} + +table CreateActiveSpeakerObserverRequest { + rtp_observer_id: string (required); + options: FBS.ActiveSpeakerObserver.ActiveSpeakerObserverOptions (required); +} + +table CloseTransportRequest { + transport_id: string (required); +} + +table CloseRtpObserverRequest { + rtp_observer_id: string (required); +} + diff --git a/worker/fbs/rtpObserver.fbs b/worker/fbs/rtpObserver.fbs new file mode 100644 index 0000000000..68f2a40c14 --- /dev/null +++ b/worker/fbs/rtpObserver.fbs @@ -0,0 +1,10 @@ +namespace FBS.RtpObserver; + +table AddProducerRequest { + producer_id: string (required); +} + +table RemoveProducerRequest { + producer_id: string (required); +} + diff --git a/worker/fbs/rtpPacket.fbs b/worker/fbs/rtpPacket.fbs new file mode 100644 index 0000000000..3975d9d748 --- /dev/null +++ b/worker/fbs/rtpPacket.fbs @@ -0,0 +1,21 @@ +include "common.fbs"; + +namespace FBS.RtpPacket; + +table Dump { + payload_type: uint8; + sequence_number: uint16; + timestamp: uint32; + marker: bool; + ssrc: uint32; + is_key_frame: bool; + size: uint64; + payload_size: uint64; + spatial_layer: uint8; + temporal_layer: uint8; + mid: string; + rid: string; + rrid: string; + wide_sequence_number: uint16 = null; +} + diff --git a/worker/fbs/rtpParameters.fbs b/worker/fbs/rtpParameters.fbs new file mode 100644 index 0000000000..c3b61fe55a --- /dev/null +++ b/worker/fbs/rtpParameters.fbs @@ -0,0 +1,129 @@ +namespace FBS.RtpParameters; + +enum MediaKind: uint8 { + AUDIO, + VIDEO +} + +enum Type: uint8 { + SIMPLE, + SIMULCAST, + SVC, + PIPE +} + +// Boolean is a uint8/byte type. +table Boolean { + value: uint8; +} + +table Integer32 { + value: int32; +} + +table Integer32Array { + value: [int32]; +} + +table Double { + value: double; +} + +table String { + value: string (required); +} + +union Value { + Boolean, + Integer32, + Double, + String, + Integer32Array, +} + +table Parameter { + name: string (required); + value: Value (required); +} + +table RtcpFeedback { + // TODO: Create a specifc type. + type: string (required); + parameter: string; +} + +table RtpCodecParameters { + mime_type: string (required); + payload_type: uint8; + clock_rate: uint32; + channels: uint8 = null; + parameters: [Parameter]; + rtcp_feedback: [RtcpFeedback]; +} + +enum RtpHeaderExtensionUri: uint8 { + Mid, + RtpStreamId, + RepairRtpStreamId, + FrameMarkingDraft07, + FrameMarking, + AudioLevel, + VideoOrientation, + TimeOffset, + TransportWideCcDraft01, + AbsSendTime, + AbsCaptureTime, + PlayoutDelay, +} + +table RtpHeaderExtensionParameters { + uri: RtpHeaderExtensionUri; + id: uint8; + encrypt: bool = false; + parameters: [Parameter]; +} + +table Rtx { + ssrc: uint32; +} + +table RtpEncodingParameters { + ssrc: uint32 = null; + rid: string; + codec_payload_type: uint8 = null; + rtx: Rtx; + dtx: bool = false; + scalability_mode: string; + max_bitrate: uint32 = null; +} + +table RtcpParameters { + cname: string; + reduced_size: bool = true; +} + +table RtpParameters { + mid: string; + codecs: [RtpCodecParameters] (required); + header_extensions: [RtpHeaderExtensionParameters] (required); + encodings: [RtpEncodingParameters] (required); + rtcp: RtcpParameters (required); +} + +table CodecMapping { + payload_type: uint8; + mapped_payload_type: uint8; +} + +table EncodingMapping { + rid: string; + ssrc: uint32 = null; + scalability_mode: string; + mapped_ssrc: uint32; +} + +table RtpMapping { + codecs: [CodecMapping] (required); + encodings: [EncodingMapping] (required); +} + diff --git a/worker/fbs/rtpStream.fbs b/worker/fbs/rtpStream.fbs new file mode 100644 index 0000000000..e30713ce4a --- /dev/null +++ b/worker/fbs/rtpStream.fbs @@ -0,0 +1,82 @@ +include "rtpParameters.fbs"; +include "rtxStream.fbs"; + +namespace FBS.RtpStream; + +table Params { + encoding_idx: uint32; + ssrc: uint32; + payload_type: uint8; + mime_type: string (required); + clock_rate: uint32; + rid: string; + cname: string (required); + rtx_ssrc: uint32 = null; + rtx_payload_type: uint8 = null; + use_nack: bool; + use_pli: bool; + use_fir: bool; + use_in_band_fec: bool; + use_dtx: bool; + spatial_layers: uint8; + temporal_layers: uint8; +} + +table Dump { + params: Params (required); + score: uint8; + rtx_stream: FBS.RtxStream.RtxDump; +} + +table BitrateByLayer { + layer: string (required); + bitrate: uint32; +} + +union StatsData { + BaseStats, + RecvStats, + SendStats, +} + +table Stats { + data: StatsData (required); +} + +table BaseStats { + timestamp: uint64; + ssrc: uint32; + kind: FBS.RtpParameters.MediaKind; + mime_type: string (required); + packets_lost: uint64; + fraction_lost: uint8; + packets_discarded: uint64; + packets_retransmitted: uint64; + packets_repaired: uint64; + nack_count: uint64; + nack_packet_count: uint64; + pli_count: uint64; + fir_count: uint64; + score: uint8; + rid: string; + rtx_ssrc: uint32 = null; + rtx_packets_discarded: uint64; + round_trip_time: float; +} + +table RecvStats { + base: Stats (required); + jitter: uint32; + packet_count: uint64; + byte_count: uint64; + bitrate: uint32; + bitrate_by_layer: [BitrateByLayer] (required); +} + +table SendStats { + base: Stats (required); + packet_count: uint64; + byte_count: uint64; + bitrate: uint32; +} + diff --git a/worker/fbs/rtxStream.fbs b/worker/fbs/rtxStream.fbs new file mode 100644 index 0000000000..693d27ab77 --- /dev/null +++ b/worker/fbs/rtxStream.fbs @@ -0,0 +1,16 @@ +namespace FBS.RtxStream; + +table Params { + ssrc: uint32; + payload_type: uint8; + mime_type: string (required); + clock_rate: uint32; + rrid: string; + cname: string (required); +} + +// NOTE: Naming this Dump collides in TS generation for Producer.DumpResponse. +table RtxDump { + params: Params (required); +} + diff --git a/worker/fbs/sctpAssociation.fbs b/worker/fbs/sctpAssociation.fbs new file mode 100644 index 0000000000..85e83c12a6 --- /dev/null +++ b/worker/fbs/sctpAssociation.fbs @@ -0,0 +1,10 @@ +namespace FBS.SctpAssociation; + +enum SctpState: uint8 { + NEW = 0, + CONNECTING, + CONNECTED, + FAILED, + CLOSED, +} + diff --git a/worker/fbs/sctpParameters.fbs b/worker/fbs/sctpParameters.fbs new file mode 100644 index 0000000000..019eecd9b2 --- /dev/null +++ b/worker/fbs/sctpParameters.fbs @@ -0,0 +1,25 @@ +namespace FBS.SctpParameters; + +table NumSctpStreams { + os: uint16 = 1024; + mis: uint16 = 1024; +} + +table SctpParameters { + // Port is always 5000. + port: uint16 = 5000; + os: uint16; + mis: uint16; + max_message_size: uint32; + send_buffer_size: uint32; + sctp_buffered_amount: uint32; + is_data_channel: bool; +} + +table SctpStreamParameters { + stream_id: uint16; + ordered: bool = null; + max_packet_life_time: uint16 = null; + max_retransmits: uint16 = null; +} + diff --git a/worker/fbs/srtpParameters.fbs b/worker/fbs/srtpParameters.fbs new file mode 100644 index 0000000000..8f6a194469 --- /dev/null +++ b/worker/fbs/srtpParameters.fbs @@ -0,0 +1,14 @@ +namespace FBS.SrtpParameters; + +enum SrtpCryptoSuite: uint8 { + AEAD_AES_256_GCM, + AEAD_AES_128_GCM, + AES_CM_128_HMAC_SHA1_80, + AES_CM_128_HMAC_SHA1_32, +} + +table SrtpParameters { + crypto_suite: FBS.SrtpParameters.SrtpCryptoSuite; + key_base64: string (required); +} + diff --git a/worker/fbs/transport.fbs b/worker/fbs/transport.fbs new file mode 100644 index 0000000000..655db6389e --- /dev/null +++ b/worker/fbs/transport.fbs @@ -0,0 +1,254 @@ +include "common.fbs"; +include "consumer.fbs"; +include "dataProducer.fbs"; +include "rtpParameters.fbs"; +include "sctpAssociation.fbs"; +include "sctpParameters.fbs"; +include "srtpParameters.fbs"; + +namespace FBS.Transport; + +enum Protocol: uint8 { + UDP = 1, + TCP +} + +table PortRange { + min: uint16 = 0; + max: uint16 = 0; +} + +table SocketFlags { + ipv6_only: bool = false; + udp_reuse_port: bool = false; +} + +table ListenInfo { + protocol: Protocol = UDP; + ip: string (required); + announced_address: string; + port: uint16 = 0; + port_range: PortRange (required); + flags: SocketFlags (required); + send_buffer_size: uint32 = 0; + recv_buffer_size: uint32 = 0; +} + +table RestartIceResponse { + username_fragment: string (required); + password: string (required); + ice_lite: bool; +} + +table ProduceRequest { + producer_id: string (required); + kind: FBS.RtpParameters.MediaKind; + rtp_parameters: FBS.RtpParameters.RtpParameters (required); + rtp_mapping: FBS.RtpParameters.RtpMapping (required); + key_frame_request_delay: uint32; + paused: bool = false; +} + +table ProduceResponse { + type: FBS.RtpParameters.Type; +} + +table ConsumeRequest { + consumer_id: string (required); + producer_id: string (required); + kind: FBS.RtpParameters.MediaKind; + rtp_parameters: FBS.RtpParameters.RtpParameters (required); + type: FBS.RtpParameters.Type; + consumable_rtp_encodings: [FBS.RtpParameters.RtpEncodingParameters] (required); + paused: bool = false; + preferred_layers: FBS.Consumer.ConsumerLayers; + ignore_dtx: bool = false; +} + +table ConsumeResponse { + paused: bool; + producer_paused: bool; + score: FBS.Consumer.ConsumerScore (required); + preferred_layers: FBS.Consumer.ConsumerLayers; +} + +table ProduceDataRequest { + data_producer_id: string (required); + type: FBS.DataProducer.Type; + sctp_stream_parameters: FBS.SctpParameters.SctpStreamParameters; + label: string; + protocol: string; + paused: bool = false; +} + +table ConsumeDataRequest { + data_consumer_id: string (required); + data_producer_id: string (required); + type: FBS.DataProducer.Type; + sctp_stream_parameters: FBS.SctpParameters.SctpStreamParameters; + label: string; + protocol: string; + paused: bool = false; + subchannels: [uint16]; +} + +table Tuple { + local_address: string (required); + local_port: uint16; + remote_ip: string; + remote_port: uint16; + protocol: Protocol = UDP; +} + +table RtpListener { + ssrc_table: [FBS.Common.Uint32String] (required); + mid_table: [FBS.Common.StringString] (required); + rid_table: [FBS.Common.StringString] (required); +} + +table SctpListener { + stream_id_table: [FBS.Common.Uint16String] (required); +} + +table RecvRtpHeaderExtensions { + mid: uint8 = null; + rid: uint8 = null; + rrid: uint8 = null; + abs_send_time: uint8 = null; + transport_wide_cc01: uint8 = null; +} + +table Options { + direct: bool = false; + + /// Only needed for DirectTransport. This value is handled by base Transport. + max_message_size: uint32 = null; + initial_available_outgoing_bitrate: uint32 = null; + enable_sctp: bool = false; + num_sctp_streams: FBS.SctpParameters.NumSctpStreams; + max_sctp_message_size: uint32; + sctp_send_buffer_size: uint32; + is_data_channel: bool = false; +} + +enum TraceEventType: uint8 { + PROBATION = 0, + BWE +} + +table Dump { + id: string (required); + direct: bool = false; + producer_ids: [string] (required); + consumer_ids: [string] (required); + map_ssrc_consumer_id: [FBS.Common.Uint32String] (required); + map_rtx_ssrc_consumer_id: [FBS.Common.Uint32String] (required); + data_producer_ids: [string] (required); + data_consumer_ids: [string] (required); + recv_rtp_header_extensions: RecvRtpHeaderExtensions (required); + rtp_listener: RtpListener (required); + max_message_size: uint32; + sctp_parameters: FBS.SctpParameters.SctpParameters; + sctp_state: FBS.SctpAssociation.SctpState = null; + sctp_listener: SctpListener; + trace_event_types: [TraceEventType] (required); +} + +table Stats { + transport_id: string (required); + timestamp: uint64; + sctp_state: FBS.SctpAssociation.SctpState = null; + bytes_received: uint64; + recv_bitrate: uint32; + bytes_sent: uint64; + send_bitrate: uint32; + rtp_bytes_received: uint64; + rtp_recv_bitrate: uint32; + rtp_bytes_sent: uint64; + rtp_send_bitrate: uint32; + rtx_bytes_received: uint64; + rtx_recv_bitrate: uint32; + rtx_bytes_sent: uint64; + rtx_send_bitrate: uint32; + probation_bytes_sent: uint64; + probation_send_bitrate: uint32; + available_outgoing_bitrate: uint32 = null; + available_incoming_bitrate: uint32 = null; + max_incoming_bitrate: uint32 = null; + max_outgoing_bitrate: uint32 = null; + min_outgoing_bitrate: uint32 = null; + rtp_packet_loss_received: float64 = null; + rtp_packet_loss_sent: float64 = null; +} + +table SetMaxIncomingBitrateRequest { + max_incoming_bitrate: uint32; +} + +table SetMaxOutgoingBitrateRequest { + max_outgoing_bitrate: uint32; +} + +table SetMinOutgoingBitrateRequest { + min_outgoing_bitrate: uint32; +} + +table EnableTraceEventRequest { + events: [TraceEventType] (required); +} + +table CloseProducerRequest { + producer_id: string (required); +} + +table CloseConsumerRequest { + consumer_id: string (required); +} + +table CloseDataProducerRequest { + data_producer_id: string (required); +} + +table CloseDataConsumerRequest { + data_consumer_id: string (required); +} + +// Notifications to Worker. + +table SendRtcpNotification { + data: [uint8] (required); +} + +// Notifications from Worker. + +table SctpStateChangeNotification { + sctp_state: FBS.SctpAssociation.SctpState; +} + +union TraceInfo { + BweTraceInfo, +} + +enum BweType: uint8 { + TRANSPORT_CC = 0, + REMB +} + +table BweTraceInfo { + bwe_type: BweType; + desired_bitrate: uint32; + effective_desired_bitrate: uint32; + min_bitrate: uint32; + max_bitrate: uint32; + start_bitrate: uint32; + max_padding_bitrate: uint32; + available_bitrate: uint32; +} + +table TraceNotification { + type: TraceEventType; + timestamp: uint64; + direction: FBS.Common.TraceDirection; + info: TraceInfo; +} + diff --git a/worker/fbs/webRtcServer.fbs b/worker/fbs/webRtcServer.fbs new file mode 100644 index 0000000000..7d9c4edde1 --- /dev/null +++ b/worker/fbs/webRtcServer.fbs @@ -0,0 +1,28 @@ +include "transport.fbs"; + +namespace FBS.WebRtcServer; + +table IpPort { + ip: string (required); + port: uint16; +} + +table IceUserNameFragment { + local_ice_username_fragment: string (required); + web_rtc_transport_id: string (required); +} + +table TupleHash { + tuple_hash: uint64; + web_rtc_transport_id: string (required); +} + +table DumpResponse { + id: string (required); + udp_sockets: [IpPort] (required); + tcp_servers: [IpPort] (required); + web_rtc_transport_ids: [string] (required); + local_ice_username_fragments: [IceUserNameFragment] (required); + tuple_hashes: [TupleHash] (required); +} + diff --git a/worker/fbs/webRtcTransport.fbs b/worker/fbs/webRtcTransport.fbs new file mode 100644 index 0000000000..4d66c7a511 --- /dev/null +++ b/worker/fbs/webRtcTransport.fbs @@ -0,0 +1,138 @@ +include "transport.fbs"; +include "sctpParameters.fbs"; + +namespace FBS.WebRtcTransport; + +table ListenIndividual { + listen_infos: [FBS.Transport.ListenInfo] (required); +} + +table ListenServer { + web_rtc_server_id: string (required); +} + +union Listen { + ListenIndividual, + ListenServer, +} + +table WebRtcTransportOptions { + base: FBS.Transport.Options (required); + listen: Listen (required); + enable_udp: bool = true; + enable_tcp: bool = true; + prefer_udp: bool = false; + prefer_tcp: bool = false; + ice_consent_timeout: uint8 = 30; +} + +enum FingerprintAlgorithm: uint8 { + SHA1, + SHA224, + SHA256, + SHA384, + SHA512, +} + +table Fingerprint { + algorithm: FingerprintAlgorithm; + value: string (required); +} + +enum DtlsRole: uint8 { + AUTO, + CLIENT, + SERVER +} + +enum DtlsState: uint8 { + NEW, + CONNECTING, + CONNECTED, + FAILED, + CLOSED +} + +table DtlsParameters { + fingerprints: [Fingerprint] (required); + role: DtlsRole = AUTO; +} + +table IceParameters { + username_fragment: string (required); + password: string (required); + ice_lite: bool = true; +} + +enum IceCandidateType: uint8 { + HOST +} + +enum IceCandidateTcpType: uint8 { + PASSIVE +} + +enum IceRole: uint8 { + CONTROLLED, + CONTROLLING +} + +enum IceState: uint8 { + NEW, + CONNECTED, + COMPLETED, + DISCONNECTED, +} + +table IceCandidate { + foundation: string (required); + priority: uint32; + address: string (required); + protocol: FBS.Transport.Protocol = UDP; + port: uint16; + type: IceCandidateType; + tcp_type: IceCandidateTcpType = null; +} + +table ConnectRequest { + dtls_parameters: DtlsParameters (required); +} + +table ConnectResponse { + dtls_local_role: DtlsRole; +} + +table DumpResponse { + base: FBS.Transport.Dump (required); + ice_role: IceRole; + ice_parameters: IceParameters (required); + ice_candidates: [IceCandidate] (required); + ice_state: IceState; + ice_selected_tuple: FBS.Transport.Tuple; + dtls_parameters: DtlsParameters (required); + dtls_state: DtlsState; +} + +table GetStatsResponse { + base: FBS.Transport.Stats (required); + ice_role: IceRole; + ice_state: IceState; + ice_selected_tuple: FBS.Transport.Tuple; + dtls_state: DtlsState; +} + +// Notifications from Worker. + +table IceSelectedTupleChangeNotification { + tuple: FBS.Transport.Tuple (required); +} + +table IceStateChangeNotification { + ice_state: IceState; +} + +table DtlsStateChangeNotification { + dtls_state: DtlsState; + remote_cert: string; +} + diff --git a/worker/fbs/worker.fbs b/worker/fbs/worker.fbs new file mode 100644 index 0000000000..2dc7702b15 --- /dev/null +++ b/worker/fbs/worker.fbs @@ -0,0 +1,59 @@ +include "liburing.fbs"; +include "transport.fbs"; + +namespace FBS.Worker; + +table ChannelMessageHandlers { + channel_request_handlers: [string] (required); + channel_notification_handlers: [string] (required); +} + +table DumpResponse { + pid: uint32; + web_rtc_server_ids: [string] (required); + router_ids: [string] (required); + channel_message_handlers: ChannelMessageHandlers (required); + liburing: FBS.LibUring.Dump; +} + +table ResourceUsageResponse { + ru_utime: uint64; + ru_stime: uint64; + ru_maxrss: uint64; + ru_ixrss: uint64; + ru_idrss: uint64; + ru_isrss: uint64; + ru_minflt: uint64; + ru_majflt: uint64; + ru_nswap: uint64; + ru_inblock: uint64; + ru_oublock: uint64; + ru_msgsnd: uint64; + ru_msgrcv: uint64; + ru_nsignals: uint64; + ru_nvcsw: uint64; + ru_nivcsw: uint64; +} + +table UpdateSettingsRequest { + log_level: string; + log_tags: [string]; +} + +table CreateWebRtcServerRequest { + web_rtc_server_id: string (required); + listen_infos: [FBS.Transport.ListenInfo]; +} + +table CloseWebRtcServerRequest { + web_rtc_server_id: string (required); +} + +table CreateRouterRequest { + router_id: string (required); +} + +table CloseRouterRequest { + router_id: string (required); +} + diff --git a/worker/fuzzer/include/RTC/Codecs/FuzzerH264.hpp b/worker/fuzzer/include/RTC/Codecs/FuzzerH264.hpp new file mode 100644 index 0000000000..21a2364365 --- /dev/null +++ b/worker/fuzzer/include/RTC/Codecs/FuzzerH264.hpp @@ -0,0 +1,20 @@ +#ifndef MS_FUZZER_RTC_CODECS_H264_HPP +#define MS_FUZZER_RTC_CODECS_H264_HPP + +#include "common.hpp" + +namespace Fuzzer +{ + namespace RTC + { + namespace Codecs + { + namespace H264 + { + void Fuzz(const uint8_t* data, size_t len); + } + } // namespace Codecs + } // namespace RTC +} // namespace Fuzzer + +#endif diff --git a/worker/fuzzer/include/RTC/Codecs/FuzzerH264_SVC.hpp b/worker/fuzzer/include/RTC/Codecs/FuzzerH264_SVC.hpp new file mode 100644 index 0000000000..d72125dc5d --- /dev/null +++ b/worker/fuzzer/include/RTC/Codecs/FuzzerH264_SVC.hpp @@ -0,0 +1,20 @@ +#ifndef MS_FUZZER_RTC_CODECS_H264_SVC_HPP +#define MS_FUZZER_RTC_CODECS_H264_SVC_HPP + +#include "common.hpp" + +namespace Fuzzer +{ + namespace RTC + { + namespace Codecs + { + namespace H264_SVC + { + void Fuzz(const uint8_t* data, size_t len); + } + } // namespace Codecs + } // namespace RTC +} // namespace Fuzzer + +#endif diff --git a/worker/fuzzer/include/RTC/Codecs/FuzzerOpus.hpp b/worker/fuzzer/include/RTC/Codecs/FuzzerOpus.hpp new file mode 100644 index 0000000000..2e52616600 --- /dev/null +++ b/worker/fuzzer/include/RTC/Codecs/FuzzerOpus.hpp @@ -0,0 +1,20 @@ +#ifndef MS_FUZZER_RTC_CODECS_OPUS_HPP +#define MS_FUZZER_RTC_CODECS_OPUS_HPP + +#include "common.hpp" + +namespace Fuzzer +{ + namespace RTC + { + namespace Codecs + { + namespace Opus + { + void Fuzz(const uint8_t* data, size_t len); + } + } // namespace Codecs + } // namespace RTC +} // namespace Fuzzer + +#endif diff --git a/worker/fuzzer/include/RTC/Codecs/FuzzerVP8.hpp b/worker/fuzzer/include/RTC/Codecs/FuzzerVP8.hpp new file mode 100644 index 0000000000..dfa87efec8 --- /dev/null +++ b/worker/fuzzer/include/RTC/Codecs/FuzzerVP8.hpp @@ -0,0 +1,20 @@ +#ifndef MS_FUZZER_RTC_CODECS_VP8_HPP +#define MS_FUZZER_RTC_CODECS_VP8_HPP + +#include "common.hpp" + +namespace Fuzzer +{ + namespace RTC + { + namespace Codecs + { + namespace VP8 + { + void Fuzz(const uint8_t* data, size_t len); + } + } // namespace Codecs + } // namespace RTC +} // namespace Fuzzer + +#endif diff --git a/worker/fuzzer/include/RTC/Codecs/FuzzerVP9.hpp b/worker/fuzzer/include/RTC/Codecs/FuzzerVP9.hpp new file mode 100644 index 0000000000..93743cbeae --- /dev/null +++ b/worker/fuzzer/include/RTC/Codecs/FuzzerVP9.hpp @@ -0,0 +1,20 @@ +#ifndef MS_FUZZER_RTC_CODECS_VP9_HPP +#define MS_FUZZER_RTC_CODECS_VP9_HPP + +#include "common.hpp" + +namespace Fuzzer +{ + namespace RTC + { + namespace Codecs + { + namespace VP9 + { + void Fuzz(const uint8_t* data, size_t len); + } + } // namespace Codecs + } // namespace RTC +} // namespace Fuzzer + +#endif diff --git a/worker/fuzzer/include/RTC/FuzzerDtlsTransport.hpp b/worker/fuzzer/include/RTC/FuzzerDtlsTransport.hpp new file mode 100644 index 0000000000..f5f6baa780 --- /dev/null +++ b/worker/fuzzer/include/RTC/FuzzerDtlsTransport.hpp @@ -0,0 +1,39 @@ +#ifndef MS_FUZZER_RTC_DTLS_TRANSPORT_HPP +#define MS_FUZZER_RTC_DTLS_TRANSPORT_HPP + +#include "common.hpp" +#include "RTC/DtlsTransport.hpp" + +namespace Fuzzer +{ + namespace RTC + { + namespace DtlsTransport + { + class DtlsTransportListener : public ::RTC::DtlsTransport::Listener + { + /* Pure virtual methods inherited from RTC::DtlsTransport::Listener. */ + public: + void OnDtlsTransportConnecting(const ::RTC::DtlsTransport* dtlsTransport) override; + void OnDtlsTransportConnected( + const ::RTC::DtlsTransport* dtlsTransport, + ::RTC::SrtpSession::CryptoSuite srtpCryptoSuite, + uint8_t* srtpLocalKey, + size_t srtpLocalKeyLen, + uint8_t* srtpRemoteKey, + size_t srtpRemoteKeyLen, + std::string& remoteCert) override; + void OnDtlsTransportFailed(const ::RTC::DtlsTransport* dtlsTransport) override; + void OnDtlsTransportClosed(const ::RTC::DtlsTransport* dtlsTransport) override; + void OnDtlsTransportSendData( + const ::RTC::DtlsTransport* dtlsTransport, const uint8_t* data, size_t len) override; + void OnDtlsTransportApplicationDataReceived( + const ::RTC::DtlsTransport* dtlsTransport, const uint8_t* data, size_t len) override; + }; + + void Fuzz(const uint8_t* data, size_t len); + } // namespace DtlsTransport + } // namespace RTC +} // namespace Fuzzer + +#endif diff --git a/worker/fuzzer/include/RTC/FuzzerRateCalculator.hpp b/worker/fuzzer/include/RTC/FuzzerRateCalculator.hpp new file mode 100644 index 0000000000..c7386bb3db --- /dev/null +++ b/worker/fuzzer/include/RTC/FuzzerRateCalculator.hpp @@ -0,0 +1,17 @@ +#ifndef MS_FUZZER_RTC_RATE_CALCULATOR_HPP +#define MS_FUZZER_RTC_RATE_CALCULATOR_HPP + +#include "common.hpp" + +namespace Fuzzer +{ + namespace RTC + { + namespace RateCalculator + { + void Fuzz(const uint8_t* data, size_t len); + } + } // namespace RTC +} // namespace Fuzzer + +#endif diff --git a/worker/fuzzer/include/RTC/FuzzerRtpRetransmissionBuffer.hpp b/worker/fuzzer/include/RTC/FuzzerRtpRetransmissionBuffer.hpp new file mode 100644 index 0000000000..2a084b7960 --- /dev/null +++ b/worker/fuzzer/include/RTC/FuzzerRtpRetransmissionBuffer.hpp @@ -0,0 +1,17 @@ +#ifndef MS_FUZZER_RTC_RTP_RETRANSMISSION_BUFFER_HPP +#define MS_FUZZER_RTC_RTP_RETRANSMISSION_BUFFER_HPP + +#include "common.hpp" + +namespace Fuzzer +{ + namespace RTC + { + namespace RtpRetransmissionBuffer + { + void Fuzz(const uint8_t* data, size_t len); + } + } // namespace RTC +} // namespace Fuzzer + +#endif diff --git a/worker/fuzzer/include/RTC/FuzzerSeqManager.hpp b/worker/fuzzer/include/RTC/FuzzerSeqManager.hpp new file mode 100644 index 0000000000..e3644e1f1f --- /dev/null +++ b/worker/fuzzer/include/RTC/FuzzerSeqManager.hpp @@ -0,0 +1,17 @@ +#ifndef MS_FUZZER_RTC_SEQ_MANAGER_HPP +#define MS_FUZZER_RTC_SEQ_MANAGER_HPP + +#include "common.hpp" + +namespace Fuzzer +{ + namespace RTC + { + namespace SeqManager + { + void Fuzz(const uint8_t* data, size_t len); + } + } // namespace RTC +} // namespace Fuzzer + +#endif diff --git a/worker/fuzzer/reports/crash-2d204ea940d313bbdb69874f035ec5e7664b7181 b/worker/fuzzer/reports/crash-2d204ea940d313bbdb69874f035ec5e7664b7181 new file mode 100644 index 0000000000..7e768a24a6 Binary files /dev/null and b/worker/fuzzer/reports/crash-2d204ea940d313bbdb69874f035ec5e7664b7181 differ diff --git a/worker/fuzzer/reports/crash-7e7caf72377ad55d353719f28febb5238eadfc9e b/worker/fuzzer/reports/crash-7e7caf72377ad55d353719f28febb5238eadfc9e new file mode 100644 index 0000000000..e570989278 --- /dev/null +++ b/worker/fuzzer/reports/crash-7e7caf72377ad55d353719f28febb5238eadfc9e @@ -0,0 +1 @@ +88t \ No newline at end of file diff --git a/worker/fuzzer/reports/crash-9cc885b84ba02d766f422c6512ead3808ded0189 b/worker/fuzzer/reports/crash-9cc885b84ba02d766f422c6512ead3808ded0189 new file mode 100644 index 0000000000..e450f03ee8 Binary files /dev/null and b/worker/fuzzer/reports/crash-9cc885b84ba02d766f422c6512ead3808ded0189 differ diff --git a/worker/fuzzer/reports/crash-b75c1208384621922270e954b4902442592c3ca9 b/worker/fuzzer/reports/crash-b75c1208384621922270e954b4902442592c3ca9 new file mode 100644 index 0000000000..4a61033691 --- /dev/null +++ b/worker/fuzzer/reports/crash-b75c1208384621922270e954b4902442592c3ca9 @@ -0,0 +1 @@ +d˙ta˙˙t˙€ \ No newline at end of file diff --git a/worker/fuzzer/src/FuzzerUtils.cpp b/worker/fuzzer/src/FuzzerUtils.cpp index 10ef166519..7ed8565d27 100644 --- a/worker/fuzzer/src/FuzzerUtils.cpp +++ b/worker/fuzzer/src/FuzzerUtils.cpp @@ -12,10 +12,13 @@ void Fuzzer::Utils::Fuzz(const uint8_t* data, size_t len) /* IP class. */ std::string ip; + ip = std::string(reinterpret_cast(data2), INET6_ADDRSTRLEN / 2); ::Utils::IP::GetFamily(ip); + ip = std::string(reinterpret_cast(data2), INET6_ADDRSTRLEN); ::Utils::IP::GetFamily(ip); + ip = std::string(reinterpret_cast(data2), INET6_ADDRSTRLEN * 2); ::Utils::IP::GetFamily(ip); @@ -23,6 +26,7 @@ void Fuzzer::Utils::Fuzz(const uint8_t* data, size_t len) try { auto ip = std::string(reinterpret_cast(data2), len); + ::Utils::IP::NormalizeIp(ip); } catch (const MediaSoupError& error) @@ -60,6 +64,7 @@ void Fuzzer::Utils::Fuzz(const uint8_t* data, size_t len) try { size_t outLen; + ::Utils::String::Base64Encode(data2, len); ::Utils::String::Base64Decode(data2, len, outLen); } @@ -70,6 +75,7 @@ void Fuzzer::Utils::Fuzz(const uint8_t* data, size_t len) /* Time class. */ auto ntp = ::Utils::Time::TimeMs2Ntp(static_cast(len)); + ::Utils::Time::Ntp2TimeMs(ntp); ::Utils::Time::IsNewerTimestamp(static_cast(len), static_cast(len * len)); ::Utils::Time::IsNewerTimestamp(static_cast(len * len), static_cast(len)); diff --git a/worker/fuzzer/src/RTC/Codecs/FuzzerH264.cpp b/worker/fuzzer/src/RTC/Codecs/FuzzerH264.cpp new file mode 100644 index 0000000000..48404e41e8 --- /dev/null +++ b/worker/fuzzer/src/RTC/Codecs/FuzzerH264.cpp @@ -0,0 +1,14 @@ +#include "RTC/Codecs/FuzzerH264.hpp" +#include "RTC/Codecs/H264.hpp" + +void Fuzzer::RTC::Codecs::H264::Fuzz(const uint8_t* data, size_t len) +{ + ::RTC::Codecs::H264::PayloadDescriptor* descriptor = ::RTC::Codecs::H264::Parse(data, len); + + if (!descriptor) + { + return; + } + + delete descriptor; +} diff --git a/worker/fuzzer/src/RTC/Codecs/FuzzerH264_SVC.cpp b/worker/fuzzer/src/RTC/Codecs/FuzzerH264_SVC.cpp new file mode 100644 index 0000000000..b0af2e6012 --- /dev/null +++ b/worker/fuzzer/src/RTC/Codecs/FuzzerH264_SVC.cpp @@ -0,0 +1,14 @@ +#include "RTC/Codecs/FuzzerH264_SVC.hpp" +#include "RTC/Codecs/H264_SVC.hpp" + +void Fuzzer::RTC::Codecs::H264_SVC::Fuzz(const uint8_t* data, size_t len) +{ + ::RTC::Codecs::H264_SVC::PayloadDescriptor* descriptor = ::RTC::Codecs::H264_SVC::Parse(data, len); + + if (!descriptor) + { + return; + } + + delete descriptor; +} diff --git a/worker/fuzzer/src/RTC/Codecs/FuzzerOpus.cpp b/worker/fuzzer/src/RTC/Codecs/FuzzerOpus.cpp new file mode 100644 index 0000000000..188762c8b8 --- /dev/null +++ b/worker/fuzzer/src/RTC/Codecs/FuzzerOpus.cpp @@ -0,0 +1,14 @@ +#include "RTC/Codecs/FuzzerOpus.hpp" +#include "RTC/Codecs/Opus.hpp" + +void Fuzzer::RTC::Codecs::Opus::Fuzz(const uint8_t* data, size_t len) +{ + ::RTC::Codecs::Opus::PayloadDescriptor* descriptor = ::RTC::Codecs::Opus::Parse(data, len); + + if (!descriptor) + { + return; + } + + delete descriptor; +} diff --git a/worker/fuzzer/src/RTC/Codecs/FuzzerVP8.cpp b/worker/fuzzer/src/RTC/Codecs/FuzzerVP8.cpp new file mode 100644 index 0000000000..a628bd5e11 --- /dev/null +++ b/worker/fuzzer/src/RTC/Codecs/FuzzerVP8.cpp @@ -0,0 +1,14 @@ +#include "RTC/Codecs/FuzzerVP8.hpp" +#include "RTC/Codecs/VP8.hpp" + +void Fuzzer::RTC::Codecs::VP8::Fuzz(const uint8_t* data, size_t len) +{ + ::RTC::Codecs::VP8::PayloadDescriptor* descriptor = ::RTC::Codecs::VP8::Parse(data, len); + + if (!descriptor) + { + return; + } + + delete descriptor; +} diff --git a/worker/fuzzer/src/RTC/Codecs/FuzzerVP9.cpp b/worker/fuzzer/src/RTC/Codecs/FuzzerVP9.cpp new file mode 100644 index 0000000000..c10a177a79 --- /dev/null +++ b/worker/fuzzer/src/RTC/Codecs/FuzzerVP9.cpp @@ -0,0 +1,14 @@ +#include "RTC/Codecs/FuzzerVP9.hpp" +#include "RTC/Codecs/VP9.hpp" + +void Fuzzer::RTC::Codecs::VP9::Fuzz(const uint8_t* data, size_t len) +{ + ::RTC::Codecs::VP9::PayloadDescriptor* descriptor = ::RTC::Codecs::VP9::Parse(data, len); + + if (!descriptor) + { + return; + } + + delete descriptor; +} diff --git a/worker/fuzzer/src/RTC/FuzzerDtlsTransport.cpp b/worker/fuzzer/src/RTC/FuzzerDtlsTransport.cpp new file mode 100644 index 0000000000..91e1993897 --- /dev/null +++ b/worker/fuzzer/src/RTC/FuzzerDtlsTransport.cpp @@ -0,0 +1,118 @@ +#define MS_CLASS "Fuzzer::RTC::DtlsTransport" +// #define MS_LOG_DEV_LEVEL 3 + +#include "RTC/FuzzerDtlsTransport.hpp" +#include "Logger.hpp" +#include "Utils.hpp" + +// DtlsTransport singleton. It's reset every time DTLS handshake fails or DTLS +// is closed. +thread_local static ::RTC::DtlsTransport* dtlsTransportSingleton{ nullptr }; +// DtlsTransport Listener singleton. It's reset every time the DtlsTransport +// singletonDTLS is reset. +thread_local static Fuzzer::RTC::DtlsTransport::DtlsTransportListener* dtlsTransportListenerSingleton{ + nullptr +}; + +void Fuzzer::RTC::DtlsTransport::Fuzz(const uint8_t* data, size_t len) +{ + if (!::RTC::DtlsTransport::IsDtls(data, len)) + { + return; + } + + if (!dtlsTransportSingleton) + { + MS_DEBUG_DEV("no DtlsTransport singleton, creating it"); + + delete dtlsTransportListenerSingleton; + dtlsTransportListenerSingleton = new DtlsTransportListener(); + + dtlsTransportSingleton = new ::RTC::DtlsTransport(dtlsTransportListenerSingleton); + + ::RTC::DtlsTransport::Role localRole; + ::RTC::DtlsTransport::Fingerprint dtlsRemoteFingerprint; + + // Local DTLS role must be 'server' or 'client'. Choose it based on + // randomness of first given byte. + if (data[0] / 2 == 0) + { + localRole = ::RTC::DtlsTransport::Role::SERVER; + } + else + { + localRole = ::RTC::DtlsTransport::Role::CLIENT; + } + + // Remote DTLS fingerprint random generation. + // NOTE: Use a random integer in range 1..5 since FingerprintAlgorithm enum + // has 5 possible values starting with value 1. + dtlsRemoteFingerprint.algorithm = + static_cast<::RTC::DtlsTransport::FingerprintAlgorithm>(::Utils::Crypto::GetRandomUInt(1u, 5u)); + + dtlsRemoteFingerprint.value = + ::Utils::Crypto::GetRandomString(::Utils::Crypto::GetRandomUInt(3u, 20u)); + + dtlsTransportSingleton->Run(localRole); + dtlsTransportSingleton->SetRemoteFingerprint(dtlsRemoteFingerprint); + } + + dtlsTransportSingleton->ProcessDtlsData(data, len); + + // DTLS may have failed or closed after ProcessDtlsData(). If so, unset it. + if ( + dtlsTransportSingleton->GetState() == ::RTC::DtlsTransport::DtlsState::FAILED || + dtlsTransportSingleton->GetState() == ::RTC::DtlsTransport::DtlsState::CLOSED) + { + MS_DEBUG_DEV("DtlsTransport singleton state is 'failed' or 'closed', unsetting it"); + + delete dtlsTransportSingleton; + dtlsTransportSingleton = nullptr; + } + else + { + dtlsTransportSingleton->SendApplicationData(data, len); + } +} + +void Fuzzer::RTC::DtlsTransport::DtlsTransportListener::OnDtlsTransportConnecting( + const ::RTC::DtlsTransport* /*dtlsTransport*/) +{ + MS_DEBUG_DEV("DtlsTransport singleton connecting"); +} + +void Fuzzer::RTC::DtlsTransport::DtlsTransportListener::OnDtlsTransportConnected( + const ::RTC::DtlsTransport* /*dtlsTransport*/, + ::RTC::SrtpSession::CryptoSuite /*srtpCryptoSuite*/, + uint8_t* /*srtpLocalKey*/, + size_t /*srtpLocalKeyLen*/, + uint8_t* /*srtpRemoteKey*/, + size_t /*srtpRemoteKeyLen*/, + std::string& /*remoteCert*/) +{ + MS_DEBUG_DEV("DtlsTransport singleton connected"); +} + +void Fuzzer::RTC::DtlsTransport::DtlsTransportListener::OnDtlsTransportFailed( + const ::RTC::DtlsTransport* /*dtlsTransport*/) +{ + MS_DEBUG_DEV("DtlsTransport singleton failed"); +} + +void Fuzzer::RTC::DtlsTransport::DtlsTransportListener::OnDtlsTransportClosed( + const ::RTC::DtlsTransport* /*dtlsTransport*/) +{ + MS_DEBUG_DEV("DtlsTransport singleton closed"); +} + +void Fuzzer::RTC::DtlsTransport::DtlsTransportListener::OnDtlsTransportSendData( + const ::RTC::DtlsTransport* /*dtlsTransport*/, const uint8_t* /*data*/, size_t /*len*/) +{ + MS_DEBUG_DEV("DtlsTransport singleton wants to send data"); +} + +void Fuzzer::RTC::DtlsTransport::DtlsTransportListener::OnDtlsTransportApplicationDataReceived( + const ::RTC::DtlsTransport* /*dtlsTransport*/, const uint8_t* /*data*/, size_t /*len*/) +{ + MS_DEBUG_DEV("DtlsTransport singleton received application data"); +} diff --git a/worker/fuzzer/src/RTC/FuzzerRateCalculator.cpp b/worker/fuzzer/src/RTC/FuzzerRateCalculator.cpp new file mode 100644 index 0000000000..380e146c05 --- /dev/null +++ b/worker/fuzzer/src/RTC/FuzzerRateCalculator.cpp @@ -0,0 +1,47 @@ +#include "RTC/FuzzerRateCalculator.hpp" +#include "DepLibUV.hpp" +#include "Utils.hpp" +#include "RTC/RateCalculator.hpp" +#include "RTC/RtpPacket.hpp" // RTC::MtuSize + +static ::RTC::RateCalculator rateCalculator; +static uint64_t nowMs; + +// This Init() function must be declared static, otherwise linking will fail if +// another source file defines same non static Init() function. +static int Init(); + +void Fuzzer::RTC::RateCalculator::Fuzz(const uint8_t* data, size_t len) +{ + // Trick to initialize our stuff just once. + static int unused = Init(); + + // Avoid [-Wunused-variable]. + unused++; + + // We need at least 2 bytes of |data|. + if (len < 2) + { + return; + } + + auto size = + static_cast(Utils::Crypto::GetRandomUInt(0u, static_cast(::RTC::MtuSize))); + + nowMs += Utils::Crypto::GetRandomUInt(0u, 1000u); + + rateCalculator.Update(size, nowMs); + + // Only get rate from time to time. + if (Utils::Byte::Get2Bytes(data, 0) % 100 == 0) + { + rateCalculator.GetRate(nowMs); + } +} + +int Init() +{ + nowMs = DepLibUV::GetTimeMs(); + + return 0; +} diff --git a/worker/fuzzer/src/RTC/FuzzerRtpPacket.cpp b/worker/fuzzer/src/RTC/FuzzerRtpPacket.cpp index 4167e371d5..1d6e56a4ce 100644 --- a/worker/fuzzer/src/RTC/FuzzerRtpPacket.cpp +++ b/worker/fuzzer/src/RTC/FuzzerRtpPacket.cpp @@ -7,7 +7,9 @@ void Fuzzer::RTC::RtpPacket::Fuzz(const uint8_t* data, size_t len) { if (!::RTC::RtpPacket::IsRtp(data, len)) + { return; + } // We need to clone the given data into a separate buffer because setters // below will try to write into packet memory. @@ -21,6 +23,8 @@ void Fuzzer::RTC::RtpPacket::Fuzz(const uint8_t* data, size_t len) bool flip; uint16_t rotation; uint32_t absSendTime; + uint16_t playoutDelayMinDelay; + uint16_t playoutDelayMaxDelay; uint16_t wideSeqNumber; std::string mid; std::string rid; @@ -31,7 +35,9 @@ void Fuzzer::RTC::RtpPacket::Fuzz(const uint8_t* data, size_t len) ::RTC::RtpPacket* packet = ::RTC::RtpPacket::Parse(data2, len); if (!packet) + { return; + } // packet->Dump(); packet->GetData(); @@ -87,6 +93,11 @@ void Fuzzer::RTC::RtpPacket::Fuzz(const uint8_t* data, size_t len) packet->GetExtension(2, extenLen); packet->ReadVideoOrientation(camera, flip, rotation); + packet->SetPlayoutDelayExtensionId(8); + packet->HasExtension(8); + packet->GetExtension(8, extenLen); + packet->ReadPlayoutDelay(playoutDelayMinDelay, playoutDelayMaxDelay); + packet->HasExtension(6); packet->HasExtension(7); packet->HasExtension(8); @@ -174,6 +185,11 @@ void Fuzzer::RTC::RtpPacket::Fuzz(const uint8_t* data, size_t len) packet->GetExtension(12, extenLen); packet->ReadVideoOrientation(camera, flip, rotation); + packet->SetPlayoutDelayExtensionId(15); + packet->HasExtension(15); + packet->GetExtension(15, extenLen); + packet->ReadPlayoutDelay(playoutDelayMinDelay, playoutDelayMaxDelay); + packet->GetPayload(); packet->GetPayloadLength(); packet->GetPayloadPadding(); diff --git a/worker/fuzzer/src/RTC/FuzzerRtpRetransmissionBuffer.cpp b/worker/fuzzer/src/RTC/FuzzerRtpRetransmissionBuffer.cpp new file mode 100644 index 0000000000..1bc6af4bca --- /dev/null +++ b/worker/fuzzer/src/RTC/FuzzerRtpRetransmissionBuffer.cpp @@ -0,0 +1,44 @@ +#include "RTC/FuzzerRtpRetransmissionBuffer.hpp" +#include "Utils.hpp" +#include "RTC/RtpPacket.hpp" +#include "RTC/RtpRetransmissionBuffer.hpp" + +void Fuzzer::RTC::RtpRetransmissionBuffer::Fuzz(const uint8_t* data, size_t len) +{ + uint16_t maxItems{ 2500u }; + uint32_t maxRetransmissionDelayMs{ 2000u }; + uint32_t clockRate{ 90000 }; + + // Trick to initialize our stuff just once (use static). + static ::RTC::RtpRetransmissionBuffer retransmissionBuffer( + maxItems, maxRetransmissionDelayMs, clockRate); + + // clang-format off + uint8_t buffer[] = + { + 0b10000000, 0b01111011, 0b01010010, 0b00001110, + 0b01011011, 0b01101011, 0b11001010, 0b10110101, + 0, 0, 0, 2 + }; + // clang-format on + + // Create base RtpPacket instance. + auto* packet = ::RTC::RtpPacket::Parse(buffer, 12); + size_t offset{ 0u }; + + while (len >= 4u) + { + std::shared_ptr<::RTC::RtpPacket> sharedPacket; + + // Set 'random' sequence number and timestamp. + packet->SetSequenceNumber(Utils::Byte::Get2Bytes(data, offset)); + packet->SetTimestamp(Utils::Byte::Get4Bytes(data, offset)); + + retransmissionBuffer.Insert(packet, sharedPacket); + + len -= 4u; + offset += 4; + } + + delete packet; +} diff --git a/worker/fuzzer/src/RTC/FuzzerRtpStreamSend.cpp b/worker/fuzzer/src/RTC/FuzzerRtpStreamSend.cpp index 88f8a806fb..d5f52b55e6 100644 --- a/worker/fuzzer/src/RTC/FuzzerRtpStreamSend.cpp +++ b/worker/fuzzer/src/RTC/FuzzerRtpStreamSend.cpp @@ -27,7 +27,6 @@ void Fuzzer::RTC::RtpStreamSend::Fuzz(const uint8_t* data, size_t len) // Create base RtpPacket instance. auto* packet = ::RTC::RtpPacket::Parse(buffer, 12); - size_t offset{ 0u }; // Create a RtpStreamSend instance. TestRtpStreamListener testRtpStreamListener; @@ -44,6 +43,7 @@ void Fuzzer::RTC::RtpStreamSend::Fuzz(const uint8_t* data, size_t len) std::string mid; auto* stream = new ::RTC::RtpStreamSend(&testRtpStreamListener, params, mid); + size_t offset{ 0u }; while (len >= 4u) { diff --git a/worker/fuzzer/src/RTC/FuzzerSeqManager.cpp b/worker/fuzzer/src/RTC/FuzzerSeqManager.cpp new file mode 100644 index 0000000000..4ea5385038 --- /dev/null +++ b/worker/fuzzer/src/RTC/FuzzerSeqManager.cpp @@ -0,0 +1,22 @@ +#include "RTC/FuzzerSeqManager.hpp" +#include "Utils.hpp" +#include "RTC/SeqManager.hpp" +#include + +void Fuzzer::RTC::SeqManager::Fuzz(const uint8_t* data, size_t len) +{ + if (len < 10) + { + return; + } + + ::RTC::SeqManager seqManager; + + uint16_t output; + + for (size_t count = 0; count < 7; count++) + { + seqManager.Input(Utils::Byte::Get2Bytes(data, count), output); + seqManager.Drop(Utils::Byte::Get2Bytes(data, count + 2)); + } +} diff --git a/worker/fuzzer/src/RTC/FuzzerStunPacket.cpp b/worker/fuzzer/src/RTC/FuzzerStunPacket.cpp index 177a530673..37d567778d 100644 --- a/worker/fuzzer/src/RTC/FuzzerStunPacket.cpp +++ b/worker/fuzzer/src/RTC/FuzzerStunPacket.cpp @@ -1,15 +1,22 @@ #include "RTC/FuzzerStunPacket.hpp" #include "RTC/StunPacket.hpp" +static constexpr size_t StunSerializeBufferSize{ 65536 }; +thread_local static uint8_t StunSerializeBuffer[StunSerializeBufferSize]; + void Fuzzer::RTC::StunPacket::Fuzz(const uint8_t* data, size_t len) { if (!::RTC::StunPacket::IsStun(data, len)) + { return; + } ::RTC::StunPacket* packet = ::RTC::StunPacket::Parse(data, len); if (!packet) + { return; + } // packet->Dump(); packet->GetClass(); @@ -17,6 +24,7 @@ void Fuzzer::RTC::StunPacket::Fuzz(const uint8_t* data, size_t len) packet->GetData(); packet->GetSize(); packet->SetUsername("foo", 3); + packet->SetPassword("lalala"); packet->SetPriority(123); packet->SetIceControlling(123); packet->SetIceControlled(123); @@ -33,13 +41,21 @@ void Fuzzer::RTC::StunPacket::Fuzz(const uint8_t* data, size_t len) packet->GetErrorCode(); packet->HasMessageIntegrity(); packet->HasFingerprint(); - packet->CheckAuthentication("foo", "bar"); - // TODO: packet->CreateSuccessResponse(); // This cannot be easily tested. - // TODO: packet->CreateErrorResponse(); // This cannot be easily tested. - packet->Authenticate("lalala"); - // TODO: Cannot test Serialize() because we don't know the exact required - // buffer size (setters above may change the total size). - // TODO: packet->Serialize(); + packet->CheckAuthentication("foo", "xxx"); + + if (packet->GetClass() == ::RTC::StunPacket::Class::REQUEST) + { + auto* successResponse = packet->CreateSuccessResponse(); + auto* errorResponse = packet->CreateErrorResponse(444); + + delete successResponse; + delete errorResponse; + } + + if (len < StunSerializeBufferSize - 1000) + { + packet->Serialize(StunSerializeBuffer); + } delete packet; } diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsFir.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsFir.cpp index dc48fcc4ca..27929efb9f 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsFir.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsFir.cpp @@ -10,8 +10,7 @@ void Fuzzer::RTC::RTCP::FeedbackPsFir::Fuzz(::RTC::RTCP::FeedbackPsFirPacket* pa // TODO. // AddItem(Item* item); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& item = (*it); diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsLei.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsLei.cpp index 51128fdd86..c1cf7e1d1a 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsLei.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsLei.cpp @@ -10,8 +10,7 @@ void Fuzzer::RTC::RTCP::FeedbackPsLei::Fuzz(::RTC::RTCP::FeedbackPsLeiPacket* pa // TODO. // AddItem(Item* item); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& item = (*it); diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsRpsi.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsRpsi.cpp index 51024f2a65..95d270581e 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsRpsi.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsRpsi.cpp @@ -10,8 +10,7 @@ void Fuzzer::RTC::RTCP::FeedbackPsRpsi::Fuzz(::RTC::RTCP::FeedbackPsRpsiPacket* // TODO. // AddItem(Item* item); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& item = (*it); diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsSli.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsSli.cpp index db69ff0593..989ca41171 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsSli.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsSli.cpp @@ -10,8 +10,7 @@ void Fuzzer::RTC::RTCP::FeedbackPsSli::Fuzz(::RTC::RTCP::FeedbackPsSliPacket* pa // TODO. // AddItem(Item* item); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& item = (*it); diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsTst.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsTst.cpp index 68b209b943..6d62d371fc 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsTst.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsTst.cpp @@ -10,8 +10,7 @@ void Fuzzer::RTC::RTCP::FeedbackPsTstn::Fuzz(::RTC::RTCP::FeedbackPsTstnPacket* // TODO. // AddItem(Item* item); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& item = (*it); @@ -34,8 +33,7 @@ void Fuzzer::RTC::RTCP::FeedbackPsTstr::Fuzz(::RTC::RTCP::FeedbackPsTstrPacket* // TODO. // AddItem(Item* item); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& item = (*it); diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsVbcm.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsVbcm.cpp index 17542d5fcc..9ea8543acc 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsVbcm.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackPsVbcm.cpp @@ -12,8 +12,7 @@ void Fuzzer::RTC::RTCP::FeedbackPsVbcm::Fuzz(::RTC::RTCP::FeedbackPsVbcmPacket* // TODO. // AddItem(Item* item); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& item = (*it); diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpEcn.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpEcn.cpp index 54941c5e37..2e5199be52 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpEcn.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpEcn.cpp @@ -10,8 +10,7 @@ void Fuzzer::RTC::RTCP::FeedbackRtpEcn::Fuzz(::RTC::RTCP::FeedbackRtpEcnPacket* // TODO. // AddItem(Item* item); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& item = (*it); diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpNack.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpNack.cpp index f2725f9864..9070672208 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpNack.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpNack.cpp @@ -10,8 +10,7 @@ void Fuzzer::RTC::RTCP::FeedbackRtpNack::Fuzz(::RTC::RTCP::FeedbackRtpNackPacket // TODO. // AddItem(Item* item); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& item = (*it); diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpTllei.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpTllei.cpp index 58bd8b88ae..e3d76edc0f 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpTllei.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpTllei.cpp @@ -10,8 +10,7 @@ void Fuzzer::RTC::RTCP::FeedbackRtpTllei::Fuzz(::RTC::RTCP::FeedbackRtpTlleiPack // TODO. // AddItem(Item* item); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& item = (*it); diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpTmmb.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpTmmb.cpp index bbb8d773f9..1488e668ca 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpTmmb.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerFeedbackRtpTmmb.cpp @@ -10,8 +10,7 @@ void Fuzzer::RTC::RTCP::FeedbackRtpTmmbn::Fuzz(::RTC::RTCP::FeedbackRtpTmmbnPack // TODO. // AddItem(Item* item); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& item = (*it); @@ -37,8 +36,7 @@ void Fuzzer::RTC::RTCP::FeedbackRtpTmmbr::Fuzz(::RTC::RTCP::FeedbackRtpTmmbrPack // TODO. // AddItem(Item* item); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& item = (*it); diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerPacket.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerPacket.cpp index 43279e2aad..d1498ebc52 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerPacket.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerPacket.cpp @@ -11,7 +11,9 @@ void Fuzzer::RTC::RTCP::Packet::Fuzz(const uint8_t* data, size_t len) { if (!::RTC::RTCP::Packet::IsRtcp(data, len)) + { return; + } // We need to clone the given data into a separate buffer because setters // below will try to write into packet memory. @@ -22,7 +24,9 @@ void Fuzzer::RTC::RTCP::Packet::Fuzz(const uint8_t* data, size_t len) ::RTC::RTCP::Packet* packet = ::RTC::RTCP::Packet::Parse(data2, len); if (!packet) + { return; + } while (packet != nullptr) { diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerReceiverReport.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerReceiverReport.cpp index a7aa6c97bc..36d8028beb 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerReceiverReport.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerReceiverReport.cpp @@ -14,8 +14,7 @@ void Fuzzer::RTC::RTCP::ReceiverReport::Fuzz(::RTC::RTCP::ReceiverReportPacket* // TODO. // AddReport(ReceiverReport* report); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& report = (*it); diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerSdes.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerSdes.cpp index 9d7442634b..aafc012295 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerSdes.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerSdes.cpp @@ -10,8 +10,7 @@ void Fuzzer::RTC::RTCP::Sdes::Fuzz(::RTC::RTCP::SdesPacket* packet) // TODO. // AddChunk(SdesChunk* chunk); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& chunk = (*it); @@ -24,15 +23,13 @@ void Fuzzer::RTC::RTCP::Sdes::Fuzz(::RTC::RTCP::SdesPacket* packet) // TODO // AddItem(SdesItem* item); - auto it2 = chunk->Begin(); - for (; it2 != chunk->End(); ++it2) + for (auto it2 = chunk->Begin(); it2 != chunk->End(); ++it2) { auto& item = (*it2); // item->Dump(); item->Serialize(::RTC::RTCP::Buffer); item->GetSize(); - item->GetType(); item->GetLength(); item->GetValue(); diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerSenderReport.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerSenderReport.cpp index 3d91cc394a..26627cb34c 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerSenderReport.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerSenderReport.cpp @@ -5,7 +5,9 @@ void Fuzzer::RTC::RTCP::SenderReport::Fuzz(::RTC::RTCP::SenderReportPacket* pack { // A well formed packet must have a single report. if (packet->GetCount() == 1) + { packet->Serialize(::RTC::RTCP::Buffer); + } // packet->Dump(); packet->GetCount(); @@ -14,8 +16,7 @@ void Fuzzer::RTC::RTCP::SenderReport::Fuzz(::RTC::RTCP::SenderReportPacket* pack // TODO. // AddReport(SenderReport* report); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& report = (*it); diff --git a/worker/fuzzer/src/RTC/RTCP/FuzzerXr.cpp b/worker/fuzzer/src/RTC/RTCP/FuzzerXr.cpp index b81d128a03..ea65cbee12 100644 --- a/worker/fuzzer/src/RTC/RTCP/FuzzerXr.cpp +++ b/worker/fuzzer/src/RTC/RTCP/FuzzerXr.cpp @@ -10,8 +10,7 @@ void Fuzzer::RTC::RTCP::ExtendedReport::Fuzz(::RTC::RTCP::ExtendedReportPacket* packet->GetSsrc(); packet->SetSsrc(1111); - auto it = packet->Begin(); - for (; it != packet->End(); ++it) + for (auto it = packet->Begin(); it != packet->End(); ++it) { auto& report = (*it); diff --git a/worker/fuzzer/src/fuzzer.cpp b/worker/fuzzer/src/fuzzer.cpp index eaad253255..7c5c4dc996 100644 --- a/worker/fuzzer/src/fuzzer.cpp +++ b/worker/fuzzer/src/fuzzer.cpp @@ -9,8 +9,18 @@ #include "LogLevel.hpp" #include "Settings.hpp" #include "Utils.hpp" +#include "RTC/Codecs/FuzzerH264.hpp" +#include "RTC/Codecs/FuzzerH264_SVC.hpp" +#include "RTC/Codecs/FuzzerOpus.hpp" +#include "RTC/Codecs/FuzzerVP8.hpp" +#include "RTC/Codecs/FuzzerVP9.hpp" +#include "RTC/DtlsTransport.hpp" +#include "RTC/FuzzerDtlsTransport.hpp" +#include "RTC/FuzzerRateCalculator.hpp" #include "RTC/FuzzerRtpPacket.hpp" +#include "RTC/FuzzerRtpRetransmissionBuffer.hpp" #include "RTC/FuzzerRtpStreamSend.hpp" +#include "RTC/FuzzerSeqManager.hpp" #include "RTC/FuzzerStunPacket.hpp" #include "RTC/FuzzerTrendCalculator.hpp" #include "RTC/RTCP/FuzzerPacket.hpp" @@ -19,13 +29,16 @@ #include #include -bool fuzzStun = false; -bool fuzzRtp = false; -bool fuzzRtcp = false; -bool fuzzRtpStream = false; -bool fuzzUtils = false; +bool fuzzStun = false; +bool fuzzDtls = false; +bool fuzzRtp = false; +bool fuzzRtcp = false; +bool fuzzCodecs = false; +bool fuzzUtils = false; -int Init(); +// This Init() function must be declared static, otherwise linking will fail if +// another source file defines same non static Init() function. +static int Init(); extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t len) { @@ -36,16 +49,37 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t len) unused++; if (fuzzStun) + { Fuzzer::RTC::StunPacket::Fuzz(data, len); + } + + if (fuzzDtls) + { + Fuzzer::RTC::DtlsTransport::Fuzz(data, len); + } if (fuzzRtp) + { Fuzzer::RTC::RtpPacket::Fuzz(data, len); + Fuzzer::RTC::RtpStreamSend::Fuzz(data, len); + Fuzzer::RTC::RtpRetransmissionBuffer::Fuzz(data, len); + Fuzzer::RTC::SeqManager::Fuzz(data, len); + Fuzzer::RTC::RateCalculator::Fuzz(data, len); + } if (fuzzRtcp) + { Fuzzer::RTC::RTCP::Packet::Fuzz(data, len); + } - if (fuzzRtpStream) - Fuzzer::RTC::RtpStreamSend::Fuzz(data, len); + if (fuzzCodecs) + { + Fuzzer::RTC::Codecs::Opus::Fuzz(data, len); + Fuzzer::RTC::Codecs::VP8::Fuzz(data, len); + Fuzzer::RTC::Codecs::VP9::Fuzz(data, len); + Fuzzer::RTC::Codecs::H264::Fuzz(data, len); + Fuzzer::RTC::Codecs::H264_SVC::Fuzz(data, len); + } if (fuzzUtils) { @@ -64,53 +98,67 @@ int Init() if (std::getenv("MS_FUZZ_LOG_LEVEL")) { if (std::string(std::getenv("MS_FUZZ_LOG_LEVEL")) == "debug") + { logLevel = LogLevel::LOG_DEBUG; + } else if (std::string(std::getenv("MS_FUZZ_LOG_LEVEL")) == "warn") + { logLevel = LogLevel::LOG_WARN; + } else if (std::string(std::getenv("MS_FUZZ_LOG_LEVEL")) == "error") + { logLevel = LogLevel::LOG_ERROR; + } } // Select what to fuzz. + if (std::getenv("MS_FUZZ_STUN") && std::string(std::getenv("MS_FUZZ_STUN")) == "1") { - std::cout << "[fuzzer] STUN fuzzers enabled" << std::endl; + std::cout << "[fuzzer] STUN fuzzer enabled" << std::endl; fuzzStun = true; } + if (std::getenv("MS_FUZZ_DTLS") && std::string(std::getenv("MS_FUZZ_DTLS")) == "1") + { + std::cout << "[fuzzer] DTLS fuzzer enabled" << std::endl; + + fuzzDtls = true; + } if (std::getenv("MS_FUZZ_RTP") && std::string(std::getenv("MS_FUZZ_RTP")) == "1") { - std::cout << "[fuzzer] RTP fuzzers enabled" << std::endl; + std::cout << "[fuzzer] RTP fuzzer enabled" << std::endl; fuzzRtp = true; } if (std::getenv("MS_FUZZ_RTCP") && std::string(std::getenv("MS_FUZZ_RTCP")) == "1") { - std::cout << "[fuzzer] RTCP fuzzers enabled" << std::endl; + std::cout << "[fuzzer] RTCP fuzzer enabled" << std::endl; fuzzRtcp = true; } - if (std::getenv("MS_FUZZ_RTP_STREAM") && std::string(std::getenv("MS_FUZZ_RTP_STREAM")) == "1") + if (std::getenv("MS_FUZZ_CODECS") && std::string(std::getenv("MS_FUZZ_CODECS")) == "1") { - std::cout << "[fuzzer] RTP Stream fuzzers enabled" << std::endl; + std::cout << "[fuzzer] codecs fuzzer enabled" << std::endl; - fuzzRtpStream = true; + fuzzCodecs = true; } if (std::getenv("MS_FUZZ_UTILS") && std::string(std::getenv("MS_FUZZ_UTILS")) == "1") { - std::cout << "[fuzzer] Utils fuzzers enabled" << std::endl; + std::cout << "[fuzzer] Utils fuzzer enabled" << std::endl; fuzzUtils = true; } - if (!fuzzUtils && !fuzzStun && !fuzzRtcp && !fuzzRtp && !fuzzRtpStream) + if (!fuzzStun && !fuzzDtls && !fuzzRtp && !fuzzRtcp && !fuzzCodecs && !fuzzUtils) { std::cout << "[fuzzer] all fuzzers enabled" << std::endl; - fuzzStun = true; - fuzzRtp = true; - fuzzRtcp = true; - fuzzRtpStream = true; - fuzzUtils = true; + fuzzStun = true; + fuzzDtls = true; + fuzzRtp = true; + fuzzRtcp = true; + fuzzCodecs = true; + fuzzUtils = true; } Settings::configuration.logLevel = logLevel; @@ -122,6 +170,7 @@ int Init() DepUsrSCTP::ClassInit(); DepLibWebRTC::ClassInit(); Utils::Crypto::ClassInit(); + ::RTC::DtlsTransport::ClassInit(); return 0; } diff --git a/worker/include/Channel/ChannelNotification.hpp b/worker/include/Channel/ChannelNotification.hpp new file mode 100644 index 0000000000..2aa940656d --- /dev/null +++ b/worker/include/Channel/ChannelNotification.hpp @@ -0,0 +1,33 @@ +#ifndef MS_CHANNEL_NOTIFICATION_HPP +#define MS_CHANNEL_NOTIFICATION_HPP + +#include "common.hpp" +#include "FBS/notification.h" +#include +#include + +namespace Channel +{ + class ChannelNotification + { + public: + using Event = FBS::Notification::Event; + + private: + static absl::flat_hash_map event2String; + + public: + explicit ChannelNotification(const FBS::Notification::Notification* notification); + ~ChannelNotification() = default; + + public: + // Passed by argument. + Event event; + // Others. + const char* eventCStr; + std::string handlerId; + const FBS::Notification::Notification* data{ nullptr }; + }; +} // namespace Channel + +#endif diff --git a/worker/include/Channel/ChannelNotifier.hpp b/worker/include/Channel/ChannelNotifier.hpp index 5f3eabf5be..097c1398c0 100644 --- a/worker/include/Channel/ChannelNotifier.hpp +++ b/worker/include/Channel/ChannelNotifier.hpp @@ -3,11 +3,8 @@ #include "common.hpp" #include "Channel/ChannelSocket.hpp" -#include #include -using json = nlohmann::json; - namespace Channel { class ChannelNotifier @@ -16,14 +13,49 @@ namespace Channel explicit ChannelNotifier(Channel::ChannelSocket* channel); public: - void Emit(uint64_t targetId, const char* event); - void Emit(const std::string& targetId, const char* event); - void Emit(const std::string& targetId, const char* event, json& data); - void Emit(const std::string& targetId, const char* event, const std::string& data); + flatbuffers::FlatBufferBuilder& GetBufferBuilder() + { + return this->bufferBuilder; + } + + template + void Emit( + const std::string& targetId, + FBS::Notification::Event event, + FBS::Notification::Body type, + flatbuffers::Offset& body) + { + auto& builder = this->bufferBuilder; + auto notification = FBS::Notification::CreateNotificationDirect( + builder, targetId.c_str(), event, type, body.Union()); + + auto message = + FBS::Message::CreateMessage(builder, FBS::Message::Body::Notification, notification.Union()); + + builder.FinishSizePrefixed(message); + this->channel->Send(builder.GetBufferPointer(), builder.GetSize()); + builder.Clear(); + } + + void Emit(const std::string& targetId, FBS::Notification::Event event) + { + auto& builder = ChannelNotifier::bufferBuilder; + auto notification = + FBS::Notification::CreateNotificationDirect(builder, targetId.c_str(), event); + + auto message = + FBS::Message::CreateMessage(builder, FBS::Message::Body::Notification, notification.Union()); + + builder.FinishSizePrefixed(message); + this->channel->Send(builder.GetBufferPointer(), builder.GetSize()); + builder.Clear(); + } private: // Passed by argument. Channel::ChannelSocket* channel{ nullptr }; + // Others. + flatbuffers::FlatBufferBuilder bufferBuilder{}; }; } // namespace Channel diff --git a/worker/include/Channel/ChannelRequest.hpp b/worker/include/Channel/ChannelRequest.hpp index be10df42a6..2df2d5dea2 100644 --- a/worker/include/Channel/ChannelRequest.hpp +++ b/worker/include/Channel/ChannelRequest.hpp @@ -2,12 +2,13 @@ #define MS_CHANNEL_REQUEST_HPP #include "common.hpp" +#include "FBS/message.h" +#include "FBS/request.h" +#include "FBS/response.h" +#include #include -#include #include -using json = nlohmann::json; - namespace Channel { // Avoid cyclic #include problem by declaring classes instead of including @@ -17,88 +18,54 @@ namespace Channel class ChannelRequest { public: - enum class MethodId - { - WORKER_CLOSE = 1, - WORKER_DUMP, - WORKER_GET_RESOURCE_USAGE, - WORKER_UPDATE_SETTINGS, - WORKER_CREATE_WEBRTC_SERVER, - WORKER_CREATE_ROUTER, - WORKER_WEBRTC_SERVER_CLOSE, - WEBRTC_SERVER_DUMP, - WORKER_CLOSE_ROUTER, - ROUTER_DUMP, - ROUTER_CREATE_WEBRTC_TRANSPORT, - ROUTER_CREATE_WEBRTC_TRANSPORT_WITH_SERVER, - ROUTER_CREATE_PLAIN_TRANSPORT, - ROUTER_CREATE_PIPE_TRANSPORT, - ROUTER_CREATE_DIRECT_TRANSPORT, - ROUTER_CLOSE_TRANSPORT, - ROUTER_CREATE_ACTIVE_SPEAKER_OBSERVER, - ROUTER_CREATE_AUDIO_LEVEL_OBSERVER, - ROUTER_CLOSE_RTP_OBSERVER, - TRANSPORT_DUMP, - TRANSPORT_GET_STATS, - TRANSPORT_CONNECT, - TRANSPORT_SET_MAX_INCOMING_BITRATE, - TRANSPORT_SET_MAX_OUTGOING_BITRATE, - TRANSPORT_RESTART_ICE, - TRANSPORT_PRODUCE, - TRANSPORT_CONSUME, - TRANSPORT_PRODUCE_DATA, - TRANSPORT_CONSUME_DATA, - TRANSPORT_ENABLE_TRACE_EVENT, - TRANSPORT_CLOSE_PRODUCER, - PRODUCER_DUMP, - PRODUCER_GET_STATS, - PRODUCER_PAUSE, - PRODUCER_RESUME, - PRODUCER_ENABLE_TRACE_EVENT, - TRANSPORT_CLOSE_CONSUMER, - CONSUMER_DUMP, - CONSUMER_GET_STATS, - CONSUMER_PAUSE, - CONSUMER_RESUME, - CONSUMER_SET_PREFERRED_LAYERS, - CONSUMER_SET_PRIORITY, - CONSUMER_REQUEST_KEY_FRAME, - CONSUMER_ENABLE_TRACE_EVENT, - TRANSPORT_CLOSE_DATA_PRODUCER, - DATA_PRODUCER_DUMP, - DATA_PRODUCER_GET_STATS, - TRANSPORT_CLOSE_DATA_CONSUMER, - DATA_CONSUMER_DUMP, - DATA_CONSUMER_GET_STATS, - DATA_CONSUMER_GET_BUFFERED_AMOUNT, - DATA_CONSUMER_SET_BUFFERED_AMOUNT_LOW_THRESHOLD, - RTP_OBSERVER_PAUSE, - RTP_OBSERVER_RESUME, - RTP_OBSERVER_ADD_PRODUCER, - RTP_OBSERVER_REMOVE_PRODUCER - }; + using Method = FBS::Request::Method; - private: - static absl::flat_hash_map string2MethodId; + public: + static absl::flat_hash_map method2String; + thread_local static flatbuffers::FlatBufferBuilder bufferBuilder; public: - ChannelRequest(Channel::ChannelSocket* channel, const char* msg, size_t msgLen); - virtual ~ChannelRequest(); + ChannelRequest(Channel::ChannelSocket* channel, const FBS::Request::Request* request); + ~ChannelRequest() = default; + flatbuffers::FlatBufferBuilder& GetBufferBuilder() + { + return ChannelRequest::bufferBuilder; + } void Accept(); - void Accept(json& data); + template + void Accept(FBS::Response::Body type, flatbuffers::Offset& body) + { + assert(!this->replied); + + this->replied = true; + + auto& builder = ChannelRequest::bufferBuilder; + auto response = FBS::Response::CreateResponse(builder, this->id, true, type, body.Union()); + + auto message = + FBS::Message::CreateMessage(builder, FBS::Message::Body::Response, response.Union()); + + builder.FinishSizePrefixed(message); + this->Send(builder.GetBufferPointer(), builder.GetSize()); + builder.Clear(); + } void Error(const char* reason = nullptr); void TypeError(const char* reason = nullptr); + private: + void Send(uint8_t* buffer, size_t size) const; + void SendResponse(const flatbuffers::Offset& response); + public: // Passed by argument. Channel::ChannelSocket* channel{ nullptr }; + const FBS::Request::Request* data{ nullptr }; + // Others. uint32_t id{ 0u }; - std::string method; - MethodId methodId; + Method method; + const char* methodCStr; std::string handlerId; - json data; - // Others. bool replied{ false }; }; } // namespace Channel diff --git a/worker/include/Channel/ChannelSocket.hpp b/worker/include/Channel/ChannelSocket.hpp index fdc98b8e7e..83935b8b81 100644 --- a/worker/include/Channel/ChannelSocket.hpp +++ b/worker/include/Channel/ChannelSocket.hpp @@ -2,16 +2,14 @@ #define MS_CHANNEL_SOCKET_HPP #include "common.hpp" +#include "Channel/ChannelNotification.hpp" #include "Channel/ChannelRequest.hpp" -#include "handles/UnixStreamSocket.hpp" -#include +#include "handles/UnixStreamSocketHandle.hpp" #include -using json = nlohmann::json; - namespace Channel { - class ConsumerSocket : public ::UnixStreamSocket + class ConsumerSocket : public ::UnixStreamSocketHandle { public: class Listener @@ -26,9 +24,9 @@ namespace Channel public: ConsumerSocket(int fd, size_t bufferSize, Listener* listener); - ~ConsumerSocket(); + ~ConsumerSocket() override; - /* Pure virtual methods inherited from ::UnixStreamSocket. */ + /* Pure virtual methods inherited from ::UnixStreamSocketHandle. */ public: void UserOnUnixStreamRead() override; void UserOnUnixStreamSocketClosed() override; @@ -38,12 +36,12 @@ namespace Channel Listener* listener{ nullptr }; }; - class ProducerSocket : public ::UnixStreamSocket + class ProducerSocket : public ::UnixStreamSocketHandle { public: ProducerSocket(int fd, size_t bufferSize); - /* Pure virtual methods inherited from ::UnixStreamSocket. */ + /* Pure virtual methods inherited from ::UnixStreamSocketHandle. */ public: void UserOnUnixStreamRead() override { @@ -65,10 +63,19 @@ namespace Channel virtual void HandleRequest(Channel::ChannelRequest* request) = 0; }; - class Listener : public RequestHandler + class NotificationHandler { public: - virtual ~Listener() = default; + virtual ~NotificationHandler() = default; + + public: + virtual void HandleNotification(Channel::ChannelNotification* notification) = 0; + }; + + class Listener : public RequestHandler, public NotificationHandler + { + public: + ~Listener() override = default; public: virtual void OnChannelClosed(Channel::ChannelSocket* channel) = 0; @@ -81,14 +88,13 @@ namespace Channel ChannelReadCtx channelReadCtx, ChannelWriteFn channelWriteFn, ChannelWriteCtx channelWriteCtx); - virtual ~ChannelSocket(); + ~ChannelSocket() override; public: void Close(); void SetListener(Listener* listener); - void Send(json& jsonMessage); - void Send(const std::string& message); - void SendLog(const char* message, uint32_t messageLen); + void Send(const uint8_t* data, uint32_t dataLen); + void SendLog(const char* data, uint32_t dataLen); bool CallbackRead(); private: @@ -111,7 +117,7 @@ namespace Channel ChannelWriteFn channelWriteFn{ nullptr }; ChannelWriteCtx channelWriteCtx{ nullptr }; uv_async_t* uvReadHandle{ nullptr }; - uint8_t* writeBuffer{ nullptr }; + flatbuffers::FlatBufferBuilder bufferBuilder{}; }; } // namespace Channel diff --git a/worker/include/ChannelMessageRegistrator.hpp b/worker/include/ChannelMessageRegistrator.hpp index f01300a658..f27e708ff1 100644 --- a/worker/include/ChannelMessageRegistrator.hpp +++ b/worker/include/ChannelMessageRegistrator.hpp @@ -3,9 +3,7 @@ #include "common.hpp" #include "Channel/ChannelSocket.hpp" -#include "PayloadChannel/PayloadChannelSocket.hpp" #include -#include #include class ChannelMessageRegistrator @@ -15,25 +13,19 @@ class ChannelMessageRegistrator ~ChannelMessageRegistrator(); public: - void FillJson(json& jsonObject); + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder); void RegisterHandler( const std::string& id, Channel::ChannelSocket::RequestHandler* channelRequestHandler, - PayloadChannel::PayloadChannelSocket::RequestHandler* payloadChannelRequestHandler, - PayloadChannel::PayloadChannelSocket::NotificationHandler* payloadChannelNotificationHandler); + Channel::ChannelSocket::NotificationHandler* channelNotificationHandler); void UnregisterHandler(const std::string& id); Channel::ChannelSocket::RequestHandler* GetChannelRequestHandler(const std::string& id); - PayloadChannel::PayloadChannelSocket::RequestHandler* GetPayloadChannelRequestHandler( - const std::string& id); - PayloadChannel::PayloadChannelSocket::NotificationHandler* GetPayloadChannelNotificationHandler( - const std::string& id); + Channel::ChannelSocket::NotificationHandler* GetChannelNotificationHandler(const std::string& id); private: absl::flat_hash_map mapChannelRequestHandlers; - absl::flat_hash_map - mapPayloadChannelRequestHandlers; - absl::flat_hash_map - mapPayloadChannelNotificationHandlers; + absl::flat_hash_map mapChannelNotificationHandlers; }; #endif diff --git a/worker/include/DepLibUring.hpp b/worker/include/DepLibUring.hpp new file mode 100644 index 0000000000..67b64fef07 --- /dev/null +++ b/worker/include/DepLibUring.hpp @@ -0,0 +1,143 @@ +#ifndef MS_DEP_LIBURING_HPP +#define MS_DEP_LIBURING_HPP + +#include "DepLibUV.hpp" +#include "FBS/liburing.h" +#include +#include +#include + +class DepLibUring +{ +public: + using onSendCallback = const std::function; + + /* Struct for the user data field of SQE and CQE. */ + struct UserData + { + // Pointer to send buffer. + uint8_t* store{ nullptr }; + // Frame len buffer for TCP. + uint8_t frameLen[2] = { 0 }; + // iovec for TCP, first item for framing, second item for payload. + struct iovec iov[2]; + // Send callback. + onSendCallback* cb{ nullptr }; + // Index in userDatas array. + size_t idx{ 0 }; + }; + + /* Number of submission queue entries (SQE). */ + static constexpr size_t QueueDepth{ 1024 * 4 }; + static constexpr size_t SendBufferSize{ 1500 }; + + using SendBuffer = uint8_t[SendBufferSize]; + + static void ClassInit(); + static void ClassDestroy(); + static bool CheckRuntimeSupport(); + static bool IsEnabled(); + static flatbuffers::Offset FillBuffer(flatbuffers::FlatBufferBuilder& builder); + static void StartPollingCQEs(); + static void StopPollingCQEs(); + static uint8_t* GetSendBuffer(); + static bool PrepareSend( + int sockfd, const uint8_t* data, size_t len, const struct sockaddr* addr, onSendCallback* cb); + static bool PrepareWrite( + int sockfd, const uint8_t* data1, size_t len1, const uint8_t* data2, size_t len2, onSendCallback* cb); + static void Submit(); + static void SetActive(); + static bool IsActive(); + + class LibUring; + + // Whether liburing is enabled or not after runtime checks. + thread_local static bool enabled; + thread_local static LibUring* liburing; + +public: + // Singleton. + class LibUring + { + public: + LibUring(); + ~LibUring(); + flatbuffers::Offset FillBuffer(flatbuffers::FlatBufferBuilder& builder) const; + void StartPollingCQEs(); + void StopPollingCQEs(); + uint8_t* GetSendBuffer(); + bool PrepareSend( + int sockfd, const uint8_t* data, size_t len, const struct sockaddr* addr, onSendCallback* cb); + bool PrepareWrite( + int sockfd, + const uint8_t* data1, + size_t len1, + const uint8_t* data2, + size_t len2, + onSendCallback* cb); + void Submit(); + void SetActive() + { + this->active = true; + } + bool IsActive() const + { + return this->active; + } + bool IsZeroCopyEnabled() const + { + return this->zeroCopyEnabled; + } + io_uring* GetRing() + { + return std::addressof(this->ring); + } + int GetEventFd() const + { + return this->efd; + } + void ReleaseUserDataEntry(size_t idx) + { + this->availableUserDataEntries.push(idx); + } + + private: + void SetInactive() + { + this->active = false; + } + UserData* GetUserData(); + bool IsDataInSendBuffers(const uint8_t* data) const + { + return data >= this->sendBuffers[0] && data <= this->sendBuffers[DepLibUring::QueueDepth - 1]; + } + + private: + // io_uring instance. + io_uring ring; + // Event file descriptor to watch for io_uring completions. + int efd; + // libuv handle used to poll io_uring completions. + uv_poll_t* uvHandle{ nullptr }; + // Whether we are currently sending RTP over io_uring. + bool active{ false }; + // Whether Zero Copy feature is enabled. + bool zeroCopyEnabled{ true }; + // Pre-allocated UserData's. + UserData userDatas[QueueDepth]{}; + // Indexes of available UserData entries. + std::queue availableUserDataEntries; + // Pre-allocated SendBuffer's. + SendBuffer sendBuffers[QueueDepth]; + // iovec structs to be registered for Zero Copy. + struct iovec iovecs[QueueDepth]; + // Submission queue entry process count. + uint64_t sqeProcessCount{ 0u }; + // Submission queue entry miss count. + uint64_t sqeMissCount{ 0u }; + // User data miss count. + uint64_t userDataMissCount{ 0u }; + }; +}; + +#endif diff --git a/worker/include/DepUsrSCTP.hpp b/worker/include/DepUsrSCTP.hpp index b7df08620b..122cb63c47 100644 --- a/worker/include/DepUsrSCTP.hpp +++ b/worker/include/DepUsrSCTP.hpp @@ -3,13 +3,13 @@ #include "common.hpp" #include "RTC/SctpAssociation.hpp" -#include "handles/Timer.hpp" +#include "handles/TimerHandle.hpp" #include class DepUsrSCTP { private: - class Checker : public Timer::Listener + class Checker : public TimerHandle::Listener { public: Checker(); @@ -19,12 +19,12 @@ class DepUsrSCTP void Start(); void Stop(); - /* Pure virtual methods inherited from Timer::Listener. */ + /* Pure virtual methods inherited from TimerHandle::Listener. */ public: - void OnTimer(Timer* timer) override; + void OnTimer(TimerHandle* timer) override; private: - Timer* timer{ nullptr }; + TimerHandle* timer{ nullptr }; uint64_t lastCalledAtMs{ 0u }; }; diff --git a/worker/include/Logger.hpp b/worker/include/Logger.hpp index db523dbc05..b10f600a20 100644 --- a/worker/include/Logger.hpp +++ b/worker/include/Logger.hpp @@ -96,8 +96,10 @@ // clang-format off +// NOLINTBEGIN #define _MS_TAG_ENABLED(tag) Settings::configuration.logTags.tag #define _MS_TAG_ENABLED_2(tag1, tag2) (Settings::configuration.logTags.tag1 || Settings::configuration.logTags.tag2) +// NOLINTEND #if !defined(MS_LOG_DEV_LEVEL) #define MS_LOG_DEV_LEVEL 0 @@ -126,20 +128,34 @@ ((value & 0x02) ? '1' : '0'), \ ((value & 0x01) ? '1' : '0') +// Usage: +// MS_DEBUG_DEV("Leading text "MS_UINT8_TO_BINARY_PATTERN, MS_UINT8_TO_BINARY(value)); +#define MS_UINT8_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c" +#define MS_UINT8_TO_BINARY(value) \ + ((value & 0x80) ? '1' : '0'), \ + ((value & 0x40) ? '1' : '0'), \ + ((value & 0x20) ? '1' : '0'), \ + ((value & 0x10) ? '1' : '0'), \ + ((value & 0x08) ? '1' : '0'), \ + ((value & 0x04) ? '1' : '0'), \ + ((value & 0x02) ? '1' : '0'), \ + ((value & 0x01) ? '1' : '0') + class Logger { public: static void ClassInit(Channel::ChannelSocket* channel); public: - static const uint64_t pid; + static const uint64_t Pid; thread_local static Channel::ChannelSocket* channel; - static const size_t bufferSize {50000}; + static const size_t BufferSize {50000}; thread_local static char buffer[]; }; /* Logging macros. */ +// NOLINTBEGIN #define _MS_LOG_SEPARATOR_CHAR_STD "\n" #ifdef MS_LOG_FILE_LINE @@ -152,6 +168,7 @@ class Logger #define _MS_LOG_STR_DESC _MS_LOG_STR " | " #define _MS_LOG_ARG MS_CLASS, __FUNCTION__ #endif +// NOLINTEND #ifdef MS_LOG_TRACE #define MS_TRACE() \ @@ -159,7 +176,7 @@ class Logger { \ if (Settings::configuration.logLevel == LogLevel::LOG_DEBUG) \ { \ - const int loggerWritten = std::snprintf(Logger::buffer, Logger::bufferSize, "D(trace) " _MS_LOG_STR, _MS_LOG_ARG); \ + const int loggerWritten = std::snprintf(Logger::buffer, Logger::BufferSize, "D(trace) " _MS_LOG_STR, _MS_LOG_ARG); \ Logger::channel->SendLog(Logger::buffer, static_cast(loggerWritten)); \ } \ } \ @@ -191,7 +208,7 @@ class Logger { \ if (Settings::configuration.logLevel == LogLevel::LOG_DEBUG && _MS_TAG_ENABLED(tag)) \ { \ - const int loggerWritten = std::snprintf(Logger::buffer, Logger::bufferSize, "D" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ + const int loggerWritten = std::snprintf(Logger::buffer, Logger::BufferSize, "D" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ Logger::channel->SendLog(Logger::buffer, static_cast(loggerWritten)); \ } \ } \ @@ -213,7 +230,7 @@ class Logger { \ if (Settings::configuration.logLevel >= LogLevel::LOG_WARN && _MS_TAG_ENABLED(tag)) \ { \ - const int loggerWritten = std::snprintf(Logger::buffer, Logger::bufferSize, "W" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ + const int loggerWritten = std::snprintf(Logger::buffer, Logger::BufferSize, "W" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ Logger::channel->SendLog(Logger::buffer, static_cast(loggerWritten)); \ } \ } \ @@ -235,7 +252,7 @@ class Logger { \ if (Settings::configuration.logLevel == LogLevel::LOG_DEBUG && _MS_TAG_ENABLED_2(tag1, tag2)) \ { \ - const int loggerWritten = std::snprintf(Logger::buffer, Logger::bufferSize, "D" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ + const int loggerWritten = std::snprintf(Logger::buffer, Logger::BufferSize, "D" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ Logger::channel->SendLog(Logger::buffer, static_cast(loggerWritten)); \ } \ } \ @@ -257,7 +274,7 @@ class Logger { \ if (Settings::configuration.logLevel >= LogLevel::LOG_WARN && _MS_TAG_ENABLED_2(tag1, tag2)) \ { \ - const int loggerWritten = std::snprintf(Logger::buffer, Logger::bufferSize, "W" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ + const int loggerWritten = std::snprintf(Logger::buffer, Logger::BufferSize, "W" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ Logger::channel->SendLog(Logger::buffer, static_cast(loggerWritten)); \ } \ } \ @@ -278,7 +295,7 @@ class Logger #define MS_DEBUG_DEV(desc, ...) \ do \ { \ - const int loggerWritten = std::snprintf(Logger::buffer, Logger::bufferSize, "D" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ + const int loggerWritten = std::snprintf(Logger::buffer, Logger::BufferSize, "D" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ Logger::channel->SendLog(Logger::buffer, static_cast(loggerWritten)); \ } \ while (false) @@ -300,7 +317,7 @@ class Logger #define MS_WARN_DEV(desc, ...) \ do \ { \ - const int loggerWritten = std::snprintf(Logger::buffer, Logger::bufferSize, "W" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ + const int loggerWritten = std::snprintf(Logger::buffer, Logger::BufferSize, "W" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ Logger::channel->SendLog(Logger::buffer, static_cast(loggerWritten)); \ } \ while (false) @@ -320,7 +337,7 @@ class Logger #define MS_DUMP(desc, ...) \ do \ { \ - const int loggerWritten = std::snprintf(Logger::buffer, Logger::bufferSize, "X" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ + const int loggerWritten = std::snprintf(Logger::buffer, Logger::BufferSize, "X" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ Logger::channel->SendLog(Logger::buffer, static_cast(loggerWritten)); \ } \ while (false) @@ -336,7 +353,7 @@ class Logger #define MS_DUMP_DATA(data, len) \ do \ { \ - const int loggerWritten = std::snprintf(Logger::buffer, Logger::bufferSize, "X(data) " _MS_LOG_STR, _MS_LOG_ARG); \ + const int loggerWritten = std::snprintf(Logger::buffer, Logger::BufferSize, "X(data) " _MS_LOG_STR, _MS_LOG_ARG); \ Logger::channel->SendLog(Logger::buffer, static_cast(loggerWritten)); \ size_t bufferDataLen{ 0 }; \ for (size_t i{0}; i < len; ++i) \ @@ -348,14 +365,16 @@ class Logger Logger::channel->SendLog(Logger::buffer, static_cast(bufferDataLen)); \ bufferDataLen = 0; \ } \ - const int loggerWritten = std::snprintf(Logger::buffer + bufferDataLen, Logger::bufferSize, "X%06X ", static_cast(i)); \ + const int loggerWritten = std::snprintf(Logger::buffer + bufferDataLen, Logger::BufferSize, "X%06X ", static_cast(i)); \ bufferDataLen += loggerWritten; \ } \ - const int loggerWritten = std::snprintf(Logger::buffer + bufferDataLen, Logger::bufferSize, "%02X ", static_cast(data[i])); \ + const int loggerWritten = std::snprintf(Logger::buffer + bufferDataLen, Logger::BufferSize, "%02X ", static_cast(data[i])); \ bufferDataLen += loggerWritten; \ } \ if (bufferDataLen != 0) \ + { \ Logger::channel->SendLog(Logger::buffer, static_cast(bufferDataLen)); \ + } \ } \ while (false) @@ -374,10 +393,10 @@ class Logger std::fprintf(stdout, "%s", Logger::buffer); \ bufferDataLen = 0; \ } \ - const int loggerWritten = std::snprintf(Logger::buffer + bufferDataLen, Logger::bufferSize, "\n%06X ", static_cast(i)); \ + const int loggerWritten = std::snprintf(Logger::buffer + bufferDataLen, Logger::BufferSize, "\n%06X ", static_cast(i)); \ bufferDataLen += loggerWritten; \ } \ - const int loggerWritten = std::snprintf(Logger::buffer + bufferDataLen, Logger::bufferSize, "%02X ", static_cast(data[i])); \ + const int loggerWritten = std::snprintf(Logger::buffer + bufferDataLen, Logger::BufferSize, "%02X ", static_cast(data[i])); \ bufferDataLen += loggerWritten; \ } \ if (bufferDataLen != 0) \ @@ -394,7 +413,7 @@ class Logger { \ if (Settings::configuration.logLevel >= LogLevel::LOG_ERROR || MS_LOG_DEV_LEVEL >= 1) \ { \ - const int loggerWritten = std::snprintf(Logger::buffer, Logger::bufferSize, "E" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ + const int loggerWritten = std::snprintf(Logger::buffer, Logger::BufferSize, "E" _MS_LOG_STR_DESC desc, _MS_LOG_ARG, ##__VA_ARGS__); \ Logger::channel->SendLog(Logger::buffer, static_cast(loggerWritten)); \ } \ } \ diff --git a/worker/include/MediaSoupErrors.hpp b/worker/include/MediaSoupErrors.hpp index 22697992cf..90d3b07ec8 100644 --- a/worker/include/MediaSoupErrors.hpp +++ b/worker/include/MediaSoupErrors.hpp @@ -13,7 +13,7 @@ class MediaSoupError : public std::runtime_error } public: - static const size_t bufferSize{ 2000 }; + static const size_t BufferSize{ 2000 }; thread_local static char buffer[]; }; @@ -30,7 +30,7 @@ class MediaSoupTypeError : public MediaSoupError do \ { \ MS_ERROR("throwing MediaSoupError: " desc, ##__VA_ARGS__); \ - std::snprintf(MediaSoupError::buffer, MediaSoupError::bufferSize, desc, ##__VA_ARGS__); \ + std::snprintf(MediaSoupError::buffer, MediaSoupError::BufferSize, desc, ##__VA_ARGS__); \ throw MediaSoupError(MediaSoupError::buffer); \ } while (false) @@ -38,7 +38,7 @@ class MediaSoupTypeError : public MediaSoupError do \ { \ MS_ERROR_STD("throwing MediaSoupError: " desc, ##__VA_ARGS__); \ - std::snprintf(MediaSoupError::buffer, MediaSoupError::bufferSize, desc, ##__VA_ARGS__); \ + std::snprintf(MediaSoupError::buffer, MediaSoupError::BufferSize, desc, ##__VA_ARGS__); \ throw MediaSoupError(MediaSoupError::buffer); \ } while (false) @@ -46,7 +46,7 @@ class MediaSoupTypeError : public MediaSoupError do \ { \ MS_ERROR("throwing MediaSoupTypeError: " desc, ##__VA_ARGS__); \ - std::snprintf(MediaSoupError::buffer, MediaSoupError::bufferSize, desc, ##__VA_ARGS__); \ + std::snprintf(MediaSoupError::buffer, MediaSoupError::BufferSize, desc, ##__VA_ARGS__); \ throw MediaSoupTypeError(MediaSoupError::buffer); \ } while (false) @@ -54,7 +54,7 @@ class MediaSoupTypeError : public MediaSoupError do \ { \ MS_ERROR_STD("throwing MediaSoupTypeError: " desc, ##__VA_ARGS__); \ - std::snprintf(MediaSoupError::buffer, MediaSoupError::bufferSize, desc, ##__VA_ARGS__); \ + std::snprintf(MediaSoupError::buffer, MediaSoupError::BufferSize, desc, ##__VA_ARGS__); \ throw MediaSoupTypeError(MediaSoupError::buffer); \ } while (false) // clang-format on diff --git a/worker/include/PayloadChannel/PayloadChannelNotification.hpp b/worker/include/PayloadChannel/PayloadChannelNotification.hpp deleted file mode 100644 index 5f4f3c2f86..0000000000 --- a/worker/include/PayloadChannel/PayloadChannelNotification.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef MS_PAYLOAD_CHANNEL_NOTIFICATION_HPP -#define MS_PAYLOAD_CHANNEL_NOTIFICATION_HPP - -#include "common.hpp" -#include -#include - -namespace PayloadChannel -{ - class PayloadChannelNotification - { - public: - enum class EventId - { - TRANSPORT_SEND_RTCP = 1, - PRODUCER_SEND, - DATA_PRODUCER_SEND - }; - - public: - static bool IsNotification(const char* msg, size_t msgLen); - - private: - static absl::flat_hash_map string2EventId; - - public: - PayloadChannelNotification(const char* msg, size_t msgLen); - virtual ~PayloadChannelNotification(); - - public: - void SetPayload(const uint8_t* payload, size_t payloadLen); - - public: - // Passed by argument. - std::string event; - EventId eventId; - std::string handlerId; - std::string data; - const uint8_t* payload{ nullptr }; - size_t payloadLen{ 0u }; - }; -} // namespace PayloadChannel - -#endif diff --git a/worker/include/PayloadChannel/PayloadChannelNotifier.hpp b/worker/include/PayloadChannel/PayloadChannelNotifier.hpp deleted file mode 100644 index c7df84a484..0000000000 --- a/worker/include/PayloadChannel/PayloadChannelNotifier.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#ifndef MS_PAYLOAD_CHANNEL_NOTIFIER_HPP -#define MS_PAYLOAD_CHANNEL_NOTIFIER_HPP - -#include "common.hpp" -#include "PayloadChannel/PayloadChannelSocket.hpp" -#include -#include - -using json = nlohmann::json; - -namespace PayloadChannel -{ - class PayloadChannelNotifier - { - public: - explicit PayloadChannelNotifier(PayloadChannel::PayloadChannelSocket* payloadChannel); - - public: - void Emit(const std::string& targetId, const char* event, const uint8_t* payload, size_t payloadLen); - void Emit( - const std::string& targetId, - const char* event, - json& data, - const uint8_t* payload, - size_t payloadLen); - void Emit( - const std::string& targetId, - const char* event, - const std::string& data, - const uint8_t* payload, - size_t payloadLen); - - private: - // Passed by argument. - PayloadChannel::PayloadChannelSocket* payloadChannel{ nullptr }; - }; -} // namespace PayloadChannel - -#endif diff --git a/worker/include/PayloadChannel/PayloadChannelRequest.hpp b/worker/include/PayloadChannel/PayloadChannelRequest.hpp deleted file mode 100644 index 0ae60fec3b..0000000000 --- a/worker/include/PayloadChannel/PayloadChannelRequest.hpp +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef MS_PAYLOAD_CHANNEL_REQUEST_HPP -#define MS_PAYLOAD_CHANNEL_REQUEST_HPP - -#include "common.hpp" -#include -#include -#include - -using json = nlohmann::json; - -namespace PayloadChannel -{ - // Avoid cyclic #include problem by declaring classes instead of including - // the corresponding header files. - class PayloadChannelSocket; - - class PayloadChannelRequest - { - public: - enum class MethodId - { - DATA_CONSUMER_SEND - }; - - public: - static bool IsRequest(const char* msg, size_t msgLen); - - private: - static absl::flat_hash_map string2MethodId; - - public: - PayloadChannelRequest(PayloadChannel::PayloadChannelSocket* channel, char* msg, size_t msgLen); - virtual ~PayloadChannelRequest(); - - public: - void Accept(); - void Accept(json& data); - void Error(const char* reason = nullptr); - void TypeError(const char* reason = nullptr); - void SetPayload(const uint8_t* payload, size_t payloadLen); - - public: - // Passed by argument. - PayloadChannel::PayloadChannelSocket* channel{ nullptr }; - uint32_t id{ 0u }; - std::string method; - MethodId methodId; - std::string handlerId; - std::string data; - const uint8_t* payload{ nullptr }; - size_t payloadLen{ 0u }; - // Others. - bool replied{ false }; - }; -} // namespace PayloadChannel - -#endif diff --git a/worker/include/PayloadChannel/PayloadChannelSocket.hpp b/worker/include/PayloadChannel/PayloadChannelSocket.hpp deleted file mode 100644 index 4191706246..0000000000 --- a/worker/include/PayloadChannel/PayloadChannelSocket.hpp +++ /dev/null @@ -1,133 +0,0 @@ -#ifndef MS_PAYLOAD_CHANNEL_SOCKET_HPP -#define MS_PAYLOAD_CHANNEL_SOCKET_HPP - -#include "common.hpp" -#include "PayloadChannel/PayloadChannelNotification.hpp" -#include "PayloadChannel/PayloadChannelRequest.hpp" -#include "handles/UnixStreamSocket.hpp" -#include -#include - -using json = nlohmann::json; - -namespace PayloadChannel -{ - class ConsumerSocket : public ::UnixStreamSocket - { - public: - class Listener - { - public: - virtual ~Listener() = default; - - public: - virtual void OnConsumerSocketMessage(ConsumerSocket* consumerSocket, char* msg, size_t msgLen) = 0; - virtual void OnConsumerSocketClosed(ConsumerSocket* consumerSocket) = 0; - }; - - public: - ConsumerSocket(int fd, size_t bufferSize, Listener* listener); - ~ConsumerSocket(); - - /* Pure virtual methods inherited from ::UnixStreamSocket. */ - public: - void UserOnUnixStreamRead() override; - void UserOnUnixStreamSocketClosed() override; - - private: - // Passed by argument. - Listener* listener{ nullptr }; - }; - - class ProducerSocket : public ::UnixStreamSocket - { - public: - ProducerSocket(int fd, size_t bufferSize); - - /* Pure virtual methods inherited from ::UnixStreamSocket. */ - public: - void UserOnUnixStreamRead() override - { - } - void UserOnUnixStreamSocketClosed() override - { - } - }; - - class PayloadChannelSocket : public ConsumerSocket::Listener - { - public: - class RequestHandler - { - public: - virtual ~RequestHandler() = default; - - public: - virtual void HandleRequest(PayloadChannel::PayloadChannelRequest* request) = 0; - }; - - class NotificationHandler - { - public: - virtual ~NotificationHandler() = default; - - public: - virtual void HandleNotification(PayloadChannel::PayloadChannelNotification* notification) = 0; - }; - - class Listener : public RequestHandler, public NotificationHandler - { - public: - virtual ~Listener() = default; - - public: - virtual void OnPayloadChannelClosed(PayloadChannel::PayloadChannelSocket* payloadChannel) = 0; - }; - - public: - explicit PayloadChannelSocket(int consumerFd, int producerFd); - explicit PayloadChannelSocket( - PayloadChannelReadFn payloadChannelReadFn, - PayloadChannelReadCtx payloadChannelReadCtx, - PayloadChannelWriteFn payloadChannelWriteFn, - PayloadChannelWriteCtx payloadChannelWriteCtx); - virtual ~PayloadChannelSocket(); - - public: - void Close(); - void SetListener(Listener* listener); - void Send(json& jsonMessage, const uint8_t* payload, size_t payloadLen); - void Send(const std::string& message, const uint8_t* payload, size_t payloadLen); - void Send(json& jsonMessage); - void Send(const std::string& message); - bool CallbackRead(); - - private: - void SendImpl(const uint8_t* message, uint32_t messageLen); - void SendImpl( - const uint8_t* message, uint32_t messageLen, const uint8_t* payload, uint32_t payloadLen); - - /* Pure virtual methods inherited from ConsumerSocket::Listener. */ - public: - void OnConsumerSocketMessage(ConsumerSocket* consumerSocket, char* msg, size_t msgLen) override; - void OnConsumerSocketClosed(ConsumerSocket* consumerSocket) override; - - private: - // Passed by argument. - Listener* listener{ nullptr }; - // Others. - bool closed{ false }; - ConsumerSocket* consumerSocket{ nullptr }; - ProducerSocket* producerSocket{ nullptr }; - PayloadChannelReadFn payloadChannelReadFn{ nullptr }; - PayloadChannelReadCtx payloadChannelReadCtx{ nullptr }; - PayloadChannelWriteFn payloadChannelWriteFn{ nullptr }; - PayloadChannelWriteCtx payloadChannelWriteCtx{ nullptr }; - PayloadChannel::PayloadChannelNotification* ongoingNotification{ nullptr }; - PayloadChannel::PayloadChannelRequest* ongoingRequest{ nullptr }; - uv_async_t* uvReadHandle{ nullptr }; - uint8_t* writeBuffer{ nullptr }; - }; -} // namespace PayloadChannel - -#endif diff --git a/worker/include/RTC/ActiveSpeakerObserver.hpp b/worker/include/RTC/ActiveSpeakerObserver.hpp index cf4be62223..293aa3d8d8 100644 --- a/worker/include/RTC/ActiveSpeakerObserver.hpp +++ b/worker/include/RTC/ActiveSpeakerObserver.hpp @@ -3,9 +3,8 @@ #include "RTC/RtpObserver.hpp" #include "RTC/Shared.hpp" -#include "handles/Timer.hpp" +#include "handles/TimerHandle.hpp" #include -#include #include #include @@ -16,7 +15,7 @@ // https://github.com/jitsi/jitsi-utils/blob/master/src/main/java/org/jitsi/utils/dsi/DominantSpeakerIdentification.java namespace RTC { - class ActiveSpeakerObserver : public RTC::RtpObserver, public Timer::Listener + class ActiveSpeakerObserver : public RTC::RtpObserver, public TimerHandle::Listener { private: class Speaker @@ -24,7 +23,7 @@ namespace RTC public: Speaker(); void EvalActivityScores(); - double GetActivityScore(uint8_t interval); + double GetActivityScore(uint8_t interval) const; void LevelChanged(uint32_t level, uint64_t now); void LevelTimedOut(uint64_t now); @@ -71,7 +70,10 @@ namespace RTC public: ActiveSpeakerObserver( - RTC::Shared* shared, const std::string& id, RTC::RtpObserver::Listener* listener, json& data); + RTC::Shared* shared, + const std::string& id, + RTC::RtpObserver::Listener* listener, + const FBS::ActiveSpeakerObserver::ActiveSpeakerObserverOptions* options); ~ActiveSpeakerObserver() override; public: @@ -88,14 +90,14 @@ namespace RTC bool CalculateActiveSpeaker(); void TimeoutIdleLevels(uint64_t now); - /* Pure virtual methods inherited from Timer. */ + /* Pure virtual methods inherited from TimerHandle. */ protected: - void OnTimer(Timer* timer) override; + void OnTimer(TimerHandle* timer) override; private: - double relativeSpeachActivities[RelativeSpeachActivitiesLen]; + double relativeSpeachActivities[RelativeSpeachActivitiesLen]{}; std::string dominantId; - Timer* periodicTimer{ nullptr }; + TimerHandle* periodicTimer{ nullptr }; uint16_t interval{ 300u }; // Map of ProducerSpeakers indexed by Producer id. absl::flat_hash_map mapProducerSpeakers; diff --git a/worker/include/RTC/AudioLevelObserver.hpp b/worker/include/RTC/AudioLevelObserver.hpp index c0007e7301..49b17dcaf8 100644 --- a/worker/include/RTC/AudioLevelObserver.hpp +++ b/worker/include/RTC/AudioLevelObserver.hpp @@ -3,15 +3,12 @@ #include "RTC/RtpObserver.hpp" #include "RTC/Shared.hpp" -#include "handles/Timer.hpp" +#include "handles/TimerHandle.hpp" #include -#include - -using json = nlohmann::json; namespace RTC { - class AudioLevelObserver : public RTC::RtpObserver, public Timer::Listener + class AudioLevelObserver : public RTC::RtpObserver, public TimerHandle::Listener { private: struct DBovs @@ -22,7 +19,10 @@ namespace RTC public: AudioLevelObserver( - RTC::Shared* shared, const std::string& id, RTC::RtpObserver::Listener* listener, json& data); + RTC::Shared* shared, + const std::string& id, + RTC::RtpObserver::Listener* listener, + const FBS::AudioLevelObserver::AudioLevelObserverOptions* options); ~AudioLevelObserver() override; public: @@ -38,9 +38,9 @@ namespace RTC void Update(); void ResetMapProducerDBovs(); - /* Pure virtual methods inherited from Timer. */ + /* Pure virtual methods inherited from TimerHandle. */ protected: - void OnTimer(Timer* timer) override; + void OnTimer(TimerHandle* timer) override; private: // Passed by argument. @@ -48,7 +48,7 @@ namespace RTC int8_t threshold{ -80 }; uint16_t interval{ 1000u }; // Allocated by this. - Timer* periodicTimer{ nullptr }; + TimerHandle* periodicTimer{ nullptr }; // Others. absl::flat_hash_map mapProducerDBovs; bool silence{ true }; diff --git a/worker/include/RTC/Codecs/H264.hpp b/worker/include/RTC/Codecs/H264.hpp index 788c86aa0e..8fda3227de 100644 --- a/worker/include/RTC/Codecs/H264.hpp +++ b/worker/include/RTC/Codecs/H264.hpp @@ -28,6 +28,7 @@ namespace RTC uint8_t tid{ 0 }; // Temporal layer id. uint8_t lid{ 0 }; // Spatial layer id. uint8_t tl0picidx{ 0 }; // TL0PICIDX + // Parsed values. bool hasLid{ false }; bool hasTid{ false }; diff --git a/worker/include/RTC/Codecs/Opus.hpp b/worker/include/RTC/Codecs/Opus.hpp index a703e224de..ac889e149c 100644 --- a/worker/include/RTC/Codecs/Opus.hpp +++ b/worker/include/RTC/Codecs/Opus.hpp @@ -20,6 +20,9 @@ namespace RTC void Dump() const override; + // Mandatory fields. + uint8_t stereo : 1; + uint8_t code : 2; // Parsed values. bool isDtx{ false }; }; diff --git a/worker/include/RTC/Codecs/Tools.hpp b/worker/include/RTC/Codecs/Tools.hpp index 33c9d0768a..6604e2c86b 100644 --- a/worker/include/RTC/Codecs/Tools.hpp +++ b/worker/include/RTC/Codecs/Tools.hpp @@ -106,11 +106,6 @@ namespace RTC { switch (type) { - case RTC::RtpParameters::Type::NONE: - { - return false; - } - case RTC::RtpParameters::Type::SIMPLE: { return true; @@ -166,11 +161,6 @@ namespace RTC { return true; } - - default: - { - return false; - } } } diff --git a/worker/include/RTC/Consumer.hpp b/worker/include/RTC/Consumer.hpp index 016a5ed085..28b4239b62 100644 --- a/worker/include/RTC/Consumer.hpp +++ b/worker/include/RTC/Consumer.hpp @@ -4,6 +4,7 @@ #include "common.hpp" #include "Channel/ChannelRequest.hpp" #include "Channel/ChannelSocket.hpp" +#include "FBS/consumer.h" #include "RTC/RTCP/CompoundPacket.hpp" #include "RTC/RTCP/FeedbackPs.hpp" #include "RTC/RTCP/FeedbackPsFir.hpp" @@ -14,15 +15,13 @@ #include "RTC/RtpHeaderExtensionIds.hpp" #include "RTC/RtpPacket.hpp" #include "RTC/RtpStream.hpp" +#include "RTC/RtpStreamRecv.hpp" #include "RTC/RtpStreamSend.hpp" #include "RTC/Shared.hpp" #include -#include #include #include -using json = nlohmann::json; - namespace RTC { class Consumer : public Channel::ChannelSocket::RequestHandler @@ -65,14 +64,20 @@ namespace RTC const std::string& id, const std::string& producerId, RTC::Consumer::Listener* listener, - json& data, + const FBS::Transport::ConsumeRequest* data, RTC::RtpParameters::Type type); - virtual ~Consumer(); + ~Consumer() override; public: - virtual void FillJson(json& jsonObject) const; - virtual void FillJsonStats(json& jsonArray) const = 0; - virtual void FillJsonScore(json& jsonObject) const = 0; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; + virtual flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) = 0; + virtual flatbuffers::Offset FillBufferScore( + flatbuffers::FlatBufferBuilder& /*builder*/) const + { + return 0; + }; RTC::Media::Kind GetKind() const { return this->kind; @@ -129,12 +134,12 @@ namespace RTC } void ProducerPaused(); void ProducerResumed(); - virtual void ProducerRtpStream(RTC::RtpStream* rtpStream, uint32_t mappedSsrc) = 0; - virtual void ProducerNewRtpStream(RTC::RtpStream* rtpStream, uint32_t mappedSsrc) = 0; + virtual void ProducerRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) = 0; + virtual void ProducerNewRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) = 0; void ProducerRtpStreamScores(const std::vector* scores); virtual void ProducerRtpStreamScore( - RTC::RtpStream* rtpStream, uint8_t score, uint8_t previousScore) = 0; - virtual void ProducerRtcpSenderReport(RTC::RtpStream* rtpStream, bool first) = 0; + RTC::RtpStreamRecv* rtpStream, uint8_t score, uint8_t previousScore) = 0; + virtual void ProducerRtcpSenderReport(RTC::RtpStreamRecv* rtpStream, bool first) = 0; void ProducerClosed(); void SetExternallyManagedBitrate() { @@ -166,6 +171,7 @@ namespace RTC void EmitTraceEventPliType(uint32_t ssrc) const; void EmitTraceEventFirType(uint32_t ssrc) const; void EmitTraceEventNackType() const; + void EmitTraceEvent(flatbuffers::Offset& notification) const; private: virtual void UserOnTransportConnected() = 0; @@ -184,7 +190,7 @@ namespace RTC RTC::Consumer::Listener* listener{ nullptr }; RTC::Media::Kind kind; RTC::RtpParameters rtpParameters; - RTC::RtpParameters::Type type{ RTC::RtpParameters::Type::NONE }; + RTC::RtpParameters::Type type; std::vector consumableRtpEncodings; struct RTC::RtpHeaderExtensionIds rtpHeaderExtensionIds; const std::vector* producerRtpStreamScores{ nullptr }; diff --git a/worker/include/RTC/DataConsumer.hpp b/worker/include/RTC/DataConsumer.hpp index 46d1f15629..994b616ed7 100644 --- a/worker/include/RTC/DataConsumer.hpp +++ b/worker/include/RTC/DataConsumer.hpp @@ -4,11 +4,9 @@ #include "common.hpp" #include "Channel/ChannelRequest.hpp" #include "Channel/ChannelSocket.hpp" -#include "PayloadChannel/PayloadChannelRequest.hpp" -#include "PayloadChannel/PayloadChannelSocket.hpp" #include "RTC/SctpDictionaries.hpp" #include "RTC/Shared.hpp" -#include +#include #include namespace RTC @@ -17,8 +15,7 @@ namespace RTC // (this is to avoid circular dependencies). class SctpAssociation; - class DataConsumer : public Channel::ChannelSocket::RequestHandler, - public PayloadChannel::PayloadChannelSocket::RequestHandler + class DataConsumer : public Channel::ChannelSocket::RequestHandler { protected: using onQueuedCallback = const std::function; @@ -32,9 +29,9 @@ namespace RTC public: virtual void OnDataConsumerSendMessage( RTC::DataConsumer* dataConsumer, - uint32_t ppid, const uint8_t* msg, size_t len, + uint32_t ppid, onQueuedCallback* cb) = 0; virtual void OnDataConsumerDataProducerClosed(RTC::DataConsumer* dataConsumer) = 0; }; @@ -53,13 +50,15 @@ namespace RTC const std::string& dataProducerId, RTC::SctpAssociation* sctpAssociation, RTC::DataConsumer::Listener* listener, - json& data, + const FBS::Transport::ConsumeDataRequest* data, size_t maxMessageSize); - virtual ~DataConsumer(); + ~DataConsumer() override; public: - void FillJson(json& jsonObject) const; - void FillJsonStats(json& jsonArray) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; + flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) const; Type GetType() const { return this->type; @@ -70,31 +69,47 @@ namespace RTC } bool IsActive() const { + // It's active it DataConsumer and DataProducer are not paused and the transport + // is connected. // clang-format off return ( this->transportConnected && (this->type == DataConsumer::Type::DIRECT || this->sctpAssociationConnected) && + !this->paused && + !this->dataProducerPaused && !this->dataProducerClosed ); // clang-format on } void TransportConnected(); void TransportDisconnected(); + bool IsPaused() const + { + return this->paused; + } + bool IsDataProducerPaused() const + { + return this->dataProducerPaused; + } + void DataProducerPaused(); + void DataProducerResumed(); void SctpAssociationConnected(); void SctpAssociationClosed(); void SctpAssociationBufferedAmount(uint32_t bufferedAmount); void SctpAssociationSendBufferFull(); void DataProducerClosed(); - void SendMessage(uint32_t ppid, const uint8_t* msg, size_t len, onQueuedCallback* = nullptr); + void SendMessage( + const uint8_t* msg, + size_t len, + uint32_t ppid, + std::vector& subchannels, + std::optional requiredSubchannel, + onQueuedCallback* cb = nullptr); /* Methods inherited from Channel::ChannelSocket::RequestHandler. */ public: void HandleRequest(Channel::ChannelRequest* request) override; - /* Methods inherited from PayloadChannel::PayloadChannelSocket::RequestHandler. */ - public: - void HandleRequest(PayloadChannel::PayloadChannelRequest* request) override; - public: // Passed by argument. const std::string id; @@ -108,12 +123,14 @@ namespace RTC size_t maxMessageSize{ 0u }; // Others. Type type; - std::string typeString; RTC::SctpStreamParameters sctpStreamParameters; std::string label; std::string protocol; + absl::flat_hash_set subchannels; bool transportConnected{ false }; bool sctpAssociationConnected{ false }; + bool paused{ false }; + bool dataProducerPaused{ false }; bool dataProducerClosed{ false }; size_t messagesSent{ 0u }; size_t bytesSent{ 0u }; diff --git a/worker/include/RTC/DataProducer.hpp b/worker/include/RTC/DataProducer.hpp index 952cc0df9f..6c0a8d3988 100644 --- a/worker/include/RTC/DataProducer.hpp +++ b/worker/include/RTC/DataProducer.hpp @@ -4,17 +4,16 @@ #include "common.hpp" #include "Channel/ChannelRequest.hpp" #include "Channel/ChannelSocket.hpp" -#include "PayloadChannel/PayloadChannelSocket.hpp" #include "RTC/RTCP/Packet.hpp" #include "RTC/SctpDictionaries.hpp" #include "RTC/Shared.hpp" -#include #include +#include namespace RTC { class DataProducer : public Channel::ChannelSocket::RequestHandler, - public PayloadChannel::PayloadChannelSocket::NotificationHandler + public Channel::ChannelSocket::NotificationHandler { public: class Listener @@ -25,7 +24,14 @@ namespace RTC public: virtual void OnDataProducerReceiveData(RTC::DataProducer* producer, size_t len) = 0; virtual void OnDataProducerMessageReceived( - RTC::DataProducer* dataProducer, uint32_t ppid, const uint8_t* msg, size_t len) = 0; + RTC::DataProducer* dataProducer, + const uint8_t* msg, + size_t len, + uint32_t ppid, + std::vector& subchannels, + std::optional requiredSubchannel) = 0; + virtual void OnDataProducerPaused(RTC::DataProducer* dataProducer) = 0; + virtual void OnDataProducerResumed(RTC::DataProducer* dataProducer) = 0; }; public: @@ -41,12 +47,14 @@ namespace RTC const std::string& id, size_t maxMessageSize, RTC::DataProducer::Listener* listener, - json& data); - virtual ~DataProducer(); + const FBS::Transport::ProduceDataRequest* data); + ~DataProducer() override; public: - void FillJson(json& jsonObject) const; - void FillJsonStats(json& jsonArray) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; + flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) const; Type GetType() const { return this->type; @@ -55,15 +63,24 @@ namespace RTC { return this->sctpStreamParameters; } - void ReceiveMessage(uint32_t ppid, const uint8_t* msg, size_t len); + bool IsPaused() const + { + return this->paused; + } + void ReceiveMessage( + const uint8_t* msg, + size_t len, + uint32_t ppid, + std::vector& subchannels, + std::optional requiredSubchannel); /* Methods inherited from Channel::ChannelSocket::RequestHandler. */ public: void HandleRequest(Channel::ChannelRequest* request) override; - /* Methods inherited from PayloadChannel::PayloadChannelSocket::NotificationHandler. */ + /* Methods inherited from Channel::ChannelSocket::NotificationHandler. */ public: - void HandleNotification(PayloadChannel::PayloadChannelNotification* notification) override; + void HandleNotification(Channel::ChannelNotification* notification) override; public: // Passed by argument. @@ -76,10 +93,10 @@ namespace RTC RTC::DataProducer::Listener* listener{ nullptr }; // Others. Type type; - std::string typeString; RTC::SctpStreamParameters sctpStreamParameters; std::string label; std::string protocol; + bool paused{ false }; size_t messagesReceived{ 0u }; size_t bytesReceived{ 0u }; }; diff --git a/worker/include/RTC/DirectTransport.hpp b/worker/include/RTC/DirectTransport.hpp index 046ac8d0ce..7352f0ad75 100644 --- a/worker/include/RTC/DirectTransport.hpp +++ b/worker/include/RTC/DirectTransport.hpp @@ -10,12 +10,17 @@ namespace RTC { public: DirectTransport( - RTC::Shared* shared, const std::string& id, RTC::Transport::Listener* listener, json& data); + RTC::Shared* shared, + const std::string& id, + RTC::Transport::Listener* listener, + const FBS::DirectTransport::DirectTransportOptions* options); ~DirectTransport() override; public: - void FillJson(json& jsonObject) const override; - void FillJsonStats(json& jsonArray) override; + flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder); + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; private: bool IsConnected() const override; @@ -27,9 +32,9 @@ namespace RTC void SendRtcpCompoundPacket(RTC::RTCP::CompoundPacket* packet) override; void SendMessage( RTC::DataConsumer* dataConsumer, - uint32_t ppid, const uint8_t* msg, size_t len, + uint32_t ppid, onQueuedCallback* cb = nullptr) override; void SendSctpData(const uint8_t* data, size_t len) override; void RecvStreamClosed(uint32_t ssrc) override; @@ -39,9 +44,9 @@ namespace RTC public: void HandleRequest(Channel::ChannelRequest* request) override; - /* Methods inherited from PayloadChannel::PayloadChannelSocket::NotificationHandler. */ + /* Methods inherited from Channel::ChannelSocket::NotificationHandler. */ public: - void HandleNotification(PayloadChannel::PayloadChannelNotification* notification) override; + void HandleNotification(Channel::ChannelNotification* notification) override; }; } // namespace RTC diff --git a/worker/include/RTC/DtlsTransport.hpp b/worker/include/RTC/DtlsTransport.hpp index be0e0fdeb9..27f4bd847f 100644 --- a/worker/include/RTC/DtlsTransport.hpp +++ b/worker/include/RTC/DtlsTransport.hpp @@ -2,8 +2,9 @@ #define MS_RTC_DTLS_TRANSPORT_HPP #include "common.hpp" +#include "FBS/webRtcTransport.h" #include "RTC/SrtpSession.hpp" -#include "handles/Timer.hpp" +#include "handles/TimerHandle.hpp" #include #include #include @@ -13,7 +14,7 @@ namespace RTC { - class DtlsTransport : public Timer::Listener + class DtlsTransport : public TimerHandle::Listener { public: enum class DtlsState @@ -28,7 +29,6 @@ namespace RTC public: enum class Role { - NONE = 0, AUTO = 1, CLIENT, SERVER @@ -37,7 +37,6 @@ namespace RTC public: enum class FingerprintAlgorithm { - NONE = 0, SHA1 = 1, SHA224, SHA256, @@ -48,7 +47,7 @@ namespace RTC public: struct Fingerprint { - FingerprintAlgorithm algorithm{ FingerprintAlgorithm::NONE }; + FingerprintAlgorithm algorithm; std::string value; }; @@ -97,30 +96,11 @@ namespace RTC public: static void ClassInit(); static void ClassDestroy(); - static Role StringToRole(const std::string& role) - { - auto it = DtlsTransport::string2Role.find(role); - - if (it != DtlsTransport::string2Role.end()) - return it->second; - else - return DtlsTransport::Role::NONE; - } - static FingerprintAlgorithm GetFingerprintAlgorithm(const std::string& fingerprint) - { - auto it = DtlsTransport::string2FingerprintAlgorithm.find(fingerprint); - - if (it != DtlsTransport::string2FingerprintAlgorithm.end()) - return it->second; - else - return DtlsTransport::FingerprintAlgorithm::NONE; - } - static std::string& GetFingerprintAlgorithmString(FingerprintAlgorithm fingerprint) - { - auto it = DtlsTransport::fingerprintAlgorithm2String.find(fingerprint); - - return it->second; - } + static Role RoleFromFbs(FBS::WebRtcTransport::DtlsRole role); + static FBS::WebRtcTransport::DtlsRole RoleToFbs(Role role); + static FBS::WebRtcTransport::DtlsState StateToFbs(DtlsState state); + static FingerprintAlgorithm AlgorithmFromFbs(FBS::WebRtcTransport::FingerprintAlgorithm algorithm); + static FBS::WebRtcTransport::FingerprintAlgorithm AlgorithmToFbs(FingerprintAlgorithm algorithm); static bool IsDtls(const uint8_t* data, size_t len) { // clang-format off @@ -132,6 +112,10 @@ namespace RTC ); // clang-format on } + static std::vector& GetLocalFingerprints() + { + return DtlsTransport::localFingerprints; + } private: static void GenerateCertificateAndPrivateKey(); @@ -157,21 +141,19 @@ namespace RTC public: void Dump() const; void Run(Role localRole); - std::vector& GetLocalFingerprints() const - { - return DtlsTransport::localFingerprints; - } bool SetRemoteFingerprint(const Fingerprint& fingerprint); void ProcessDtlsData(const uint8_t* data, size_t len); DtlsState GetState() const { return this->state; } - Role GetLocalRole() const + std::optional GetLocalRole() const { return this->localRole; } void SendApplicationData(const uint8_t* data, size_t len); + // This method must be public since it's called within an OpenSSL callback. + void SendDtlsData(const uint8_t* data, size_t len); private: bool IsRunning() const @@ -193,20 +175,19 @@ namespace RTC } void Reset(); bool CheckStatus(int returnCode); - void SendPendingOutgoingDtlsData(); bool SetTimeout(); bool ProcessHandshake(); bool CheckRemoteFingerprint(); void ExtractSrtpKeys(RTC::SrtpSession::CryptoSuite srtpCryptoSuite); - RTC::SrtpSession::CryptoSuite GetNegotiatedSrtpCryptoSuite(); + std::optional GetNegotiatedSrtpCryptoSuite(); /* Callbacks fired by OpenSSL events. */ public: void OnSslInfo(int where, int ret); - /* Pure virtual methods inherited from Timer::Listener. */ + /* Pure virtual methods inherited from TimerHandle::Listener. */ public: - void OnTimer(Timer* timer) override; + void OnTimer(TimerHandle* timer) override; private: // Passed by argument. @@ -215,11 +196,11 @@ namespace RTC SSL* ssl{ nullptr }; BIO* sslBioFromNetwork{ nullptr }; // The BIO from which ssl reads. BIO* sslBioToNetwork{ nullptr }; // The BIO in which ssl writes. - Timer* timer{ nullptr }; + TimerHandle* timer{ nullptr }; // Others. DtlsState state{ DtlsState::NEW }; - Role localRole{ Role::NONE }; - Fingerprint remoteFingerprint; + std::optional localRole; + std::optional remoteFingerprint; bool handshakeDone{ false }; bool handshakeDoneNow{ false }; std::string remoteCert; diff --git a/worker/include/RTC/IceCandidate.hpp b/worker/include/RTC/IceCandidate.hpp index 7b684365a0..cb634271b8 100644 --- a/worker/include/RTC/IceCandidate.hpp +++ b/worker/include/RTC/IceCandidate.hpp @@ -2,23 +2,18 @@ #define MS_RTC_ICE_CANDIDATE_HPP #include "common.hpp" +#include "FBS/webRtcTransport.h" #include "RTC/TcpServer.hpp" +#include "RTC/TransportTuple.hpp" #include "RTC/UdpSocket.hpp" -#include +#include #include -using json = nlohmann::json; - namespace RTC { class IceCandidate { - public: - enum class Protocol - { - UDP = 1, - TCP - }; + using Protocol = TransportTuple::Protocol; public: enum class CandidateType @@ -32,40 +27,47 @@ namespace RTC PASSIVE = 1 }; + public: + static CandidateType CandidateTypeFromFbs(FBS::WebRtcTransport::IceCandidateType type); + static FBS::WebRtcTransport::IceCandidateType CandidateTypeToFbs(CandidateType type); + static TcpCandidateType TcpCandidateTypeFromFbs(FBS::WebRtcTransport::IceCandidateTcpType type); + static FBS::WebRtcTransport::IceCandidateTcpType TcpCandidateTypeToFbs(TcpCandidateType type); + public: IceCandidate(RTC::UdpSocket* udpSocket, uint32_t priority) - : foundation("udpcandidate"), priority(priority), ip(udpSocket->GetLocalIp()), - protocol(Protocol::UDP), port(udpSocket->GetLocalPort()), type(CandidateType::HOST) + : foundation("udpcandidate"), priority(priority), address(udpSocket->GetLocalIp()), + protocol(Protocol::UDP), port(udpSocket->GetLocalPort()) { } - IceCandidate(RTC::UdpSocket* udpSocket, uint32_t priority, std::string& announcedIp) - : foundation("udpcandidate"), priority(priority), ip(announcedIp), protocol(Protocol::UDP), - port(udpSocket->GetLocalPort()), type(CandidateType::HOST) + IceCandidate(RTC::UdpSocket* udpSocket, uint32_t priority, std::string& announcedAddress) + : foundation("udpcandidate"), priority(priority), address(announcedAddress), + protocol(Protocol::UDP), port(udpSocket->GetLocalPort()) { } IceCandidate(RTC::TcpServer* tcpServer, uint32_t priority) - : foundation("tcpcandidate"), priority(priority), ip(tcpServer->GetLocalIp()), - protocol(Protocol::TCP), port(tcpServer->GetLocalPort()), type(CandidateType::HOST), - tcpType(TcpCandidateType::PASSIVE) + : foundation("tcpcandidate"), priority(priority), address(tcpServer->GetLocalIp()), + protocol(Protocol::TCP), port(tcpServer->GetLocalPort()) + { } - IceCandidate(RTC::TcpServer* tcpServer, uint32_t priority, std::string& announcedIp) - : foundation("tcpcandidate"), priority(priority), ip(announcedIp), protocol(Protocol::TCP), - port(tcpServer->GetLocalPort()), type(CandidateType::HOST), - tcpType(TcpCandidateType::PASSIVE) + IceCandidate(RTC::TcpServer* tcpServer, uint32_t priority, std::string& announcedAddress) + : foundation("tcpcandidate"), priority(priority), address(announcedAddress), + protocol(Protocol::TCP), port(tcpServer->GetLocalPort()) + { } - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; private: // Others. std::string foundation; uint32_t priority{ 0u }; - std::string ip; + std::string address; Protocol protocol; uint16_t port{ 0u }; CandidateType type{ CandidateType::HOST }; diff --git a/worker/include/RTC/IceServer.hpp b/worker/include/RTC/IceServer.hpp index 51fa85919d..6c1be537bd 100644 --- a/worker/include/RTC/IceServer.hpp +++ b/worker/include/RTC/IceServer.hpp @@ -2,14 +2,17 @@ #define MS_RTC_ICE_SERVER_HPP #include "common.hpp" +#include "Utils.hpp" +#include "FBS/webRtcTransport.h" #include "RTC/StunPacket.hpp" #include "RTC/TransportTuple.hpp" +#include "handles/TimerHandle.hpp" #include #include namespace RTC { - class IceServer + class IceServer : public TimerHandle::Listener { public: enum class IceState @@ -17,9 +20,13 @@ namespace RTC NEW = 1, CONNECTED, COMPLETED, - DISCONNECTED + DISCONNECTED, }; + public: + static IceState RoleFromFbs(FBS::WebRtcTransport::IceState state); + static FBS::WebRtcTransport::IceState IceStateToFbs(IceState state); + public: class Listener { @@ -48,8 +55,12 @@ namespace RTC }; public: - IceServer(Listener* listener, const std::string& usernameFragment, const std::string& password); - ~IceServer(); + IceServer( + Listener* listener, + const std::string& usernameFragment, + const std::string& password, + uint8_t consentTimeoutSec); + ~IceServer() override; public: void ProcessStunPacket(RTC::StunPacket* packet, RTC::TransportTuple* tuple); @@ -69,35 +80,19 @@ namespace RTC { return this->selectedTuple; } - void RestartIce(const std::string& usernameFragment, const std::string& password) - { - if (!this->oldUsernameFragment.empty()) - { - this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment); - } - - this->oldUsernameFragment = this->usernameFragment; - this->usernameFragment = usernameFragment; - - this->oldPassword = this->password; - this->password = password; - - this->remoteNomination = 0u; - - // Notify the listener. - this->listener->OnIceServerLocalUsernameFragmentAdded(this, usernameFragment); - - // NOTE: Do not call listener->OnIceServerLocalUsernameFragmentRemoved() - // yet with old usernameFragment. Wait until we receive a STUN packet - // with the new one. - } + void RestartIce(const std::string& usernameFragment, const std::string& password); bool IsValidTuple(const RTC::TransportTuple* tuple) const; void RemoveTuple(RTC::TransportTuple* tuple); - // This should be just called in 'connected' or completed' state - // and the given tuple must be an already valid tuple. - void ForceSelectedTuple(const RTC::TransportTuple* tuple); + /** + * This should be just called in 'connected' or 'completed' state and the + * given tuple must be an already valid tuple. + */ + void MayForceSelectedTuple(const RTC::TransportTuple* tuple); private: + void ProcessStunRequest(RTC::StunPacket* request, RTC::TransportTuple* tuple); + void ProcessStunIndication(RTC::StunPacket* indication); + void ProcessStunResponse(RTC::StunPacket* response); void HandleTuple( RTC::TransportTuple* tuple, bool hasUseCandidate, bool hasNomination, uint32_t nomination); /** @@ -113,19 +108,38 @@ namespace RTC * NOTE: The given tuple MUST be already stored within the list. */ void SetSelectedTuple(RTC::TransportTuple* storedTuple); + bool IsConsentCheckSupported() const + { + return this->consentTimeoutMs != 0u; + } + bool IsConsentCheckRunning() const + { + return (this->consentCheckTimer && this->consentCheckTimer->IsActive()); + } + void StartConsentCheck(); + void RestartConsentCheck(); + void StopConsentCheck(); + + /* Pure virtual methods inherited from TimerHandle::Listener. */ + public: + void OnTimer(TimerHandle* timer) override; private: // Passed by argument. Listener* listener{ nullptr }; - // Others. std::string usernameFragment; std::string password; + uint16_t consentTimeoutMs{ 30000u }; + // Others. std::string oldUsernameFragment; std::string oldPassword; - uint32_t remoteNomination{ 0u }; IceState state{ IceState::NEW }; + uint32_t remoteNomination{ 0u }; std::list tuples; RTC::TransportTuple* selectedTuple{ nullptr }; + TimerHandle* consentCheckTimer{ nullptr }; + uint64_t lastConsentRequestReceivedAtMs{ 0u }; + bool isRemovingTuples{ false }; }; } // namespace RTC diff --git a/worker/include/RTC/KeyFrameRequestManager.hpp b/worker/include/RTC/KeyFrameRequestManager.hpp index d3d52c6b34..76aaa6716c 100644 --- a/worker/include/RTC/KeyFrameRequestManager.hpp +++ b/worker/include/RTC/KeyFrameRequestManager.hpp @@ -1,12 +1,12 @@ #ifndef MS_KEY_FRAME_REQUEST_MANAGER_HPP #define MS_KEY_FRAME_REQUEST_MANAGER_HPP -#include "handles/Timer.hpp" +#include "handles/TimerHandle.hpp" #include namespace RTC { - class PendingKeyFrameInfo : public Timer::Listener + class PendingKeyFrameInfo : public TimerHandle::Listener { public: class Listener @@ -20,7 +20,7 @@ namespace RTC public: PendingKeyFrameInfo(Listener* listener, uint32_t ssrc); - ~PendingKeyFrameInfo(); + ~PendingKeyFrameInfo() override; uint32_t GetSsrc() const { @@ -39,18 +39,18 @@ namespace RTC return this->timer->Restart(); } - /* Pure virtual methods inherited from Timer::Listener. */ + /* Pure virtual methods inherited from TimerHandle::Listener. */ public: - void OnTimer(Timer* timer) override; + void OnTimer(TimerHandle* timer) override; private: Listener* listener{ nullptr }; uint32_t ssrc; - Timer* timer{ nullptr }; + TimerHandle* timer{ nullptr }; bool retryOnTimeout{ true }; }; - class KeyFrameRequestDelayer : public Timer::Listener + class KeyFrameRequestDelayer : public TimerHandle::Listener { public: class Listener @@ -64,7 +64,7 @@ namespace RTC public: KeyFrameRequestDelayer(Listener* listener, uint32_t ssrc, uint32_t delay); - ~KeyFrameRequestDelayer(); + ~KeyFrameRequestDelayer() override; uint32_t GetSsrc() const { @@ -79,14 +79,14 @@ namespace RTC this->keyFrameRequested = flag; } - /* Pure virtual methods inherited from Timer::Listener. */ + /* Pure virtual methods inherited from TimerHandle::Listener. */ public: - void OnTimer(Timer* timer) override; + void OnTimer(TimerHandle* timer) override; private: Listener* listener{ nullptr }; uint32_t ssrc; - Timer* timer{ nullptr }; + TimerHandle* timer{ nullptr }; bool keyFrameRequested{ false }; }; @@ -105,7 +105,7 @@ namespace RTC public: explicit KeyFrameRequestManager(Listener* listener, uint32_t keyFrameRequestDelay); - virtual ~KeyFrameRequestManager(); + ~KeyFrameRequestManager() override; void KeyFrameNeeded(uint32_t ssrc); void ForceKeyFrameNeeded(uint32_t ssrc); diff --git a/worker/include/RTC/NackGenerator.hpp b/worker/include/RTC/NackGenerator.hpp index f7924f6bd0..f46ab43fe7 100644 --- a/worker/include/RTC/NackGenerator.hpp +++ b/worker/include/RTC/NackGenerator.hpp @@ -4,16 +4,14 @@ #include "common.hpp" #include "RTC/RtpPacket.hpp" #include "RTC/SeqManager.hpp" -#include "handles/Timer.hpp" -#include -#include +#include "handles/TimerHandle.hpp" #include #include #include namespace RTC { - class NackGenerator : public Timer::Listener + class NackGenerator : public TimerHandle::Listener { public: class Listener @@ -69,20 +67,20 @@ namespace RTC std::vector GetNackBatch(NackFilter filter); void MayRunTimer() const; - /* Pure virtual methods inherited from Timer::Listener. */ + /* Pure virtual methods inherited from TimerHandle::Listener. */ public: - void OnTimer(Timer* timer) override; + void OnTimer(TimerHandle* timer) override; private: // Passed by argument. Listener* listener{ nullptr }; unsigned int sendNackDelayMs{ 0u }; // Allocated by this. - Timer* timer{ nullptr }; + TimerHandle* timer{ nullptr }; // Others. - absl::btree_map::SeqLowerThan> nackList; - absl::btree_set::SeqLowerThan> keyFrameList; - absl::btree_set::SeqLowerThan> recoveredList; + std::map::SeqLowerThan> nackList; + std::set::SeqLowerThan> keyFrameList; + std::set::SeqLowerThan> recoveredList; bool started{ false }; uint16_t lastSeq{ 0u }; // Seq number of last valid packet. uint32_t rtt{ 0u }; // Round trip time (ms). diff --git a/worker/include/RTC/Parameters.hpp b/worker/include/RTC/Parameters.hpp index eb7cd8f188..0bbd89cde6 100644 --- a/worker/include/RTC/Parameters.hpp +++ b/worker/include/RTC/Parameters.hpp @@ -2,13 +2,11 @@ #define MS_RTC_PARAMETERS_HPP #include "common.hpp" +#include "FBS/rtpParameters.h" #include -#include #include #include -using json = nlohmann::json; - namespace RTC { class Parameters @@ -62,8 +60,9 @@ namespace RTC }; public: - void FillJson(json& jsonObject) const; - void Set(json& data); + std::vector> FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; + void Set(const flatbuffers::Vector>* data); bool HasBoolean(const std::string& key) const; bool HasInteger(const std::string& key) const; bool HasPositiveInteger(const std::string& key) const; diff --git a/worker/include/RTC/PipeConsumer.hpp b/worker/include/RTC/PipeConsumer.hpp index 3de9030ee4..5a3e646467 100644 --- a/worker/include/RTC/PipeConsumer.hpp +++ b/worker/include/RTC/PipeConsumer.hpp @@ -1,8 +1,7 @@ -#ifndef MS_RTC_PIPE_CONSUMER_HPP -#define MS_RTC_PIPE_CONSUMER_HPP +#ifndef MS_RTC_PIPECONSUMER_HPP +#define MS_RTC_PIPECONSUMER_HPP #include "RTC/Consumer.hpp" -#include "RTC/RtpStreamSend.hpp" #include "RTC/SeqManager.hpp" #include "RTC/Shared.hpp" @@ -16,17 +15,21 @@ namespace RTC const std::string& id, const std::string& producerId, RTC::Consumer::Listener* listener, - json& data); + const FBS::Transport::ConsumeRequest* data); ~PipeConsumer() override; public: - void FillJson(json& jsonObject) const override; - void FillJsonStats(json& jsonArray) const override; - void FillJsonScore(json& jsonObject) const override; - void ProducerRtpStream(RTC::RtpStream* rtpStream, uint32_t mappedSsrc) override; - void ProducerNewRtpStream(RTC::RtpStream* rtpStream, uint32_t mappedSsrc) override; - void ProducerRtpStreamScore(RTC::RtpStream* rtpStream, uint8_t score, uint8_t previousScore) override; - void ProducerRtcpSenderReport(RTC::RtpStream* rtpStream, bool first) override; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; + flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) override; + flatbuffers::Offset FillBufferScore( + flatbuffers::FlatBufferBuilder& builder) const override; + void ProducerRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) override; + void ProducerNewRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) override; + void ProducerRtpStreamScore( + RTC::RtpStreamRecv* rtpStream, uint8_t score, uint8_t previousScore) override; + void ProducerRtcpSenderReport(RTC::RtpStreamRecv* rtpStream, bool first) override; uint8_t GetBitratePriority() const override; uint32_t IncreaseLayer(uint32_t bitrate, bool considerLoss) override; void ApplyLayers() override; @@ -70,7 +73,8 @@ namespace RTC absl::flat_hash_map mapSsrcRtpStream; bool keyFrameSupported{ false }; absl::flat_hash_map mapRtpStreamSyncRequired; - absl::flat_hash_map> mapRtpStreamRtpSeqManager; + absl::flat_hash_map>> + mapRtpStreamRtpSeqManager; }; } // namespace RTC diff --git a/worker/include/RTC/PipeTransport.hpp b/worker/include/RTC/PipeTransport.hpp index 2f0b9c39eb..3a5a38fd93 100644 --- a/worker/include/RTC/PipeTransport.hpp +++ b/worker/include/RTC/PipeTransport.hpp @@ -1,6 +1,7 @@ #ifndef MS_RTC_PIPE_TRANSPORT_HPP #define MS_RTC_PIPE_TRANSPORT_HPP +#include "FBS/pipeTransport.h" #include "RTC/Shared.hpp" #include "RTC/SrtpSession.hpp" #include "RTC/Transport.hpp" @@ -11,13 +12,6 @@ namespace RTC { class PipeTransport : public RTC::Transport, public RTC::UdpSocket::Listener { - private: - struct ListenIp - { - std::string ip; - std::string announcedIp; - }; - private: static RTC::SrtpSession::CryptoSuite srtpCryptoSuite; static std::string srtpCryptoSuiteString; @@ -25,20 +19,25 @@ namespace RTC public: PipeTransport( - RTC::Shared* shared, const std::string& id, RTC::Transport::Listener* listener, json& data); + RTC::Shared* shared, + const std::string& id, + RTC::Transport::Listener* listener, + const FBS::PipeTransport::PipeTransportOptions* options); ~PipeTransport() override; public: - void FillJson(json& jsonObject) const override; - void FillJsonStats(json& jsonArray) override; + flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder); + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; /* Methods inherited from Channel::ChannelSocket::RequestHandler. */ public: void HandleRequest(Channel::ChannelRequest* request) override; - /* Methods inherited from PayloadChannel::PayloadChannelSocket::NotificationHandler. */ + /* Methods inherited from Channel::ChannelSocket::NotificationHandler. */ public: - void HandleNotification(PayloadChannel::PayloadChannelNotification* notification) override; + void HandleNotification(Channel::ChannelNotification* notification) override; private: bool IsConnected() const override; @@ -51,9 +50,9 @@ namespace RTC void SendRtcpCompoundPacket(RTC::RTCP::CompoundPacket* packet) override; void SendMessage( RTC::DataConsumer* dataConsumer, - uint32_t ppid, const uint8_t* msg, size_t len, + uint32_t ppid, onQueuedCallback* cb = nullptr) override; void SendSctpData(const uint8_t* data, size_t len) override; void RecvStreamClosed(uint32_t ssrc) override; @@ -75,8 +74,10 @@ namespace RTC RTC::SrtpSession* srtpRecvSession{ nullptr }; RTC::SrtpSession* srtpSendSession{ nullptr }; // Others. - ListenIp listenIp; - struct sockaddr_storage remoteAddrStorage; + ListenInfo listenInfo; + struct sockaddr_storage remoteAddrStorage + { + }; bool rtx{ false }; std::string srtpKey; std::string srtpKeyBase64; diff --git a/worker/include/RTC/PlainTransport.hpp b/worker/include/RTC/PlainTransport.hpp index 2df62205d1..2c7b5c20d3 100644 --- a/worker/include/RTC/PlainTransport.hpp +++ b/worker/include/RTC/PlainTransport.hpp @@ -1,6 +1,7 @@ #ifndef MS_RTC_PLAIN_TRANSPORT_HPP #define MS_RTC_PLAIN_TRANSPORT_HPP +#include "FBS/plainTransport.h" #include "RTC/Shared.hpp" #include "RTC/SrtpSession.hpp" #include "RTC/Transport.hpp" @@ -12,33 +13,24 @@ namespace RTC { class PlainTransport : public RTC::Transport, public RTC::UdpSocket::Listener { - private: - struct ListenIp - { - std::string ip; - std::string announcedIp; - }; - - private: - static absl::flat_hash_map string2SrtpCryptoSuite; - static absl::flat_hash_map srtpCryptoSuite2String; - public: PlainTransport( - RTC::Shared* shared, const std::string& id, RTC::Transport::Listener* listener, json& data); + RTC::Shared* shared, + const std::string& id, + RTC::Transport::Listener* listener, + const FBS::PlainTransport::PlainTransportOptions* options); ~PlainTransport() override; public: - void FillJson(json& jsonObject) const override; - void FillJsonStats(json& jsonArray) override; + flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder); + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; /* Methods inherited from Channel::ChannelSocket::RequestHandler. */ public: void HandleRequest(Channel::ChannelRequest* request) override; - - /* Methods inherited from PayloadChannel::PayloadChannelSocket::NotificationHandler. */ - public: - void HandleNotification(PayloadChannel::PayloadChannelNotification* notification) override; + void HandleNotification(Channel::ChannelNotification* notification) override; private: bool IsConnected() const override; @@ -52,9 +44,9 @@ namespace RTC void SendRtcpCompoundPacket(RTC::RTCP::CompoundPacket* packet) override; void SendMessage( RTC::DataConsumer* dataConsumer, - uint32_t ppid, const uint8_t* msg, size_t len, + uint32_t ppid, onQueuedCallback* cb = nullptr) override; void SendSctpData(const uint8_t* data, size_t len) override; void RecvStreamClosed(uint32_t ssrc) override; @@ -63,6 +55,8 @@ namespace RTC void OnRtpDataReceived(RTC::TransportTuple* tuple, const uint8_t* data, size_t len); void OnRtcpDataReceived(RTC::TransportTuple* tuple, const uint8_t* data, size_t len); void OnSctpDataReceived(RTC::TransportTuple* tuple, const uint8_t* data, size_t len); + void EmitTuple() const; + void EmitRtcpTuple() const; /* Pure virtual methods inherited from RTC::UdpSocket::Listener. */ public: @@ -78,11 +72,16 @@ namespace RTC RTC::SrtpSession* srtpRecvSession{ nullptr }; RTC::SrtpSession* srtpSendSession{ nullptr }; // Others. - ListenIp listenIp; + ListenInfo listenInfo; + ListenInfo rtcpListenInfo; bool rtcpMux{ true }; bool comedia{ false }; - struct sockaddr_storage remoteAddrStorage; - struct sockaddr_storage rtcpRemoteAddrStorage; + struct sockaddr_storage remoteAddrStorage + { + }; + struct sockaddr_storage rtcpRemoteAddrStorage + { + }; RTC::SrtpSession::CryptoSuite srtpCryptoSuite{ RTC::SrtpSession::CryptoSuite::AES_CM_128_HMAC_SHA1_80 }; diff --git a/worker/include/RTC/PortManager.hpp b/worker/include/RTC/PortManager.hpp index ca925ef46f..3e8c6aa9da 100644 --- a/worker/include/RTC/PortManager.hpp +++ b/worker/include/RTC/PortManager.hpp @@ -2,10 +2,9 @@ #define MS_RTC_PORT_MANAGER_HPP #include "common.hpp" -#include "Settings.hpp" +#include "RTC/Transport.hpp" #include #include -#include #include #include @@ -14,48 +13,72 @@ namespace RTC class PortManager { private: - enum class Transport : uint8_t + enum class Protocol : uint8_t { UDP = 1, TCP }; - public: - static uv_udp_t* BindUdp(std::string& ip) - { - return reinterpret_cast(Bind(Transport::UDP, ip)); - } - static uv_udp_t* BindUdp(std::string& ip, uint16_t port) + private: + struct PortRange { - return reinterpret_cast(Bind(Transport::UDP, ip, port)); - } - static uv_tcp_t* BindTcp(std::string& ip) + explicit PortRange(uint16_t numPorts, uint16_t minPort) + : ports(numPorts, false), minPort(minPort) + { + } + + std::vector ports; + uint16_t minPort{ 0u }; + uint16_t numUsedPorts{ 0u }; + }; + + public: + static uv_udp_t* BindUdp(std::string& ip, uint16_t port, RTC::Transport::SocketFlags& flags) { - return reinterpret_cast(Bind(Transport::TCP, ip)); + return reinterpret_cast(Bind(Protocol::UDP, ip, port, flags)); } - static uv_tcp_t* BindTcp(std::string& ip, uint16_t port) + static uv_udp_t* BindUdp( + std::string& ip, + uint16_t minPort, + uint16_t maxPort, + RTC::Transport::SocketFlags& flags, + uint64_t& hash) { - return reinterpret_cast(Bind(Transport::TCP, ip, port)); + return reinterpret_cast(Bind(Protocol::UDP, ip, minPort, maxPort, flags, hash)); } - static void UnbindUdp(std::string& ip, uint16_t port) + static uv_tcp_t* BindTcp(std::string& ip, uint16_t port, RTC::Transport::SocketFlags& flags) { - return Unbind(Transport::UDP, ip, port); + return reinterpret_cast(Bind(Protocol::TCP, ip, port, flags)); } - static void UnbindTcp(std::string& ip, uint16_t port) + static uv_tcp_t* BindTcp( + std::string& ip, + uint16_t minPort, + uint16_t maxPort, + RTC::Transport::SocketFlags& flags, + uint64_t& hash) { - return Unbind(Transport::TCP, ip, port); + return reinterpret_cast(Bind(Protocol::TCP, ip, minPort, maxPort, flags, hash)); } - static void FillJson(json& jsonObject); + static void Unbind(uint64_t hash, uint16_t port); + static void Dump(); private: - static uv_handle_t* Bind(Transport transport, std::string& ip); - static uv_handle_t* Bind(Transport transport, std::string& ip, uint16_t port); - static void Unbind(Transport transport, std::string& ip, uint16_t port); - static std::vector& GetPorts(Transport transport, const std::string& ip); + static uv_handle_t* Bind( + Protocol protocol, std::string& ip, uint16_t port, RTC::Transport::SocketFlags& flags); + static uv_handle_t* Bind( + Protocol protocol, + std::string& ip, + uint16_t minPort, + uint16_t maxPort, + RTC::Transport::SocketFlags& flags, + uint64_t& hash); + static uint64_t GeneratePortRangeHash( + Protocol protocol, sockaddr_storage* bindAddr, uint16_t minPort, uint16_t maxPort); + static PortRange& GetOrCreatePortRange(uint64_t hash, uint16_t minPort, uint16_t maxPort); + static uint8_t ConvertSocketFlags(RTC::Transport::SocketFlags& flags, Protocol protocol, int family); private: - thread_local static absl::flat_hash_map> mapUdpIpPorts; - thread_local static absl::flat_hash_map> mapTcpIpPorts; + thread_local static absl::flat_hash_map mapPortRanges; }; } // namespace RTC diff --git a/worker/include/RTC/Producer.hpp b/worker/include/RTC/Producer.hpp index 9fb4306586..d7562587c3 100644 --- a/worker/include/RTC/Producer.hpp +++ b/worker/include/RTC/Producer.hpp @@ -4,7 +4,6 @@ #include "common.hpp" #include "Channel/ChannelRequest.hpp" #include "Channel/ChannelSocket.hpp" -#include "PayloadChannel/PayloadChannelSocket.hpp" #include "RTC/KeyFrameRequestManager.hpp" #include "RTC/RTCP/CompoundPacket.hpp" #include "RTC/RTCP/Packet.hpp" @@ -15,18 +14,15 @@ #include "RTC/RtpPacket.hpp" #include "RTC/RtpStreamRecv.hpp" #include "RTC/Shared.hpp" -#include #include #include -using json = nlohmann::json; - namespace RTC { class Producer : public RTC::RtpStreamRecv::Listener, public RTC::KeyFrameRequestManager::Listener, public Channel::ChannelSocket::RequestHandler, - public PayloadChannel::PayloadChannelSocket::NotificationHandler + public Channel::ChannelSocket::NotificationHandler { public: class Listener @@ -40,11 +36,14 @@ namespace RTC virtual void OnProducerPaused(RTC::Producer* producer) = 0; virtual void OnProducerResumed(RTC::Producer* producer) = 0; virtual void OnProducerNewRtpStream( - RTC::Producer* producer, RTC::RtpStream* rtpStream, uint32_t mappedSsrc) = 0; + RTC::Producer* producer, RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) = 0; virtual void OnProducerRtpStreamScore( - RTC::Producer* producer, RTC::RtpStream* rtpStream, uint8_t score, uint8_t previousScore) = 0; + RTC::Producer* producer, + RTC::RtpStreamRecv* rtpStream, + uint8_t score, + uint8_t previousScore) = 0; virtual void OnProducerRtcpSenderReport( - RTC::Producer* producer, RTC::RtpStream* rtpStream, bool first) = 0; + RTC::Producer* producer, RTC::RtpStreamRecv* rtpStream, bool first) = 0; virtual void OnProducerRtpPacketReceived(RTC::Producer* producer, RTC::RtpPacket* packet) = 0; virtual void OnProducerSendRtcpPacket(RTC::Producer* producer, RTC::RTCP::Packet* packet) = 0; virtual void OnProducerNeedWorstRemoteFractionLost( @@ -90,15 +89,22 @@ namespace RTC bool nack{ false }; bool pli{ false }; bool fir{ false }; + bool sr{ false }; }; public: - Producer(RTC::Shared* shared, const std::string& id, RTC::Producer::Listener* listener, json& data); - virtual ~Producer(); + Producer( + RTC::Shared* shared, + const std::string& id, + RTC::Producer::Listener* listener, + const FBS::Transport::ProduceRequest* data); + ~Producer() override; public: - void FillJson(json& jsonObject) const; - void FillJsonStats(json& jsonArray) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; + flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder); RTC::Media::Kind GetKind() const { return this->kind; @@ -137,9 +143,9 @@ namespace RTC public: void HandleRequest(Channel::ChannelRequest* request) override; - /* Methods inherited from PayloadChannel::PayloadChannelSocket::NotificationHandler. */ + /* Methods inherited from Channel::ChannelSocket::NotificationHandler. */ public: - void HandleNotification(PayloadChannel::PayloadChannelNotification* notification) override; + void HandleNotification(Channel::ChannelNotification* notification) override; private: RTC::RtpStreamRecv* GetRtpStream(RTC::RtpPacket* packet); @@ -155,6 +161,8 @@ namespace RTC void EmitTraceEventPliType(uint32_t ssrc) const; void EmitTraceEventFirType(uint32_t ssrc) const; void EmitTraceEventNackType() const; + void EmitTraceEventSrType(RTC::RTCP::SenderReport* report) const; + void EmitTraceEvent(flatbuffers::Offset& notification) const; /* Pure virtual methods inherited from RTC::RtpStreamRecv::Listener. */ public: @@ -181,7 +189,7 @@ namespace RTC // Others. RTC::Media::Kind kind; RTC::RtpParameters rtpParameters; - RTC::RtpParameters::Type type{ RTC::RtpParameters::Type::NONE }; + RTC::RtpParameters::Type type; struct RtpMapping rtpMapping; std::vector rtpStreamByEncodingIdx; std::vector rtpStreamScores; diff --git a/worker/include/RTC/RTCP/CompoundPacket.hpp b/worker/include/RTC/RTCP/CompoundPacket.hpp index 3e377d394a..228ba06d73 100644 --- a/worker/include/RTC/RTCP/CompoundPacket.hpp +++ b/worker/include/RTC/RTCP/CompoundPacket.hpp @@ -48,14 +48,16 @@ namespace RTC // Adds the given data and returns true if there is enough space to hold it, // false otherwise. bool Add( - SenderReport* senderReport, SdesChunk* sdesChunk, DelaySinceLastRr* delaySinceLastRrReport); + SenderReport* senderReport, + SdesChunk* sdesChunk, + DelaySinceLastRr::SsrcInfo* delaySinceLastRrSsrcInfo); // RTCP additions per Consumer (pipe). // Adds the given data and returns true if there is enough space to hold it, // false otherwise. bool Add( std::vector& senderReports, std::vector& sdesChunks, - std::vector& delaySinceLastRrReports); + std::vector& delaySinceLastRrSsrcInfos); // RTCP additions per Producer. // Adds the given data and returns true if there is enough space to hold it, // false otherwise. @@ -63,8 +65,6 @@ namespace RTC void AddSenderReport(SenderReport* report); void AddReceiverReport(ReceiverReport* report); void AddSdesChunk(SdesChunk* chunk); - void AddReceiverReferenceTime(ReceiverReferenceTime* report); - void AddDelaySinceLastRr(DelaySinceLastRr* report); bool HasSenderReport() { return this->senderReportPacket.Begin() != this->senderReportPacket.End(); @@ -77,6 +77,14 @@ namespace RTC [](const ExtendedReportBlock* report) { return report->GetType() == ExtendedReportBlock::Type::RRT; }); } + bool HasDelaySinceLastRr() + { + return std::any_of( + this->xrPacket.Begin(), + this->xrPacket.End(), + [](const ExtendedReportBlock* report) + { return report->GetType() == ExtendedReportBlock::Type::DLRR; }); + } void Serialize(uint8_t* data); private: @@ -85,6 +93,7 @@ namespace RTC ReceiverReportPacket receiverReportPacket; SdesPacket sdesPacket; ExtendedReportPacket xrPacket; + DelaySinceLastRr* delaySinceLastRr{ nullptr }; }; } // namespace RTCP } // namespace RTC diff --git a/worker/include/RTC/RTCP/FeedbackItem.hpp b/worker/include/RTC/RTCP/FeedbackItem.hpp index 3649ab41c8..9a453a7d99 100644 --- a/worker/include/RTC/RTCP/FeedbackItem.hpp +++ b/worker/include/RTC/RTCP/FeedbackItem.hpp @@ -15,7 +15,9 @@ namespace RTC { // data size must be >= header. if (Item::HeaderSize > len) + { return nullptr; + } auto* header = const_cast(reinterpret_cast(data)); diff --git a/worker/include/RTC/RTCP/FeedbackPsRpsi.hpp b/worker/include/RTC/RTCP/FeedbackPsRpsi.hpp index 123aa692f5..a3a531e5ea 100644 --- a/worker/include/RTC/RTCP/FeedbackPsRpsi.hpp +++ b/worker/include/RTC/RTCP/FeedbackPsRpsi.hpp @@ -10,9 +10,8 @@ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | PB | - |0| Payload Type| - | Native RPSI bit string | + | PB |0| Payload Type| Native RPSI bit string | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | defined per codec ... | Padding (0) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ diff --git a/worker/include/RTC/RTCP/FeedbackRtpTransport.hpp b/worker/include/RTC/RTCP/FeedbackRtpTransport.hpp index 58cbc6f151..fff4d3fbe0 100644 --- a/worker/include/RTC/RTCP/FeedbackRtpTransport.hpp +++ b/worker/include/RTC/RTCP/FeedbackRtpTransport.hpp @@ -209,12 +209,18 @@ namespace RTC { } FeedbackRtpTransportPacket(CommonHeader* commonHeader, size_t availableLen); - ~FeedbackRtpTransportPacket(); + ~FeedbackRtpTransportPacket() override; public: + bool IsBaseSet() const + { + return this->baseSet; + } + void SetBase(uint16_t sequenceNumber, uint64_t timestamp); AddPacketResult AddPacket(uint16_t sequenceNumber, uint64_t timestamp, size_t maxRtcpPacketLen); - void Finish(); // Just for locally generated packets. - bool IsFull() + // Just for locally generated packets. + void Finish(); + bool IsFull() const { // NOTE: Since AddPendingChunks() is called at the end, we cannot track // the exact ongoing value of packetStatusCount. Hence, let's reserve 7 @@ -223,7 +229,7 @@ namespace RTC } bool IsSerializable() const { - return this->deltas.size() > 0; + return !this->deltas.empty(); } bool IsCorrect() const // Just for locally generated packets. { @@ -241,7 +247,8 @@ namespace RTC { return this->referenceTime; } - void SetReferenceTime(uint64_t referenceTime) // We only use this for testing purpose. + // NOTE: We only use this for testing purpose. + void SetReferenceTime(int64_t referenceTime) { this->referenceTime = (referenceTime % TimeWrapPeriod) / BaseTimeTick; } @@ -291,7 +298,9 @@ namespace RTC size_t GetSize() const override { if (this->size) + { return this->size; + } // Fixed packet size. size_t size = FeedbackRtpPacket::GetSize(); @@ -313,15 +322,21 @@ namespace RTC void AddPendingChunks(); private: + // Whether baseSequenceNumber has been set. + bool baseSet{ false }; uint16_t baseSequenceNumber{ 0u }; - uint32_t referenceTime{ 0 }; - uint16_t latestSequenceNumber{ 0u }; // Just for locally generated packets. - uint64_t latestTimestamp{ 0u }; // Just for locally generated packets. + // 24 bits signed integer. + int32_t referenceTime{ 0 }; + // Just for locally generated packets. + uint16_t latestSequenceNumber{ 0u }; + // Just for locally generated packets. + uint64_t latestTimestamp{ 0u }; uint16_t packetStatusCount{ 0u }; uint8_t feedbackPacketCount{ 0u }; std::vector chunks; std::vector deltas; - Context context; // Just for locally generated packets. + // Just for locally generated packets. + Context context; size_t deltasAndChunksSize{ 0u }; size_t size{ 0 }; bool isCorrect{ true }; diff --git a/worker/include/RTC/RTCP/Packet.hpp b/worker/include/RTC/RTCP/Packet.hpp index bf771fad22..f60731e271 100644 --- a/worker/include/RTC/RTCP/Packet.hpp +++ b/worker/include/RTC/RTCP/Packet.hpp @@ -52,7 +52,7 @@ namespace RTC static const size_t CommonHeaderSize{ 4 }; static bool IsRtcp(const uint8_t* data, size_t len) { - auto header = const_cast(reinterpret_cast(data)); + auto* header = const_cast(reinterpret_cast(data)); // clang-format off return ( diff --git a/worker/include/RTC/RTCP/ReceiverReport.hpp b/worker/include/RTC/RTCP/ReceiverReport.hpp index 26f10394ff..21de42211e 100644 --- a/worker/include/RTC/RTCP/ReceiverReport.hpp +++ b/worker/include/RTC/RTCP/ReceiverReport.hpp @@ -71,11 +71,15 @@ namespace RTC // Possitive value. if (((value >> 23) & 1) == 0) + { return value; + } // Negative value. if (value != 0x0800000) + { value &= ~(1 << 23); + } return -value; } @@ -102,9 +106,9 @@ namespace RTC { return uint32_t{ ntohl(this->header->jitter) }; } - void SetJitter(uint32_t jitter) + void SetJitter(float jitter) { - this->header->jitter = uint32_t{ htonl(jitter) }; + this->header->jitter = uint32_t{ htonl(static_cast(jitter)) }; } uint32_t GetLastSenderReport() const { @@ -170,7 +174,9 @@ namespace RTC auto it = std::find(this->reports.begin(), this->reports.end(), report); if (it != this->reports.end()) + { this->reports.erase(it); + } } Iterator Begin() { diff --git a/worker/include/RTC/RTCP/Sdes.hpp b/worker/include/RTC/RTCP/Sdes.hpp index eb7750f980..45ac1b752d 100644 --- a/worker/include/RTC/RTCP/Sdes.hpp +++ b/worker/include/RTC/RTCP/Sdes.hpp @@ -121,6 +121,9 @@ namespace RTC size += item->GetSize(); } + // Add the mandatory null octet. + ++size; + // Consider pading to 32 bits (4 bytes) boundary. // http://stackoverflow.com/questions/11642210/computing-padding-required-for-n-byte-alignment return (size + 3) & ~3; @@ -185,7 +188,9 @@ namespace RTC auto it = std::find(this->chunks.begin(), this->chunks.end(), chunk); if (it != this->chunks.end()) + { this->chunks.erase(it); + } } Iterator Begin() { @@ -209,7 +214,7 @@ namespace RTC // A serialized packet can contain a maximum of 31 chunks. // If number of chunks exceeds 31 then the required number of packets // will be serialized which will take the size calculated below. - size_t size = Packet::CommonHeaderSize * ((this->GetCount() / MaxChunksPerPacket) + 1); + size_t size = Packet::CommonHeaderSize * ((this->GetCount() / (MaxChunksPerPacket + 1)) + 1); for (auto* chunk : this->chunks) { diff --git a/worker/include/RTC/RTCP/SenderReport.hpp b/worker/include/RTC/RTCP/SenderReport.hpp index 80b2fcc38a..d9f4f9cf16 100644 --- a/worker/include/RTC/RTCP/SenderReport.hpp +++ b/worker/include/RTC/RTCP/SenderReport.hpp @@ -133,7 +133,9 @@ namespace RTC auto it = std::find(this->reports.begin(), this->reports.end(), report); if (it != this->reports.end()) + { this->reports.erase(it); + } } Iterator Begin() { diff --git a/worker/include/RTC/RTCP/XR.hpp b/worker/include/RTC/RTCP/XR.hpp index aadc54aff8..efdfb3a64a 100644 --- a/worker/include/RTC/RTCP/XR.hpp +++ b/worker/include/RTC/RTCP/XR.hpp @@ -107,7 +107,9 @@ namespace RTC auto it = std::find(this->reports.begin(), this->reports.end(), report); if (it != this->reports.end()) + { this->reports.erase(it); + } } uint32_t GetSsrc() const { diff --git a/worker/include/RTC/RTCP/XrDelaySinceLastRr.hpp b/worker/include/RTC/RTCP/XrDelaySinceLastRr.hpp index b2a7ec2c88..eac430e28d 100644 --- a/worker/include/RTC/RTCP/XrDelaySinceLastRr.hpp +++ b/worker/include/RTC/RTCP/XrDelaySinceLastRr.hpp @@ -119,6 +119,19 @@ namespace RTC { this->ssrcInfos.push_back(ssrcInfo); } + // NOTE: This method not only removes given number of ssrc info sub-blocks + // but also deletes their SsrcInfo instances. + void RemoveLastSsrcInfos(size_t number) + { + while (!this->ssrcInfos.empty() && number-- > 0) + { + auto* ssrcInfo = this->ssrcInfos.back(); + + this->ssrcInfos.pop_back(); + + delete ssrcInfo; + } + } Iterator Begin() { return this->ssrcInfos.begin(); @@ -130,9 +143,9 @@ namespace RTC /* Pure virtual methods inherited from ExtendedReportBlock. */ public: - virtual void Dump() const override; - virtual size_t Serialize(uint8_t* buffer) override; - virtual size_t GetSize() const override + void Dump() const override; + size_t Serialize(uint8_t* buffer) override; + size_t GetSize() const override { size_t size{ 4u }; // Common header. diff --git a/worker/include/RTC/RTCP/XrReceiverReferenceTime.hpp b/worker/include/RTC/RTCP/XrReceiverReferenceTime.hpp index ecafaf1ad0..0083618588 100644 --- a/worker/include/RTC/RTCP/XrReceiverReferenceTime.hpp +++ b/worker/include/RTC/RTCP/XrReceiverReferenceTime.hpp @@ -68,9 +68,9 @@ namespace RTC /* Pure virtual methods inherited from ExtendedReportBlock. */ public: - virtual void Dump() const override; - virtual size_t Serialize(uint8_t* buffer) override; - virtual size_t GetSize() const override + void Dump() const override; + size_t Serialize(uint8_t* buffer) override; + size_t GetSize() const override { size_t size{ 4 }; // Common header. diff --git a/worker/include/RTC/RateCalculator.hpp b/worker/include/RTC/RateCalculator.hpp index d4e974e740..871d4e5977 100644 --- a/worker/include/RTC/RateCalculator.hpp +++ b/worker/include/RTC/RateCalculator.hpp @@ -17,7 +17,7 @@ namespace RTC static constexpr uint16_t DefaultWindowItems{ 100u }; public: - RateCalculator( + explicit RateCalculator( size_t windowSizeMs = DefaultWindowSize, float scale = DefaultBpsScale, uint16_t windowItems = DefaultWindowItems) diff --git a/worker/include/RTC/Router.hpp b/worker/include/RTC/Router.hpp index 6aa38d072c..cd9ab24687 100644 --- a/worker/include/RTC/Router.hpp +++ b/worker/include/RTC/Router.hpp @@ -2,25 +2,22 @@ #define MS_RTC_ROUTER_HPP #include "common.hpp" +#include "Channel/ChannelNotification.hpp" #include "Channel/ChannelRequest.hpp" -#include "PayloadChannel/PayloadChannelNotification.hpp" -#include "PayloadChannel/PayloadChannelRequest.hpp" #include "RTC/Consumer.hpp" #include "RTC/DataConsumer.hpp" #include "RTC/DataProducer.hpp" #include "RTC/Producer.hpp" #include "RTC/RtpObserver.hpp" #include "RTC/RtpPacket.hpp" -#include "RTC/RtpStream.hpp" +#include "RTC/RtpStreamRecv.hpp" #include "RTC/Shared.hpp" #include "RTC/Transport.hpp" #include "RTC/WebRtcServer.hpp" #include -#include +#include #include -#include - -using json = nlohmann::json; +#include namespace RTC { @@ -41,20 +38,21 @@ namespace RTC public: explicit Router(RTC::Shared* shared, const std::string& id, Listener* listener); - virtual ~Router(); + ~Router() override; public: - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; /* Methods inherited from Channel::ChannelSocket::RequestHandler. */ public: void HandleRequest(Channel::ChannelRequest* request) override; private: - void SetNewTransportIdFromData(json& data, std::string& transportId) const; - RTC::Transport* GetTransportFromData(json& data) const; - void SetNewRtpObserverIdFromData(json& data, std::string& rtpObserverId) const; - RTC::RtpObserver* GetRtpObserverFromData(json& data) const; + RTC::Transport* GetTransportById(const std::string& transportId) const; + RTC::RtpObserver* GetRtpObserverById(const std::string& rtpObserverId) const; + void CheckNoTransport(const std::string& transportId) const; + void CheckNoRtpObserver(const std::string& rtpObserverId) const; /* Pure virtual methods inherited from RTC::Transport::Listener. */ public: @@ -65,16 +63,19 @@ namespace RTC void OnTransportProducerNewRtpStream( RTC::Transport* transport, RTC::Producer* producer, - RTC::RtpStream* rtpStream, + RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) override; void OnTransportProducerRtpStreamScore( RTC::Transport* transport, RTC::Producer* producer, - RTC::RtpStream* rtpStream, + RTC::RtpStreamRecv* rtpStream, uint8_t score, uint8_t previousScore) override; void OnTransportProducerRtcpSenderReport( - RTC::Transport* transport, RTC::Producer* producer, RTC::RtpStream* rtpStream, bool first) override; + RTC::Transport* transport, + RTC::Producer* producer, + RTC::RtpStreamRecv* rtpStream, + bool first) override; void OnTransportProducerRtpPacketReceived( RTC::Transport* transport, RTC::Producer* producer, RTC::RtpPacket* packet) override; void OnTransportNeedWorstRemoteFractionLost( @@ -83,19 +84,24 @@ namespace RTC uint32_t mappedSsrc, uint8_t& worstRemoteFractionLost) override; void OnTransportNewConsumer( - RTC::Transport* transport, RTC::Consumer* consumer, std::string& producerId) override; + RTC::Transport* transport, RTC::Consumer* consumer, const std::string& producerId) override; void OnTransportConsumerClosed(RTC::Transport* transport, RTC::Consumer* consumer) override; void OnTransportConsumerProducerClosed(RTC::Transport* transport, RTC::Consumer* consumer) override; void OnTransportConsumerKeyFrameRequested( RTC::Transport* transport, RTC::Consumer* consumer, uint32_t mappedSsrc) override; void OnTransportNewDataProducer(RTC::Transport* transport, RTC::DataProducer* dataProducer) override; void OnTransportDataProducerClosed(RTC::Transport* transport, RTC::DataProducer* dataProducer) override; + void OnTransportDataProducerPaused(RTC::Transport* transport, RTC::DataProducer* dataProducer) override; + void OnTransportDataProducerResumed( + RTC::Transport* transport, RTC::DataProducer* dataProducer) override; void OnTransportDataProducerMessageReceived( RTC::Transport* transport, RTC::DataProducer* dataProducer, - uint32_t ppid, const uint8_t* msg, - size_t len) override; + size_t len, + uint32_t ppid, + std::vector& subchannels, + std::optional requiredSubchannel) override; void OnTransportNewDataConsumer( RTC::Transport* transport, RTC::DataConsumer* dataConsumer, std::string& dataProducerId) override; void OnTransportDataConsumerClosed(RTC::Transport* transport, RTC::DataConsumer* dataConsumer) override; @@ -105,7 +111,7 @@ namespace RTC /* Pure virtual methods inherited from RTC::RtpObserver::Listener. */ public: - RTC::Producer* RtpObserverGetProducer(RTC::RtpObserver*, const std::string& id) override; + RTC::Producer* RtpObserverGetProducer(RTC::RtpObserver* rtpObserver, const std::string& id) override; void OnRtpObserverAddProducer(RTC::RtpObserver* rtpObserver, RTC::Producer* producer) override; void OnRtpObserverRemoveProducer(RTC::RtpObserver* rtpObserver, RTC::Producer* producer) override; diff --git a/worker/include/RTC/RtcLogger.hpp b/worker/include/RTC/RtcLogger.hpp new file mode 100644 index 0000000000..334e9df552 --- /dev/null +++ b/worker/include/RTC/RtcLogger.hpp @@ -0,0 +1,59 @@ +#ifndef MS_RTC_RTC_LOGGER_HPP +#define MS_RTC_RTC_LOGGER_HPP + +#include "common.hpp" +#include + +namespace RTC +{ + namespace RtcLogger + { + class RtpPacket + { + public: + enum class DropReason : uint8_t + { + NONE = 0, + PRODUCER_NOT_FOUND, + RECV_RTP_STREAM_NOT_FOUND, + RECV_RTP_STREAM_DISCARDED, + CONSUMER_INACTIVE, + INVALID_TARGET_LAYER, + UNSUPPORTED_PAYLOAD_TYPE, + NOT_A_KEYFRAME, + EMPTY_PAYLOAD, + SPATIAL_LAYER_MISMATCH, + TOO_HIGH_TIMESTAMP_EXTRA_NEEDED, + PACKET_PREVIOUS_TO_SPATIAL_LAYER_SWITCH, + DROPPED_BY_CODEC, + SEND_RTP_STREAM_DISCARDED, + }; + + static absl::flat_hash_map dropReason2String; + + RtpPacket() = default; + ~RtpPacket() = default; + void Sent(); + void Dropped(DropReason dropReason); + + private: + void Log() const; + void Clear(); + + public: + uint64_t timestamp{}; + std::string recvTransportId{}; + std::string sendTransportId{}; + std::string routerId{}; + std::string producerId{}; + std::string consumerId{}; + uint32_t recvRtpTimestamp{}; + uint32_t sendRtpTimestamp{}; + uint16_t recvSeqNumber{}; + uint16_t sendSeqNumber{}; + bool dropped{}; + DropReason dropReason{ DropReason::NONE }; + }; + }; // namespace RtcLogger +} // namespace RTC +#endif diff --git a/worker/include/RTC/RtpDictionaries.hpp b/worker/include/RTC/RtpDictionaries.hpp index 7cf12f2afa..f9fa22a5dd 100644 --- a/worker/include/RTC/RtpDictionaries.hpp +++ b/worker/include/RTC/RtpDictionaries.hpp @@ -2,14 +2,12 @@ #define MS_RTC_RTP_DICTIONARIES_HPP #include "common.hpp" +#include "FBS/rtpParameters.h" #include "RTC/Parameters.hpp" #include -#include #include #include -using json = nlohmann::json; - namespace RTC { class Media @@ -17,19 +15,9 @@ namespace RTC public: enum class Kind : uint8_t { - ALL = 0, AUDIO, VIDEO }; - - public: - static Kind GetKind(std::string& str); - static Kind GetKind(std::string&& str); - static const std::string& GetString(Kind kind); - - private: - static absl::flat_hash_map string2Kind; - static absl::flat_hash_map kind2String; }; class RtpCodecMimeType @@ -37,7 +25,6 @@ namespace RTC public: enum class Type : uint8_t { - UNSET = 0, AUDIO, VIDEO }; @@ -45,7 +32,6 @@ namespace RTC public: enum class Subtype : uint16_t { - UNSET = 0, // Audio codecs: OPUS = 100, // Multi-channel Opus. @@ -118,8 +104,8 @@ namespace RTC } public: - Type type{ Type::UNSET }; - Subtype subtype{ Subtype::UNSET }; + Type type; + Subtype subtype; private: std::string mimeType; @@ -130,7 +116,6 @@ namespace RTC public: enum class Type : uint8_t { - UNKNOWN = 0, MID = 1, RTP_STREAM_ID = 2, REPAIRED_RTP_STREAM_ID = 3, @@ -142,22 +127,22 @@ namespace RTC VIDEO_ORIENTATION = 11, TOFFSET = 12, ABS_CAPTURE_TIME = 13, + PLAYOUT_DELAY = 14, }; - private: - static absl::flat_hash_map string2Type; - public: - static Type GetType(std::string& uri); + static RtpHeaderExtensionUri::Type TypeFromFbs(FBS::RtpParameters::RtpHeaderExtensionUri uri); + static FBS::RtpParameters::RtpHeaderExtensionUri TypeToFbs(RtpHeaderExtensionUri::Type uri); }; class RtcpFeedback { public: RtcpFeedback() = default; - explicit RtcpFeedback(json& data); + explicit RtcpFeedback(const FBS::RtpParameters::RtcpFeedback* data); - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; public: std::string type; @@ -168,9 +153,10 @@ namespace RTC { public: RtpCodecParameters() = default; - explicit RtpCodecParameters(json& data); + explicit RtpCodecParameters(const FBS::RtpParameters::RtpCodecParameters* data); - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; private: void CheckCodec(); @@ -188,9 +174,9 @@ namespace RTC { public: RtpRtxParameters() = default; - explicit RtpRtxParameters(json& data); + explicit RtpRtxParameters(const FBS::RtpParameters::Rtx* data); - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer(flatbuffers::FlatBufferBuilder& builder) const; public: uint32_t ssrc{ 0u }; @@ -200,9 +186,10 @@ namespace RTC { public: RtpEncodingParameters() = default; - explicit RtpEncodingParameters(json& data); + explicit RtpEncodingParameters(const FBS::RtpParameters::RtpEncodingParameters* data); - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; public: uint32_t ssrc{ 0u }; @@ -214,7 +201,7 @@ namespace RTC uint32_t maxBitrate{ 0u }; double maxFramerate{ 0 }; bool dtx{ false }; - std::string scalabilityMode; + std::string scalabilityMode{ "S1T1" }; uint8_t spatialLayers{ 1u }; uint8_t temporalLayers{ 1u }; bool ksvc{ false }; @@ -224,12 +211,12 @@ namespace RTC { public: RtpHeaderExtensionParameters() = default; - explicit RtpHeaderExtensionParameters(json& data); + explicit RtpHeaderExtensionParameters(const FBS::RtpParameters::RtpHeaderExtensionParameters* data); - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; public: - std::string uri; RtpHeaderExtensionUri::Type type; uint8_t id{ 0u }; bool encrypt{ false }; @@ -240,13 +227,13 @@ namespace RTC { public: RtcpParameters() = default; - explicit RtcpParameters(json& data); + explicit RtcpParameters(const FBS::RtpParameters::RtcpParameters* data); - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; public: std::string cname; - uint32_t ssrc{ 0u }; bool reducedSize{ true }; }; @@ -255,7 +242,6 @@ namespace RTC public: enum class Type : uint8_t { - NONE = 0, SIMPLE, SIMULCAST, SVC, @@ -263,20 +249,19 @@ namespace RTC }; public: - static Type GetType(const RtpParameters& rtpParameters); - static Type GetType(std::string& str); - static Type GetType(std::string&& str); + static std::optional GetType(const RtpParameters& rtpParameters); static std::string& GetTypeString(Type type); + static FBS::RtpParameters::Type TypeToFbs(Type type); private: - static absl::flat_hash_map string2Type; static absl::flat_hash_map type2String; public: RtpParameters() = default; - explicit RtpParameters(json& data); + explicit RtpParameters(const FBS::RtpParameters::RtpParameters* data); - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; const RTC::RtpCodecParameters* GetCodecForEncoding(RtpEncodingParameters& encoding) const; const RTC::RtpCodecParameters* GetRtxCodecForEncoding(RtpEncodingParameters& encoding) const; @@ -291,7 +276,6 @@ namespace RTC std::vector encodings; std::vector headerExtensions; RtcpParameters rtcp; - bool hasRtcp{ false }; }; } // namespace RTC diff --git a/worker/include/RTC/RtpHeaderExtensionIds.hpp b/worker/include/RTC/RtpHeaderExtensionIds.hpp index 355b25137c..78cd262fe5 100644 --- a/worker/include/RTC/RtpHeaderExtensionIds.hpp +++ b/worker/include/RTC/RtpHeaderExtensionIds.hpp @@ -19,6 +19,7 @@ namespace RTC uint8_t videoOrientation{ 0u }; uint8_t toffset{ 0u }; uint8_t absCaptureTime{ 0u }; + uint8_t playoutDelay{ 0u }; }; } // namespace RTC diff --git a/worker/include/RTC/RtpListener.hpp b/worker/include/RTC/RtpListener.hpp index b6f6d65d37..0591c4ba4f 100644 --- a/worker/include/RTC/RtpListener.hpp +++ b/worker/include/RTC/RtpListener.hpp @@ -4,18 +4,16 @@ #include "common.hpp" #include "RTC/Producer.hpp" #include "RTC/RtpPacket.hpp" -#include #include #include -using json = nlohmann::json; - namespace RTC { class RtpListener { public: - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; void AddProducer(RTC::Producer* producer); void RemoveProducer(RTC::Producer* producer); RTC::Producer* GetProducer(const RTC::RtpPacket* packet); diff --git a/worker/include/RTC/RtpObserver.hpp b/worker/include/RTC/RtpObserver.hpp index 00c8010456..eee22f8cf7 100644 --- a/worker/include/RTC/RtpObserver.hpp +++ b/worker/include/RTC/RtpObserver.hpp @@ -1,5 +1,5 @@ -#ifndef MS_RTC_RTP_PACKET_OBSERVER_HPP -#define MS_RTC_RTP_PACKET_OBSERVER_HPP +#ifndef MS_RTC_RTP_OBSERVER_HPP +#define MS_RTC_RTP_OBSERVER_HPP #include "common.hpp" #include "RTC/Producer.hpp" @@ -27,7 +27,7 @@ namespace RTC public: RtpObserver(RTC::Shared* shared, const std::string& id, RTC::RtpObserver::Listener* listener); - virtual ~RtpObserver(); + ~RtpObserver() override; public: void Pause(); @@ -50,9 +50,6 @@ namespace RTC virtual void Paused() = 0; virtual void Resumed() = 0; - private: - std::string GetProducerIdFromData(json& data) const; - public: // Passed by argument. const std::string id; diff --git a/worker/include/RTC/RtpPacket.hpp b/worker/include/RTC/RtpPacket.hpp index 0dece98d5b..e737b5b868 100644 --- a/worker/include/RTC/RtpPacket.hpp +++ b/worker/include/RTC/RtpPacket.hpp @@ -3,15 +3,17 @@ #include "common.hpp" #include "Utils.hpp" +#include "FBS/rtpPacket.h" #include "RTC/Codecs/PayloadDescriptorHandler.hpp" +#ifdef MS_RTC_LOGGER_RTP +#include "RTC/RtcLogger.hpp" +#endif +#include #include #include -#include #include #include -using json = nlohmann::json; - namespace RTC { // Max MTU size. @@ -118,7 +120,7 @@ namespace RTC { // NOTE: RtcpPacket::IsRtcp() must always be called before this method. - auto header = const_cast(reinterpret_cast(data)); + auto* header = const_cast(reinterpret_cast(data)); // clang-format off return ( @@ -146,8 +148,7 @@ namespace RTC ~RtpPacket(); void Dump() const; - - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer(flatbuffers::FlatBufferBuilder& builder) const; const uint8_t* GetData() const { @@ -216,7 +217,7 @@ namespace RTC bool HasHeaderExtension() const { - return (this->headerExtension ? true : false); + return (this->headerExtension != nullptr); } // After calling this method, all the extension ids are reset to 0. @@ -225,7 +226,9 @@ namespace RTC uint16_t GetHeaderExtensionId() const { if (!this->headerExtension) + { return 0u; + } return uint16_t{ ntohs(this->headerExtension->id) }; } @@ -233,7 +236,9 @@ namespace RTC size_t GetHeaderExtensionLength() const { if (!this->headerExtension) + { return 0u; + } return static_cast(ntohs(this->headerExtension->length) * 4); } @@ -241,7 +246,9 @@ namespace RTC uint8_t* GetHeaderExtensionValue() const { if (!this->headerExtension) + { return nullptr; + } return this->headerExtension->value; } @@ -302,13 +309,20 @@ namespace RTC this->videoOrientationExtensionId = id; } + void SetPlayoutDelayExtensionId(uint8_t id) + { + this->playoutDelayExtensionId = id; + } + bool ReadMid(std::string& mid) const { uint8_t extenLen; uint8_t* extenValue = GetExtension(this->midExtensionId, extenLen); if (!extenValue || extenLen == 0u) + { return false; + } mid.assign(reinterpret_cast(extenValue), static_cast(extenLen)); @@ -348,20 +362,24 @@ namespace RTC uint8_t* extenValue = GetExtension(this->absSendTimeExtensionId, extenLen); if (!extenValue || extenLen != 3u) + { return false; + } absSendtime = Utils::Byte::Get3Bytes(extenValue, 0); return true; } - bool UpdateAbsSendTime(uint64_t ms) + bool UpdateAbsSendTime(uint64_t ms) const { uint8_t extenLen; uint8_t* extenValue = GetExtension(this->absSendTimeExtensionId, extenLen); if (!extenValue || extenLen != 3u) + { return false; + } auto absSendTime = Utils::Time::TimeMsToAbsSendTime(ms); @@ -376,20 +394,24 @@ namespace RTC uint8_t* extenValue = GetExtension(this->transportWideCc01ExtensionId, extenLen); if (!extenValue || extenLen != 2u) + { return false; + } wideSeqNumber = Utils::Byte::Get2Bytes(extenValue, 0); return true; } - bool UpdateTransportWideCc01(uint16_t wideSeqNumber) + bool UpdateTransportWideCc01(uint16_t wideSeqNumber) const { uint8_t extenLen; uint8_t* extenValue = GetExtension(this->transportWideCc01ExtensionId, extenLen); if (!extenValue || extenLen != 2u) + { return false; + } Utils::Byte::Set2Bytes(extenValue, 0, wideSeqNumber); @@ -403,10 +425,14 @@ namespace RTC // NOTE: Remove this once framemarking draft becomes RFC. if (!extenValue) + { extenValue = GetExtension(this->frameMarking07ExtensionId, extenLen); + } if (!extenValue || extenLen > 3u) + { return false; + } *frameMarking = reinterpret_cast(extenValue); length = extenLen; @@ -420,10 +446,12 @@ namespace RTC uint8_t* extenValue = GetExtension(this->ssrcAudioLevelExtensionId, extenLen); if (!extenValue || extenLen != 1u) + { return false; + } volume = Utils::Byte::Get1Byte(extenValue, 0); - voice = (volume & (1 << 7)) ? true : false; + voice = (volume & (1 << 7)) != 0; volume &= ~(1 << 7); return true; @@ -435,15 +463,17 @@ namespace RTC uint8_t* extenValue = GetExtension(this->videoOrientationExtensionId, extenLen); if (!extenValue || extenLen != 1u) + { return false; + } - uint8_t cvoByte = Utils::Byte::Get1Byte(extenValue, 0); - uint8_t cameraValue = ((cvoByte & 0b00001000) >> 3); - uint8_t flipValue = ((cvoByte & 0b00000100) >> 2); - uint8_t rotationValue = (cvoByte & 0b00000011); + const uint8_t cvoByte = Utils::Byte::Get1Byte(extenValue, 0); + const uint8_t cameraValue = ((cvoByte & 0b00001000) >> 3); + const uint8_t flipValue = ((cvoByte & 0b00000100) >> 2); + const uint8_t rotationValue = (cvoByte & 0b00000011); - camera = cameraValue ? true : false; - flip = flipValue ? true : false; + camera = cameraValue != 0; + flip = flipValue != 0; // Using counter clockwise values. switch (rotationValue) @@ -464,6 +494,22 @@ namespace RTC return true; } + bool ReadPlayoutDelay(uint16_t& minDelay, uint16_t& maxDelay) const + { + uint8_t extenLen; + uint8_t* extenValue = GetExtension(this->playoutDelayExtensionId, extenLen); + + if (extenLen != 3) + { + return false; + } + + uint32_t v = Utils::Byte::Get3Bytes(extenValue, 0); + minDelay = v >> 12u; + maxDelay = v & 0xFFFu; + return true; + } + bool HasExtension(uint8_t id) const { if (id == 0u) @@ -473,7 +519,9 @@ namespace RTC else if (HasOneByteExtensions()) { if (id > 14) + { return false; + } // `-1` because we have 14 elements total 0..13 and `id` is in the range 1..14. return this->oneByteExtensions[id - 1] != nullptr; @@ -483,15 +531,14 @@ namespace RTC auto it = this->mapTwoBytesExtensions.find(id); if (it == this->mapTwoBytesExtensions.end()) + { return false; + } auto* extension = it->second; // In Two-Byte extensions value length may be zero. If so, return false. - if (extension->len == 0u) - return false; - - return true; + return extension->len != 0u; } else { @@ -510,13 +557,17 @@ namespace RTC else if (HasOneByteExtensions()) { if (id > 14) + { return nullptr; + } // `-1` because we have 14 elements total 0..13 and `id` is in the range 1..14. auto* extension = this->oneByteExtensions[id - 1]; if (!extension) + { return nullptr; + } // In One-Byte extensions value length 0 means 1. len = extension->len + 1; @@ -528,7 +579,9 @@ namespace RTC auto it = this->mapTwoBytesExtensions.find(id); if (it == this->mapTwoBytesExtensions.end()) + { return nullptr; + } auto* extension = it->second; @@ -536,7 +589,9 @@ namespace RTC // In Two-Byte extensions value length may be zero. If so, return nullptr. if (extension->len == 0u) + { return nullptr; + } return extension->value; } @@ -568,7 +623,9 @@ namespace RTC uint8_t GetSpatialLayer() const { if (!this->payloadDescriptorHandler) + { return 0u; + } return this->payloadDescriptorHandler->GetSpatialLayer(); } @@ -576,7 +633,9 @@ namespace RTC uint8_t GetTemporalLayer() const { if (!this->payloadDescriptorHandler) + { return 0u; + } return this->payloadDescriptorHandler->GetTemporalLayer(); } @@ -584,7 +643,9 @@ namespace RTC bool IsKeyFrame() const { if (!this->payloadDescriptorHandler) + { return false; + } return this->payloadDescriptorHandler->IsKeyFrame(); } @@ -606,6 +667,11 @@ namespace RTC void ShiftPayload(size_t payloadOffset, size_t shift, bool expand = true); +#ifdef MS_RTC_LOGGER_RTP + public: + RtcLogger::RtpPacket logger; +#endif + private: void ParseExtensions(); @@ -616,7 +682,7 @@ namespace RTC HeaderExtension* headerExtension{ nullptr }; // There might be up to 14 one-byte header extensions // (https://datatracker.ietf.org/doc/html/rfc5285#section-4.2), use std::array. - std::array oneByteExtensions; + std::array oneByteExtensions{}; absl::flat_hash_map mapTwoBytesExtensions; uint8_t midExtensionId{ 0u }; uint8_t ridExtensionId{ 0u }; @@ -627,6 +693,7 @@ namespace RTC uint8_t frameMarkingExtensionId{ 0u }; uint8_t ssrcAudioLevelExtensionId{ 0u }; uint8_t videoOrientationExtensionId{ 0u }; + uint8_t playoutDelayExtensionId{ 0u }; uint8_t* payload{ nullptr }; size_t payloadLength{ 0u }; uint8_t payloadPadding{ 0u }; diff --git a/worker/include/RTC/RtpRetransmissionBuffer.hpp b/worker/include/RTC/RtpRetransmissionBuffer.hpp new file mode 100644 index 0000000000..47229d7efe --- /dev/null +++ b/worker/include/RTC/RtpRetransmissionBuffer.hpp @@ -0,0 +1,68 @@ +#ifndef MS_RTC_RTP_RETRANSMISSION_BUFFER_HPP +#define MS_RTC_RTP_RETRANSMISSION_BUFFER_HPP + +#include "common.hpp" +#include "RTC/RtpPacket.hpp" +#include + +namespace RTC +{ + // Special container that stores `Item`* elements addressable by their `uint16_t` + // sequence number, while only taking as little memory as necessary to store + // the range covering a maximum of `MaxRetransmissionDelayForVideoMs` or + // `MaxRetransmissionDelayForAudioMs` ms. + class RtpRetransmissionBuffer + { + public: + struct Item + { + void Reset(); + + // Original packet. + std::shared_ptr packet{ nullptr }; + // Correct SSRC since original packet may not have the same. + uint32_t ssrc{ 0u }; + // Correct sequence number since original packet may not have the same. + uint16_t sequenceNumber{ 0u }; + // Correct timestamp since original packet may not have the same. + uint32_t timestamp{ 0u }; + // Last time this packet was resent. + uint64_t resentAtMs{ 0u }; + // Number of times this packet was resent. + uint8_t sentTimes{ 0u }; + }; + + private: + static Item* FillItem( + Item* item, RTC::RtpPacket* packet, std::shared_ptr& sharedPacket); + + public: + RtpRetransmissionBuffer(uint16_t maxItems, uint32_t maxRetransmissionDelayMs, uint32_t clockRate); + ~RtpRetransmissionBuffer(); + + Item* Get(uint16_t seq) const; + void Insert(RTC::RtpPacket* packet, std::shared_ptr& sharedPacket); + void Clear(); + void Dump() const; + + private: + Item* GetOldest() const; + Item* GetNewest() const; + void RemoveOldest(); + void RemoveOldest(uint16_t numItems); + bool ClearTooOldByTimestamp(uint32_t newestTimestamp); + bool IsTooOldTimestamp(uint32_t timestamp, uint32_t newestTimestamp) const; + + protected: + // Make buffer protected for testing purposes. + std::deque buffer; + + private: + // Given as argument. + uint16_t maxItems; + uint32_t maxRetransmissionDelayMs; + uint32_t clockRate; + }; +} // namespace RTC + +#endif diff --git a/worker/include/RTC/RtpStream.hpp b/worker/include/RTC/RtpStream.hpp index 45cbac96e5..80e86a0dd5 100644 --- a/worker/include/RTC/RtpStream.hpp +++ b/worker/include/RTC/RtpStream.hpp @@ -3,6 +3,7 @@ #include "common.hpp" #include "DepLibUV.hpp" +#include "FBS/rtpStream.h" #include "RTC/RTCP/FeedbackPsFir.hpp" #include "RTC/RTCP/FeedbackPsPli.hpp" #include "RTC/RTCP/FeedbackRtpNack.hpp" @@ -13,13 +14,11 @@ #include "RTC/RTCP/XrDelaySinceLastRr.hpp" #include "RTC/RTCP/XrReceiverReferenceTime.hpp" #include "RTC/RtpDictionaries.hpp" +#include "RTC/RtpPacket.hpp" #include "RTC/RtxStream.hpp" -#include #include #include -using json = nlohmann::json; - namespace RTC { class RtpStream @@ -37,7 +36,8 @@ namespace RTC public: struct Params { - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; size_t encodingIdx{ 0u }; uint32_t ssrc{ 0u }; @@ -61,8 +61,9 @@ namespace RTC RtpStream(RTC::RtpStream::Listener* listener, RTC::RtpStream::Params& params, uint8_t initialScore); virtual ~RtpStream(); - void FillJson(json& jsonObject) const; - virtual void FillJsonStats(json& jsonObject); + flatbuffers::Offset FillBuffer(flatbuffers::FlatBufferBuilder& builder) const; + virtual flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder); uint32_t GetEncodingIdx() const { return this->params.encodingIdx; @@ -174,18 +175,28 @@ namespace RTC private: void InitSeq(uint16_t seq); + /* Pure virtual method that must be implemented by the subclass. */ + protected: + virtual void UserOnSequenceNumberReset() = 0; + protected: // Given as argument. RTC::RtpStream::Listener* listener{ nullptr }; Params params; // Others. // https://tools.ietf.org/html/rfc3550#appendix-A.1 stuff. - uint16_t maxSeq{ 0u }; // Highest seq. number seen. - uint32_t cycles{ 0u }; // Shifted count of seq. number cycles. - uint32_t baseSeq{ 0u }; // Base seq number. - uint32_t badSeq{ 0u }; // Last 'bad' seq number + 1. - uint32_t maxPacketTs{ 0u }; // Highest timestamp seen. - uint64_t maxPacketMs{ 0u }; // When the packet with highest timestammp was seen. + // Highest seq. number seen. + uint16_t maxSeq{ 0u }; + // Shifted count of seq. number cycles. + uint32_t cycles{ 0u }; + // Base seq number. + uint32_t baseSeq{ 0u }; + // Last 'bad' seq number + 1. + uint32_t badSeq{ 0u }; + // Highest timestamp seen. + uint32_t maxPacketTs{ 0u }; + // When the packet with highest timestammp was seen. + uint64_t maxPacketMs{ 0u }; uint32_t packetsLost{ 0u }; uint8_t fractionLost{ 0u }; size_t packetsDiscarded{ 0u }; @@ -195,14 +206,15 @@ namespace RTC size_t nackPacketCount{ 0u }; size_t pliCount{ 0u }; size_t firCount{ 0u }; - size_t repairedPriorScore{ 0u }; // Packets repaired at last interval for score calculation. - size_t retransmittedPriorScore{ - 0u - }; // Packets retransmitted at last interval for score calculation. - uint64_t lastSenderReportNtpMs{ 0u }; // NTP timestamp in last Sender Report (in ms). - uint32_t lastSenderReportTs{ 0u }; // RTP timestamp in last Sender Report. - float rtt{ 0 }; - bool hasRtt{ false }; + // Packets repaired at last interval for score calculation. + size_t repairedPriorScore{ 0u }; + // Packets retransmitted at last interval for score calculation. + size_t retransmittedPriorScore{ 0u }; + // NTP timestamp in last Sender Report (in ms). + uint64_t lastSenderReportNtpMs{ 0u }; + // RTP timestamp in last Sender Report. + uint32_t lastSenderReportTs{ 0u }; + float rtt{ 0.0f }; // Instance of RtxStream. RTC::RtxStream* rtxStream{ nullptr }; diff --git a/worker/include/RTC/RtpStreamRecv.hpp b/worker/include/RTC/RtpStreamRecv.hpp index e71e85e6e3..932f52cbae 100644 --- a/worker/include/RTC/RtpStreamRecv.hpp +++ b/worker/include/RTC/RtpStreamRecv.hpp @@ -5,14 +5,14 @@ #include "RTC/RTCP/XrDelaySinceLastRr.hpp" #include "RTC/RateCalculator.hpp" #include "RTC/RtpStream.hpp" -#include "handles/Timer.hpp" +#include "handles/TimerHandle.hpp" #include namespace RTC { class RtpStreamRecv : public RTC::RtpStream, public RTC::NackGenerator::Listener, - public Timer::Listener + public TimerHandle::Listener { public: class Listener : public RTC::RtpStream::Listener @@ -45,10 +45,12 @@ namespace RTC RtpStreamRecv( RTC::RtpStreamRecv::Listener* listener, RTC::RtpStream::Params& params, - unsigned int sendNackDelayMs); - ~RtpStreamRecv(); + unsigned int sendNackDelayMs, + bool useRtpInactivityCheck); + ~RtpStreamRecv() override; - void FillJsonStats(json& jsonObject) override; + flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) override; bool ReceivePacket(RTC::RtpPacket* packet); bool ReceiveRtxPacket(RTC::RtpPacket* packet); RTC::RTCP::ReceiverReport* GetRtcpReceiverReport(); @@ -75,14 +77,22 @@ namespace RTC { return this->transmissionCounter.GetLayerBitrate(nowMs, spatialLayer, temporalLayer); } + bool HasRtpInactivityCheckEnabled() const + { + return this->useRtpInactivityCheck; + } private: void CalculateJitter(uint32_t rtpTimestamp); void UpdateScore(); - /* Pure virtual methods inherited from Timer. */ + /* Pure virtual methods inherited from RTC::RtpStream. */ + public: + void UserOnSequenceNumberReset() override; + + /* Pure virtual methods inherited from TimerHandle. */ protected: - void OnTimer(Timer* timer) override; + void OnTimer(TimerHandle* timer) override; /* Pure virtual methods inherited from RTC::NackGenerator. */ protected: @@ -92,25 +102,35 @@ namespace RTC private: // Passed by argument. unsigned int sendNackDelayMs{ 0u }; + bool useRtpInactivityCheck{ false }; // Others. - uint32_t expectedPrior{ 0u }; // Packets expected at last interval. - uint32_t expectedPriorScore{ 0u }; // Packets expected at last interval for score calculation. - uint32_t receivedPrior{ 0u }; // Packets received at last interval. - uint32_t receivedPriorScore{ 0u }; // Packets received at last interval for score calculation. - uint32_t lastSrTimestamp{ 0u }; // The middle 32 bits out of 64 in the NTP - // timestamp received in the most recent - // sender report. - uint64_t lastSrReceived{ 0u }; // Wallclock time representing the most recent - // sender report arrival. - int32_t transit{ 0u }; // Relative transit time for prev packet. - uint32_t jitter{ 0u }; + // Packets expected at last interval. + uint32_t expectedPrior{ 0u }; + // Packets expected at last interval for score calculation. + uint32_t expectedPriorScore{ 0u }; + // Packets received at last interval. + uint32_t receivedPrior{ 0u }; + // Packets received at last interval for score calculation. + uint32_t receivedPriorScore{ 0u }; + // The middle 32 bits out of 64 in the NTP timestamp received in the most + // recent sender report. + uint32_t lastSrTimestamp{ 0u }; + // Wallclock time representing the most recent sender report arrival. + uint64_t lastSrReceived{ 0u }; + // Relative transit time for prev packet. + int32_t transit{ 0u }; + // Jitter in RTP timestamp units. As per spec it's kept as floating value + // although it's exposed as integer in the stats. + float jitter{ 0 }; uint8_t firSeqNumber{ 0u }; uint32_t reportedPacketLost{ 0u }; std::unique_ptr nackGenerator; - Timer* inactivityCheckPeriodicTimer{ nullptr }; + TimerHandle* inactivityCheckPeriodicTimer{ nullptr }; bool inactive{ false }; - TransmissionCounter transmissionCounter; // Valid media + valid RTX. - RTC::RtpDataCounter mediaTransmissionCounter; // Just valid media. + // Valid media + valid RTX. + TransmissionCounter transmissionCounter; + // Just valid media. + RTC::RtpDataCounter mediaTransmissionCounter; }; } // namespace RTC diff --git a/worker/include/RTC/RtpStreamSend.hpp b/worker/include/RTC/RtpStreamSend.hpp index e4b7e43f28..a5fe733b84 100644 --- a/worker/include/RTC/RtpStreamSend.hpp +++ b/worker/include/RTC/RtpStreamSend.hpp @@ -2,18 +2,18 @@ #define MS_RTC_RTP_STREAM_SEND_HPP #include "RTC/RateCalculator.hpp" +#include "RTC/RtpRetransmissionBuffer.hpp" #include "RTC/RtpStream.hpp" -#include namespace RTC { class RtpStreamSend : public RTC::RtpStream { public: - // Minimum retransmission buffer size (ms). - const static uint32_t MinRetransmissionDelay; - // Maximum retransmission buffer size (ms). - const static uint32_t MaxRetransmissionDelay; + // Maximum retransmission buffer size for video (ms). + const static uint32_t MaxRetransmissionDelayForVideoMs; + // Maximum retransmission buffer size for audio (ms). + const static uint32_t MaxRetransmissionDelayForAudioMs; public: class Listener : public RTC::RtpStream::Listener @@ -23,53 +23,13 @@ namespace RTC RTC::RtpStreamSend* rtpStream, RTC::RtpPacket* packet) = 0; }; - public: - struct StorageItem - { - void Reset(); - - // Original packet. - std::shared_ptr packet{ nullptr }; - // Correct SSRC since original packet may not have the same. - uint32_t ssrc{ 0 }; - // Correct sequence number since original packet may not have the same. - uint16_t sequenceNumber{ 0 }; - // Correct timestamp since original packet may not have the same. - uint32_t timestamp{ 0 }; - // Last time this packet was resent. - uint64_t resentAtMs{ 0u }; - // Number of times this packet was resent. - uint8_t sentTimes{ 0u }; - }; - - private: - // Special container that stores `StorageItem*` elements addressable by - // their `uint16_t` sequence number, while only taking as little memory as - // necessary to store the range covering a maximum of - // `MaxRetransmissionDelay` milliseconds. - class StorageItemBuffer - { - public: - ~StorageItemBuffer(); - - StorageItem* GetFirst() const; - StorageItem* Get(uint16_t seq) const; - size_t GetBufferSize() const; - void Insert(uint16_t seq, StorageItem* storageItem); - void RemoveFirst(); - void Clear(); - - private: - uint16_t startSeq{ 0 }; - std::deque buffer; - }; - public: RtpStreamSend( RTC::RtpStreamSend::Listener* listener, RTC::RtpStream::Params& params, std::string& mid); ~RtpStreamSend() override; - void FillJsonStats(json& jsonObject) override; + flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) override; void SetRtx(uint8_t payloadType, uint32_t ssrc) override; bool ReceivePacket(RTC::RtpPacket* packet, std::shared_ptr& sharedPacket); void ReceiveNack(RTC::RTCP::FeedbackRtpNackPacket* nackPacket); @@ -77,7 +37,7 @@ namespace RTC void ReceiveRtcpReceiverReport(RTC::RTCP::ReceiverReport* report); void ReceiveRtcpXrReceiverReferenceTime(RTC::RTCP::ReceiverReferenceTime* report); RTC::RTCP::SenderReport* GetRtcpSenderReport(uint64_t nowMs); - RTC::RTCP::DelaySinceLastRr::SsrcInfo* GetRtcpXrDelaySinceLastRr(uint64_t nowMs); + RTC::RTCP::DelaySinceLastRr::SsrcInfo* GetRtcpXrDelaySinceLastRrSsrcInfo(uint64_t nowMs); RTC::RTCP::SdesChunk* GetRtcpSdesChunk(); void Pause() override; void Resume() override; @@ -91,24 +51,28 @@ namespace RTC private: void StorePacket(RTC::RtpPacket* packet, std::shared_ptr& sharedPacket); - void ClearOldPackets(const RtpPacket* packet); - void ClearBuffer(); void FillRetransmissionContainer(uint16_t seq, uint16_t bitmask); void UpdateScore(RTC::RTCP::ReceiverReport* report); + /* Pure virtual methods inherited from RTC::RtpStream. */ + public: + void UserOnSequenceNumberReset() override; + private: - uint32_t lostPriorScore{ 0u }; // Packets lost at last interval for score calculation. - uint32_t sentPriorScore{ 0u }; // Packets sent at last interval for score calculation. - StorageItemBuffer storageItemBuffer; + // Packets lost at last interval for score calculation. + uint32_t lostPriorScore{ 0u }; + // Packets sent at last interval for score calculation. + uint32_t sentPriorScore{ 0u }; std::string mid; - uint32_t retransmissionBufferSize; uint16_t rtxSeq{ 0u }; RTC::RtpDataCounter transmissionCounter; - uint32_t lastRrTimestamp{ 0u }; // The middle 32 bits out of 64 in the NTP - // timestamp received in the most recent - // receiver reference timestamp. - uint64_t lastRrReceivedMs{ 0u }; // Wallclock time representing the most recent - // receiver reference timestamp arrival. + RTC::RtpRetransmissionBuffer* retransmissionBuffer{ nullptr }; + // The middle 32 bits out of 64 in the NTP timestamp received in the most + // recent receiver reference timestamp. + uint32_t lastRrTimestamp{ 0u }; + // Wallclock time representing the most recent receiver reference timestamp + // arrival. + uint64_t lastRrReceivedMs{ 0u }; }; } // namespace RTC diff --git a/worker/include/RTC/RtxStream.hpp b/worker/include/RTC/RtxStream.hpp index 216c3ba201..e8c312ab92 100644 --- a/worker/include/RTC/RtxStream.hpp +++ b/worker/include/RTC/RtxStream.hpp @@ -3,16 +3,14 @@ #include "common.hpp" #include "DepLibUV.hpp" +#include "FBS/rtxStream.h" #include "RTC/RTCP/Packet.hpp" #include "RTC/RTCP/ReceiverReport.hpp" #include "RTC/RTCP/SenderReport.hpp" #include "RTC/RtpDictionaries.hpp" #include "RTC/RtpPacket.hpp" -#include #include -using json = nlohmann::json; - namespace RTC { class RtxStream @@ -20,7 +18,8 @@ namespace RTC public: struct Params { - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; uint32_t ssrc{ 0 }; uint8_t payloadType{ 0 }; @@ -34,7 +33,7 @@ namespace RTC explicit RtxStream(RTC::RtxStream::Params& params); virtual ~RtxStream(); - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer(flatbuffers::FlatBufferBuilder& builder) const; uint32_t GetSsrc() const { return this->params.ssrc; diff --git a/worker/include/RTC/SctpAssociation.hpp b/worker/include/RTC/SctpAssociation.hpp index 6b6626f04d..e2f3fe7922 100644 --- a/worker/include/RTC/SctpAssociation.hpp +++ b/worker/include/RTC/SctpAssociation.hpp @@ -6,9 +6,6 @@ #include "RTC/DataConsumer.hpp" #include "RTC/DataProducer.hpp" #include -#include - -using json = nlohmann::json; namespace RTC { @@ -50,9 +47,9 @@ namespace RTC virtual void OnSctpAssociationMessageReceived( RTC::SctpAssociation* sctpAssociation, uint16_t streamId, - uint32_t ppid, const uint8_t* msg, - size_t len) = 0; + size_t len, + uint32_t ppid) = 0; virtual void OnSctpAssociationBufferedAmount( RTC::SctpAssociation* sctpAssociation, uint32_t len) = 0; }; @@ -81,7 +78,8 @@ namespace RTC ~SctpAssociation(); public: - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; void TransportConnected(); SctpState GetState() const { @@ -91,19 +89,19 @@ namespace RTC { return this->sctpBufferedAmount; } - void ProcessSctpData(const uint8_t* data, size_t len); + void ProcessSctpData(const uint8_t* data, size_t len) const; void SendSctpMessage( RTC::DataConsumer* dataConsumer, - uint32_t ppid, const uint8_t* msg, size_t len, + uint32_t ppid, onQueuedCallback* cb = nullptr); void HandleDataConsumer(RTC::DataConsumer* dataConsumer); void DataProducerClosed(RTC::DataProducer* dataProducer); void DataConsumerClosed(RTC::DataConsumer* dataConsumer); private: - void ResetSctpStream(uint16_t streamId, StreamDirection); + void ResetSctpStream(uint16_t streamId, StreamDirection direction); void AddOutgoingStreams(bool force = false); /* Callbacks fired by usrsctp events. */ diff --git a/worker/include/RTC/SctpDictionaries.hpp b/worker/include/RTC/SctpDictionaries.hpp index 9ceebb03a8..25862a70a1 100644 --- a/worker/include/RTC/SctpDictionaries.hpp +++ b/worker/include/RTC/SctpDictionaries.hpp @@ -2,10 +2,7 @@ #define MS_RTC_SCTP_DICTIONARIES_HPP #include "common.hpp" -#include -#include - -using json = nlohmann::json; +#include namespace RTC { @@ -13,9 +10,10 @@ namespace RTC { public: SctpStreamParameters() = default; - explicit SctpStreamParameters(json& data); + explicit SctpStreamParameters(const FBS::SctpParameters::SctpStreamParameters* data); - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; public: uint16_t streamId{ 0u }; diff --git a/worker/include/RTC/SctpListener.hpp b/worker/include/RTC/SctpListener.hpp index 4988805e65..504d7d978a 100644 --- a/worker/include/RTC/SctpListener.hpp +++ b/worker/include/RTC/SctpListener.hpp @@ -3,17 +3,15 @@ #include "common.hpp" #include "RTC/DataProducer.hpp" -#include #include -using json = nlohmann::json; - namespace RTC { class SctpListener { public: - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; void AddDataProducer(RTC::DataProducer* dataProducer); void RemoveDataProducer(RTC::DataProducer* dataProducer); RTC::DataProducer* GetDataProducer(uint16_t streamId); diff --git a/worker/include/RTC/SenderBandwidthEstimator.hpp b/worker/include/RTC/SenderBandwidthEstimator.hpp index b25f5df385..7b715deee9 100644 --- a/worker/include/RTC/SenderBandwidthEstimator.hpp +++ b/worker/include/RTC/SenderBandwidthEstimator.hpp @@ -6,7 +6,6 @@ #include "RTC/RateCalculator.hpp" #include "RTC/SeqManager.hpp" #include "RTC/TrendCalculator.hpp" -#include #include namespace RTC @@ -89,7 +88,7 @@ namespace RTC public: void TransportConnected(); void TransportDisconnected(); - void RtpPacketSent(SentInfo& sentInfo); + void RtpPacketSent(const SentInfo& sentInfo); void ReceiveRtcpTransportFeedback(const RTC::RTCP::FeedbackRtpTransportPacket* feedback); void EstimateAvailableBitrate(CummulativeResult& cummulativeResult); void UpdateRtt(float rtt); @@ -103,7 +102,7 @@ namespace RTC uint32_t initialAvailableBitrate{ 0u }; uint32_t availableBitrate{ 0u }; uint64_t lastAvailableBitrateEventAtMs{ 0u }; - absl::btree_map::SeqLowerThan> sentInfos; + std::map::SeqLowerThan> sentInfos; float rtt{ 0 }; // Round trip time in ms. CummulativeResult cummulativeResult; CummulativeResult probationCummulativeResult; diff --git a/worker/include/RTC/SeqManager.hpp b/worker/include/RTC/SeqManager.hpp index 3bf0e74dc8..9f2cbd5374 100644 --- a/worker/include/RTC/SeqManager.hpp +++ b/worker/include/RTC/SeqManager.hpp @@ -18,35 +18,40 @@ namespace RTC public: struct SeqLowerThan { - bool operator()(const T lhs, const T rhs) const; + bool operator()(T lhs, T rhs) const; }; struct SeqHigherThan { - bool operator()(const T lhs, const T rhs) const; + bool operator()(T lhs, T rhs) const; }; - private: - static const SeqLowerThan isSeqLowerThan; - static const SeqHigherThan isSeqHigherThan; - static T Delta(const T lhs, const T rhs); - public: - static bool IsSeqLowerThan(const T lhs, const T rhs); - static bool IsSeqHigherThan(const T lhs, const T rhs); + static bool IsSeqLowerThan(T lhs, T rhs); + static bool IsSeqHigherThan(T lhs, T rhs); + + private: + static const SeqLowerThan isSeqLowerThan; // NOLINT(readability-identifier-naming) + static const SeqHigherThan isSeqHigherThan; // NOLINT(readability-identifier-naming) public: SeqManager() = default; + SeqManager(T initialOutput); public: void Sync(T input); void Drop(T input); - void Offset(T offset); - bool Input(const T input, T& output); + bool Input(T input, T& output); T GetMaxInput() const; T GetMaxOutput() const; private: + void ClearDropped(); + + private: + // Whether at least a sequence number has been inserted. + bool started{ false }; + T initialOutput{ 0 }; T base{ 0 }; T maxOutput{ 0 }; T maxInput{ 0 }; diff --git a/worker/include/RTC/Shared.hpp b/worker/include/RTC/Shared.hpp index 90e07f058a..2a71781283 100644 --- a/worker/include/RTC/Shared.hpp +++ b/worker/include/RTC/Shared.hpp @@ -3,7 +3,6 @@ #include "ChannelMessageRegistrator.hpp" #include "Channel/ChannelNotifier.hpp" -#include "PayloadChannel/PayloadChannelNotifier.hpp" namespace RTC { @@ -12,14 +11,12 @@ namespace RTC public: explicit Shared( ChannelMessageRegistrator* channelMessageRegistrator, - Channel::ChannelNotifier* channelNotifier, - PayloadChannel::PayloadChannelNotifier* payloadChannelNotifier); + Channel::ChannelNotifier* channelNotifier); ~Shared(); public: ChannelMessageRegistrator* channelMessageRegistrator{ nullptr }; Channel::ChannelNotifier* channelNotifier{ nullptr }; - PayloadChannel::PayloadChannelNotifier* payloadChannelNotifier{ nullptr }; }; } // namespace RTC diff --git a/worker/include/RTC/SimpleConsumer.hpp b/worker/include/RTC/SimpleConsumer.hpp index 7932d13e2f..d215d2b8ed 100644 --- a/worker/include/RTC/SimpleConsumer.hpp +++ b/worker/include/RTC/SimpleConsumer.hpp @@ -1,8 +1,8 @@ #ifndef MS_RTC_SIMPLE_CONSUMER_HPP #define MS_RTC_SIMPLE_CONSUMER_HPP +#include "FBS/transport.h" #include "RTC/Consumer.hpp" -#include "RTC/RtpStreamSend.hpp" #include "RTC/SeqManager.hpp" #include "RTC/Shared.hpp" @@ -16,27 +16,33 @@ namespace RTC const std::string& id, const std::string& producerId, RTC::Consumer::Listener* listener, - json& data); + const FBS::Transport::ConsumeRequest* data); ~SimpleConsumer() override; public: - void FillJson(json& jsonObject) const override; - void FillJsonStats(json& jsonArray) const override; - void FillJsonScore(json& jsonObject) const override; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; + flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) override; + flatbuffers::Offset FillBufferScore( + flatbuffers::FlatBufferBuilder& builder) const override; bool IsActive() const override { // clang-format off return ( RTC::Consumer::IsActive() && this->producerRtpStream && - (this->producerRtpStream->GetScore() > 0u || this->producerRtpStream->HasDtx()) + // If there is no RTP inactivity check do not consider the stream + // inactive despite it has score 0. + (this->producerRtpStream->GetScore() > 0u || !this->producerRtpStream->HasRtpInactivityCheckEnabled()) ); // clang-format on } - void ProducerRtpStream(RTC::RtpStream* rtpStream, uint32_t mappedSsrc) override; - void ProducerNewRtpStream(RTC::RtpStream* rtpStream, uint32_t mappedSsrc) override; - void ProducerRtpStreamScore(RTC::RtpStream* rtpStream, uint8_t score, uint8_t previousScore) override; - void ProducerRtcpSenderReport(RTC::RtpStream* rtpStream, bool first) override; + void ProducerRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) override; + void ProducerNewRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) override; + void ProducerRtpStreamScore( + RTC::RtpStreamRecv* rtpStream, uint8_t score, uint8_t previousScore) override; + void ProducerRtcpSenderReport(RTC::RtpStreamRecv* rtpStream, bool first) override; uint8_t GetBitratePriority() const override; uint32_t IncreaseLayer(uint32_t bitrate, bool considerLoss) override; void ApplyLayers() override; @@ -78,10 +84,10 @@ namespace RTC RTC::RtpStreamSend* rtpStream{ nullptr }; // Others. std::vector rtpStreams; - RTC::RtpStream* producerRtpStream{ nullptr }; + RTC::RtpStreamRecv* producerRtpStream{ nullptr }; bool keyFrameSupported{ false }; bool syncRequired{ false }; - RTC::SeqManager rtpSeqManager; + std::unique_ptr> rtpSeqManager; bool managingBitrate{ false }; std::unique_ptr encodingContext; }; diff --git a/worker/include/RTC/SimulcastConsumer.hpp b/worker/include/RTC/SimulcastConsumer.hpp index c08dd83af2..70c328971c 100644 --- a/worker/include/RTC/SimulcastConsumer.hpp +++ b/worker/include/RTC/SimulcastConsumer.hpp @@ -1,9 +1,9 @@ #ifndef MS_RTC_SIMULCAST_CONSUMER_HPP #define MS_RTC_SIMULCAST_CONSUMER_HPP +#include "FBS/consumer.h" #include "RTC/Codecs/PayloadDescriptorHandler.hpp" #include "RTC/Consumer.hpp" -#include "RTC/RtpStreamSend.hpp" #include "RTC/SeqManager.hpp" #include "RTC/Shared.hpp" @@ -17,13 +17,16 @@ namespace RTC const std::string& id, const std::string& producerId, RTC::Consumer::Listener* listener, - json& data); + const FBS::Transport::ConsumeRequest* data); ~SimulcastConsumer() override; public: - void FillJson(json& jsonObject) const override; - void FillJsonStats(json& jsonArray) const override; - void FillJsonScore(json& jsonObject) const override; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; + flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) override; + flatbuffers::Offset FillBufferScore( + flatbuffers::FlatBufferBuilder& builder) const override; RTC::Consumer::Layers GetPreferredLayers() const override { RTC::Consumer::Layers layers; @@ -41,18 +44,21 @@ namespace RTC std::any_of( this->producerRtpStreams.begin(), this->producerRtpStreams.end(), - [](const RTC::RtpStream* rtpStream) + [](const RTC::RtpStreamRecv* rtpStream) { - return (rtpStream != nullptr && (rtpStream->GetScore() > 0u || rtpStream->HasDtx())); + // If there is no RTP inactivity check do not consider the stream + // inactive despite it has score 0. + return (rtpStream != nullptr && (rtpStream->GetScore() > 0u || !rtpStream->HasRtpInactivityCheckEnabled())); } ) ); // clang-format on } - void ProducerRtpStream(RTC::RtpStream* rtpStream, uint32_t mappedSsrc) override; - void ProducerNewRtpStream(RTC::RtpStream* rtpStream, uint32_t mappedSsrc) override; - void ProducerRtpStreamScore(RTC::RtpStream* rtpStream, uint8_t score, uint8_t previousScore) override; - void ProducerRtcpSenderReport(RTC::RtpStream* rtpStream, bool first) override; + void ProducerRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) override; + void ProducerNewRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) override; + void ProducerRtpStreamScore( + RTC::RtpStreamRecv* rtpStream, uint8_t score, uint8_t previousScore) override; + void ProducerRtcpSenderReport(RTC::RtpStreamRecv* rtpStream, bool first) override; uint8_t GetBitratePriority() const override; uint32_t IncreaseLayer(uint32_t bitrate, bool considerLoss) override; void ApplyLayers() override; @@ -90,9 +96,9 @@ namespace RTC bool CanSwitchToSpatialLayer(int16_t spatialLayer) const; void EmitScore() const; void EmitLayersChange() const; - RTC::RtpStream* GetProducerCurrentRtpStream() const; - RTC::RtpStream* GetProducerTargetRtpStream() const; - RTC::RtpStream* GetProducerTsReferenceRtpStream() const; + RTC::RtpStreamRecv* GetProducerCurrentRtpStream() const; + RTC::RtpStreamRecv* GetProducerTargetRtpStream() const; + RTC::RtpStreamRecv* GetProducerTsReferenceRtpStream() const; /* Pure virtual methods inherited from RtpStreamSend::Listener. */ public: @@ -105,11 +111,11 @@ namespace RTC // Others. absl::flat_hash_map mapMappedSsrcSpatialLayer; std::vector rtpStreams; - std::vector producerRtpStreams; // Indexed by spatial layer. + std::vector producerRtpStreams; // Indexed by spatial layer. bool syncRequired{ false }; int16_t spatialLayerToSync{ -1 }; bool lastSentPacketHasMarker{ false }; - RTC::SeqManager rtpSeqManager; + std::unique_ptr> rtpSeqManager; int16_t preferredSpatialLayer{ -1 }; int16_t preferredTemporalLayer{ -1 }; int16_t provisionalTargetSpatialLayer{ -1 }; diff --git a/worker/include/RTC/SrtpSession.hpp b/worker/include/RTC/SrtpSession.hpp index 7b6f34c0f0..b50f6d9b87 100644 --- a/worker/include/RTC/SrtpSession.hpp +++ b/worker/include/RTC/SrtpSession.hpp @@ -2,6 +2,7 @@ #define MS_RTC_SRTP_SESSION_HPP #include "common.hpp" +#include "FBS/srtpParameters.h" #include namespace RTC @@ -11,8 +12,7 @@ namespace RTC public: enum class CryptoSuite { - NONE = 0, - AEAD_AES_256_GCM = 1, + AEAD_AES_256_GCM = 0, AEAD_AES_128_GCM, AES_CM_128_HMAC_SHA1_80, AES_CM_128_HMAC_SHA1_32, @@ -27,6 +27,8 @@ namespace RTC public: static void ClassInit(); + static FBS::SrtpParameters::SrtpCryptoSuite CryptoSuiteToFbs(CryptoSuite cryptoSuite); + static CryptoSuite CryptoSuiteFromFbs(FBS::SrtpParameters::SrtpCryptoSuite cryptoSuite); private: static void OnSrtpEvent(srtp_event_data_t* data); @@ -36,13 +38,13 @@ namespace RTC ~SrtpSession(); public: - bool EncryptRtp(const uint8_t** data, int* len); - bool DecryptSrtp(uint8_t* data, int* len); - bool EncryptRtcp(const uint8_t** data, int* len); - bool DecryptSrtcp(uint8_t* data, int* len); + bool EncryptRtp(const uint8_t** data, size_t* len); + bool DecryptSrtp(uint8_t* data, size_t* len); + bool EncryptRtcp(const uint8_t** data, size_t* len); + bool DecryptSrtcp(uint8_t* data, size_t* len); void RemoveStream(uint32_t ssrc) { - srtp_remove_stream(this->session, uint32_t{ htonl(ssrc) }); + srtp_stream_remove(this->session, uint32_t{ htonl(ssrc) }); } private: diff --git a/worker/include/RTC/StunPacket.hpp b/worker/include/RTC/StunPacket.hpp index 98e1f527eb..1d781fff43 100644 --- a/worker/include/RTC/StunPacket.hpp +++ b/worker/include/RTC/StunPacket.hpp @@ -50,7 +50,7 @@ namespace RTC { OK = 0, UNAUTHORIZED = 1, - BAD_REQUEST = 2 + BAD_MESSAGE = 2 }; public: @@ -63,15 +63,15 @@ namespace RTC // DOC: https://tools.ietf.org/html/draft-ietf-avtcore-rfc5764-mux-fixes (data[0] < 3) && // Magic cookie must match. - (data[4] == StunPacket::magicCookie[0]) && (data[5] == StunPacket::magicCookie[1]) && - (data[6] == StunPacket::magicCookie[2]) && (data[7] == StunPacket::magicCookie[3]) + (data[4] == StunPacket::MagicCookie[0]) && (data[5] == StunPacket::MagicCookie[1]) && + (data[6] == StunPacket::MagicCookie[2]) && (data[7] == StunPacket::MagicCookie[3]) ); // clang-format on } static StunPacket* Parse(const uint8_t* data, size_t len); private: - static const uint8_t magicCookie[]; + static const uint8_t MagicCookie[]; public: StunPacket( @@ -95,6 +95,10 @@ namespace RTC { return this->size; } + const uint8_t* GetTransactionId() const + { + return this->transactionId; + } void SetUsername(const char* username, size_t len) { this->username.assign(username, len); @@ -127,6 +131,10 @@ namespace RTC { this->errorCode = errorCode; } + void SetSoftware(const char* software, size_t len) + { + this->software.assign(software, len); + } void SetMessageIntegrity(const uint8_t* messageIntegrity) { this->messageIntegrity = messageIntegrity; @@ -139,6 +147,7 @@ namespace RTC { return this->username; } + void SetPassword(const std::string& password); uint32_t GetPriority() const { return this->priority; @@ -171,19 +180,24 @@ namespace RTC { return this->errorCode; } + std::string GetSoftware() const + { + return this->software; + } bool HasMessageIntegrity() const { - return (this->messageIntegrity ? true : false); + return (this->messageIntegrity != nullptr); } bool HasFingerprint() const { return this->hasFingerprint; } Authentication CheckAuthentication( - const std::string& localUsername, const std::string& localPassword); + // The first username fragment in the USERNAME attribute. + const std::string& usernameFragment1, + const std::string& password); StunPacket* CreateSuccessResponse(); StunPacket* CreateErrorResponse(uint16_t errorCode); - void Authenticate(const std::string& password); void Serialize(uint8_t* buffer); private: @@ -194,7 +208,8 @@ namespace RTC uint8_t* data{ nullptr }; // Pointer to binary data. size_t size{ 0u }; // The full message size (including header). // STUN attributes. - std::string username; // Less than 513 bytes. + std::string username; // Less than 513 bytes. + std::string password; uint32_t priority{ 0u }; // 4 bytes unsigned integer. uint64_t iceControlling{ 0u }; // 8 bytes unsigned integer. uint64_t iceControlled{ 0u }; // 8 bytes unsigned integer. @@ -205,7 +220,7 @@ namespace RTC bool hasFingerprint{ false }; // 4 bytes. const struct sockaddr* xorMappedAddress{ nullptr }; // 8 or 20 bytes. uint16_t errorCode{ 0u }; // 4 bytes (no reason phrase). - std::string password; + std::string software; // Less than 763 bytes. }; } // namespace RTC diff --git a/worker/include/RTC/SvcConsumer.hpp b/worker/include/RTC/SvcConsumer.hpp index 949a86ef65..cb860709d6 100644 --- a/worker/include/RTC/SvcConsumer.hpp +++ b/worker/include/RTC/SvcConsumer.hpp @@ -3,7 +3,6 @@ #include "RTC/Codecs/PayloadDescriptorHandler.hpp" #include "RTC/Consumer.hpp" -#include "RTC/RtpStreamSend.hpp" #include "RTC/SeqManager.hpp" #include "RTC/Shared.hpp" #include @@ -18,13 +17,16 @@ namespace RTC const std::string& id, const std::string& producerId, RTC::Consumer::Listener* listener, - json& data); + const FBS::Transport::ConsumeRequest* data); ~SvcConsumer() override; public: - void FillJson(json& jsonObject) const override; - void FillJsonStats(json& jsonArray) const override; - void FillJsonScore(json& jsonObject) const override; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; + flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) override; + flatbuffers::Offset FillBufferScore( + flatbuffers::FlatBufferBuilder& builder) const override; RTC::Consumer::Layers GetPreferredLayers() const override { RTC::Consumer::Layers layers; @@ -40,14 +42,17 @@ namespace RTC return ( RTC::Consumer::IsActive() && this->producerRtpStream && - (this->producerRtpStream->GetScore() > 0u || this->producerRtpStream->HasDtx()) + // If there is no RTP inactivity check do not consider the stream + // inactive despite it has score 0. + (this->producerRtpStream->GetScore() > 0u || !this->producerRtpStream->HasRtpInactivityCheckEnabled()) ); // clang-format on } - void ProducerRtpStream(RTC::RtpStream* rtpStream, uint32_t mappedSsrc) override; - void ProducerNewRtpStream(RTC::RtpStream* rtpStream, uint32_t mappedSsrc) override; - void ProducerRtpStreamScore(RTC::RtpStream* rtpStream, uint8_t score, uint8_t previousScore) override; - void ProducerRtcpSenderReport(RTC::RtpStream* rtpStream, bool first) override; + void ProducerRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) override; + void ProducerNewRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) override; + void ProducerRtpStreamScore( + RTC::RtpStreamRecv* rtpStream, uint8_t score, uint8_t previousScore) override; + void ProducerRtcpSenderReport(RTC::RtpStreamRecv* rtpStream, bool first) override; uint8_t GetBitratePriority() const override; uint32_t IncreaseLayer(uint32_t bitrate, bool considerLoss) override; void ApplyLayers() override; @@ -93,9 +98,9 @@ namespace RTC RTC::RtpStreamSend* rtpStream{ nullptr }; // Others. std::vector rtpStreams; - RTC::RtpStream* producerRtpStream{ nullptr }; + RTC::RtpStreamRecv* producerRtpStream{ nullptr }; bool syncRequired{ false }; - RTC::SeqManager rtpSeqManager; + std::unique_ptr> rtpSeqManager; int16_t preferredSpatialLayer{ -1 }; int16_t preferredTemporalLayer{ -1 }; int16_t provisionalTargetSpatialLayer{ -1 }; diff --git a/worker/include/RTC/TcpConnection.hpp b/worker/include/RTC/TcpConnection.hpp index 0aa17ebb62..27d5aaa786 100644 --- a/worker/include/RTC/TcpConnection.hpp +++ b/worker/include/RTC/TcpConnection.hpp @@ -2,11 +2,11 @@ #define MS_RTC_TCP_CONNECTION_HPP #include "common.hpp" -#include "handles/TcpConnectionHandler.hpp" +#include "handles/TcpConnectionHandle.hpp" namespace RTC { - class TcpConnection : public ::TcpConnectionHandler + class TcpConnection : public ::TcpConnectionHandle { public: class Listener @@ -24,9 +24,9 @@ namespace RTC ~TcpConnection() override; public: - void Send(const uint8_t* data, size_t len, ::TcpConnectionHandler::onSendCallback* cb); + void Send(const uint8_t* data, size_t len, ::TcpConnectionHandle::onSendCallback* cb); - /* Pure virtual methods inherited from ::TcpConnectionHandler. */ + /* Pure virtual methods inherited from ::TcpConnectionHandle. */ public: void UserOnTcpConnectionRead() override; diff --git a/worker/include/RTC/TcpServer.hpp b/worker/include/RTC/TcpServer.hpp index 859785cce5..383ae16ef4 100644 --- a/worker/include/RTC/TcpServer.hpp +++ b/worker/include/RTC/TcpServer.hpp @@ -3,13 +3,14 @@ #include "common.hpp" #include "RTC/TcpConnection.hpp" -#include "handles/TcpConnectionHandler.hpp" -#include "handles/TcpServerHandler.hpp" +#include "RTC/Transport.hpp" +#include "handles/TcpConnectionHandle.hpp" +#include "handles/TcpServerHandle.hpp" #include namespace RTC { - class TcpServer : public ::TcpServerHandler + class TcpServer : public ::TcpServerHandle { public: class Listener @@ -23,21 +24,33 @@ namespace RTC }; public: - TcpServer(Listener* listener, RTC::TcpConnection::Listener* connListener, std::string& ip); TcpServer( - Listener* listener, RTC::TcpConnection::Listener* connListener, std::string& ip, uint16_t port); + Listener* listener, + RTC::TcpConnection::Listener* connListener, + std::string& ip, + uint16_t port, + RTC::Transport::SocketFlags& flags); + TcpServer( + Listener* listener, + RTC::TcpConnection::Listener* connListener, + std::string& ip, + uint16_t minPort, + uint16_t maxPort, + RTC::Transport::SocketFlags& flags, + uint64_t& portRangeHash); ~TcpServer() override; - /* Pure virtual methods inherited from ::TcpServerHandler. */ + /* Pure virtual methods inherited from ::TcpServerHandle. */ public: void UserOnTcpConnectionAlloc() override; - void UserOnTcpConnectionClosed(::TcpConnectionHandler* connection) override; + void UserOnTcpConnectionClosed(::TcpConnectionHandle* connection) override; private: // Passed by argument. Listener* listener{ nullptr }; RTC::TcpConnection::Listener* connListener{ nullptr }; bool fixedPort{ false }; + uint64_t portRangeHash{ 0u }; }; } // namespace RTC diff --git a/worker/include/RTC/Transport.hpp b/worker/include/RTC/Transport.hpp index c52a197616..3448555a59 100644 --- a/worker/include/RTC/Transport.hpp +++ b/worker/include/RTC/Transport.hpp @@ -4,10 +4,10 @@ #include "common.hpp" #include "DepLibUV.hpp" +#include "Channel/ChannelNotification.hpp" #include "Channel/ChannelRequest.hpp" #include "Channel/ChannelSocket.hpp" -#include "PayloadChannel/PayloadChannelNotification.hpp" -#include "PayloadChannel/PayloadChannelRequest.hpp" +#include "FBS/transport.h" #include "RTC/Consumer.hpp" #include "RTC/DataConsumer.hpp" #include "RTC/DataProducer.hpp" @@ -27,12 +27,10 @@ #endif #include "RTC/TransportCongestionControlClient.hpp" #include "RTC/TransportCongestionControlServer.hpp" -#include "handles/Timer.hpp" +#include "handles/TimerHandle.hpp" #include -#include #include - -using json = nlohmann::json; +#include namespace RTC { @@ -44,12 +42,11 @@ namespace RTC public RTC::TransportCongestionControlClient::Listener, public RTC::TransportCongestionControlServer::Listener, public Channel::ChannelSocket::RequestHandler, - public PayloadChannel::PayloadChannelSocket::RequestHandler, - public PayloadChannel::PayloadChannelSocket::NotificationHandler, + public Channel::ChannelSocket::NotificationHandler, #ifdef ENABLE_RTC_SENDER_BANDWIDTH_ESTIMATOR public RTC::SenderBandwidthEstimator::Listener, #endif - public Timer::Listener + public TimerHandle::Listener { protected: using onSendCallback = const std::function; @@ -69,16 +66,19 @@ namespace RTC virtual void OnTransportProducerNewRtpStream( RTC::Transport* transport, RTC::Producer* producer, - RTC::RtpStream* rtpStream, + RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) = 0; virtual void OnTransportProducerRtpStreamScore( RTC::Transport* transport, RTC::Producer* producer, - RTC::RtpStream* rtpStream, + RTC::RtpStreamRecv* rtpStream, uint8_t score, uint8_t previousScore) = 0; virtual void OnTransportProducerRtcpSenderReport( - RTC::Transport* transport, RTC::Producer* producer, RTC::RtpStream* rtpStream, bool first) = 0; + RTC::Transport* transport, + RTC::Producer* producer, + RTC::RtpStreamRecv* rtpStream, + bool first) = 0; virtual void OnTransportProducerRtpPacketReceived( RTC::Transport* transport, RTC::Producer* producer, RTC::RtpPacket* packet) = 0; virtual void OnTransportNeedWorstRemoteFractionLost( @@ -87,10 +87,14 @@ namespace RTC uint32_t mappedSsrc, uint8_t& worstRemoteFractionLost) = 0; virtual void OnTransportNewConsumer( - RTC::Transport* transport, RTC::Consumer* consumer, std::string& producerId) = 0; + RTC::Transport* transport, RTC::Consumer* consumer, const std::string& producerId) = 0; virtual void OnTransportConsumerClosed(RTC::Transport* transport, RTC::Consumer* consumer) = 0; virtual void OnTransportConsumerProducerClosed( RTC::Transport* transport, RTC::Consumer* consumer) = 0; + virtual void OnTransportDataProducerPaused( + RTC::Transport* transport, RTC::DataProducer* dataProducer) = 0; + virtual void OnTransportDataProducerResumed( + RTC::Transport* transport, RTC::DataProducer* dataProducer) = 0; virtual void OnTransportConsumerKeyFrameRequested( RTC::Transport* transport, RTC::Consumer* consumer, uint32_t mappedSsrc) = 0; virtual void OnTransportNewDataProducer( @@ -100,9 +104,11 @@ namespace RTC virtual void OnTransportDataProducerMessageReceived( RTC::Transport* transport, RTC::DataProducer* dataProducer, - uint32_t ppid, const uint8_t* msg, - size_t len) = 0; + size_t len, + uint32_t ppid, + std::vector& subchannels, + std::optional requiredSubchannel) = 0; virtual void OnTransportNewDataConsumer( RTC::Transport* transport, RTC::DataConsumer* dataConsumer, std::string& dataProducerId) = 0; virtual void OnTransportDataConsumerClosed( @@ -112,6 +118,30 @@ namespace RTC virtual void OnTransportListenServerClosed(RTC::Transport* transport) = 0; }; + public: + struct SocketFlags + { + bool ipv6Only{ false }; + bool udpReusePort{ false }; + }; + + struct PortRange + { + uint16_t min{ 0u }; + uint16_t max{ 0u }; + }; + + struct ListenInfo + { + std::string ip; + std::string announcedAddress; + uint16_t port{ 0u }; + PortRange portRange; + SocketFlags flags; + uint32_t sendBufferSize{ 0u }; + uint32_t recvBufferSize{ 0u }; + }; + private: struct TraceEventTypes { @@ -120,30 +150,31 @@ namespace RTC }; public: - Transport(RTC::Shared* shared, const std::string& id, Listener* listener, json& data); - virtual ~Transport(); + Transport( + RTC::Shared* shared, + const std::string& id, + RTC::Transport::Listener* listener, + const FBS::Transport::Options* options); + ~Transport() override; public: void CloseProducersAndConsumers(); void ListenServerClosed(); // Subclasses must also invoke the parent Close(). - virtual void FillJson(json& jsonObject) const; - virtual void FillJsonStats(json& jsonArray); + flatbuffers::Offset FillBufferStats(flatbuffers::FlatBufferBuilder& builder); + flatbuffers::Offset FillBuffer(flatbuffers::FlatBufferBuilder& builder) const; /* Methods inherited from Channel::ChannelSocket::RequestHandler. */ public: void HandleRequest(Channel::ChannelRequest* request) override; - /* Methods inherited from PayloadChannel::PayloadChannelSocket::RequestHandler. */ + /* Methods inherited from Channel::ChannelSocket::NotificationHandler. */ public: - void HandleRequest(PayloadChannel::PayloadChannelRequest* request) override; - - /* Methods inherited from PayloadChannel::PayloadChannelSocket::NotificationHandler. */ - public: - void HandleNotification(PayloadChannel::PayloadChannelNotification* notification) override; + void HandleNotification(Channel::ChannelNotification* notification) override; protected: // Must be called from the subclass. + void Destroying(); void Connected(); void Disconnected(); void DataReceived(size_t len) @@ -157,16 +188,12 @@ namespace RTC void ReceiveRtpPacket(RTC::RtpPacket* packet); void ReceiveRtcpPacket(RTC::RTCP::Packet* packet); void ReceiveSctpData(const uint8_t* data, size_t len); - void SetNewProducerIdFromData(json& data, std::string& producerId) const; - RTC::Producer* GetProducerFromData(json& data) const; - void SetNewConsumerIdFromData(json& data, std::string& consumerId) const; - RTC::Consumer* GetConsumerFromData(json& data) const; + RTC::Producer* GetProducerById(const std::string& producerId) const; + RTC::Consumer* GetConsumerById(const std::string& consumerId) const; RTC::Consumer* GetConsumerByMediaSsrc(uint32_t ssrc) const; RTC::Consumer* GetConsumerByRtxSsrc(uint32_t ssrc) const; - void SetNewDataProducerIdFromData(json& data, std::string& dataProducerId) const; - RTC::DataProducer* GetDataProducerFromData(json& data) const; - void SetNewDataConsumerIdFromData(json& data, std::string& dataConsumerId) const; - RTC::DataConsumer* GetDataConsumerFromData(json& data) const; + RTC::DataProducer* GetDataProducerById(const std::string& dataProducerId) const; + RTC::DataConsumer* GetDataConsumerById(const std::string& dataConsumerId) const; private: virtual bool IsConnected() const = 0; @@ -178,9 +205,9 @@ namespace RTC virtual void SendRtcpCompoundPacket(RTC::RTCP::CompoundPacket* packet) = 0; virtual void SendMessage( RTC::DataConsumer* dataConsumer, - uint32_t ppid, const uint8_t* msg, size_t len, + uint32_t ppid, onQueuedCallback* = nullptr) = 0; virtual void SendSctpData(const uint8_t* data, size_t len) = 0; virtual void RecvStreamClosed(uint32_t ssrc) = 0; @@ -189,6 +216,9 @@ namespace RTC void ComputeOutgoingDesiredBitrate(bool forceBitrate = false); void EmitTraceEventProbationType(RTC::RtpPacket* packet) const; void EmitTraceEventBweType(RTC::TransportCongestionControlClient::Bitrates& bitrates) const; + void CheckNoProducer(const std::string& producerId) const; + void CheckNoDataProducer(const std::string& dataProducerId) const; + void CheckNoDataConsumer(const std::string& dataConsumerId) const; /* Pure virtual methods inherited from RTC::Producer::Listener. */ public: @@ -203,11 +233,14 @@ namespace RTC void OnProducerPaused(RTC::Producer* producer) override; void OnProducerResumed(RTC::Producer* producer) override; void OnProducerNewRtpStream( - RTC::Producer* producer, RTC::RtpStream* rtpStream, uint32_t mappedSsrc) override; + RTC::Producer* producer, RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) override; void OnProducerRtpStreamScore( - RTC::Producer* producer, RTC::RtpStream* rtpStream, uint8_t score, uint8_t previousScore) override; + RTC::Producer* producer, + RTC::RtpStreamRecv* rtpStream, + uint8_t score, + uint8_t previousScore) override; void OnProducerRtcpSenderReport( - RTC::Producer* producer, RTC::RtpStream* rtpStream, bool first) override; + RTC::Producer* producer, RTC::RtpStreamRecv* rtpStream, bool first) override; void OnProducerRtpPacketReceived(RTC::Producer* producer, RTC::RtpPacket* packet) override; void OnProducerSendRtcpPacket(RTC::Producer* producer, RTC::RTCP::Packet* packet) override; void OnProducerNeedWorstRemoteFractionLost( @@ -229,16 +262,23 @@ namespace RTC this->DataReceived(len); } void OnDataProducerMessageReceived( - RTC::DataProducer* dataProducer, uint32_t ppid, const uint8_t* msg, size_t len) override; + RTC::DataProducer* dataProducer, + const uint8_t* msg, + size_t len, + uint32_t ppid, + std::vector& subchannels, + std::optional requiredSubchannel) override; + void OnDataProducerPaused(RTC::DataProducer* dataProducer) override; + void OnDataProducerResumed(RTC::DataProducer* dataProducer) override; /* Pure virtual methods inherited from RTC::DataConsumer::Listener. */ public: void OnDataConsumerSendMessage( RTC::DataConsumer* dataConsumer, - uint32_t ppid, const uint8_t* msg, size_t len, - onQueuedCallback* = nullptr) override; + uint32_t ppid, + onQueuedCallback* cb = nullptr) override; void OnDataConsumerDataProducerClosed(RTC::DataConsumer* dataConsumer) override; /* Pure virtual methods inherited from RTC::SctpAssociation::Listener. */ @@ -252,9 +292,9 @@ namespace RTC void OnSctpAssociationMessageReceived( RTC::SctpAssociation* sctpAssociation, uint16_t streamId, - uint32_t ppid, const uint8_t* msg, - size_t len) override; + size_t len, + uint32_t ppid) override; void OnSctpAssociationBufferedAmount( RTC::SctpAssociation* sctpAssociation, uint32_t bufferedAmount) override; @@ -282,9 +322,9 @@ namespace RTC uint32_t previousAvailableBitrate) override; #endif - /* Pure virtual methods inherited from Timer::Listener. */ + /* Pure virtual methods inherited from TimerHandle::Listener. */ public: - void OnTimer(Timer* timer) override; + void OnTimer(TimerHandle* timer) override; public: // Passed by argument. @@ -306,14 +346,14 @@ namespace RTC absl::flat_hash_map mapDataConsumers; absl::flat_hash_map mapSsrcConsumer; absl::flat_hash_map mapRtxSsrcConsumer; - Timer* rtcpTimer{ nullptr }; + TimerHandle* rtcpTimer{ nullptr }; std::shared_ptr tccClient{ nullptr }; std::shared_ptr tccServer{ nullptr }; #ifdef ENABLE_RTC_SENDER_BANDWIDTH_ESTIMATOR std::shared_ptr senderBwe{ nullptr }; #endif // Others. - bool direct{ false }; // Whether this Transport allows PayloadChannel comm. + bool direct{ false }; // Whether this Transport allows direct communication. bool destroying{ false }; struct RTC::RtpHeaderExtensionIds recvRtpHeaderExtensionIds; RTC::RtpListener rtpListener; @@ -329,6 +369,7 @@ namespace RTC uint32_t initialAvailableOutgoingBitrate{ 600000u }; uint32_t maxIncomingBitrate{ 0u }; uint32_t maxOutgoingBitrate{ 0u }; + uint32_t minOutgoingBitrate{ 0u }; struct TraceEventTypes traceEventTypes; }; } // namespace RTC diff --git a/worker/include/RTC/TransportCongestionControlClient.hpp b/worker/include/RTC/TransportCongestionControlClient.hpp index 17f7684f04..50df900b58 100644 --- a/worker/include/RTC/TransportCongestionControlClient.hpp +++ b/worker/include/RTC/TransportCongestionControlClient.hpp @@ -8,7 +8,7 @@ #include "RTC/RtpPacket.hpp" #include "RTC/RtpProbationGenerator.hpp" #include "RTC/TrendCalculator.hpp" -#include "handles/Timer.hpp" +#include "handles/TimerHandle.hpp" #include #include #include @@ -21,7 +21,7 @@ namespace RTC class TransportCongestionControlClient : public webrtc::PacketRouter, public webrtc::TargetTransferRateObserver, - public Timer::Listener + public TimerHandle::Listener { public: struct Bitrates @@ -56,8 +56,9 @@ namespace RTC RTC::TransportCongestionControlClient::Listener* listener, RTC::BweType bweType, uint32_t initialAvailableBitrate, - uint32_t maxOutgoingBitrate); - virtual ~TransportCongestionControlClient(); + uint32_t maxOutgoingBitrate, + uint32_t minOutgoingBitrate); + ~TransportCongestionControlClient() override; public: RTC::BweType GetBweType() const @@ -68,12 +69,13 @@ namespace RTC void TransportDisconnected(); void InsertPacket(webrtc::RtpPacketSendInfo& packetInfo); webrtc::PacedPacketInfo GetPacingInfo(); - void PacketSent(webrtc::RtpPacketSendInfo& packetInfo, int64_t nowMs); + void PacketSent(const webrtc::RtpPacketSendInfo& packetInfo, int64_t nowMs); void ReceiveEstimatedBitrate(uint32_t bitrate); void ReceiveRtcpReceiverReport(RTC::RTCP::ReceiverReportPacket* packet, float rtt, int64_t nowMs); void ReceiveRtcpTransportFeedback(const RTC::RTCP::FeedbackRtpTransportPacket* feedback); void SetDesiredBitrate(uint32_t desiredBitrate, bool force); void SetMaxOutgoingBitrate(uint32_t maxBitrate); + void SetMinOutgoingBitrate(uint32_t minBitrate); const Bitrates& GetBitrates() const { return this->bitrates; @@ -102,9 +104,9 @@ namespace RTC void SendPacket(RTC::RtpPacket* packet, const webrtc::PacedPacketInfo& pacingInfo) override; RTC::RtpPacket* GeneratePadding(size_t size) override; - /* Pure virtual methods inherited from RTC::Timer. */ + /* Pure virtual methods inherited from RTC::TimerHandle. */ public: - void OnTimer(Timer* timer) override; + void OnTimer(TimerHandle* timer) override; private: // Passed by argument. @@ -113,11 +115,12 @@ namespace RTC webrtc::NetworkControllerFactoryInterface* controllerFactory{ nullptr }; webrtc::RtpTransportControllerSend* rtpTransportControllerSend{ nullptr }; RTC::RtpProbationGenerator* probationGenerator{ nullptr }; - Timer* processTimer{ nullptr }; + TimerHandle* processTimer{ nullptr }; // Others. RTC::BweType bweType; uint32_t initialAvailableBitrate{ 0u }; uint32_t maxOutgoingBitrate{ 0u }; + uint32_t minOutgoingBitrate{ 0u }; Bitrates bitrates; bool availableBitrateEventCalled{ false }; uint64_t lastAvailableBitrateEventAtMs{ 0u }; diff --git a/worker/include/RTC/TransportCongestionControlServer.hpp b/worker/include/RTC/TransportCongestionControlServer.hpp index 5747d13d03..af5e6d7482 100644 --- a/worker/include/RTC/TransportCongestionControlServer.hpp +++ b/worker/include/RTC/TransportCongestionControlServer.hpp @@ -6,14 +6,15 @@ #include "RTC/RTCP/FeedbackRtpTransport.hpp" #include "RTC/RTCP/Packet.hpp" #include "RTC/RtpPacket.hpp" -#include "handles/Timer.hpp" +#include "RTC/SeqManager.hpp" +#include "handles/TimerHandle.hpp" #include #include namespace RTC { class TransportCongestionControlServer : public webrtc::RemoteBitrateEstimator::Listener, - public Timer::Listener + public TimerHandle::Listener { public: class Listener @@ -31,7 +32,7 @@ namespace RTC RTC::TransportCongestionControlServer::Listener* listener, RTC::BweType bweType, size_t maxRtcpPacketLen); - virtual ~TransportCongestionControlServer(); + ~TransportCongestionControlServer() override; public: RTC::BweType GetBweType() const @@ -54,11 +55,15 @@ namespace RTC double GetPacketLoss() const; void IncomingPacket(uint64_t nowMs, const RTC::RtpPacket* packet); void SetMaxIncomingBitrate(uint32_t bitrate); + void FillAndSendTransportCcFeedback(); private: - void SendTransportCcFeedback(); - void MaySendLimitationRembFeedback(); + // Returns true if a feedback packet was sent. + bool SendTransportCcFeedback(); + void MayDropOldPacketArrivalTimes(uint16_t seqNum, uint64_t nowMs); + void MaySendLimitationRembFeedback(uint64_t nowMs); void UpdatePacketLoss(double packetLoss); + void ResetTransportCcFeedback(uint8_t feedbackPacketCount); /* Pure virtual methods inherited from webrtc::RemoteBitrateEstimator::Listener. */ public: @@ -67,15 +72,15 @@ namespace RTC const std::vector& ssrcs, uint32_t availableBitrate) override; - /* Pure virtual methods inherited from Timer::Listener. */ + /* Pure virtual methods inherited from TimerHandle::Listener. */ public: - void OnTimer(Timer* timer) override; + void OnTimer(TimerHandle* timer) override; private: // Passed by argument. Listener* listener{ nullptr }; // Allocated by this. - Timer* transportCcFeedbackSendPeriodicTimer{ nullptr }; + TimerHandle* transportCcFeedbackSendPeriodicTimer{ nullptr }; std::unique_ptr transportCcFeedbackPacket; webrtc::RemoteBitrateEstimatorAbsSendTime* rembServer{ nullptr }; // Others. @@ -89,6 +94,11 @@ namespace RTC uint8_t unlimitedRembCounter{ 0u }; std::deque packetLossHistory; double packetLoss{ 0 }; + // Whether any packet with transport wide sequence number was received. + bool transportWideSeqNumberReceived{ false }; + uint16_t transportCcFeedbackWideSeqNumStart{ 0u }; + // Map of arrival timestamp (ms) indexed by wide seq number. + std::map::SeqLowerThan> mapPacketArrivalTimes; }; } // namespace RTC diff --git a/worker/include/RTC/TransportTuple.hpp b/worker/include/RTC/TransportTuple.hpp index 535d221a68..8b01dbc476 100644 --- a/worker/include/RTC/TransportTuple.hpp +++ b/worker/include/RTC/TransportTuple.hpp @@ -3,13 +3,12 @@ #include "common.hpp" #include "Utils.hpp" +#include "FBS/transport.h" #include "RTC/TcpConnection.hpp" #include "RTC/UdpSocket.hpp" -#include +#include #include -using json = nlohmann::json; - namespace RTC { class TransportTuple @@ -18,12 +17,15 @@ namespace RTC using onSendCallback = const std::function; public: - enum class Protocol + enum class Protocol : uint8_t { UDP = 1, TCP }; + static Protocol ProtocolFromFbs(FBS::Transport::Protocol protocol); + static FBS::Transport::Protocol ProtocolToFbs(Protocol protocol); + public: TransportTuple(RTC::UdpSocket* udpSocket, const struct sockaddr* udpRemoteAddr) : udpSocket(udpSocket), udpRemoteAddr((struct sockaddr*)udpRemoteAddr), protocol(Protocol::UDP) @@ -39,31 +41,19 @@ namespace RTC explicit TransportTuple(const TransportTuple* tuple) : hash(tuple->hash), udpSocket(tuple->udpSocket), udpRemoteAddr(tuple->udpRemoteAddr), - tcpConnection(tuple->tcpConnection), localAnnouncedIp(tuple->localAnnouncedIp), + tcpConnection(tuple->tcpConnection), localAnnouncedAddress(tuple->localAnnouncedAddress), protocol(tuple->protocol) { if (protocol == TransportTuple::Protocol::UDP) + { StoreUdpRemoteAddress(); + } } public: - void Close() - { - if (this->protocol == Protocol::UDP) - this->udpSocket->Close(); - else - this->tcpConnection->Close(); - } + void CloseTcpConnection(); - bool IsClosed() - { - if (this->protocol == Protocol::UDP) - return this->udpSocket->IsClosed(); - else - return this->tcpConnection->IsClosed(); - } - - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer(flatbuffers::FlatBufferBuilder& builder) const; void Dump() const; @@ -80,17 +70,21 @@ namespace RTC return this->hash == tuple->hash; } - void SetLocalAnnouncedIp(std::string& localAnnouncedIp) + void SetLocalAnnouncedAddress(std::string& localAnnouncedAddress) { - this->localAnnouncedIp = localAnnouncedIp; + this->localAnnouncedAddress = localAnnouncedAddress; } void Send(const uint8_t* data, size_t len, RTC::TransportTuple::onSendCallback* cb = nullptr) { if (this->protocol == Protocol::UDP) + { this->udpSocket->Send(data, len, this->udpRemoteAddr, cb); + } else + { this->tcpConnection->Send(data, len, cb); + } } Protocol GetProtocol() const @@ -101,106 +95,54 @@ namespace RTC const struct sockaddr* GetLocalAddress() const { if (this->protocol == Protocol::UDP) + { return this->udpSocket->GetLocalAddress(); + } else + { return this->tcpConnection->GetLocalAddress(); + } } const struct sockaddr* GetRemoteAddress() const { if (this->protocol == Protocol::UDP) - return (const struct sockaddr*)this->udpRemoteAddr; + { + return static_cast(this->udpRemoteAddr); + } else + { return this->tcpConnection->GetPeerAddress(); + } } size_t GetRecvBytes() const { if (this->protocol == Protocol::UDP) + { return this->udpSocket->GetRecvBytes(); + } else + { return this->tcpConnection->GetRecvBytes(); + } } size_t GetSentBytes() const { if (this->protocol == Protocol::UDP) - return this->udpSocket->GetSentBytes(); - else - return this->tcpConnection->GetSentBytes(); - } - - private: - /* - * Hash for IPv4 - * - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | PORT | IP | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | IP | |F|P| - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * - * Hash for IPv6 - * - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | PORT | IP[0] ^ IP[1] ^ IP[2] ^ IP[3]| - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |IP[0] ^ IP[1] ^ IP[2] ^ IP[3] | IP[0] >> 16 |F|P| - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - void SetHash() - { - const struct sockaddr* remoteSockAddr = GetRemoteAddress(); - - switch (remoteSockAddr->sa_family) { - case AF_INET: - { - auto* remoteSockAddrIn = reinterpret_cast(remoteSockAddr); - - const uint64_t address = ntohl(remoteSockAddrIn->sin_addr.s_addr); - const uint64_t port = (ntohs(remoteSockAddrIn->sin_port)); - - this->hash = port << 48; - this->hash |= address << 16; - this->hash |= 0x0000; // AF_INET. - - break; - } - - case AF_INET6: - { - auto* remoteSockAddrIn6 = reinterpret_cast(remoteSockAddr); - auto* a = reinterpret_cast(std::addressof(remoteSockAddrIn6->sin6_addr)); - - const auto address1 = a[0] ^ a[1] ^ a[2] ^ a[3]; - const auto address2 = a[0]; - const uint64_t port = ntohs(remoteSockAddrIn6->sin6_port); - - this->hash = port << 48; - this->hash |= static_cast(address1) << 16; - this->hash |= address2 >> 16 & 0xFFFC; - this->hash |= 0x0002; // AF_INET6. - - break; - } - } - - // Override least significant bit with protocol information: - // - If UDP, start with 0. - // - If TCP, start with 1. - if (this->protocol == Protocol::UDP) - { - this->hash |= 0x0000; + return this->udpSocket->GetSentBytes(); } else { - this->hash |= 0x0001; + return this->tcpConnection->GetSentBytes(); } } + private: + void SetHash(); + public: uint64_t hash{ 0u }; @@ -209,9 +151,11 @@ namespace RTC RTC::UdpSocket* udpSocket{ nullptr }; struct sockaddr* udpRemoteAddr{ nullptr }; RTC::TcpConnection* tcpConnection{ nullptr }; - std::string localAnnouncedIp; + std::string localAnnouncedAddress; // Others. - struct sockaddr_storage udpRemoteAddrStorage; + struct sockaddr_storage udpRemoteAddrStorage + { + }; Protocol protocol; }; } // namespace RTC diff --git a/worker/include/RTC/TrendCalculator.hpp b/worker/include/RTC/TrendCalculator.hpp index 1e082a862a..a9150d8724 100644 --- a/worker/include/RTC/TrendCalculator.hpp +++ b/worker/include/RTC/TrendCalculator.hpp @@ -11,7 +11,7 @@ namespace RTC static constexpr float DecreaseFactor{ 0.05f }; // per second. public: - TrendCalculator(float decreaseFactor = DecreaseFactor); + explicit TrendCalculator(float decreaseFactor = DecreaseFactor); public: uint32_t GetValue() const diff --git a/worker/include/RTC/UdpSocket.hpp b/worker/include/RTC/UdpSocket.hpp index 070209f734..3a15f9cc0b 100644 --- a/worker/include/RTC/UdpSocket.hpp +++ b/worker/include/RTC/UdpSocket.hpp @@ -2,12 +2,13 @@ #define MS_RTC_UDP_SOCKET_HPP #include "common.hpp" -#include "handles/UdpSocketHandler.hpp" +#include "RTC/Transport.hpp" +#include "handles/UdpSocketHandle.hpp" #include namespace RTC { - class UdpSocket : public ::UdpSocketHandler + class UdpSocket : public ::UdpSocketHandle { public: class Listener @@ -21,11 +22,17 @@ namespace RTC }; public: - UdpSocket(Listener* listener, std::string& ip); - UdpSocket(Listener* listener, std::string& ip, uint16_t port); + UdpSocket(Listener* listener, std::string& ip, uint16_t port, RTC::Transport::SocketFlags& flags); + UdpSocket( + Listener* listener, + std::string& ip, + uint16_t minPort, + uint16_t maxPort, + RTC::Transport::SocketFlags& flags, + uint64_t& portRangeHash); ~UdpSocket() override; - /* Pure virtual methods inherited from ::UdpSocketHandler. */ + /* Pure virtual methods inherited from ::UdpSocketHandle. */ public: void UserOnUdpDatagramReceived(const uint8_t* data, size_t len, const struct sockaddr* addr) override; @@ -33,6 +40,7 @@ namespace RTC // Passed by argument. Listener* listener{ nullptr }; bool fixedPort{ false }; + uint64_t portRangeHash{ 0u }; }; } // namespace RTC diff --git a/worker/include/RTC/WebRtcServer.hpp b/worker/include/RTC/WebRtcServer.hpp index f0197b2d3a..11f3901dfb 100644 --- a/worker/include/RTC/WebRtcServer.hpp +++ b/worker/include/RTC/WebRtcServer.hpp @@ -10,9 +10,9 @@ #include "RTC/TransportTuple.hpp" #include "RTC/UdpSocket.hpp" #include "RTC/WebRtcTransport.hpp" +#include #include #include -#include #include #include @@ -24,44 +24,42 @@ namespace RTC public RTC::WebRtcTransport::WebRtcTransportListener, public Channel::ChannelSocket::RequestHandler { - private: - struct ListenInfo - { - RTC::TransportTuple::Protocol protocol; - std::string ip; - std::string announcedIp; - uint16_t port; - }; - private: struct UdpSocketOrTcpServer { // Expose a constructor to use vector.emplace_back(). - UdpSocketOrTcpServer(RTC::UdpSocket* udpSocket, RTC::TcpServer* tcpServer, std::string& announcedIp) - : udpSocket(udpSocket), tcpServer(tcpServer), announcedIp(announcedIp) + UdpSocketOrTcpServer( + RTC::UdpSocket* udpSocket, RTC::TcpServer* tcpServer, std::string& announcedAddress) + : udpSocket(udpSocket), tcpServer(tcpServer), announcedAddress(announcedAddress) { } RTC::UdpSocket* udpSocket; RTC::TcpServer* tcpServer; - std::string announcedIp; + std::string announcedAddress; }; + private: + static std::string GetLocalIceUsernameFragmentFromReceivedStunPacket(RTC::StunPacket* packet); + public: - WebRtcServer(RTC::Shared* shared, const std::string& id, json& data); - ~WebRtcServer(); + WebRtcServer( + RTC::Shared* shared, + const std::string& id, + const flatbuffers::Vector>* listenInfos); + ~WebRtcServer() override; public: - void FillJson(json& jsonObject) const; + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; std::vector GetIceCandidates( - bool enableUdp, bool enableTcp, bool preferUdp, bool preferTcp); + bool enableUdp, bool enableTcp, bool preferUdp, bool preferTcp) const; /* Methods inherited from Channel::ChannelSocket::RequestHandler. */ public: void HandleRequest(Channel::ChannelRequest* request) override; private: - std::string GetLocalIceUsernameFragmentFromReceivedStunPacket(RTC::StunPacket* packet) const; void OnPacketReceived(RTC::TransportTuple* tuple, const uint8_t* data, size_t len); void OnStunDataReceived(RTC::TransportTuple* tuple, const uint8_t* data, size_t len); void OnNonStunDataReceived(RTC::TransportTuple* tuple, const uint8_t* data, size_t len); @@ -108,6 +106,8 @@ namespace RTC absl::flat_hash_map mapLocalIceUsernameFragmentWebRtcTransport; // Map of WebRtcTransports indexed by TransportTuple.hash. absl::flat_hash_map mapTupleWebRtcTransport; + // Whether the destructor has been called. + bool closing{ false }; }; } // namespace RTC diff --git a/worker/include/RTC/WebRtcTransport.hpp b/worker/include/RTC/WebRtcTransport.hpp index 2eae6d856a..31c7a1e484 100644 --- a/worker/include/RTC/WebRtcTransport.hpp +++ b/worker/include/RTC/WebRtcTransport.hpp @@ -23,13 +23,6 @@ namespace RTC public RTC::IceServer::Listener, public RTC::DtlsTransport::Listener { - private: - struct ListenIp - { - std::string ip; - std::string announcedIp; - }; - public: class WebRtcTransportListener { @@ -51,19 +44,24 @@ namespace RTC public: WebRtcTransport( - RTC::Shared* shared, const std::string& id, RTC::Transport::Listener* listener, json& data); + RTC::Shared* shared, + const std::string& id, + RTC::Transport::Listener* listener, + const FBS::WebRtcTransport::WebRtcTransportOptions* options); WebRtcTransport( RTC::Shared* shared, const std::string& id, RTC::Transport::Listener* listener, WebRtcTransportListener* webRtcTransportListener, - std::vector& iceCandidates, - json& data); + const std::vector& iceCandidates, + const FBS::WebRtcTransport::WebRtcTransportOptions* options); ~WebRtcTransport() override; public: - void FillJson(json& jsonObject) const override; - void FillJsonStats(json& jsonArray) override; + flatbuffers::Offset FillBufferStats( + flatbuffers::FlatBufferBuilder& builder); + flatbuffers::Offset FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const; void ProcessStunPacketFromWebRtcServer(RTC::TransportTuple* tuple, RTC::StunPacket* packet); void ProcessNonStunPacketFromWebRtcServer( RTC::TransportTuple* tuple, const uint8_t* data, size_t len); @@ -73,9 +71,9 @@ namespace RTC public: void HandleRequest(Channel::ChannelRequest* request) override; - /* Methods inherited from PayloadChannel::PayloadChannelSocket::NotificationHandler. */ + /* Methods inherited from Channel::ChannelSocket::NotificationHandler. */ public: - void HandleNotification(PayloadChannel::PayloadChannelNotification* notification) override; + void HandleNotification(Channel::ChannelNotification* notification) override; private: bool IsConnected() const override; @@ -88,9 +86,9 @@ namespace RTC void SendRtcpCompoundPacket(RTC::RTCP::CompoundPacket* packet) override; void SendMessage( RTC::DataConsumer* dataConsumer, - uint32_t ppid, const uint8_t* msg, size_t len, + uint32_t ppid, onQueuedCallback* cb = nullptr) override; void SendSctpData(const uint8_t* data, size_t len) override; void RecvStreamClosed(uint32_t ssrc) override; @@ -155,14 +153,15 @@ namespace RTC WebRtcTransportListener* webRtcTransportListener{ nullptr }; // Allocated by this. RTC::IceServer* iceServer{ nullptr }; - // Map of UdpSocket/TcpServer and local announced IP (if any). + // Map of UdpSocket/TcpServer and local announced address (if any). absl::flat_hash_map udpSockets; absl::flat_hash_map tcpServers; RTC::DtlsTransport* dtlsTransport{ nullptr }; RTC::SrtpSession* srtpRecvSession{ nullptr }; RTC::SrtpSession* srtpSendSession{ nullptr }; // Others. - bool connectCalled{ false }; // Whether connect() was succesfully called. + // Whether connect() was succesfully called. + bool connectCalled{ false }; std::vector iceCandidates; RTC::DtlsTransport::Role dtlsRole{ RTC::DtlsTransport::Role::AUTO }; }; diff --git a/worker/include/Settings.hpp b/worker/include/Settings.hpp index bd3343546d..1025260e3d 100644 --- a/worker/include/Settings.hpp +++ b/worker/include/Settings.hpp @@ -39,6 +39,7 @@ class Settings std::string dtlsCertificateFile; std::string dtlsPrivateKeyFile; std::string libwebrtcFieldTrials{ "WebRTC-Bwe-AlrLimitedBackoff/Enabled/" }; + bool liburingDisabled{ false }; }; public: @@ -55,8 +56,8 @@ class Settings thread_local static struct Configuration configuration; private: - static absl::flat_hash_map string2LogLevel; - static absl::flat_hash_map logLevel2String; + static absl::flat_hash_map String2LogLevel; // NOLINT(readability-identifier-naming) + static absl::flat_hash_map LogLevel2String; // NOLINT(readability-identifier-naming) }; #endif diff --git a/worker/include/Utils.hpp b/worker/include/Utils.hpp index b19415c346..53bb2d3cde 100644 --- a/worker/include/Utils.hpp +++ b/worker/include/Utils.hpp @@ -5,7 +5,6 @@ #include #include #include // std::memcmp(), std::memcpy() -#include #include #include #ifdef _WIN32 @@ -15,8 +14,6 @@ #define __builtin_popcount __popcnt #endif -using json = nlohmann::json; - namespace Utils { class IP @@ -26,10 +23,15 @@ namespace Utils static void GetAddressInfo(const struct sockaddr* addr, int& family, std::string& ip, uint16_t& port); + static size_t GetAddressLen(const struct sockaddr* addr); + static bool CompareAddresses(const struct sockaddr* addr1, const struct sockaddr* addr2) { // Compare family. - if (addr1->sa_family != addr2->sa_family || (addr1->sa_family != AF_INET && addr1->sa_family != AF_INET6)) + if ( + addr1->sa_family != addr2->sa_family || + (addr1->sa_family != AF_INET && addr1->sa_family != AF_INET6) || + (addr2->sa_family != AF_INET && addr2->sa_family != AF_INET6)) { return false; } @@ -58,9 +60,7 @@ namespace Utils std::memcmp( std::addressof(reinterpret_cast(addr1)->sin6_addr), std::addressof(reinterpret_cast(addr2)->sin6_addr), - 16) == 0 - ? true - : false); + 16) == 0); } default: @@ -72,7 +72,9 @@ namespace Utils static struct sockaddr_storage CopyAddress(const struct sockaddr* addr) { - struct sockaddr_storage copiedAddr; + struct sockaddr_storage copiedAddr + { + }; switch (addr->sa_family) { @@ -119,6 +121,19 @@ namespace Utils return uint32_t{ data[i + 2] } | uint32_t{ data[i + 1] } << 8 | uint32_t{ data[i] } << 16; } + static int32_t Get3BytesSigned(const uint8_t* data, size_t i) + { + auto byte2 = data[i]; // The most significant byte. + auto byte1 = data[i + 1]; + auto byte0 = data[i + 2]; // The less significant byte. + + // Check bit 7 (sign). + const uint8_t extension = byte2 & 0b10000000 ? 0b11111111 : 0b00000000; + + return int32_t{ byte0 } | (int32_t{ byte1 } << 8) | (int32_t{ byte2 } << 16) | + (int32_t{ extension } << 24); + } + static uint32_t Get4Bytes(const uint8_t* data, size_t i) { return uint32_t{ data[i + 3] } | uint32_t{ data[i + 2] } << 8 | @@ -148,6 +163,13 @@ namespace Utils data[i] = static_cast(value >> 16); } + static void Set3BytesSigned(uint8_t* data, size_t i, int32_t value) + { + data[i + 2] = static_cast(value); + data[i + 1] = static_cast(value >> 8); + data[i] = static_cast(value >> 16); + } + static void Set4Bytes(uint8_t* data, size_t i, uint32_t value) { data[i + 3] = static_cast(value); @@ -172,18 +194,26 @@ namespace Utils { // If size is not multiple of 32 bits then pad it. if (size & 0x03) + { return (size & 0xFFFC) + 4; + } else + { return size; + } } static uint32_t PadTo4Bytes(uint32_t size) { // If size is not multiple of 32 bits then pad it. if (size & 0x03) + { return (size & 0xFFFFFFFC) + 4; + } else + { return size; + } } }; @@ -213,30 +243,36 @@ namespace Utils // Special case. if (max == 4294967295) + { --max; + } if (min > max) + { min = max; + } return (((Crypto::seed >> 4) & 0x7FFF7FFF) % (max - min + 1)) + min; } - static const std::string GetRandomString(size_t len) + static std::string GetRandomString(size_t len) { char buffer[64]; - static const char chars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', + static const char Chars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' }; if (len > 64) + { len = 64; + } for (size_t i{ 0 }; i < len; ++i) { - buffer[i] = chars[GetRandomUInt(0, sizeof(chars) - 1)]; + buffer[i] = Chars[GetRandomUInt(0, sizeof(Chars) - 1)]; } - return std::string(buffer, len); + return { buffer, len }; } static uint32_t GetCRC32(const uint8_t* data, size_t size) @@ -246,7 +282,7 @@ namespace Utils while (size--) { - crc = Crypto::crc32Table[(crc ^ *p++) & 0xFF] ^ (crc >> 8); + crc = Crypto::Crc32Table[(crc ^ *p++) & 0xFF] ^ (crc >> 8); } return crc ^ ~0U; @@ -259,7 +295,7 @@ namespace Utils thread_local static EVP_MAC* mac; thread_local static EVP_MAC_CTX* hmacSha1Ctx; thread_local static uint8_t hmacSha1Buffer[]; - static const uint32_t crc32Table[256]; + static const uint32_t Crc32Table[256]; }; class String @@ -277,8 +313,6 @@ namespace Utils static uint8_t* Base64Decode(const uint8_t* data, size_t len, size_t& outLen); static uint8_t* Base64Decode(const std::string& str, size_t& outLen); - - static std::vector Split(const std::string& str, char separator, size_t limit = 0); }; class Time @@ -297,7 +331,7 @@ namespace Utils static Time::Ntp TimeMs2Ntp(uint64_t ms) { - Time::Ntp ntp; // NOLINT(cppcoreguidelines-pro-type-member-init) + Time::Ntp ntp{}; // NOLINT(cppcoreguidelines-pro-type-member-init) ntp.seconds = ms / 1000; ntp.fractions = @@ -323,10 +357,12 @@ namespace Utils // IsNewer(t2,t1)=false // rather than having IsNewer(t1,t2) = IsNewer(t2,t1) = false. if (static_cast(timestamp - prevTimestamp) == 0x80000000) + { return timestamp > prevTimestamp; + } - return timestamp != prevTimestamp && - static_cast(timestamp - prevTimestamp) < 0x80000000; + return ( + timestamp != prevTimestamp && static_cast(timestamp - prevTimestamp) < 0x80000000); } static uint32_t LatestTimestamp(uint32_t timestamp1, uint32_t timestamp2) @@ -339,20 +375,6 @@ namespace Utils return static_cast(((ms << 18) + 500) / 1000) & 0x00FFFFFF; } }; - - class Json - { - public: - static bool IsPositiveInteger(const json& value) - { - if (value.is_number_unsigned()) - return true; - else if (value.is_number_integer()) - return value.get() >= 0; - else - return false; - } - }; } // namespace Utils #endif diff --git a/worker/include/Worker.hpp b/worker/include/Worker.hpp index 48d993bf7a..24483766b6 100644 --- a/worker/include/Worker.hpp +++ b/worker/include/Worker.hpp @@ -4,60 +4,46 @@ #include "common.hpp" #include "Channel/ChannelRequest.hpp" #include "Channel/ChannelSocket.hpp" -#include "PayloadChannel/PayloadChannelNotification.hpp" -#include "PayloadChannel/PayloadChannelRequest.hpp" -#include "PayloadChannel/PayloadChannelSocket.hpp" +#include "FBS/worker.h" #include "RTC/Router.hpp" #include "RTC/Shared.hpp" #include "RTC/WebRtcServer.hpp" -#include "handles/SignalsHandler.hpp" +#include "handles/SignalHandle.hpp" +#include #include -#include #include -using json = nlohmann::json; - class Worker : public Channel::ChannelSocket::Listener, - public PayloadChannel::PayloadChannelSocket::Listener, - public SignalsHandler::Listener, + public SignalHandle::Listener, public RTC::Router::Listener { public: - explicit Worker(Channel::ChannelSocket* channel, PayloadChannel::PayloadChannelSocket* payloadChannel); + explicit Worker(Channel::ChannelSocket* channel); ~Worker(); private: void Close(); - void FillJson(json& jsonObject) const; - void FillJsonResourceUsage(json& jsonObject) const; - void SetNewWebRtcServerIdFromData(json& data, std::string& webRtcServerId) const; - RTC::WebRtcServer* GetWebRtcServerFromData(json& data) const; - void SetNewRouterIdFromData(json& data, std::string& routerId) const; - RTC::Router* GetRouterFromData(json& data) const; + flatbuffers::Offset FillBuffer(flatbuffers::FlatBufferBuilder& builder) const; + flatbuffers::Offset FillBufferResourceUsage( + flatbuffers::FlatBufferBuilder& builder) const; + void SetNewRouterId(std::string& routerId) const; + RTC::WebRtcServer* GetWebRtcServer(const std::string& webRtcServerId) const; + RTC::Router* GetRouter(const std::string& routerId) const; + void CheckNoWebRtcServer(const std::string& webRtcServerId) const; + void CheckNoRouter(const std::string& routerId) const; /* Methods inherited from Channel::ChannelSocket::RequestHandler. */ public: void HandleRequest(Channel::ChannelRequest* request) override; + void HandleNotification(Channel::ChannelNotification* notification) override; /* Methods inherited from Channel::ChannelSocket::Listener. */ public: void OnChannelClosed(Channel::ChannelSocket* channel) override; - /* Methods inherited from PayloadChannel::PayloadChannelSocket::RequestHandler. */ -public: - void HandleRequest(PayloadChannel::PayloadChannelRequest* request) override; - - /* Methods inherited from PayloadChannel::PayloadChannelSocket::NotificationHandler. */ -public: - void HandleNotification(PayloadChannel::PayloadChannelNotification* notification) override; - - /* Methods inherited from PayloadChannel::PayloadChannelSocket::Listener. */ -public: - void OnPayloadChannelClosed(PayloadChannel::PayloadChannelSocket* payloadChannel) override; - - /* Methods inherited from SignalsHandler::Listener. */ + /* Methods inherited from SignalHandle::Listener. */ public: - void OnSignal(SignalsHandler* signalsHandler, int signum) override; + void OnSignal(SignalHandle* signalsHandler, int signum) override; /* Pure virtual methods inherited from RTC::Router::Listener. */ public: @@ -66,9 +52,8 @@ class Worker : public Channel::ChannelSocket::Listener, private: // Passed by argument. Channel::ChannelSocket* channel{ nullptr }; - PayloadChannel::PayloadChannelSocket* payloadChannel{ nullptr }; // Allocated by this. - SignalsHandler* signalsHandler{ nullptr }; + SignalHandle* signalHandle{ nullptr }; RTC::Shared* shared{ nullptr }; absl::flat_hash_map mapWebRtcServers; absl::flat_hash_map mapRouters; diff --git a/worker/include/common.hpp b/worker/include/common.hpp index 1662579563..3934db04b8 100644 --- a/worker/include/common.hpp +++ b/worker/include/common.hpp @@ -7,9 +7,10 @@ #include // uint8_t, etc #include // std::function #include // std::addressof() +#include #ifdef _WIN32 #include -// avoid uv/win.h: error C2628 'intptr_t' followed by 'int' is illegal. +// Avoid uv/win.h: error C2628 'intptr_t' followed by 'int' is illegal. #if !defined(_SSIZE_T_) && !defined(_SSIZE_T_DEFINED) #include typedef SSIZE_T ssize_t; @@ -25,41 +26,20 @@ typedef SSIZE_T ssize_t; using ChannelReadCtx = void*; using ChannelReadFreeFn = void (*)(uint8_t*, uint32_t, size_t); -// Returns `ChannelReadFree` on successful read that must be used to free `message`. +// Returns `ChannelReadFree` on successful read that must be used to free +// `message`. using ChannelReadFn = ChannelReadFreeFn (*)( uint8_t** /* message */, uint32_t* /* messageLen */, size_t* /* messageCtx */, - // This is `uv_async_t` handle that can be called later with `uv_async_send()` when there is more - // data to read. + // This is `uv_async_t` handle that can be called later with `uv_async_send()` + // when there is more data to read. const void* /* handle */, - ChannelReadCtx /* ctx */); + ChannelReadCtx /* ctx */ +); using ChannelWriteCtx = void*; using ChannelWriteFn = void (*)(const uint8_t* /* message */, uint32_t /* messageLen */, ChannelWriteCtx /* ctx */); -using PayloadChannelReadCtx = void*; -using PayloadChannelReadFreeFn = void (*)(uint8_t*, uint32_t, size_t); -// Returns `PayloadChannelReadFree` on successful read that must be used to free `message` and `payload`. -using PayloadChannelReadFn = PayloadChannelReadFreeFn (*)( - uint8_t** /* message */, - uint32_t* /* messageLen */, - size_t* /* messageCtx */, - uint8_t** /* payload */, - uint32_t* /* payloadLen */, - size_t* /* payloadCapacity */, - // This is `uv_async_t` handle that can be called later with `uv_async_send()` when there is more - // data to read. - const void* /* handle */, - PayloadChannelReadCtx /* ctx */); - -using PayloadChannelWriteCtx = void*; -using PayloadChannelWriteFn = void (*)( - const uint8_t* /* message */, - uint32_t /* messageLen */, - const uint8_t* /* payload */, - uint32_t /* payloadLen */, - ChannelWriteCtx /* ctx */); - #endif diff --git a/worker/include/handles/SignalsHandler.hpp b/worker/include/handles/SignalHandle.hpp similarity index 65% rename from worker/include/handles/SignalsHandler.hpp rename to worker/include/handles/SignalHandle.hpp index 7c9407b7d4..462b0e7902 100644 --- a/worker/include/handles/SignalsHandler.hpp +++ b/worker/include/handles/SignalHandle.hpp @@ -1,11 +1,11 @@ -#ifndef MS_SIGNALS_HANDLER_HPP -#define MS_SIGNALS_HANDLER_HPP +#ifndef MS_SIGNAL_HANDLE_HPP +#define MS_SIGNAL_HANDLE_HPP #include #include #include -class SignalsHandler +class SignalHandle { public: class Listener @@ -14,17 +14,19 @@ class SignalsHandler virtual ~Listener() = default; public: - virtual void OnSignal(SignalsHandler* signalsHandler, int signum) = 0; + virtual void OnSignal(SignalHandle* signalsHandler, int signum) = 0; }; public: - explicit SignalsHandler(Listener* listener); - ~SignalsHandler(); + explicit SignalHandle(Listener* listener); + ~SignalHandle(); public: - void Close(); void AddSignal(int signum, const std::string& name); +private: + void InternalClose(); + /* Callbacks fired by UV events. */ public: void OnUvSignal(int signum); diff --git a/worker/include/handles/TcpConnectionHandler.hpp b/worker/include/handles/TcpConnectionHandle.hpp similarity index 76% rename from worker/include/handles/TcpConnectionHandler.hpp rename to worker/include/handles/TcpConnectionHandle.hpp index 0b536225ba..ad9cc12807 100644 --- a/worker/include/handles/TcpConnectionHandler.hpp +++ b/worker/include/handles/TcpConnectionHandle.hpp @@ -1,11 +1,11 @@ -#ifndef MS_TCP_CONNECTION_HPP -#define MS_TCP_CONNECTION_HPP +#ifndef MS_TCP_CONNECTION_HANDLE_HPP +#define MS_TCP_CONNECTION_HANDLE_HPP #include "common.hpp" #include #include -class TcpConnectionHandler +class TcpConnectionHandle { protected: using onSendCallback = const std::function; @@ -17,16 +17,15 @@ class TcpConnectionHandler virtual ~Listener() = default; public: - virtual void OnTcpConnectionClosed(TcpConnectionHandler* connection) = 0; + virtual void OnTcpConnectionClosed(TcpConnectionHandle* connection) = 0; }; public: /* Struct for the data field of uv_req_t when writing into the connection. */ struct UvWriteData { - explicit UvWriteData(size_t storeSize) + explicit UvWriteData(size_t storeSize) : store(new uint8_t[storeSize]) { - this->store = new uint8_t[storeSize]; } // Disable copy constructor because of the dynamically allocated data (store). @@ -38,24 +37,24 @@ class TcpConnectionHandler delete this->cb; } - uv_write_t req; + uv_write_t req{}; uint8_t* store{ nullptr }; - TcpConnectionHandler::onSendCallback* cb{ nullptr }; + TcpConnectionHandle::onSendCallback* cb{ nullptr }; }; public: - explicit TcpConnectionHandler(size_t bufferSize); - TcpConnectionHandler& operator=(const TcpConnectionHandler&) = delete; - TcpConnectionHandler(const TcpConnectionHandler&) = delete; - virtual ~TcpConnectionHandler(); + explicit TcpConnectionHandle(size_t bufferSize); + TcpConnectionHandle& operator=(const TcpConnectionHandle&) = delete; + TcpConnectionHandle(const TcpConnectionHandle&) = delete; + virtual ~TcpConnectionHandle(); public: - void Close(); + void TriggerClose(); bool IsClosed() const { return this->closed; } - virtual void Dump() const; + void Dump() const; void Setup( Listener* listener, struct sockaddr_storage* localAddr, @@ -71,7 +70,7 @@ class TcpConnectionHandler size_t len1, const uint8_t* data2, size_t len2, - TcpConnectionHandler::onSendCallback* cb); + TcpConnectionHandle::onSendCallback* cb); void ErrorReceiving(); const struct sockaddr* GetLocalAddress() const { @@ -111,6 +110,7 @@ class TcpConnectionHandler } private: + void InternalClose(); bool SetPeerAddress(); /* Callbacks fired by UV events. */ @@ -132,7 +132,9 @@ class TcpConnectionHandler size_t bufferDataLen{ 0u }; std::string localIp; uint16_t localPort{ 0u }; - struct sockaddr_storage peerAddr; + struct sockaddr_storage peerAddr + { + }; std::string peerIp; uint16_t peerPort{ 0u }; @@ -143,6 +145,10 @@ class TcpConnectionHandler uv_tcp_t* uvHandle{ nullptr }; // Others. struct sockaddr_storage* localAddr{ nullptr }; +#ifdef MS_LIBURING_SUPPORTED + // Local file descriptor for io_uring. + uv_os_fd_t fd{ 0u }; +#endif bool closed{ false }; size_t recvBytes{ 0u }; size_t sentBytes{ 0u }; diff --git a/worker/include/handles/TcpServerHandler.hpp b/worker/include/handles/TcpServerHandle.hpp similarity index 57% rename from worker/include/handles/TcpServerHandler.hpp rename to worker/include/handles/TcpServerHandle.hpp index ad7b52e8aa..f2ac9a0fc7 100644 --- a/worker/include/handles/TcpServerHandler.hpp +++ b/worker/include/handles/TcpServerHandle.hpp @@ -1,24 +1,23 @@ -#ifndef MS_TCP_SERVER_HPP -#define MS_TCP_SERVER_HPP +#ifndef MS_TCP_SERVER_HANDLE_HPP +#define MS_TCP_SERVER_HANDLE_HPP #include "common.hpp" -#include "handles/TcpConnectionHandler.hpp" +#include "handles/TcpConnectionHandle.hpp" #include #include #include -class TcpServerHandler : public TcpConnectionHandler::Listener +class TcpServerHandle : public TcpConnectionHandle::Listener { public: /** * uvHandle must be an already initialized and binded uv_tcp_t pointer. */ - TcpServerHandler(uv_tcp_t* uvHandle); - virtual ~TcpServerHandler() override; + explicit TcpServerHandle(uv_tcp_t* uvHandle); + ~TcpServerHandle() override; public: - void Close(); - virtual void Dump() const; + void Dump() const; const struct sockaddr* GetLocalAddress() const { return reinterpret_cast(&this->localAddr); @@ -39,28 +38,35 @@ class TcpServerHandler : public TcpConnectionHandler::Listener { return this->connections.size(); } + uint32_t GetSendBufferSize() const; + void SetSendBufferSize(uint32_t size); + uint32_t GetRecvBufferSize() const; + void SetRecvBufferSize(uint32_t size); protected: - void AcceptTcpConnection(TcpConnectionHandler* connection); + void AcceptTcpConnection(TcpConnectionHandle* connection); private: + void InternalClose(); bool SetLocalAddress(); /* Pure virtual methods that must be implemented by the subclass. */ protected: - virtual void UserOnTcpConnectionAlloc() = 0; - virtual void UserOnTcpConnectionClosed(TcpConnectionHandler* connection) = 0; + virtual void UserOnTcpConnectionAlloc() = 0; + virtual void UserOnTcpConnectionClosed(TcpConnectionHandle* connection) = 0; /* Callbacks fired by UV events. */ public: void OnUvConnection(int status); - /* Methods inherited from TcpConnectionHandler::Listener. */ + /* Methods inherited from TcpConnectionHandle::Listener. */ public: - void OnTcpConnectionClosed(TcpConnectionHandler* connection) override; + void OnTcpConnectionClosed(TcpConnectionHandle* connection) override; protected: - struct sockaddr_storage localAddr; + struct sockaddr_storage localAddr + { + }; std::string localIp; uint16_t localPort{ 0u }; @@ -68,7 +74,7 @@ class TcpServerHandler : public TcpConnectionHandler::Listener // Allocated by this (may be passed by argument). uv_tcp_t* uvHandle{ nullptr }; // Others. - absl::flat_hash_set connections; + absl::flat_hash_set connections; bool closed{ false }; }; diff --git a/worker/include/handles/Timer.hpp b/worker/include/handles/TimerHandle.hpp similarity index 69% rename from worker/include/handles/Timer.hpp rename to worker/include/handles/TimerHandle.hpp index dfb9c47ee7..7948ec608f 100644 --- a/worker/include/handles/Timer.hpp +++ b/worker/include/handles/TimerHandle.hpp @@ -1,10 +1,10 @@ -#ifndef MS_TIMER_HPP -#define MS_TIMER_HPP +#ifndef MS_TIMER_HANDLE_HPP +#define MS_TIMER_HANDLE_HPP #include "common.hpp" #include -class Timer +class TimerHandle { public: class Listener @@ -13,17 +13,16 @@ class Timer virtual ~Listener() = default; public: - virtual void OnTimer(Timer* timer) = 0; + virtual void OnTimer(TimerHandle* timer) = 0; }; public: - explicit Timer(Listener* listener); - Timer& operator=(const Timer&) = delete; - Timer(const Timer&) = delete; - ~Timer(); + explicit TimerHandle(Listener* listener); + TimerHandle& operator=(const TimerHandle&) = delete; + TimerHandle(const TimerHandle&) = delete; + ~TimerHandle(); public: - void Close(); void Start(uint64_t timeout, uint64_t repeat = 0); void Stop(); void Reset(); @@ -41,6 +40,9 @@ class Timer return uv_is_active(reinterpret_cast(this->uvHandle)) != 0; } +private: + void InternalClose(); + /* Callbacks fired by UV events. */ public: void OnUvTimer(); diff --git a/worker/include/handles/UdpSocketHandler.hpp b/worker/include/handles/UdpSocketHandle.hpp similarity index 68% rename from worker/include/handles/UdpSocketHandler.hpp rename to worker/include/handles/UdpSocketHandle.hpp index 188f23b4fa..307503b459 100644 --- a/worker/include/handles/UdpSocketHandler.hpp +++ b/worker/include/handles/UdpSocketHandle.hpp @@ -1,11 +1,11 @@ -#ifndef MS_UDP_SOCKET_HPP -#define MS_UDP_SOCKET_HPP +#ifndef MS_UDP_SOCKET_HANDLE_HPP +#define MS_UDP_SOCKET_HANDLE_HPP #include "common.hpp" #include #include -class UdpSocketHandler +class UdpSocketHandle { protected: using onSendCallback = const std::function; @@ -14,9 +14,8 @@ class UdpSocketHandler /* Struct for the data field of uv_req_t when sending a datagram. */ struct UvSendData { - explicit UvSendData(size_t storeSize) + explicit UvSendData(size_t storeSize) : store(new uint8_t[storeSize]) { - this->store = new uint8_t[storeSize]; } // Disable copy constructor because of the dynamically allocated data (store). @@ -28,29 +27,28 @@ class UdpSocketHandler delete this->cb; } - uv_udp_send_t req; + uv_udp_send_t req{}; uint8_t* store{ nullptr }; - UdpSocketHandler::onSendCallback* cb{ nullptr }; + UdpSocketHandle::onSendCallback* cb{ nullptr }; }; public: /** * uvHandle must be an already initialized and binded uv_udp_t pointer. */ - explicit UdpSocketHandler(uv_udp_t* uvHandle); - UdpSocketHandler& operator=(const UdpSocketHandler&) = delete; - UdpSocketHandler(const UdpSocketHandler&) = delete; - virtual ~UdpSocketHandler(); + explicit UdpSocketHandle(uv_udp_t* uvHandle); + UdpSocketHandle& operator=(const UdpSocketHandle&) = delete; + UdpSocketHandle(const UdpSocketHandle&) = delete; + virtual ~UdpSocketHandle(); public: - void Close(); bool IsClosed() const { return this->closed; } - virtual void Dump() const; + void Dump() const; void Send( - const uint8_t* data, size_t len, const struct sockaddr* addr, UdpSocketHandler::onSendCallback* cb); + const uint8_t* data, size_t len, const struct sockaddr* addr, UdpSocketHandle::onSendCallback* cb); const struct sockaddr* GetLocalAddress() const { return reinterpret_cast(&this->localAddr); @@ -75,15 +73,20 @@ class UdpSocketHandler { return this->sentBytes; } + uint32_t GetSendBufferSize() const; + void SetSendBufferSize(uint32_t size); + uint32_t GetRecvBufferSize() const; + void SetRecvBufferSize(uint32_t size); private: + void InternalClose(); bool SetLocalAddress(); /* Callbacks fired by UV events. */ public: void OnUvRecvAlloc(size_t suggestedSize, uv_buf_t* buf); void OnUvRecv(ssize_t nread, const uv_buf_t* buf, const struct sockaddr* addr, unsigned int flags); - void OnUvSend(int status, UdpSocketHandler::onSendCallback* cb); + void OnUvSend(int status, UdpSocketHandle::onSendCallback* cb); /* Pure virtual methods that must be implemented by the subclass. */ protected: @@ -91,7 +94,9 @@ class UdpSocketHandler const uint8_t* data, size_t len, const struct sockaddr* addr) = 0; protected: - struct sockaddr_storage localAddr; + struct sockaddr_storage localAddr + { + }; std::string localIp; uint16_t localPort{ 0u }; @@ -99,6 +104,10 @@ class UdpSocketHandler // Allocated by this (may be passed by argument). uv_udp_t* uvHandle{ nullptr }; // Others. +#ifdef MS_LIBURING_SUPPORTED + // Local file descriptor for io_uring. + uv_os_fd_t fd{ 0u }; +#endif bool closed{ false }; size_t recvBytes{ 0u }; size_t sentBytes{ 0u }; diff --git a/worker/include/handles/UnixStreamSocket.hpp b/worker/include/handles/UnixStreamSocketHandle.hpp similarity index 65% rename from worker/include/handles/UnixStreamSocket.hpp rename to worker/include/handles/UnixStreamSocketHandle.hpp index 5d3f05764e..e7dc9be5e4 100644 --- a/worker/include/handles/UnixStreamSocket.hpp +++ b/worker/include/handles/UnixStreamSocketHandle.hpp @@ -1,19 +1,18 @@ -#ifndef MS_UNIX_STREAM_SOCKET_HPP -#define MS_UNIX_STREAM_SOCKET_HPP +#ifndef MS_UNIX_STREAM_SOCKET_HANDLE_HPP +#define MS_UNIX_STREAM_SOCKET_HANDLE_HPP #include "common.hpp" #include #include -class UnixStreamSocket +class UnixStreamSocketHandle { public: /* Struct for the data field of uv_req_t when writing data. */ struct UvWriteData { - explicit UvWriteData(size_t storeSize) + explicit UvWriteData(size_t storeSize) : store(new uint8_t[storeSize]) { - this->store = new uint8_t[storeSize]; } // Disable copy constructor because of the dynamically allocated data (store). @@ -24,7 +23,7 @@ class UnixStreamSocket delete[] this->store; } - uv_write_t req; + uv_write_t req{}; uint8_t* store{ nullptr }; }; @@ -35,10 +34,10 @@ class UnixStreamSocket }; public: - UnixStreamSocket(int fd, size_t bufferSize, UnixStreamSocket::Role role); - UnixStreamSocket& operator=(const UnixStreamSocket&) = delete; - UnixStreamSocket(const UnixStreamSocket&) = delete; - virtual ~UnixStreamSocket(); + UnixStreamSocketHandle(int fd, size_t bufferSize, UnixStreamSocketHandle::Role role); + UnixStreamSocketHandle& operator=(const UnixStreamSocketHandle&) = delete; + UnixStreamSocketHandle(const UnixStreamSocketHandle&) = delete; + virtual ~UnixStreamSocketHandle(); public: void Close(); @@ -47,6 +46,10 @@ class UnixStreamSocket return this->closed; } void Write(const uint8_t* data, size_t len); + uint32_t GetSendBufferSize() const; + void SetSendBufferSize(uint32_t size); + uint32_t GetRecvBufferSize() const; + void SetRecvBufferSize(uint32_t size); /* Callbacks fired by UV events. */ public: @@ -70,7 +73,7 @@ class UnixStreamSocket protected: // Passed by argument. size_t bufferSize{ 0u }; - UnixStreamSocket::Role role; + UnixStreamSocketHandle::Role role; // Allocated by this. uint8_t* buffer{ nullptr }; // Others. diff --git a/worker/include/lib.hpp b/worker/include/lib.hpp index 36d86d2d74..8656a3c7df 100644 --- a/worker/include/lib.hpp +++ b/worker/include/lib.hpp @@ -6,13 +6,7 @@ extern "C" int mediasoup_worker_run( const char* version, int consumerChannelFd, int producerChannelFd, - int payloadConsumeChannelFd, - int payloadProduceChannelFd, ChannelReadFn channelReadFn, ChannelReadCtx channelReadCtx, ChannelWriteFn channelWriteFn, - ChannelWriteCtx channelWriteCtx, - PayloadChannelReadFn payloadChannelReadFn, - PayloadChannelReadCtx payloadChannelReadCtx, - PayloadChannelWriteFn payloadChannelWriteFn, - PayloadChannelWriteCtx payloadChannelWriteCtx); + ChannelWriteCtx channelWriteCtx); diff --git a/worker/meson.build b/worker/meson.build index ef6e6a9597..8de14990a4 100644 --- a/worker/meson.build +++ b/worker/meson.build @@ -3,10 +3,10 @@ project( ['c', 'cpp'], default_options : [ 'warning_level=1', - 'cpp_std=c++11', + 'cpp_std=c++17', 'default_library=static', ], - meson_version: '>= 0.58', + meson_version: '>= 1.1.0', ) cpp_args = [ @@ -38,6 +38,24 @@ if get_option('ms_log_file_line') ] endif +if get_option('ms_rtc_logger_rtp') + cpp_args += [ + '-DMS_RTC_LOGGER_RTP', + ] +endif + +cpp = meson.get_compiler('cpp') + +# This is a workaround to define FLATBUFFERS_LOCALE_INDEPENDENT=0 outside +# flatbuffers project, which is needed in case the current arch doesn't support +# strtoll_l or strtoull_l (such as musl in Alpine Linux). Problem is that +# flatbuffers build system not only defines it for its own usage but also relies +# on it in its include/flatbuffers/util.h file, and if such a file is included +# by mediasoup source files (and it is included) build fails with "error: +# 'strtoull_l' was not declared in this scope". +# See issue: https://github.com/versatica/mediasoup/issues/1223 +add_project_arguments('-DFLATBUFFERS_LOCALE_INDEPENDENT=@0@'.format(cpp.has_function('strtoull_l')), language: 'cpp') + common_sources = [ 'src/lib.cpp', 'src/DepLibSRTP.cpp', @@ -54,19 +72,16 @@ common_sources = [ 'src/Utils/File.cpp', 'src/Utils/IP.cpp', 'src/Utils/String.cpp', - 'src/handles/SignalsHandler.cpp', - 'src/handles/TcpConnectionHandler.cpp', - 'src/handles/TcpServerHandler.cpp', - 'src/handles/Timer.cpp', - 'src/handles/UdpSocketHandler.cpp', - 'src/handles/UnixStreamSocket.cpp', + 'src/handles/SignalHandle.cpp', + 'src/handles/TcpConnectionHandle.cpp', + 'src/handles/TcpServerHandle.cpp', + 'src/handles/TimerHandle.cpp', + 'src/handles/UdpSocketHandle.cpp', + 'src/handles/UnixStreamSocketHandle.cpp', 'src/Channel/ChannelNotifier.cpp', 'src/Channel/ChannelRequest.cpp', + 'src/Channel/ChannelNotification.cpp', 'src/Channel/ChannelSocket.cpp', - 'src/PayloadChannel/PayloadChannelNotification.cpp', - 'src/PayloadChannel/PayloadChannelNotifier.cpp', - 'src/PayloadChannel/PayloadChannelRequest.cpp', - 'src/PayloadChannel/PayloadChannelSocket.cpp', 'src/RTC/ActiveSpeakerObserver.cpp', 'src/RTC/AudioLevelObserver.cpp', 'src/RTC/Consumer.cpp', @@ -85,10 +100,12 @@ common_sources = [ 'src/RTC/Producer.cpp', 'src/RTC/RateCalculator.cpp', 'src/RTC/Router.cpp', + 'src/RTC/RtcLogger.cpp', 'src/RTC/RtpListener.cpp', 'src/RTC/RtpObserver.cpp', 'src/RTC/RtpPacket.cpp', 'src/RTC/RtpProbationGenerator.cpp', + 'src/RTC/RtpRetransmissionBuffer.cpp', 'src/RTC/RtpStream.cpp', 'src/RTC/RtpStreamRecv.cpp', 'src/RTC/RtpStreamSend.cpp', @@ -118,7 +135,6 @@ common_sources = [ 'src/RTC/Codecs/VP8.cpp', 'src/RTC/Codecs/VP9.cpp', 'src/RTC/Codecs/Opus.cpp', - 'src/RTC/RtpDictionaries/Media.cpp', 'src/RTC/RtpDictionaries/Parameters.cpp', 'src/RTC/RtpDictionaries/RtcpFeedback.cpp', 'src/RTC/RtpDictionaries/RtcpParameters.cpp', @@ -159,20 +175,13 @@ common_sources = [ 'src/RTC/RTCP/XrReceiverReferenceTime.cpp', ] -cpp = meson.get_compiler('cpp') - openssl_proj = subproject( 'openssl', default_options: [ 'warning_level=0', ], ) -nlohmann_json_proj = subproject( - 'nlohmann_json', - default_options: [ - 'warning_level=0', - ], -) + libuv_proj = subproject( 'libuv', default_options: [ @@ -181,6 +190,7 @@ libuv_proj = subproject( 'build_benchmarks=false', ], ) + libsrtp2_proj = subproject( 'libsrtp2', default_options: [ @@ -190,6 +200,7 @@ libsrtp2_proj = subproject( 'tests=disabled', ], ) + usrsctp_proj = subproject( 'usrsctp', default_options: [ @@ -197,28 +208,44 @@ usrsctp_proj = subproject( 'sctp_build_programs=false', ], ) + abseil_cpp_proj = subproject( 'abseil-cpp', default_options: [ 'warning_level=0', + 'cpp_std=c++17', ], ) + catch2_proj = subproject( 'catch2', default_options: [ 'warning_level=0', ], ) -libwebrtc_include_directories = include_directories('include') + +flatbuffers_proj = subproject( + 'flatbuffers', + default_options: [ + 'warning_level=0', + ], +) + +# flatbuffers schemas subdirectory. +subdir('fbs') + +# Add current build directory so libwebrtc has access to FBS folder. +libwebrtc_include_directories = include_directories('include', 'fbs') subdir('deps/libwebrtc') dependencies = [ abseil_cpp_proj.get_variable('absl_container_dep'), openssl_proj.get_variable('openssl_dep'), - nlohmann_json_proj.get_variable('nlohmann_json_dep'), libuv_proj.get_variable('libuv_dep'), libsrtp2_proj.get_variable('libsrtp2_dep'), usrsctp_proj.get_variable('usrsctp_dep'), + flatbuffers_proj.get_variable('flatbuffers_dep'), + flatbuffers_generator_dep, libwebrtc_dep, ] @@ -229,6 +256,7 @@ link_whole = [ libuv_proj.get_variable('libuv'), libsrtp2_proj.get_variable('libsrtp2'), usrsctp_proj.get_variable('usrsctp'), + flatbuffers_proj.get_variable('flatbuffers_lib'), libwebrtc, ] @@ -248,6 +276,28 @@ if host_machine.system() == 'windows' ] endif +if host_machine.system() == 'linux' and not get_option('ms_disable_liburing') + kernel_version = run_command('uname', '-r', check: true).stdout().strip() + + # Enable liburing for kernel versions greather than or equal to 6. + if kernel_version[0].to_int() >= 6 + liburing_proj = subproject('liburing', default_options: ['default_library=static'], required: true) + + dependencies += [ + liburing_proj.get_variable('uring'), + ] + link_whole += [ + liburing_proj.get_variable('liburing') + ] + common_sources += [ + 'src/DepLibUring.cpp', + ] + cpp_args += [ + '-DMS_LIBURING_SUPPORTED', + ] + endif +endif + libmediasoup_worker = library( 'libmediasoup-worker', name_prefix: '', @@ -272,6 +322,52 @@ executable( cpp_args: cpp_args + ['-DMS_EXECUTABLE'], ) +test_sources = [ + 'test/src/tests.cpp', + 'test/src/RTC/TestKeyFrameRequestManager.cpp', + 'test/src/RTC/TestNackGenerator.cpp', + 'test/src/RTC/TestRateCalculator.cpp', + 'test/src/RTC/TestRtpPacket.cpp', + 'test/src/RTC/TestRtpPacketH264Svc.cpp', + 'test/src/RTC/TestRtpRetransmissionBuffer.cpp', + 'test/src/RTC/TestRtpStreamSend.cpp', + 'test/src/RTC/TestRtpStreamRecv.cpp', + 'test/src/RTC/TestSeqManager.cpp', + 'test/src/RTC/TestTrendCalculator.cpp', + 'test/src/RTC/TestRtpEncodingParameters.cpp', + 'test/src/RTC/TestTransportCongestionControlServer.cpp', + 'test/src/RTC/Codecs/TestVP8.cpp', + 'test/src/RTC/Codecs/TestVP9.cpp', + 'test/src/RTC/Codecs/TestH264.cpp', + 'test/src/RTC/Codecs/TestH264_SVC.cpp', + 'test/src/RTC/RTCP/TestFeedbackPsAfb.cpp', + 'test/src/RTC/RTCP/TestFeedbackPsFir.cpp', + 'test/src/RTC/RTCP/TestFeedbackPsLei.cpp', + 'test/src/RTC/RTCP/TestFeedbackPsPli.cpp', + 'test/src/RTC/RTCP/TestFeedbackPsRemb.cpp', + 'test/src/RTC/RTCP/TestFeedbackPsRpsi.cpp', + 'test/src/RTC/RTCP/TestFeedbackPsSli.cpp', + 'test/src/RTC/RTCP/TestFeedbackPsTst.cpp', + 'test/src/RTC/RTCP/TestFeedbackPsVbcm.cpp', + 'test/src/RTC/RTCP/TestFeedbackRtpEcn.cpp', + 'test/src/RTC/RTCP/TestFeedbackRtpNack.cpp', + 'test/src/RTC/RTCP/TestFeedbackRtpSrReq.cpp', + 'test/src/RTC/RTCP/TestFeedbackRtpTllei.cpp', + 'test/src/RTC/RTCP/TestFeedbackRtpTmmb.cpp', + 'test/src/RTC/RTCP/TestFeedbackRtpTransport.cpp', + 'test/src/RTC/RTCP/TestBye.cpp', + 'test/src/RTC/RTCP/TestReceiverReport.cpp', + 'test/src/RTC/RTCP/TestSdes.cpp', + 'test/src/RTC/RTCP/TestSenderReport.cpp', + 'test/src/RTC/RTCP/TestPacket.cpp', + 'test/src/RTC/RTCP/TestXr.cpp', + 'test/src/Utils/TestBits.cpp', + 'test/src/Utils/TestByte.cpp', + 'test/src/Utils/TestIP.cpp', + 'test/src/Utils/TestString.cpp', + 'test/src/Utils/TestTime.cpp', +] + mediasoup_worker_test = executable( 'mediasoup-worker-test', build_by_default: false, @@ -280,51 +376,7 @@ mediasoup_worker_test = executable( dependencies: dependencies + [ catch2_proj.get_variable('catch2_dep'), ], - sources: common_sources + [ - 'test/src/tests.cpp', - 'test/src/PayloadChannel/TestPayloadChannelNotification.cpp', - 'test/src/PayloadChannel/TestPayloadChannelRequest.cpp', - 'test/src/RTC/TestKeyFrameRequestManager.cpp', - 'test/src/RTC/TestNackGenerator.cpp', - 'test/src/RTC/TestRateCalculator.cpp', - 'test/src/RTC/TestRtpPacket.cpp', - 'test/src/RTC/TestRtpPacketH264Svc.cpp', - 'test/src/RTC/TestRtpStreamSend.cpp', - 'test/src/RTC/TestRtpStreamRecv.cpp', - 'test/src/RTC/TestSeqManager.cpp', - 'test/src/RTC/TestTrendCalculator.cpp', - 'test/src/RTC/TestRtpEncodingParameters.cpp', - 'test/src/RTC/Codecs/TestVP8.cpp', - 'test/src/RTC/Codecs/TestVP9.cpp', - 'test/src/RTC/Codecs/TestH264.cpp', - 'test/src/RTC/Codecs/TestH264_SVC.cpp', - 'test/src/RTC/RTCP/TestFeedbackPsAfb.cpp', - 'test/src/RTC/RTCP/TestFeedbackPsFir.cpp', - 'test/src/RTC/RTCP/TestFeedbackPsLei.cpp', - 'test/src/RTC/RTCP/TestFeedbackPsPli.cpp', - 'test/src/RTC/RTCP/TestFeedbackPsRemb.cpp', - 'test/src/RTC/RTCP/TestFeedbackPsRpsi.cpp', - 'test/src/RTC/RTCP/TestFeedbackPsSli.cpp', - 'test/src/RTC/RTCP/TestFeedbackPsTst.cpp', - 'test/src/RTC/RTCP/TestFeedbackPsVbcm.cpp', - 'test/src/RTC/RTCP/TestFeedbackRtpEcn.cpp', - 'test/src/RTC/RTCP/TestFeedbackRtpNack.cpp', - 'test/src/RTC/RTCP/TestFeedbackRtpSrReq.cpp', - 'test/src/RTC/RTCP/TestFeedbackRtpTllei.cpp', - 'test/src/RTC/RTCP/TestFeedbackRtpTmmb.cpp', - 'test/src/RTC/RTCP/TestFeedbackRtpTransport.cpp', - 'test/src/RTC/RTCP/TestBye.cpp', - 'test/src/RTC/RTCP/TestReceiverReport.cpp', - 'test/src/RTC/RTCP/TestSdes.cpp', - 'test/src/RTC/RTCP/TestSenderReport.cpp', - 'test/src/RTC/RTCP/TestPacket.cpp', - 'test/src/RTC/RTCP/TestXr.cpp', - 'test/src/Utils/TestBits.cpp', - 'test/src/Utils/TestIP.cpp', - 'test/src/Utils/TestJson.cpp', - 'test/src/Utils/TestString.cpp', - 'test/src/Utils/TestTime.cpp', - ], + sources: common_sources + test_sources, include_directories: include_directories( 'include', 'test/include', @@ -341,6 +393,93 @@ test( workdir: meson.project_source_root(), ) +mediasoup_worker_test_asan_address = executable( + 'mediasoup-worker-test-asan-address', + build_by_default: false, + install: true, + install_tag: 'mediasoup-worker-test-asan-address', + dependencies: dependencies + [ + catch2_proj.get_variable('catch2_dep'), + ], + sources: common_sources + test_sources, + include_directories: include_directories( + 'include', + 'test/include', + ), + cpp_args: cpp_args + [ + '-DMS_LOG_STD', + '-DMS_TEST', + '-fsanitize=address', + ], + link_args: [ + '-fsanitize=address', + ], +) + +test( + 'mediasoup-worker-test-asan-address', + mediasoup_worker_test_asan_address, + workdir: meson.project_source_root(), +) + +mediasoup_worker_test_asan_undefined = executable( + 'mediasoup-worker-test-asan-undefined', + build_by_default: false, + install: true, + install_tag: 'mediasoup-worker-test-asan-undefined', + dependencies: dependencies + [ + catch2_proj.get_variable('catch2_dep'), + ], + sources: common_sources + test_sources, + include_directories: include_directories( + 'include', + 'test/include', + ), + cpp_args: cpp_args + [ + '-DMS_LOG_STD', + '-DMS_TEST', + '-fsanitize=undefined', + ], + link_args: [ + '-fsanitize=undefined', + ], +) + +test( + 'mediasoup-worker-test-asan-undefined', + mediasoup_worker_test_asan_undefined, + workdir: meson.project_source_root(), +) + +mediasoup_worker_test_asan_thread = executable( + 'mediasoup-worker-test-asan-thread', + build_by_default: false, + install: true, + install_tag: 'mediasoup-worker-test-asan-thread', + dependencies: dependencies + [ + catch2_proj.get_variable('catch2_dep'), + ], + sources: common_sources + test_sources, + include_directories: include_directories( + 'include', + 'test/include', + ), + cpp_args: cpp_args + [ + '-DMS_LOG_STD', + '-DMS_TEST', + '-fsanitize=thread', + ], + link_args: [ + '-fsanitize=thread', + ], +) + +test( + 'mediasoup-worker-test-asan-thread', + mediasoup_worker_test_asan_thread, + workdir: meson.project_source_root(), +) + executable( 'mediasoup-worker-fuzzer', build_by_default: false, @@ -350,10 +489,19 @@ executable( sources: common_sources + [ 'fuzzer/src/fuzzer.cpp', 'fuzzer/src/FuzzerUtils.cpp', - 'fuzzer/src/RTC/FuzzerStunPacket.cpp', + 'fuzzer/src/RTC/FuzzerDtlsTransport.cpp', + 'fuzzer/src/RTC/FuzzerRateCalculator.cpp', 'fuzzer/src/RTC/FuzzerRtpPacket.cpp', + 'fuzzer/src/RTC/FuzzerRtpRetransmissionBuffer.cpp', 'fuzzer/src/RTC/FuzzerRtpStreamSend.cpp', + 'fuzzer/src/RTC/FuzzerSeqManager.cpp', + 'fuzzer/src/RTC/FuzzerStunPacket.cpp', 'fuzzer/src/RTC/FuzzerTrendCalculator.cpp', + 'fuzzer/src/RTC/Codecs/FuzzerOpus.cpp', + 'fuzzer/src/RTC/Codecs/FuzzerVP8.cpp', + 'fuzzer/src/RTC/Codecs/FuzzerVP9.cpp', + 'fuzzer/src/RTC/Codecs/FuzzerH264.cpp', + 'fuzzer/src/RTC/Codecs/FuzzerH264_SVC.cpp', 'fuzzer/src/RTC/RTCP/FuzzerBye.cpp', 'fuzzer/src/RTC/RTCP/FuzzerFeedbackPs.cpp', 'fuzzer/src/RTC/RTCP/FuzzerFeedbackPsAfb.cpp', diff --git a/worker/meson_options.txt b/worker/meson_options.txt index ec104f5197..0d1400e97a 100644 --- a/worker/meson_options.txt +++ b/worker/meson_options.txt @@ -1,2 +1,4 @@ -option('ms_log_trace', type : 'boolean', value : false, description : 'When enabled, logs the current method/function if current log level is "debug"') -option('ms_log_file_line', type : 'boolean', value : false, description : 'When enabled, all the logging macros print more verbose information, including current file and line') +option('ms_log_trace', type : 'boolean', value : false, description : 'When set to true, logs the current method/function if current log level is "debug"') +option('ms_log_file_line', type : 'boolean', value : false, description : 'When set to true, all the logging macros print more verbose information, including current file and line') +option('ms_rtc_logger_rtp', type : 'boolean', value : false, description : 'When set to true, prints a line with information for each RTP packet') +option('ms_disable_liburing', type : 'boolean', value : false, description : 'When set to true, disables liburing integration despite current host supports it') diff --git a/worker/scripts/clang-format.mjs b/worker/scripts/clang-format.mjs new file mode 100644 index 0000000000..c6665493b8 --- /dev/null +++ b/worker/scripts/clang-format.mjs @@ -0,0 +1,76 @@ +import { execSync } from 'node:child_process'; +import { glob } from 'glob'; +import clangFormat from 'clang-format'; + +const task = process.argv.slice(2).join(' '); + +void run(); + +async function run() { + const clangFormatNativeBinary = clangFormat.getNativeBinary(); + const workerFiles = await glob([ + '../src/**/*.cpp', + '../include/**/*.hpp', + '../test/src/**/*.cpp', + '../test/include/helpers.hpp', + '../fuzzer/src/**/*.cpp', + '../fuzzer/include/**/*.hpp', + ]); + + switch (task) { + case 'lint': { + executeCmd( + `"${clangFormatNativeBinary}" --Werror --dry-run ${workerFiles.join( + ' ' + )}` + ); + + break; + } + + case 'format': { + executeCmd( + `"${clangFormatNativeBinary}" --Werror -i ${workerFiles.join(' ')}` + ); + + break; + } + + default: { + logError('unknown task'); + + exitWithError(); + } + } +} + +function executeCmd(command) { + try { + execSync(command, { stdio: ['ignore', process.stdout, process.stderr] }); + } catch (error) { + logError('executeCmd() failed'); + + exitWithError(); + } +} + +// eslint-disable-next-line no-unused-vars +function logInfo(message) { + // eslint-disable-next-line no-console + console.log(`clang-format.mjs \x1b[36m[INFO] [${task}]\x1b[0m`, message); +} + +// eslint-disable-next-line no-unused-vars +function logWarn(message) { + // eslint-disable-next-line no-console + console.warn(`clang-format.mjs \x1b[33m[WARN] [${task}]\x1b[0m`, message); +} + +function logError(message) { + // eslint-disable-next-line no-console + console.error(`clang-format.mjs \x1b[31m[ERROR] [${task}]\x1b[0m`, message); +} + +function exitWithError() { + process.exit(1); +} diff --git a/worker/scripts/cppcheck.sh b/worker/scripts/cppcheck.sh index 14ca7d5bd6..206355bdf8 100755 --- a/worker/scripts/cppcheck.sh +++ b/worker/scripts/cppcheck.sh @@ -29,7 +29,7 @@ CPPCHECKS="warning,style,performance,portability,unusedFunction" XML_FILE="/tmp/mediasoup-worker-cppcheck.xml" HTML_REPORT_DIR="/tmp/mediasoup-worker-cppcheck-report" -echo ">>> [INFO] running cppcheck ..." +echo ">>> [INFO] running cppcheck..." cppcheck --std=c++11 --enable=${CPPCHECKS} -v --quiet --report-progress --inline-suppr --error-exitcode=69 -I include src --xml-version=2 2> $XML_FILE # If exit code is 1 it means that some cppcheck option is wrong, so abort. @@ -40,12 +40,12 @@ fi echo ">>> [INFO] cppcheck XML report file generated in ${XML_FILE}" -echo ">>> [INFO] running cppcheck-htmlreport ..." +echo ">>> [INFO] running cppcheck-htmlreport..." cppcheck-htmlreport --title="mediasoup-worker" --file=${XML_FILE} --report-dir=${HTML_REPORT_DIR} --source-dir=. > /dev/null echo ">>> [INFO] cppcheck HTML report generated in ${HTML_REPORT_DIR}" if type "open" &> /dev/null; then - echo ">>> [INFO] opening HTML report ..." + echo ">>> [INFO] opening HTML report..." open ${HTML_REPORT_DIR}/index.html fi diff --git a/worker/scripts/cpu_cores.sh b/worker/scripts/cpu_cores.sh deleted file mode 100755 index e507b693d5..0000000000 --- a/worker/scripts/cpu_cores.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env sh - -set -e - -OS="$(uname -s)" -NUM_CORES= - -case "${OS}" in - Linux*|MINGW*) NUM_CORES=$(nproc);; - Darwin*|FreeBSD) NUM_CORES=$(sysctl -n hw.ncpu);; - *) NUM_CORES=1;; -esac - -if [ -n "${MEDIASOUP_MAX_CORES}" ]; then - NUM_CORES=$((MEDIASOUP_MAX_CORES>NUM_CORES ? NUM_CORES : MEDIASOUP_MAX_CORES)) -fi - -echo ${NUM_CORES} diff --git a/worker/scripts/flint++.sh b/worker/scripts/flint++.sh index 33a60239bb..66e1a4daf7 100755 --- a/worker/scripts/flint++.sh +++ b/worker/scripts/flint++.sh @@ -13,9 +13,9 @@ if ! type "flint++" &> /dev/null; then exit 1 fi -echo ">>> [INFO] running flint++ in src/ folder ..." +echo ">>> [INFO] running flint++ in src/ folder..." flint++ --recursive --verbose src/ echo -echo ">>> [INFO] running flint++ in include/ folder ..." +echo ">>> [INFO] running flint++ in include/ folder..." flint++ --recursive --verbose include/ diff --git a/worker/scripts/get-dep.sh b/worker/scripts/get-dep.sh index d538ae8606..8abe8d3bcb 100755 --- a/worker/scripts/get-dep.sh +++ b/worker/scripts/get-dep.sh @@ -7,7 +7,7 @@ DEP=$1 current_dir_name=${WORKER_PWD##*/} if [ "${current_dir_name}" != "worker" ] ; then - echo ">>> [ERROR] $(basename $0) must be called from mediasoup/worker/ directory" >&2 + echo ">>> [ERROR] $(basename $0) must be called from mediasoup/worker directory" >&2 exit 1 fi @@ -17,24 +17,24 @@ function get_dep() GIT_TAG="$2" DEST="$3" - echo ">>> [INFO] getting dep '${DEP}' ..." + echo ">>> [INFO] getting dep '${DEP}'..." if [ -d "${DEST}" ] ; then - echo ">>> [INFO] deleting ${DEST} ..." + echo ">>> [INFO] deleting ${DEST}..." git rm -rf --ignore-unmatch ${DEST} > /dev/null rm -rf ${DEST} fi - echo ">>> [INFO] cloning ${GIT_REPO} ..." + echo ">>> [INFO] cloning ${GIT_REPO}..." git clone ${GIT_REPO} ${DEST} cd ${DEST} - echo ">>> [INFO] setting '${GIT_TAG}' git tag ..." + echo ">>> [INFO] setting '${GIT_TAG}' git tag..." git checkout --quiet ${GIT_TAG} set -e - echo ">>> [INFO] adding dep source code to the repository ..." + echo ">>> [INFO] adding dep source code to the repository..." rm -rf .git git add . diff --git a/worker/scripts/gulpfile.js b/worker/scripts/gulpfile.js deleted file mode 100644 index 9859a1530d..0000000000 --- a/worker/scripts/gulpfile.js +++ /dev/null @@ -1,25 +0,0 @@ -const gulp = require('gulp'); -const clangFormat = require('gulp-clang-format'); - -const workerFiles = -[ - '../src/**/*.cpp', - '../include/**/*.hpp', - '../test/src/**/*.cpp', - '../test/include/helpers.hpp', - '../fuzzer/src/**/*.cpp', - '../fuzzer/include/**/*.hpp' -]; - -gulp.task('lint:worker', () => -{ - return gulp.src(workerFiles) - .pipe(clangFormat.checkFormat('file', null, { verbose: true, fail: true })); -}); - -gulp.task('format:worker', () => -{ - return gulp.src(workerFiles, { base: '.' }) - .pipe(clangFormat.format('file')) - .pipe(gulp.dest('.')); -}); diff --git a/worker/scripts/lcov.sh b/worker/scripts/lcov.sh index 8052478092..7170e6f284 100755 --- a/worker/scripts/lcov.sh +++ b/worker/scripts/lcov.sh @@ -21,14 +21,12 @@ make test echo ">>> [INFO] generating coverage info file..." $LCOV --no-external --capture --directory ./ --output-file ${COVERAGE_INFO} -echo ">>> [INFO] removing tests from coverage info file..." -$LCOV -r ${COVERAGE_INFO} "$(pwd)/test/*" -o ${COVERAGE_INFO} - -echo ">>> [INFO] removing deps from coverage info file..." +echo ">>> [INFO] removing subdirectories from coverage info file..." $LCOV -r ${COVERAGE_INFO} "$(pwd)/deps/*" -o ${COVERAGE_INFO} - -echo ">>> [INFO] removing json.hpp from coverage info file..." -$LCOV -r ${COVERAGE_INFO} "$(pwd)/include/json.hpp" -o ${COVERAGE_INFO} +$LCOV -r ${COVERAGE_INFO} "$(pwd)/fbs/*" -o ${COVERAGE_INFO} +$LCOV -r ${COVERAGE_INFO} "$(pwd)/fuzzer/*" -o ${COVERAGE_INFO} +$LCOV -r ${COVERAGE_INFO} "$(pwd)/subprojects/*" -o ${COVERAGE_INFO} +$LCOV -r ${COVERAGE_INFO} "$(pwd)/test/*" -o ${COVERAGE_INFO} echo ">>> [INFO] clearing previous report data..." if [[ -d ${HTML_REPORT_DIR} ]] ; then diff --git a/worker/scripts/package-lock.json b/worker/scripts/package-lock.json index 24ee1c9032..37003a5f7c 100644 --- a/worker/scripts/package-lock.json +++ b/worker/scripts/package-lock.json @@ -1,9913 +1,1027 @@ { - "name": "clang-tools", - "version": "0.0.1", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "clang-tools", - "version": "0.0.1", - "devDependencies": { - "clang-tools-prebuilt": "^0.1.4", - "gulp": "^4.0.2", - "gulp-clang-format": "^1.0.27" - } - }, - "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/ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "dev": true, - "dependencies": { - "ansi-wrap": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-gray": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", - "dev": true, - "dependencies": { - "ansi-wrap": "0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "dependencies": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "node_modules/anymatch/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/append-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", - "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", - "dev": true, - "dependencies": { - "buffer-equal": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", - "dev": true - }, - "node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-filter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", - "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", - "dev": true, - "dependencies": { - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", - "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", - "dev": true, - "dependencies": { - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-initial": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", - "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", - "dev": true, - "dependencies": { - "array-slice": "^1.0.0", - "is-number": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-initial/node_modules/is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-last": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", - "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", - "dev": true, - "dependencies": { - "is-number": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-last/node_modules/is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-slice": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-sort": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", - "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", - "dev": true, - "dependencies": { - "default-compare": "^1.0.0", - "get-value": "^2.0.6", - "kind-of": "^5.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dev": true, - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "node_modules/async-done": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", - "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.2", - "process-nextick-args": "^2.0.0", - "stream-exhaust": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, - "node_modules/async-settle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", - "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", - "dev": true, - "dependencies": { - "async-done": "^1.2.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true, - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", - "dev": true - }, - "node_modules/bach": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", - "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", - "dev": true, - "dependencies": { - "arr-filter": "^1.1.1", - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "array-each": "^1.0.0", - "array-initial": "^1.0.0", - "array-last": "^1.1.1", - "async-done": "^1.2.2", - "async-settle": "^1.0.0", - "now-and-later": "^2.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "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/base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/beeper": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", - "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "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": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/buffer-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "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/cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "dependencies": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "deprecated": "Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.", - "dev": true, - "dependencies": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "optionalDependencies": { - "fsevents": "^1.2.7" - } - }, - "node_modules/clang-format": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/clang-format/-/clang-format-1.6.0.tgz", - "integrity": "sha512-W3/L7fWkA8DoLkz9UGjrRnNi+J5a5TuS2HDLqk6WsicpOzb66MBu4eY/EcXhicHriVnAXWQVyk5/VeHWY6w4ow==", - "dev": true, - "dependencies": { - "async": "^1.5.2", - "glob": "^7.0.0", - "resolve": "^1.1.6" - }, - "bin": { - "check-clang-format": "bin/check-clang-format.js", - "clang-format": "index.js", - "git-clang-format": "bin/git-clang-format" - } - }, - "node_modules/clang-tools-prebuilt": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/clang-tools-prebuilt/-/clang-tools-prebuilt-0.1.4.tgz", - "integrity": "sha1-8gINNlN2CMDPrQeuvglNmXMFkLM=", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "home-path": "^0.1.1", - "mkdirp": "^0.5.0", - "nugget": "^1.5.1", - "path-exists": "^1.0.0" - }, - "bin": { - "clang-apply-replacements": "tools/clang-apply-replacements.js", - "clang-include-fixer": "tools/clang-include-fixer.js", - "clang-rename": "tools/clang-rename.js", - "clang-tidy": "tools/clang-tidy.js", - "find-all-symbols": "tools/find-all-symbols.js" - } - }, - "node_modules/class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cli-color": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-1.4.0.tgz", - "integrity": "sha512-xu6RvQqqrWEo6MPR1eixqGPywhYBHRs653F9jfXB2Hx4jdM/3WxiNE1vppRmxtMIfl16SFYTpYlrnqH/HsK/2w==", - "dev": true, - "dependencies": { - "ansi-regex": "^2.1.1", - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "memoizee": "^0.4.14", - "timers-ext": "^0.1.5" - } - }, - "node_modules/cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", - "dev": true - }, - "node_modules/cloneable-readable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", - "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - } - }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/collection-map": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", - "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", - "dev": true, - "dependencies": { - "arr-map": "^2.0.2", - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/copy-props": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", - "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", - "dev": true, - "dependencies": { - "each-props": "^1.3.2", - "is-plain-object": "^5.0.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "node_modules/currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "dependencies": { - "array-find-index": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "dev": true, - "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/dateformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", - "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/default-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", - "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", - "dev": true, - "dependencies": { - "kind-of": "^5.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-resolution": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", - "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "dependencies": { - "object-keys": "^1.0.12" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/diff": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-2.2.3.tgz", - "integrity": "sha1-YOr9DSjukG5Oj/ClLBIpUhAzv5k=", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, - "node_modules/duplexer2": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", - "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", - "dev": true, - "dependencies": { - "readable-stream": "~1.1.9" - } - }, - "node_modules/duplexer2/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "node_modules/duplexer2/node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/duplexer2/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "node_modules/duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/each-props": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", - "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.1", - "object.defaults": "^1.1.0" - } - }, - "node_modules/each-props/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", - "dev": true, - "dependencies": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" - } - }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "dev": true, - "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "node_modules/es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } - }, - "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": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "node_modules/event-stream": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.5.tgz", - "integrity": "sha512-vyibDcu5JL20Me1fP734QBH/kenBGLZap2n0+XXM7mvuUPzJ20Ydqj1aKcIeMdri1p+PU+4yAKugjN8KCVst+g==", - "dev": true, - "dependencies": { - "duplexer": "^0.1.1", - "from": "^0.1.7", - "map-stream": "0.0.7", - "pause-stream": "^0.0.11", - "split": "^1.0.1", - "stream-combiner": "^0.2.2", - "through": "^2.3.8" - } - }, - "node_modules/expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ext": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", - "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", - "dev": true, - "dependencies": { - "type": "^2.5.0" - } - }, - "node_modules/ext/node_modules/type": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", - "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==", - "dev": true - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true, - "engines": [ - "node >=0.6.0" - ] - }, - "node_modules/fancy-log": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", - "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", - "dev": true, - "dependencies": { - "ansi-gray": "^0.1.1", - "color-support": "^1.1.3", - "parse-node-version": "^1.0.0", - "time-stamp": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "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==", - "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": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", - "integrity": "sha1-5qdUzI8V5YmHqpy9J69m/W9OWvk=", - "dev": true - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, - "node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "dependencies": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-up/node_modules/path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "dependencies": { - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", - "dev": true, - "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", - "dev": true, - "dependencies": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/fined/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "dependencies": { - "for-in": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "dependencies": { - "map-cache": "^0.2.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", - "dev": true - }, - "node_modules/fs-mkdirp-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", - "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "node_modules/fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "node_modules/get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0" - } - }, - "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/glob-parent/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", - "dev": true, - "dependencies": { - "extend": "^3.0.0", - "glob": "^7.1.1", - "glob-parent": "^3.1.0", - "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.0", - "pumpify": "^1.3.5", - "readable-stream": "^2.1.5", - "remove-trailing-separator": "^1.0.1", - "to-absolute-glob": "^2.0.0", - "unique-stream": "^2.0.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/glob-watcher": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", - "integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==", - "dev": true, - "dependencies": { - "anymatch": "^2.0.0", - "async-done": "^1.2.0", - "chokidar": "^2.0.0", - "is-negated-glob": "^1.0.0", - "just-debounce": "^1.0.0", - "normalize-path": "^3.0.0", - "object.defaults": "^1.1.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "dependencies": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", - "dev": true, - "dependencies": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glogg": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", - "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", - "dev": true, - "dependencies": { - "sparkles": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", - "dev": true - }, - "node_modules/gulp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", - "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", - "dev": true, - "dependencies": { - "glob-watcher": "^5.0.3", - "gulp-cli": "^2.2.0", - "undertaker": "^1.2.1", - "vinyl-fs": "^3.0.0" - }, - "bin": { - "gulp": "bin/gulp.js" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-clang-format": { - "version": "1.0.27", - "resolved": "https://registry.npmjs.org/gulp-clang-format/-/gulp-clang-format-1.0.27.tgz", - "integrity": "sha512-Jj4PGuNXKdqVCh9fijvL7wdzma5TQRJz1vv8FjOjnSkfq3s/mvbdE/jq+5HG1c/q+jcYkXTEGkYT3CrdnJOLaQ==", - "dev": true, - "dependencies": { - "clang-format": "^1.0.32", - "fancy-log": "^1.3.2", - "gulp-diff": "^1.0.0", - "plugin-error": "^1.0.1", - "stream-combiner2": "^1.1.1", - "through2": "^2.0.3" - } - }, - "node_modules/gulp-cli": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", - "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", - "dev": true, - "dependencies": { - "ansi-colors": "^1.0.1", - "archy": "^1.0.0", - "array-sort": "^1.0.0", - "color-support": "^1.1.3", - "concat-stream": "^1.6.0", - "copy-props": "^2.0.1", - "fancy-log": "^1.3.2", - "gulplog": "^1.0.0", - "interpret": "^1.4.0", - "isobject": "^3.0.1", - "liftoff": "^3.1.0", - "matchdep": "^2.0.0", - "mute-stdout": "^1.0.0", - "pretty-hrtime": "^1.0.0", - "replace-homedir": "^1.0.0", - "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.2.0", - "yargs": "^7.1.0" - }, - "bin": { - "gulp": "bin/gulp.js" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-diff": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulp-diff/-/gulp-diff-1.0.0.tgz", - "integrity": "sha1-EBsjcS3WsQe9B9BauI6jrEhf7Xc=", - "dev": true, - "dependencies": { - "cli-color": "^1.0.0", - "diff": "^2.0.2", - "event-stream": "^3.1.5", - "gulp-util": "^3.0.6", - "through2": "^2.0.0" - } - }, - "node_modules/gulp-util": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", - "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", - "deprecated": "gulp-util is deprecated - replace it, following the guidelines at https://medium.com/gulpjs/gulp-util-ca3b1f9f9ac5", - "dev": true, - "dependencies": { - "array-differ": "^1.0.0", - "array-uniq": "^1.0.2", - "beeper": "^1.0.0", - "chalk": "^1.0.0", - "dateformat": "^2.0.0", - "fancy-log": "^1.1.0", - "gulplog": "^1.0.0", - "has-gulplog": "^0.1.0", - "lodash._reescape": "^3.0.0", - "lodash._reevaluate": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.template": "^3.0.0", - "minimist": "^1.1.0", - "multipipe": "^0.1.2", - "object-assign": "^3.0.0", - "replace-ext": "0.0.1", - "through2": "^2.0.0", - "vinyl": "^0.5.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/gulplog": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", - "dev": true, - "dependencies": { - "glogg": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dev": true, - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-gulplog": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", - "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", - "dev": true, - "dependencies": { - "sparkles": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/home-path": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/home-path/-/home-path-0.1.2.tgz", - "integrity": "sha1-PbJsojrcFE/uqPHi18j2yJDL/io=", - "dev": true - }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "dependencies": { - "parse-passwd": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, - "node_modules/indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "dependencies": { - "repeating": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "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==", - "dev": true - }, - "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/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "dependencies": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "node_modules/is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "dependencies": { - "binary-extensions": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "dev": true, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "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-negated-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true - }, - "node_modules/is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, - "dependencies": { - "is-unc-path": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "node_modules/is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "dependencies": { - "unc-path-regex": "^0.1.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "node_modules/is-valid-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "node_modules/json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "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": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "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": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "node_modules/jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "node_modules/just-debounce": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.1.0.tgz", - "integrity": "sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==", - "dev": true - }, - "node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/last-run": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", - "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", - "dev": true, - "dependencies": { - "default-resolution": "^2.0.0", - "es6-weak-map": "^2.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "dependencies": { - "invert-kv": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lead": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", - "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", - "dev": true, - "dependencies": { - "flush-write-stream": "^1.0.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/liftoff": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", - "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", - "dev": true, - "dependencies": { - "extend": "^3.0.0", - "findup-sync": "^3.0.0", - "fined": "^1.0.1", - "flagged-respawn": "^1.0.0", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.0", - "rechoir": "^0.6.2", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/liftoff/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "node_modules/lodash._basetostring": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", - "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", - "dev": true - }, - "node_modules/lodash._basevalues": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", - "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=", - "dev": true - }, - "node_modules/lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "node_modules/lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true - }, - "node_modules/lodash._reescape": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", - "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=", - "dev": true - }, - "node_modules/lodash._reevaluate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", - "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=", - "dev": true - }, - "node_modules/lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true - }, - "node_modules/lodash._root": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", - "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", - "dev": true - }, - "node_modules/lodash.escape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", - "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", - "dev": true, - "dependencies": { - "lodash._root": "^3.0.0" - } - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "node_modules/lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, - "node_modules/lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "dependencies": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, - "node_modules/lodash.restparam": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", - "dev": true - }, - "node_modules/lodash.template": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", - "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", - "dev": true, - "dependencies": { - "lodash._basecopy": "^3.0.0", - "lodash._basetostring": "^3.0.0", - "lodash._basevalues": "^3.0.0", - "lodash._isiterateecall": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0", - "lodash.keys": "^3.0.0", - "lodash.restparam": "^3.0.0", - "lodash.templatesettings": "^3.0.0" - } - }, - "node_modules/lodash.templatesettings": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", - "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", - "dev": true, - "dependencies": { - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0" - } - }, - "node_modules/loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "dependencies": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", - "dev": true, - "dependencies": { - "es5-ext": "~0.10.2" - } - }, - "node_modules/make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/make-iterator/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", - "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=", - "dev": true - }, - "node_modules/map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "dependencies": { - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/matchdep": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", - "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", - "dev": true, - "dependencies": { - "findup-sync": "^2.0.0", - "micromatch": "^3.0.4", - "resolve": "^1.4.0", - "stack-trace": "0.0.10" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/matchdep/node_modules/findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", - "dev": true, - "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/matchdep/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/memoizee": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", - "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", - "dev": true, - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.53", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" - } - }, - "node_modules/memoizee/node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true - }, - "node_modules/meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "dependencies": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/meow/node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/micromatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/micromatch/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/micromatch/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/micromatch/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", - "dev": true, - "dependencies": { - "mime-db": "1.51.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "node_modules/multipipe": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", - "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", - "dev": true, - "dependencies": { - "duplexer2": "0.0.2" - } - }, - "node_modules/mute-stdout": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", - "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", - "dev": true, - "optional": true - }, - "node_modules/nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "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/now-and-later": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", - "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", - "dev": true, - "dependencies": { - "once": "^1.3.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/nugget": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/nugget/-/nugget-1.6.2.tgz", - "integrity": "sha1-iMpuA7pXBqmRc/XaCQJZPWvK4Qc=", - "dev": true, - "dependencies": { - "debug": "^2.1.3", - "minimist": "^1.1.0", - "pretty-bytes": "^1.0.2", - "progress-stream": "^1.1.0", - "request": "^2.45.0", - "single-line-log": "^0.4.1", - "throttleit": "0.0.2" - }, - "bin": { - "nugget": "bin.js" - } - }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "dependencies": { - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", - "dev": true, - "dependencies": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", - "dev": true, - "dependencies": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.reduce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", - "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", - "dev": true, - "dependencies": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/ordered-read-streams": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", - "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.1" - } - }, - "node_modules/os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, - "dependencies": { - "lcid": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", - "dev": true, - "dependencies": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "dependencies": { - "error-ex": "^1.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parse-node-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "node_modules/path-exists": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz", - "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", - "dev": true, - "dependencies": { - "path-root-regex": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", - "dev": true, - "dependencies": { - "through": "~2.3" - } - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/plugin-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", - "dev": true, - "dependencies": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/plugin-error/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/plugin-error/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/plugin-error/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pretty-bytes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", - "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", - "dev": true, - "dependencies": { - "get-stdin": "^4.0.1", - "meow": "^3.1.0" - }, - "bin": { - "pretty-bytes": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "node_modules/progress-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.2.0.tgz", - "integrity": "sha1-LNPP6jO6OonJwSHsM0er6asSX3c=", - "dev": true, - "dependencies": { - "speedometer": "~0.1.2", - "through2": "~0.2.3" - } - }, - "node_modules/progress-stream/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "node_modules/progress-stream/node_modules/object-keys": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", - "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", - "dev": true - }, - "node_modules/progress-stream/node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/progress-stream/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "node_modules/progress-stream/node_modules/through2": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", - "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", - "dev": true, - "dependencies": { - "readable-stream": "~1.1.9", - "xtend": "~2.1.1" - } - }, - "node_modules/progress-stream/node_modules/xtend": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", - "dev": true, - "dependencies": { - "object-keys": "~0.4.0" - }, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "dev": true - }, - "node_modules/pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "dependencies": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - } - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "dependencies": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "dependencies": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true, - "dependencies": { - "resolve": "^1.1.6" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "dependencies": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regex-not/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regex-not/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regex-not/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remove-bom-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", - "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5", - "is-utf8": "^0.2.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remove-bom-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", - "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", - "dev": true, - "dependencies": { - "remove-bom-buffer": "^3.0.0", - "safe-buffer": "^5.1.0", - "through2": "^2.0.3" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "node_modules/repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "dependencies": { - "is-finite": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/replace-homedir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", - "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=", - "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.1", - "is-absolute": "^1.0.0", - "remove-trailing-separator": "^1.1.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dev": true, - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "node_modules/resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "dev": true, - "dependencies": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-options": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", - "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", - "dev": true, - "dependencies": { - "value-or-function": "^3.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "deprecated": "https://github.com/lydell/resolve-url#deprecated", - "dev": true - }, - "node_modules/ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "dependencies": { - "ret": "~0.1.10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/semver-greatest-satisfied-range": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", - "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=", - "dev": true, - "dependencies": { - "sver-compat": "^1.5.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/signal-exit": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", - "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", - "dev": true - }, - "node_modules/single-line-log": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-0.4.1.tgz", - "integrity": "sha1-h6VWSfdJ14PsDc2AToFA2Yc8fO4=", - "dev": true - }, - "node_modules/snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "node_modules/source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "dev": true - }, - "node_modules/sparkles": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", - "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz", - "integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==", - "dev": true - }, - "node_modules/speedometer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-0.1.4.tgz", - "integrity": "sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=", - "dev": true - }, - "node_modules/split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "dev": true, - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stream-combiner": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", - "integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=", - "dev": true, - "dependencies": { - "duplexer": "~0.1.1", - "through": "~2.3.4" - } - }, - "node_modules/stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", - "dev": true, - "dependencies": { - "duplexer2": "~0.1.0", - "readable-stream": "^2.0.2" - } - }, - "node_modules/stream-combiner2/node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/stream-exhaust": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", - "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", - "dev": true - }, - "node_modules/stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", - "dev": true - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "dependencies": { - "is-utf8": "^0.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "dependencies": { - "get-stdin": "^4.0.1" - }, - "bin": { - "strip-indent": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/sver-compat": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", - "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=", - "dev": true, - "dependencies": { - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/throttleit": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", - "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", - "dev": true - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/through2-filter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", - "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", - "dev": true, - "dependencies": { - "through2": "~2.0.0", - "xtend": "~4.0.0" - } - }, - "node_modules/time-stamp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/timers-ext": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", - "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", - "dev": true, - "dependencies": { - "es5-ext": "~0.10.46", - "next-tick": "1" - } - }, - "node_modules/to-absolute-glob": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", - "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", - "dev": true, - "dependencies": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-object-path/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-through": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", - "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", - "dev": true, - "dependencies": { - "through2": "^2.0.3" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "node_modules/type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", - "dev": true - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "node_modules/unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/undertaker": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.3.0.tgz", - "integrity": "sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "bach": "^1.0.0", - "collection-map": "^1.0.0", - "es6-weak-map": "^2.0.1", - "fast-levenshtein": "^1.0.0", - "last-run": "^1.1.0", - "object.defaults": "^1.0.0", - "object.reduce": "^1.0.0", - "undertaker-registry": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/undertaker-registry": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", - "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unique-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", - "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", - "dev": true, - "dependencies": { - "json-stable-stringify-without-jsonify": "^1.0.1", - "through2-filter": "^3.0.0" - } - }, - "node_modules/unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true, - "engines": { - "node": ">=4", - "yarn": "*" - } - }, - "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/urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "deprecated": "Please see https://github.com/lydell/urix#deprecated", - "dev": true - }, - "node_modules/use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "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/v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", - "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/value-or-function": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", - "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "node_modules/vinyl": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", - "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", - "dev": true, - "dependencies": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" - }, - "engines": { - "node": ">= 0.9" - } - }, - "node_modules/vinyl-fs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", - "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", - "dev": true, - "dependencies": { - "fs-mkdirp-stream": "^1.0.0", - "glob-stream": "^6.1.0", - "graceful-fs": "^4.0.0", - "is-valid-glob": "^1.0.0", - "lazystream": "^1.0.0", - "lead": "^1.0.0", - "object.assign": "^4.0.4", - "pumpify": "^1.3.5", - "readable-stream": "^2.3.3", - "remove-bom-buffer": "^3.0.0", - "remove-bom-stream": "^1.2.0", - "resolve-options": "^1.1.0", - "through2": "^2.0.0", - "to-through": "^2.0.0", - "value-or-function": "^3.0.0", - "vinyl": "^2.0.0", - "vinyl-sourcemap": "^1.1.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vinyl-fs/node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/vinyl-fs/node_modules/clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "node_modules/vinyl-fs/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vinyl-fs/node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", - "dev": true, - "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vinyl-sourcemap": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", - "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", - "dev": true, - "dependencies": { - "append-buffer": "^1.0.2", - "convert-source-map": "^1.5.0", - "graceful-fs": "^4.1.6", - "normalize-path": "^2.1.1", - "now-and-later": "^2.0.0", - "remove-bom-buffer": "^3.0.0", - "vinyl": "^2.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vinyl-sourcemap/node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/vinyl-sourcemap/node_modules/clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "node_modules/vinyl-sourcemap/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vinyl-sourcemap/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vinyl-sourcemap/node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", - "dev": true, - "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true - }, - "node_modules/wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", - "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", - "dev": true - }, - "node_modules/yargs": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", - "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", - "dev": true, - "dependencies": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^5.0.1" - } - }, - "node_modules/yargs-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", - "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", - "dev": true, - "dependencies": { - "camelcase": "^3.0.0", - "object.assign": "^4.1.0" - } - }, - "node_modules/yargs-parser/node_modules/camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs/node_modules/camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - } - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "dev": true, - "requires": { - "ansi-wrap": "^0.1.0" - } - }, - "ansi-gray": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", - "dev": true - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "append-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", - "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", - "dev": true, - "requires": { - "buffer-equal": "^1.0.0" - } - }, - "archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", - "dev": true - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-filter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", - "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", - "dev": true, - "requires": { - "make-iterator": "^1.0.0" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", - "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", - "dev": true, - "requires": { - "make-iterator": "^1.0.0" - } - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", - "dev": true - }, - "array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", - "dev": true - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, - "array-initial": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", - "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", - "dev": true, - "requires": { - "array-slice": "^1.0.0", - "is-number": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } - } - }, - "array-last": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", - "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", - "dev": true, - "requires": { - "is-number": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } - } - }, - "array-slice": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", - "dev": true - }, - "array-sort": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", - "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", - "dev": true, - "requires": { - "default-compare": "^1.0.0", - "get-value": "^2.0.6", - "kind-of": "^5.0.2" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "async-done": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", - "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.2", - "process-nextick-args": "^2.0.0", - "stream-exhaust": "^1.0.1" - } - }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, - "async-settle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", - "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", - "dev": true, - "requires": { - "async-done": "^1.2.2" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", - "dev": true - }, - "bach": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", - "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", - "dev": true, - "requires": { - "arr-filter": "^1.1.1", - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "array-each": "^1.0.0", - "array-initial": "^1.0.0", - "array-last": "^1.1.1", - "async-done": "^1.2.2", - "async-settle": "^1.0.0", - "now-and-later": "^2.0.0" - } - }, - "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 - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "beeper": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", - "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", - "dev": true - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "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, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - } - }, - "buffer-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", - "dev": true - }, - "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 - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "clang-format": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/clang-format/-/clang-format-1.6.0.tgz", - "integrity": "sha512-W3/L7fWkA8DoLkz9UGjrRnNi+J5a5TuS2HDLqk6WsicpOzb66MBu4eY/EcXhicHriVnAXWQVyk5/VeHWY6w4ow==", - "dev": true, - "requires": { - "async": "^1.5.2", - "glob": "^7.0.0", - "resolve": "^1.1.6" - } - }, - "clang-tools-prebuilt": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/clang-tools-prebuilt/-/clang-tools-prebuilt-0.1.4.tgz", - "integrity": "sha1-8gINNlN2CMDPrQeuvglNmXMFkLM=", - "dev": true, - "requires": { - "home-path": "^0.1.1", - "mkdirp": "^0.5.0", - "nugget": "^1.5.1", - "path-exists": "^1.0.0" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - } - } - }, - "cli-color": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-1.4.0.tgz", - "integrity": "sha512-xu6RvQqqrWEo6MPR1eixqGPywhYBHRs653F9jfXB2Hx4jdM/3WxiNE1vppRmxtMIfl16SFYTpYlrnqH/HsK/2w==", - "dev": true, - "requires": { - "ansi-regex": "^2.1.1", - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "memoizee": "^0.4.14", - "timers-ext": "^0.1.5" - } - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true - }, - "clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", - "dev": true - }, - "clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", - "dev": true - }, - "cloneable-readable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", - "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "collection-map": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", - "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", - "dev": true, - "requires": { - "arr-map": "^2.0.2", - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - } - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "copy-props": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", - "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", - "dev": true, - "requires": { - "each-props": "^1.3.2", - "is-plain-object": "^5.0.0" - } - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, - "d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "dev": true, - "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "dateformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", - "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "dev": true - }, - "default-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", - "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", - "dev": true, - "requires": { - "kind-of": "^5.0.2" - } - }, - "default-resolution": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", - "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", - "dev": true - }, - "diff": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-2.2.3.tgz", - "integrity": "sha1-YOr9DSjukG5Oj/ClLBIpUhAzv5k=", - "dev": true - }, - "duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, - "duplexer2": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", - "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", - "dev": true, - "requires": { - "readable-stream": "~1.1.9" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "each-props": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", - "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.1", - "object.defaults": "^1.1.0" - }, - "dependencies": { - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - } - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", - "dev": true, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "dev": true, - "requires": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "event-stream": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.5.tgz", - "integrity": "sha512-vyibDcu5JL20Me1fP734QBH/kenBGLZap2n0+XXM7mvuUPzJ20Ydqj1aKcIeMdri1p+PU+4yAKugjN8KCVst+g==", - "dev": true, - "requires": { - "duplexer": "^0.1.1", - "from": "^0.1.7", - "map-stream": "0.0.7", - "pause-stream": "^0.0.11", - "split": "^1.0.1", - "stream-combiner": "^0.2.2", - "through": "^2.3.8" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - } - } - }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "ext": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", - "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", - "dev": true, - "requires": { - "type": "^2.5.0" - }, - "dependencies": { - "type": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", - "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==", - "dev": true - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fancy-log": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", - "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", - "dev": true, - "requires": { - "ansi-gray": "^0.1.1", - "color-support": "^1.1.3", - "parse-node-version": "^1.0.0", - "time-stamp": "^1.0.0" - } - }, - "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==", - "dev": true - }, - "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 - }, - "fast-levenshtein": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", - "integrity": "sha1-5qdUzI8V5YmHqpy9J69m/W9OWvk=", - "dev": true - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - } - } - }, - "findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - }, - "fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" - }, - "dependencies": { - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - } - } - }, - "flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", - "dev": true - }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", - "dev": true - }, - "fs-mkdirp-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", - "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", - "dev": true, - "requires": { - "extend": "^3.0.0", - "glob": "^7.1.1", - "glob-parent": "^3.1.0", - "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.0", - "pumpify": "^1.3.5", - "readable-stream": "^2.1.5", - "remove-trailing-separator": "^1.0.1", - "to-absolute-glob": "^2.0.0", - "unique-stream": "^2.0.2" - } - }, - "glob-watcher": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", - "integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-done": "^1.2.0", - "chokidar": "^2.0.0", - "is-negated-glob": "^1.0.0", - "just-debounce": "^1.0.0", - "normalize-path": "^3.0.0", - "object.defaults": "^1.1.0" - } - }, - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - } - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - } - }, - "glogg": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", - "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", - "dev": true, - "requires": { - "sparkles": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", - "dev": true - }, - "gulp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", - "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", - "dev": true, - "requires": { - "glob-watcher": "^5.0.3", - "gulp-cli": "^2.2.0", - "undertaker": "^1.2.1", - "vinyl-fs": "^3.0.0" - } - }, - "gulp-clang-format": { - "version": "1.0.27", - "resolved": "https://registry.npmjs.org/gulp-clang-format/-/gulp-clang-format-1.0.27.tgz", - "integrity": "sha512-Jj4PGuNXKdqVCh9fijvL7wdzma5TQRJz1vv8FjOjnSkfq3s/mvbdE/jq+5HG1c/q+jcYkXTEGkYT3CrdnJOLaQ==", - "dev": true, - "requires": { - "clang-format": "^1.0.32", - "fancy-log": "^1.3.2", - "gulp-diff": "^1.0.0", - "plugin-error": "^1.0.1", - "stream-combiner2": "^1.1.1", - "through2": "^2.0.3" - } - }, - "gulp-cli": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", - "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", - "dev": true, - "requires": { - "ansi-colors": "^1.0.1", - "archy": "^1.0.0", - "array-sort": "^1.0.0", - "color-support": "^1.1.3", - "concat-stream": "^1.6.0", - "copy-props": "^2.0.1", - "fancy-log": "^1.3.2", - "gulplog": "^1.0.0", - "interpret": "^1.4.0", - "isobject": "^3.0.1", - "liftoff": "^3.1.0", - "matchdep": "^2.0.0", - "mute-stdout": "^1.0.0", - "pretty-hrtime": "^1.0.0", - "replace-homedir": "^1.0.0", - "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.2.0", - "yargs": "^7.1.0" - } - }, - "gulp-diff": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulp-diff/-/gulp-diff-1.0.0.tgz", - "integrity": "sha1-EBsjcS3WsQe9B9BauI6jrEhf7Xc=", - "dev": true, - "requires": { - "cli-color": "^1.0.0", - "diff": "^2.0.2", - "event-stream": "^3.1.5", - "gulp-util": "^3.0.6", - "through2": "^2.0.0" - } - }, - "gulp-util": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", - "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", - "dev": true, - "requires": { - "array-differ": "^1.0.0", - "array-uniq": "^1.0.2", - "beeper": "^1.0.0", - "chalk": "^1.0.0", - "dateformat": "^2.0.0", - "fancy-log": "^1.1.0", - "gulplog": "^1.0.0", - "has-gulplog": "^0.1.0", - "lodash._reescape": "^3.0.0", - "lodash._reevaluate": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.template": "^3.0.0", - "minimist": "^1.1.0", - "multipipe": "^0.1.2", - "object-assign": "^3.0.0", - "replace-ext": "0.0.1", - "through2": "^2.0.0", - "vinyl": "^0.5.0" - } - }, - "gulplog": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", - "dev": true, - "requires": { - "glogg": "^1.0.0" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "dev": true, - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-gulplog": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", - "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", - "dev": true, - "requires": { - "sparkles": "^1.0.0" - } - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "home-path": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/home-path/-/home-path-0.1.2.tgz", - "integrity": "sha1-PbJsojrcFE/uqPHi18j2yJDL/io=", - "dev": true - }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "dev": true - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true - }, - "is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "requires": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - }, - "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - }, - "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "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, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-negated-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true - }, - "is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true - }, - "is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, - "requires": { - "is-unc-path": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "requires": { - "unc-path-regex": "^0.1.2" - } - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-valid-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "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 - }, - "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": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "just-debounce": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.1.0.tgz", - "integrity": "sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==", - "dev": true - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - }, - "last-run": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", - "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", - "dev": true, - "requires": { - "default-resolution": "^2.0.0", - "es6-weak-map": "^2.0.1" - } - }, - "lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dev": true, - "requires": { - "readable-stream": "^2.0.5" - } - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "requires": { - "invert-kv": "^1.0.0" - } - }, - "lead": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", - "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", - "dev": true, - "requires": { - "flush-write-stream": "^1.0.2" - } - }, - "liftoff": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", - "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", - "dev": true, - "requires": { - "extend": "^3.0.0", - "findup-sync": "^3.0.0", - "fined": "^1.0.1", - "flagged-respawn": "^1.0.0", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.0", - "rechoir": "^0.6.2", - "resolve": "^1.1.7" - }, - "dependencies": { - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - } - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash._basetostring": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", - "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", - "dev": true - }, - "lodash._basevalues": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", - "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=", - "dev": true - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true - }, - "lodash._reescape": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", - "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=", - "dev": true - }, - "lodash._reevaluate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", - "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=", - "dev": true - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true - }, - "lodash._root": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", - "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", - "dev": true - }, - "lodash.escape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", - "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", - "dev": true, - "requires": { - "lodash._root": "^3.0.0" - } - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, - "lodash.restparam": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", - "dev": true - }, - "lodash.template": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", - "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", - "dev": true, - "requires": { - "lodash._basecopy": "^3.0.0", - "lodash._basetostring": "^3.0.0", - "lodash._basevalues": "^3.0.0", - "lodash._isiterateecall": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0", - "lodash.keys": "^3.0.0", - "lodash.restparam": "^3.0.0", - "lodash.templatesettings": "^3.0.0" - } - }, - "lodash.templatesettings": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", - "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0" - } - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", - "dev": true, - "requires": { - "es5-ext": "~0.10.2" - } - }, - "make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "map-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", - "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "matchdep": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", - "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", - "dev": true, - "requires": { - "findup-sync": "^2.0.0", - "micromatch": "^3.0.4", - "resolve": "^1.4.0", - "stack-trace": "0.0.10" - }, - "dependencies": { - "findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "memoizee": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", - "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", - "dev": true, - "requires": { - "d": "^1.0.1", - "es5-ext": "^0.10.53", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" - }, - "dependencies": { - "next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true - } - } - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - }, - "dependencies": { - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } - } - }, - "mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", - "dev": true - }, - "mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", - "dev": true, - "requires": { - "mime-db": "1.51.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - } - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "multipipe": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", - "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", - "dev": true, - "requires": { - "duplexer2": "0.0.2" - } - }, - "mute-stdout": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", - "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", - "dev": true - }, - "nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } - } - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "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 - }, - "now-and-later": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", - "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", - "dev": true, - "requires": { - "once": "^1.3.2" - } - }, - "nugget": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/nugget/-/nugget-1.6.2.tgz", - "integrity": "sha1-iMpuA7pXBqmRc/XaCQJZPWvK4Qc=", - "dev": true, - "requires": { - "debug": "^2.1.3", - "minimist": "^1.1.0", - "pretty-bytes": "^1.0.2", - "progress-stream": "^1.1.0", - "request": "^2.45.0", - "single-line-log": "^0.4.1", - "throttleit": "0.0.2" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - }, - "object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", - "dev": true, - "requires": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", - "dev": true, - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "object.reduce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", - "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", - "dev": true, - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "ordered-read-streams": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", - "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - } - }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, - "requires": { - "lcid": "^1.0.0" - } - }, - "parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", - "dev": true, - "requires": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "parse-node-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", - "dev": true - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz", - "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "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 - }, - "path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", - "dev": true, - "requires": { - "path-root-regex": "^0.1.0" - } - }, - "path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", - "dev": true - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", - "dev": true, - "requires": { - "through": "~2.3" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "plugin-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", - "dev": true, - "requires": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "pretty-bytes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", - "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1", - "meow": "^3.1.0" - } - }, - "pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "progress-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.2.0.tgz", - "integrity": "sha1-LNPP6jO6OonJwSHsM0er6asSX3c=", - "dev": true, - "requires": { - "speedometer": "~0.1.2", - "through2": "~0.2.3" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "object-keys": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", - "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "through2": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", - "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", - "dev": true, - "requires": { - "readable-stream": "~1.1.9", - "xtend": "~2.1.1" - } - }, - "xtend": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", - "dev": true, - "requires": { - "object-keys": "~0.4.0" - } - } - } - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "dev": true - }, - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "dev": true - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true, - "requires": { - "resolve": "^1.1.6" - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - } - } - }, - "remove-bom-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", - "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5", - "is-utf8": "^0.2.1" - } - }, - "remove-bom-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", - "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", - "dev": true, - "requires": { - "remove-bom-buffer": "^3.0.0", - "safe-buffer": "^5.1.0", - "through2": "^2.0.3" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "^1.0.0" - } - }, - "replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", - "dev": true - }, - "replace-homedir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", - "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1", - "is-absolute": "^1.0.0", - "remove-trailing-separator": "^1.1.0" - } - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - } - }, - "resolve-options": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", - "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", - "dev": true, - "requires": { - "value-or-function": "^3.0.0" - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "semver-greatest-satisfied-range": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", - "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=", - "dev": true, - "requires": { - "sver-compat": "^1.5.0" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - } - } - }, - "signal-exit": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", - "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", - "dev": true - }, - "single-line-log": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-0.4.1.tgz", - "integrity": "sha1-h6VWSfdJ14PsDc2AToFA2Yc8fO4=", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "dev": true - }, - "sparkles": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", - "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", - "dev": true - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz", - "integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==", - "dev": true - }, - "speedometer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-0.1.4.tgz", - "integrity": "sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=", - "dev": true - }, - "split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "dev": true, - "requires": { - "through": "2" - } - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - } - } - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - } - } - }, - "stream-combiner": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", - "integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=", - "dev": true, - "requires": { - "duplexer": "~0.1.1", - "through": "~2.3.4" - } - }, - "stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", - "dev": true, - "requires": { - "duplexer2": "~0.1.0", - "readable-stream": "^2.0.2" - }, - "dependencies": { - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - } - } - }, - "stream-exhaust": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", - "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", - "dev": true - }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "sver-compat": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", - "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=", - "dev": true, - "requires": { - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } - }, - "throttleit": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", - "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "through2-filter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", - "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", - "dev": true, - "requires": { - "through2": "~2.0.0", - "xtend": "~4.0.0" - } - }, - "time-stamp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", - "dev": true - }, - "timers-ext": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", - "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", - "dev": true, - "requires": { - "es5-ext": "~0.10.46", - "next-tick": "1" - } - }, - "to-absolute-glob": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", - "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", - "dev": true, - "requires": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.0" - } - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - } - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "to-through": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", - "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", - "dev": true, - "requires": { - "through2": "^2.0.3" - } - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", - "dev": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", - "dev": true - }, - "undertaker": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.3.0.tgz", - "integrity": "sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg==", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "bach": "^1.0.0", - "collection-map": "^1.0.0", - "es6-weak-map": "^2.0.1", - "fast-levenshtein": "^1.0.0", - "last-run": "^1.1.0", - "object.defaults": "^1.0.0", - "object.reduce": "^1.0.0", - "undertaker-registry": "^1.0.0" - } - }, - "undertaker-registry": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", - "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", - "dev": true - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "unique-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", - "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", - "dev": true, - "requires": { - "json-stable-stringify-without-jsonify": "^1.0.1", - "through2-filter": "^3.0.0" - } - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "value-or-function": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", - "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "dependencies": { - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - } - } - }, - "vinyl": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", - "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", - "dev": true, - "requires": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" - } - }, - "vinyl-fs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", - "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", - "dev": true, - "requires": { - "fs-mkdirp-stream": "^1.0.0", - "glob-stream": "^6.1.0", - "graceful-fs": "^4.0.0", - "is-valid-glob": "^1.0.0", - "lazystream": "^1.0.0", - "lead": "^1.0.0", - "object.assign": "^4.0.4", - "pumpify": "^1.3.5", - "readable-stream": "^2.3.3", - "remove-bom-buffer": "^3.0.0", - "remove-bom-stream": "^1.2.0", - "resolve-options": "^1.1.0", - "through2": "^2.0.0", - "to-through": "^2.0.0", - "value-or-function": "^3.0.0", - "vinyl": "^2.0.0", - "vinyl-sourcemap": "^1.1.0" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true - }, - "vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "vinyl-sourcemap": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", - "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", - "dev": true, - "requires": { - "append-buffer": "^1.0.2", - "convert-source-map": "^1.5.0", - "graceful-fs": "^4.1.6", - "normalize-path": "^2.1.1", - "now-and-later": "^2.0.0", - "remove-bom-buffer": "^3.0.0", - "vinyl": "^2.0.0" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true - }, - "vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true - }, - "y18n": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", - "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", - "dev": true - }, - "yargs": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", - "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", - "dev": true, - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^5.0.1" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - } - } - }, - "yargs-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", - "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", - "dev": true, - "requires": { - "camelcase": "^3.0.0", - "object.assign": "^4.1.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - } - } - } - } + "name": "mediasoup-worker-dev-tools", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "mediasoup-worker-dev-tools", + "version": "0.0.1", + "dependencies": { + "clang-format": "^1.8.0", + "glob": "^10.3.12" + } + }, + "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==", + "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==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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==" + }, + "node_modules/@isaacs/cliui/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==", + "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/@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==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/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==", + "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/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "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==" + }, + "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==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/clang-format": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/clang-format/-/clang-format-1.8.0.tgz", + "integrity": "sha512-pK8gzfu55/lHzIpQ1givIbWfn3eXnU7SfxqIwVgnn5jEM6j4ZJYjpFqFs4iSBPNedzRMmfjYjuQhu657WAXHXw==", + "dependencies": { + "async": "^3.2.3", + "glob": "^7.0.0", + "resolve": "^1.1.6" + }, + "bin": { + "check-clang-format": "bin/check-clang-format.js", + "clang-format": "index.js", + "git-clang-format": "bin/git-clang-format" + } + }, + "node_modules/clang-format/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==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/clang-format/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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/clang-format/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "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/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==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/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==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "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==", + "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/is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "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": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "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==", + "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==" + }, + "node_modules/path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dependencies": { + "lru-cache": "^10.2.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/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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/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==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "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==", + "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/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/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==", + "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==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + }, + "dependencies": { + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "requires": { + "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" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true + }, + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "clang-format": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/clang-format/-/clang-format-1.8.0.tgz", + "integrity": "sha512-pK8gzfu55/lHzIpQ1givIbWfn3eXnU7SfxqIwVgnn5jEM6j4ZJYjpFqFs4iSBPNedzRMmfjYjuQhu657WAXHXw==", + "requires": { + "async": "^3.2.3", + "glob": "^7.0.0", + "resolve": "^1.1.6" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "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" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "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==", + "requires": { + "color-name": "~1.1.4" + } + }, + "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==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "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==" + }, + "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==", + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + } + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "requires": { + "has": "^1.0.3" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==" + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "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==" + }, + "path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + } + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "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==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "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==" + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "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==" + }, + "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==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } } diff --git a/worker/scripts/package.json b/worker/scripts/package.json index c6d17b7639..2831c80c0d 100644 --- a/worker/scripts/package.json +++ b/worker/scripts/package.json @@ -1,10 +1,13 @@ { - "name": "clang-tools", - "version": "0.0.1", - "description": "mediasoup worker dev tools", - "devDependencies": { - "clang-tools-prebuilt": "^0.1.4", - "gulp": "^4.0.2", - "gulp-clang-format": "^1.0.27" - } + "name": "mediasoup-worker-dev-tools", + "version": "0.0.1", + "description": "mediasoup worker dev tools", + "scripts": { + "lint": "node clang-format.mjs lint", + "format": "node clang-format.mjs format" + }, + "dependencies": { + "clang-format": "^1.8.0", + "glob": "^10.3.12" + } } diff --git a/worker/scripts/run-fuzzer.sh b/worker/scripts/run-fuzzer.sh new file mode 100755 index 0000000000..ffe0d8ac87 --- /dev/null +++ b/worker/scripts/run-fuzzer.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +WORKER_PWD=${PWD} +DURATION_SEC=$1 + +current_dir_name=${WORKER_PWD##*/} +if [ "${current_dir_name}" != "worker" ] ; then + echo "run-fuzzer.sh [ERROR] $(basename $0) must be called from mediasoup/worker directory" >&2 + exit 1 +fi + +if [ "$#" -eq 0 ] ; then + echo "run-fuzzer.sh [ERROR] duration (in seconds) must be given as argument" >&2 + exit 1 +fi + +invoke fuzzer-run-all & + +MEDIASOUP_WORKER_FUZZER_PID=$! + +i=${DURATION_SEC} + +until [ ${i} -eq 0 ] +do + echo "run-fuzzer.sh [INFO] ${i} seconds left" + if ! kill -0 ${MEDIASOUP_WORKER_FUZZER_PID} &> /dev/null ; then + echo "run-fuzzer.sh [ERROR] mediasoup-worker-fuzzer died" >&2 + exit 1 + else + ((i=i-1)) + sleep 1 + fi +done + +echo "run-fuzzer.sh [INFO] mediasoup-worker-fuzzer is still running after given ${DURATION_SEC} seconds so no fuzzer issues so far" + +kill -SIGTERM ${MEDIASOUP_WORKER_FUZZER_PID} &> /dev/null +exit 0 diff --git a/worker/src/Channel/ChannelNotification.cpp b/worker/src/Channel/ChannelNotification.cpp new file mode 100644 index 0000000000..9facac2b7c --- /dev/null +++ b/worker/src/Channel/ChannelNotification.cpp @@ -0,0 +1,40 @@ +#define MS_CLASS "Channel::ChannelNotification" +// #define MS_LOG_DEV_LEVEL 3 + +#include "Channel/ChannelNotification.hpp" +#include "Logger.hpp" +#include "MediaSoupErrors.hpp" + +namespace Channel +{ + /* Class variables. */ + + // clang-format off + absl::flat_hash_map ChannelNotification::event2String = + { + { FBS::Notification::Event::TRANSPORT_SEND_RTCP, "transport.sendRtcp" }, + { FBS::Notification::Event::PRODUCER_SEND, "producer.send" }, + { FBS::Notification::Event::DATAPRODUCER_SEND, "dataProducer.send" }, + }; + // clang-format on + + /* Instance methods. */ + + ChannelNotification::ChannelNotification(const FBS::Notification::Notification* notification) + { + MS_TRACE(); + + this->data = notification; + this->event = notification->event(); + + auto eventCStrIt = ChannelNotification::event2String.find(this->event); + + if (eventCStrIt == ChannelNotification::event2String.end()) + { + MS_THROW_ERROR("unknown event '%" PRIu8 "'", static_cast(this->event)); + } + + this->eventCStr = eventCStrIt->second; + this->handlerId = this->data->handlerId()->str(); + } +} // namespace Channel diff --git a/worker/src/Channel/ChannelNotifier.cpp b/worker/src/Channel/ChannelNotifier.cpp index 8ef6b23b2a..78a354940b 100644 --- a/worker/src/Channel/ChannelNotifier.cpp +++ b/worker/src/Channel/ChannelNotifier.cpp @@ -10,57 +10,4 @@ namespace Channel { MS_TRACE(); } - - void ChannelNotifier::Emit(uint64_t targetId, const char* event) - { - MS_TRACE(); - - json jsonNotification = json::object(); - - jsonNotification["targetId"] = targetId; - jsonNotification["event"] = event; - - this->channel->Send(jsonNotification); - } - - void ChannelNotifier::Emit(const std::string& targetId, const char* event) - { - MS_TRACE(); - - json jsonNotification = json::object(); - - jsonNotification["targetId"] = targetId; - jsonNotification["event"] = event; - - this->channel->Send(jsonNotification); - } - - void ChannelNotifier::Emit(const std::string& targetId, const char* event, json& data) - { - MS_TRACE(); - - json jsonNotification = json::object(); - - jsonNotification["targetId"] = targetId; - jsonNotification["event"] = event; - jsonNotification["data"] = data; - - this->channel->Send(jsonNotification); - } - - void ChannelNotifier::Emit(const std::string& targetId, const char* event, const std::string& data) - { - MS_TRACE(); - - std::string notification("{\"targetId\":\""); - - notification.append(targetId); - notification.append("\",\"event\":\""); - notification.append(event); - notification.append("\",\"data\":"); - notification.append(data); - notification.append("}"); - - this->channel->Send(notification); - } } // namespace Channel diff --git a/worker/src/Channel/ChannelRequest.cpp b/worker/src/Channel/ChannelRequest.cpp index 8ed466a22d..82830bef7c 100644 --- a/worker/src/Channel/ChannelRequest.cpp +++ b/worker/src/Channel/ChannelRequest.cpp @@ -4,139 +4,115 @@ #include "Channel/ChannelRequest.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" -#include "Utils.hpp" namespace Channel { + /* Static variables. */ + + thread_local flatbuffers::FlatBufferBuilder ChannelRequest::bufferBuilder{}; + /* Class variables. */ // clang-format off - absl::flat_hash_map ChannelRequest::string2MethodId = + absl::flat_hash_map ChannelRequest::method2String = { - { "worker.close", ChannelRequest::MethodId::WORKER_CLOSE }, - { "worker.dump", ChannelRequest::MethodId::WORKER_DUMP }, - { "worker.getResourceUsage", ChannelRequest::MethodId::WORKER_GET_RESOURCE_USAGE }, - { "worker.updateSettings", ChannelRequest::MethodId::WORKER_UPDATE_SETTINGS }, - { "worker.createWebRtcServer", ChannelRequest::MethodId::WORKER_CREATE_WEBRTC_SERVER }, - { "worker.createRouter", ChannelRequest::MethodId::WORKER_CREATE_ROUTER }, - { "worker.closeWebRtcServer", ChannelRequest::MethodId::WORKER_WEBRTC_SERVER_CLOSE }, - { "webRtcServer.dump", ChannelRequest::MethodId::WEBRTC_SERVER_DUMP }, - { "worker.closeRouter", ChannelRequest::MethodId::WORKER_CLOSE_ROUTER }, - { "router.dump", ChannelRequest::MethodId::ROUTER_DUMP }, - { "router.createWebRtcTransport", ChannelRequest::MethodId::ROUTER_CREATE_WEBRTC_TRANSPORT }, - { "router.createWebRtcTransportWithServer", ChannelRequest::MethodId::ROUTER_CREATE_WEBRTC_TRANSPORT_WITH_SERVER }, - { "router.createPlainTransport", ChannelRequest::MethodId::ROUTER_CREATE_PLAIN_TRANSPORT }, - { "router.createPipeTransport", ChannelRequest::MethodId::ROUTER_CREATE_PIPE_TRANSPORT }, - { "router.createDirectTransport", ChannelRequest::MethodId::ROUTER_CREATE_DIRECT_TRANSPORT }, - { "router.closeTransport", ChannelRequest::MethodId::ROUTER_CLOSE_TRANSPORT }, - { "router.createActiveSpeakerObserver", ChannelRequest::MethodId::ROUTER_CREATE_ACTIVE_SPEAKER_OBSERVER }, - { "router.createAudioLevelObserver", ChannelRequest::MethodId::ROUTER_CREATE_AUDIO_LEVEL_OBSERVER }, - { "router.closeRtpObserver", ChannelRequest::MethodId::ROUTER_CLOSE_RTP_OBSERVER }, - { "transport.dump", ChannelRequest::MethodId::TRANSPORT_DUMP }, - { "transport.getStats", ChannelRequest::MethodId::TRANSPORT_GET_STATS }, - { "transport.connect", ChannelRequest::MethodId::TRANSPORT_CONNECT }, - { "transport.setMaxIncomingBitrate", ChannelRequest::MethodId::TRANSPORT_SET_MAX_INCOMING_BITRATE }, - { "transport.setMaxOutgoingBitrate", ChannelRequest::MethodId::TRANSPORT_SET_MAX_OUTGOING_BITRATE }, - { "transport.restartIce", ChannelRequest::MethodId::TRANSPORT_RESTART_ICE }, - { "transport.produce", ChannelRequest::MethodId::TRANSPORT_PRODUCE }, - { "transport.consume", ChannelRequest::MethodId::TRANSPORT_CONSUME }, - { "transport.produceData", ChannelRequest::MethodId::TRANSPORT_PRODUCE_DATA }, - { "transport.consumeData", ChannelRequest::MethodId::TRANSPORT_CONSUME_DATA }, - { "transport.enableTraceEvent", ChannelRequest::MethodId::TRANSPORT_ENABLE_TRACE_EVENT }, - { "transport.closeProducer", ChannelRequest::MethodId::TRANSPORT_CLOSE_PRODUCER }, - { "transport.closeConsumer", ChannelRequest::MethodId::TRANSPORT_CLOSE_CONSUMER }, - { "transport.closeDataProducer", ChannelRequest::MethodId::TRANSPORT_CLOSE_DATA_PRODUCER }, - { "transport.closeDataConsumer", ChannelRequest::MethodId::TRANSPORT_CLOSE_DATA_CONSUMER }, - { "producer.dump", ChannelRequest::MethodId::PRODUCER_DUMP }, - { "producer.getStats", ChannelRequest::MethodId::PRODUCER_GET_STATS }, - { "producer.pause", ChannelRequest::MethodId::PRODUCER_PAUSE }, - { "producer.resume" , ChannelRequest::MethodId::PRODUCER_RESUME }, - { "producer.enableTraceEvent", ChannelRequest::MethodId::PRODUCER_ENABLE_TRACE_EVENT }, - { "consumer.dump", ChannelRequest::MethodId::CONSUMER_DUMP }, - { "consumer.getStats", ChannelRequest::MethodId::CONSUMER_GET_STATS }, - { "consumer.pause", ChannelRequest::MethodId::CONSUMER_PAUSE }, - { "consumer.resume", ChannelRequest::MethodId::CONSUMER_RESUME }, - { "consumer.setPreferredLayers", ChannelRequest::MethodId::CONSUMER_SET_PREFERRED_LAYERS }, - { "consumer.setPriority", ChannelRequest::MethodId::CONSUMER_SET_PRIORITY }, - { "consumer.requestKeyFrame", ChannelRequest::MethodId::CONSUMER_REQUEST_KEY_FRAME }, - { "consumer.enableTraceEvent", ChannelRequest::MethodId::CONSUMER_ENABLE_TRACE_EVENT }, - { "dataProducer.dump", ChannelRequest::MethodId::DATA_PRODUCER_DUMP }, - { "dataProducer.getStats", ChannelRequest::MethodId::DATA_PRODUCER_GET_STATS }, - { "dataConsumer.dump", ChannelRequest::MethodId::DATA_CONSUMER_DUMP }, - { "dataConsumer.getStats", ChannelRequest::MethodId::DATA_CONSUMER_GET_STATS }, - { "dataConsumer.getBufferedAmount", ChannelRequest::MethodId::DATA_CONSUMER_GET_BUFFERED_AMOUNT }, - { "dataConsumer.setBufferedAmountLowThreshold", ChannelRequest::MethodId::DATA_CONSUMER_SET_BUFFERED_AMOUNT_LOW_THRESHOLD }, - { "rtpObserver.pause", ChannelRequest::MethodId::RTP_OBSERVER_PAUSE }, - { "rtpObserver.resume", ChannelRequest::MethodId::RTP_OBSERVER_RESUME }, - { "rtpObserver.addProducer", ChannelRequest::MethodId::RTP_OBSERVER_ADD_PRODUCER }, - { "rtpObserver.removeProducer", ChannelRequest::MethodId::RTP_OBSERVER_REMOVE_PRODUCER } + { FBS::Request::Method::WORKER_CLOSE, "worker.close" }, + { FBS::Request::Method::WORKER_DUMP, "worker.dump" }, + { FBS::Request::Method::WORKER_GET_RESOURCE_USAGE, "worker.getResourceUsage" }, + { FBS::Request::Method::WORKER_UPDATE_SETTINGS, "worker.updateSettings" }, + { FBS::Request::Method::WORKER_CREATE_WEBRTCSERVER, "worker.createWebRtcServer" }, + { FBS::Request::Method::WORKER_CREATE_ROUTER, "worker.createRouter" }, + { FBS::Request::Method::WORKER_WEBRTCSERVER_CLOSE, "worker.closeWebRtcServer" }, + { FBS::Request::Method::WORKER_CLOSE_ROUTER, "worker.closeRouter" }, + { FBS::Request::Method::WEBRTCSERVER_DUMP, "webRtcServer.dump" }, + { FBS::Request::Method::ROUTER_DUMP, "router.dump" }, + { FBS::Request::Method::ROUTER_CREATE_WEBRTCTRANSPORT, "router.createWebRtcTransport" }, + { FBS::Request::Method::ROUTER_CREATE_WEBRTCTRANSPORT_WITH_SERVER, "router.createWebRtcTransportWithServer" }, + { FBS::Request::Method::ROUTER_CREATE_PLAINTRANSPORT, "router.createPlainTransport" }, + { FBS::Request::Method::ROUTER_CREATE_PIPETRANSPORT, "router.createPipeTransport" }, + { FBS::Request::Method::ROUTER_CREATE_DIRECTTRANSPORT, "router.createDirectTransport" }, + { FBS::Request::Method::ROUTER_CLOSE_TRANSPORT, "router.closeTransport" }, + { FBS::Request::Method::ROUTER_CREATE_ACTIVESPEAKEROBSERVER, "router.createActiveSpeakerObserver" }, + { FBS::Request::Method::ROUTER_CREATE_AUDIOLEVELOBSERVER, "router.createAudioLevelObserver" }, + { FBS::Request::Method::ROUTER_CLOSE_RTPOBSERVER, "router.closeRtpObserver" }, + { FBS::Request::Method::TRANSPORT_DUMP, "transport.dump" }, + { FBS::Request::Method::TRANSPORT_GET_STATS, "transport.getStats" }, + { FBS::Request::Method::TRANSPORT_CONNECT, "transport.connect" }, + { FBS::Request::Method::TRANSPORT_SET_MAX_INCOMING_BITRATE, "transport.setMaxIncomingBitrate" }, + { FBS::Request::Method::TRANSPORT_SET_MAX_OUTGOING_BITRATE, "transport.setMaxOutgoingBitrate" }, + { FBS::Request::Method::TRANSPORT_SET_MIN_OUTGOING_BITRATE, "transport.setMinOutgoingBitrate" }, + { FBS::Request::Method::TRANSPORT_RESTART_ICE, "transport.restartIce" }, + { FBS::Request::Method::TRANSPORT_PRODUCE, "transport.produce" }, + { FBS::Request::Method::TRANSPORT_PRODUCE_DATA, "transport.produceData" }, + { FBS::Request::Method::TRANSPORT_CONSUME, "transport.consume" }, + { FBS::Request::Method::TRANSPORT_CONSUME_DATA, "transport.consumeData" }, + { FBS::Request::Method::TRANSPORT_ENABLE_TRACE_EVENT, "transport.enableTraceEvent" }, + { FBS::Request::Method::TRANSPORT_CLOSE_PRODUCER, "transport.closeProducer" }, + { FBS::Request::Method::TRANSPORT_CLOSE_CONSUMER, "transport.closeConsumer" }, + { FBS::Request::Method::TRANSPORT_CLOSE_DATAPRODUCER, "transport.closeDataProducer" }, + { FBS::Request::Method::TRANSPORT_CLOSE_DATACONSUMER, "transport.closeDataConsumer" }, + { FBS::Request::Method::PLAINTRANSPORT_CONNECT, "plainTransport.connect" }, + { FBS::Request::Method::PIPETRANSPORT_CONNECT, "pipeTransport.connect" }, + { FBS::Request::Method::WEBRTCTRANSPORT_CONNECT, "webRtcTransport.connect" }, + { FBS::Request::Method::PRODUCER_DUMP, "producer.dump" }, + { FBS::Request::Method::PRODUCER_GET_STATS, "producer.getStats" }, + { FBS::Request::Method::PRODUCER_PAUSE, "producer.pause" }, + { FBS::Request::Method::PRODUCER_RESUME, "producer.resume" }, + { FBS::Request::Method::PRODUCER_ENABLE_TRACE_EVENT, "producer.enableTraceEvent" }, + { FBS::Request::Method::CONSUMER_DUMP, "consumer.dump" }, + { FBS::Request::Method::CONSUMER_GET_STATS, "consumer.getStats" }, + { FBS::Request::Method::CONSUMER_PAUSE, "consumer.pause" }, + { FBS::Request::Method::CONSUMER_RESUME, "consumer.resume" }, + { FBS::Request::Method::CONSUMER_SET_PREFERRED_LAYERS, "consumer.setPreferredLayers" }, + { FBS::Request::Method::CONSUMER_SET_PRIORITY, "consumer.setPriority" }, + { FBS::Request::Method::CONSUMER_REQUEST_KEY_FRAME, "consumer.requestKeyFrame" }, + { FBS::Request::Method::CONSUMER_ENABLE_TRACE_EVENT, "consumer.enableTraceEvent" }, + { FBS::Request::Method::DATAPRODUCER_DUMP, "dataProducer.dump" }, + { FBS::Request::Method::DATAPRODUCER_GET_STATS, "dataProducer.getStats" }, + { FBS::Request::Method::DATAPRODUCER_PAUSE, "dataProducer.pause" }, + { FBS::Request::Method::DATAPRODUCER_RESUME, "dataProducer.resume" }, + { FBS::Request::Method::DATACONSUMER_DUMP, "dataConsumer.dump" }, + { FBS::Request::Method::DATACONSUMER_GET_STATS, "dataConsumer.getStats" }, + { FBS::Request::Method::DATACONSUMER_PAUSE, "dataConsumer.pause" }, + { FBS::Request::Method::DATACONSUMER_RESUME, "dataConsumer.resume" }, + { FBS::Request::Method::DATACONSUMER_GET_BUFFERED_AMOUNT, "dataConsumer.getBufferedAmount" }, + { FBS::Request::Method::DATACONSUMER_SET_BUFFERED_AMOUNT_LOW_THRESHOLD, "dataConsumer.setBufferedAmountLowThreshold" }, + { FBS::Request::Method::DATACONSUMER_SEND, "dataConsumer.send" }, + { FBS::Request::Method::DATACONSUMER_SET_SUBCHANNELS, "dataConsumer.setSubchannels" }, + { FBS::Request::Method::DATACONSUMER_ADD_SUBCHANNEL, "dataConsumer.addSubchannel" }, + { FBS::Request::Method::DATACONSUMER_REMOVE_SUBCHANNEL, "dataConsumer.removeSubchannel" }, + { FBS::Request::Method::RTPOBSERVER_PAUSE, "rtpObserver.pause" }, + { FBS::Request::Method::RTPOBSERVER_RESUME, "rtpObserver.resume" }, + { FBS::Request::Method::RTPOBSERVER_ADD_PRODUCER, "rtpObserver.addProducer" }, + { FBS::Request::Method::RTPOBSERVER_REMOVE_PRODUCER, "rtpObserver.removeProducer" }, }; // clang-format on /* Instance methods. */ /** - * msg contains "id:method:handlerId:data" where: - * - id: The ID of the request. - * - handlerId: The ID of the target entity - * - data: JSON object. + * msg contains the request flatbuffer. */ - ChannelRequest::ChannelRequest(Channel::ChannelSocket* channel, const char* msg, size_t msgLen) + ChannelRequest::ChannelRequest(Channel::ChannelSocket* channel, const FBS::Request::Request* request) : channel(channel) { MS_TRACE(); - auto info = Utils::String::Split(std::string(msg, msgLen), ':', 3); - - if (info.size() < 2) - MS_THROW_ERROR("too few arguments"); + this->data = request; + this->id = request->id(); + this->method = request->method(); - this->id = std::stoul(info[0]); - this->method = info[1]; + auto methodCStrIt = ChannelRequest::method2String.find(this->method); - auto methodIdIt = ChannelRequest::string2MethodId.find(this->method); - - if (methodIdIt == ChannelRequest::string2MethodId.end()) + if (methodCStrIt == ChannelRequest::method2String.end()) { Error("unknown method"); - MS_THROW_ERROR("unknown method '%s'", this->method.c_str()); - } - - this->methodId = methodIdIt->second; - - if (info.size() > 2) - { - auto& handlerId = info[2]; - - if (handlerId != "undefined") - this->handlerId = handlerId; + MS_THROW_ERROR("unknown method '%" PRIu8 "'", static_cast(this->method)); } - if (info.size() > 3) - { - auto& data = info[3]; - - if (data != "undefined") - { - try - { - this->data = json::parse(data); - - if (!this->data.is_object()) - this->data = json::object(); - } - catch (const json::parse_error& error) - { - MS_THROW_TYPE_ERROR("JSON parsing error: %s", error.what()); - } - } - } - } - - ChannelRequest::~ChannelRequest() - { - MS_TRACE(); + this->methodCStr = methodCStrIt->second; + this->handlerId = this->data->handlerId()->str(); } void ChannelRequest::Accept() @@ -147,15 +123,14 @@ namespace Channel this->replied = true; - std::string response("{\"id\":"); + auto& builder = ChannelRequest::bufferBuilder; + auto response = + FBS::Response::CreateResponse(builder, this->id, true, FBS::Response::Body::NONE, 0); - response.append(std::to_string(this->id)); - response.append(",\"accepted\":true}"); - - this->channel->Send(response); + this->SendResponse(response); } - void ChannelRequest::Accept(json& data) + void ChannelRequest::Error(const char* reason) { MS_TRACE(); @@ -163,18 +138,14 @@ namespace Channel this->replied = true; - json jsonResponse = json::object(); + auto& builder = ChannelRequest::bufferBuilder; + auto response = FBS::Response::CreateResponseDirect( + builder, this->id, false /*accepted*/, FBS::Response::Body::NONE, 0, "Error" /*Error*/, reason); - jsonResponse["id"] = this->id; - jsonResponse["accepted"] = true; - - if (data.is_structured()) - jsonResponse["data"] = data; - - this->channel->Send(jsonResponse); + this->SendResponse(response); } - void ChannelRequest::Error(const char* reason) + void ChannelRequest::TypeError(const char* reason) { MS_TRACE(); @@ -182,33 +153,26 @@ namespace Channel this->replied = true; - json jsonResponse = json::object(); + auto& builder = ChannelRequest::bufferBuilder; + auto response = FBS::Response::CreateResponseDirect( + builder, this->id, false /*accepted*/, FBS::Response::Body::NONE, 0, "TypeError" /*Error*/, reason); - jsonResponse["id"] = this->id; - jsonResponse["error"] = "Error"; - - if (reason != nullptr) - jsonResponse["reason"] = reason; - - this->channel->Send(jsonResponse); + this->SendResponse(response); } - void ChannelRequest::TypeError(const char* reason) + void ChannelRequest::Send(uint8_t* buffer, size_t size) const { - MS_TRACE(); - - MS_ASSERT(!this->replied, "request already replied"); - - this->replied = true; - - json jsonResponse = json::object(); - - jsonResponse["id"] = this->id; - jsonResponse["error"] = "TypeError"; + this->channel->Send(buffer, size); + } - if (reason != nullptr) - jsonResponse["reason"] = reason; + void ChannelRequest::SendResponse(const flatbuffers::Offset& response) + { + auto& builder = ChannelRequest::bufferBuilder; + auto message = + FBS::Message::CreateMessage(builder, FBS::Message::Body::Response, response.Union()); - this->channel->Send(jsonResponse); + builder.FinishSizePrefixed(message); + this->Send(builder.GetBufferPointer(), builder.GetSize()); + builder.Clear(); } } // namespace Channel diff --git a/worker/src/Channel/ChannelSocket.cpp b/worker/src/Channel/ChannelSocket.cpp index 22bc0a4b69..144712501b 100644 --- a/worker/src/Channel/ChannelSocket.cpp +++ b/worker/src/Channel/ChannelSocket.cpp @@ -5,7 +5,6 @@ #include "DepLibUV.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" -#include // std::ceil() #include // std::memcpy(), std::memmove() namespace Channel @@ -24,17 +23,16 @@ namespace Channel } } - inline static void onClose(uv_handle_t* handle) + inline static void onCloseAsync(uv_handle_t* handle) { - delete handle; + delete reinterpret_cast(handle); } /* Instance methods. */ ChannelSocket::ChannelSocket(int consumerFd, int producerFd) : consumerSocket(new ConsumerSocket(consumerFd, MessageMaxLen, this)), - producerSocket(new ProducerSocket(producerFd, MessageMaxLen)), - writeBuffer(static_cast(std::malloc(MessageMaxLen))) + producerSocket(new ProducerSocket(producerFd, MessageMaxLen)) { MS_TRACE_STD(); } @@ -45,13 +43,12 @@ namespace Channel ChannelWriteFn channelWriteFn, ChannelWriteCtx channelWriteCtx) : channelReadFn(channelReadFn), channelReadCtx(channelReadCtx), channelWriteFn(channelWriteFn), - channelWriteCtx(channelWriteCtx) + channelWriteCtx(channelWriteCtx), uvReadHandle(new uv_async_t) { MS_TRACE_STD(); int err; - this->uvReadHandle = new uv_async_t; this->uvReadHandle->data = static_cast(this); err = @@ -80,10 +77,10 @@ namespace Channel { MS_TRACE_STD(); - std::free(this->writeBuffer); - if (!this->closed) + { Close(); + } delete this->consumerSocket; delete this->producerSocket; @@ -94,13 +91,16 @@ namespace Channel MS_TRACE_STD(); if (this->closed) + { return; + } this->closed = true; if (this->uvReadHandle) { - uv_close(reinterpret_cast(this->uvReadHandle), static_cast(onClose)); + uv_close( + reinterpret_cast(this->uvReadHandle), static_cast(onCloseAsync)); } if (this->consumerSocket) @@ -121,59 +121,48 @@ namespace Channel this->listener = listener; } - void ChannelSocket::Send(json& jsonMessage) + void ChannelSocket::Send(const uint8_t* data, uint32_t dataLen) { MS_TRACE_STD(); if (this->closed) - return; - - std::string message = jsonMessage.dump(); - - if (message.length() > PayloadMaxLen) { - MS_ERROR_STD("message too big"); - return; } - SendImpl( - reinterpret_cast(message.c_str()), static_cast(message.length())); - } - - void ChannelSocket::Send(const std::string& message) - { - MS_TRACE_STD(); - - if (this->closed) - return; - - if (message.length() > PayloadMaxLen) + if (dataLen > PayloadMaxLen) { MS_ERROR_STD("message too big"); return; } - SendImpl( - reinterpret_cast(message.c_str()), static_cast(message.length())); + SendImpl(data, dataLen); } - void ChannelSocket::SendLog(const char* message, uint32_t messageLen) + void ChannelSocket::SendLog(const char* data, uint32_t dataLen) { MS_TRACE_STD(); if (this->closed) + { return; + } - if (messageLen > PayloadMaxLen) + if (dataLen > PayloadMaxLen) { MS_ERROR_STD("message too big"); return; } - SendImpl(reinterpret_cast(message), messageLen); + auto log = FBS::Log::CreateLogDirect(this->bufferBuilder, data); + auto message = + FBS::Message::CreateMessage(this->bufferBuilder, FBS::Message::Body::Log, log.Union()); + + this->bufferBuilder.FinishSizePrefixed(message); + this->Send(this->bufferBuilder.GetBufferPointer(), this->bufferBuilder.GetSize()); + this->bufferBuilder.Clear(); } bool ChannelSocket::CallbackRead() @@ -181,30 +170,39 @@ namespace Channel MS_TRACE_STD(); if (this->closed) + { return false; + } - uint8_t* message{ nullptr }; - uint32_t messageLen; - size_t messageCtx; + uint8_t* msg{ nullptr }; + uint32_t msgLen; + size_t msgCtx; // Try to read next message using `channelReadFn`, message, its length and context will be // stored in provided arguments. - auto free = this->channelReadFn( - &message, &messageLen, &messageCtx, this->uvReadHandle, this->channelReadCtx); + auto free = this->channelReadFn(&msg, &msgLen, &msgCtx, this->uvReadHandle, this->channelReadCtx); // Non-null free function pointer means message was successfully read above and will need to be // freed later. if (free) { - try - { - char* charMessage{ reinterpret_cast(message) }; + const auto* message = FBS::Message::GetMessage(msg); - auto* request = new Channel::ChannelRequest(this, charMessage, messageLen); +#if MS_LOG_DEV_LEVEL == 3 + auto s = flatbuffers::FlatBufferToString( + reinterpret_cast(msg), FBS::Message::MessageTypeTable()); + MS_DUMP("%s", s.c_str()); +#endif + + if (message->data_type() == FBS::Message::Body::Request) + { + ChannelRequest* request; - // Notify the listener. try { + request = new ChannelRequest(this, message->data_as()); + + // Notify the listener. this->listener->HandleRequest(request); } catch (const MediaSoupTypeError& error) @@ -216,27 +214,40 @@ namespace Channel request->Error(error.what()); } - // Delete the Request. delete request; } - catch (const json::parse_error& error) + else if (message->data_type() == FBS::Message::Body::Notification) { - MS_ERROR_STD("message parsing error: %s", error.what()); + ChannelNotification* notification; + + try + { + notification = new ChannelNotification(message->data_as()); + + // Notify the listener. + this->listener->HandleNotification(notification); + } + catch (const MediaSoupError& error) + { + MS_ERROR("notification failed: %s", error.what()); + } + + delete notification; } - catch (const MediaSoupError& error) + else { - MS_ERROR_STD("discarding wrong Channel request: %s", error.what()); + MS_ERROR("discarding wrong Channel data"); } // Message needs to be freed using stored function pointer. - free(message, messageLen, messageCtx); + free(msg, msgLen, msgCtx); } // Return `true` if something was processed. return free != nullptr; } - inline void ChannelSocket::SendImpl(const uint8_t* payload, uint32_t payloadLen) + void ChannelSocket::SendImpl(const uint8_t* payload, uint32_t payloadLen) { MS_TRACE_STD(); @@ -247,30 +258,32 @@ namespace Channel } else { - std::memcpy(this->writeBuffer, &payloadLen, sizeof(uint32_t)); - - if (payloadLen != 0) - { - std::memcpy(this->writeBuffer + sizeof(uint32_t), payload, payloadLen); - } - - size_t len = sizeof(uint32_t) + payloadLen; - - this->producerSocket->Write(this->writeBuffer, len); + this->producerSocket->Write(payload, payloadLen); } } - void ChannelSocket::OnConsumerSocketMessage(ConsumerSocket* /*consumerSocket*/, char* msg, size_t msgLen) + void ChannelSocket::OnConsumerSocketMessage( + ConsumerSocket* /*consumerSocket*/, char* msg, size_t /*msgLen*/) { - MS_TRACE_STD(); + MS_TRACE(); + + const auto* message = FBS::Message::GetMessage(msg); + +#if MS_LOG_DEV_LEVEL == 3 + auto s = flatbuffers::FlatBufferToString( + reinterpret_cast(msg), FBS::Message::MessageTypeTable()); + MS_DUMP("%s", s.c_str()); +#endif - try + if (message->data_type() == FBS::Message::Body::Request) { - auto* request = new Channel::ChannelRequest(this, msg, msgLen); + ChannelRequest* request; - // Notify the listener. try { + request = new ChannelRequest(this, message->data_as()); + + // Notify the listener. this->listener->HandleRequest(request); } catch (const MediaSoupTypeError& error) @@ -282,16 +295,29 @@ namespace Channel request->Error(error.what()); } - // Delete the Request. delete request; } - catch (const json::parse_error& error) + else if (message->data_type() == FBS::Message::Body::Notification) { - MS_ERROR_STD("JSON parsing error: %s", error.what()); + ChannelNotification* notification; + + try + { + notification = new ChannelNotification(message->data_as()); + + // Notify the listener. + this->listener->HandleNotification(notification); + } + catch (const MediaSoupError& error) + { + MS_ERROR("notification failed: %s", error.what()); + } + + delete notification; } - catch (const MediaSoupError& error) + else { - MS_ERROR_STD("discarding wrong Channel request: %s", error.what()); + MS_ERROR("discarding wrong Channel data"); } } @@ -305,7 +331,8 @@ namespace Channel /* Instance methods. */ ConsumerSocket::ConsumerSocket(int fd, size_t bufferSize, Listener* listener) - : ::UnixStreamSocket(fd, bufferSize, ::UnixStreamSocket::Role::CONSUMER), listener(listener) + : ::UnixStreamSocketHandle(fd, bufferSize, ::UnixStreamSocketHandle::Role::CONSUMER), + listener(listener) { MS_TRACE_STD(); } @@ -325,9 +352,11 @@ namespace Channel while (true) { if (IsClosed()) + { return; + } - size_t readLen = this->bufferDataLen - msgStart; + const size_t readLen = this->bufferDataLen - msgStart; if (readLen < sizeof(uint32_t)) { @@ -375,7 +404,7 @@ namespace Channel /* Instance methods. */ ProducerSocket::ProducerSocket(int fd, size_t bufferSize) - : ::UnixStreamSocket(fd, bufferSize, ::UnixStreamSocket::Role::PRODUCER) + : ::UnixStreamSocketHandle(fd, bufferSize, ::UnixStreamSocketHandle::Role::PRODUCER) { MS_TRACE_STD(); } diff --git a/worker/src/ChannelMessageRegistrator.cpp b/worker/src/ChannelMessageRegistrator.cpp index 4f5c9bbe50..647994d35c 100644 --- a/worker/src/ChannelMessageRegistrator.cpp +++ b/worker/src/ChannelMessageRegistrator.cpp @@ -15,54 +15,38 @@ ChannelMessageRegistrator::~ChannelMessageRegistrator() MS_TRACE(); this->mapChannelRequestHandlers.clear(); - this->mapPayloadChannelRequestHandlers.clear(); - this->mapPayloadChannelNotificationHandlers.clear(); + this->mapChannelNotificationHandlers.clear(); } -void ChannelMessageRegistrator::FillJson(json& jsonObject) +flatbuffers::Offset ChannelMessageRegistrator::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) { - MS_TRACE(); - - // Add channelRequestHandlers. - jsonObject["channelRequestHandlers"] = json::array(); - auto jsonChannelRequestHandlersIt = jsonObject.find("channelRequestHandlers"); - + // Add channelRequestHandlerIds. + std::vector> channelRequestHandlerIds; for (const auto& kv : this->mapChannelRequestHandlers) { const auto& handlerId = kv.first; - jsonChannelRequestHandlersIt->emplace_back(handlerId); + channelRequestHandlerIds.push_back(builder.CreateString(handlerId)); } - // Add payloadChannelRequestHandlers. - jsonObject["payloadChannelRequestHandlers"] = json::array(); - auto jsonPayloadChannelRequestHandlersIt = jsonObject.find("payloadChannelRequestHandlers"); - - for (const auto& kv : this->mapPayloadChannelRequestHandlers) + // Add channelNotificationHandlerIds. + std::vector> channelNotificationHandlerIds; + for (const auto& kv : this->mapChannelNotificationHandlers) { const auto& handlerId = kv.first; - jsonPayloadChannelRequestHandlersIt->emplace_back(handlerId); + channelNotificationHandlerIds.push_back(builder.CreateString(handlerId)); } - // Add payloadChannelNotificationHandlers. - jsonObject["payloadChannelNotificationHandlers"] = json::array(); - auto jsonPayloadChannelNotificationHandlersIt = - jsonObject.find("payloadChannelNotificationHandlers"); - - for (const auto& kv : this->mapPayloadChannelNotificationHandlers) - { - const auto& handlerId = kv.first; - - jsonPayloadChannelNotificationHandlersIt->emplace_back(handlerId); - } + return FBS::Worker::CreateChannelMessageHandlersDirect( + builder, &channelRequestHandlerIds, &channelNotificationHandlerIds); } void ChannelMessageRegistrator::RegisterHandler( const std::string& id, Channel::ChannelSocket::RequestHandler* channelRequestHandler, - PayloadChannel::PayloadChannelSocket::RequestHandler* payloadChannelRequestHandler, - PayloadChannel::PayloadChannelSocket::NotificationHandler* payloadChannelNotificationHandler) + Channel::ChannelSocket::NotificationHandler* channelNotificationHandler) { MS_TRACE(); @@ -76,41 +60,19 @@ void ChannelMessageRegistrator::RegisterHandler( this->mapChannelRequestHandlers[id] = channelRequestHandler; } - if (payloadChannelRequestHandler != nullptr) - { - if (this->mapPayloadChannelRequestHandlers.find(id) != this->mapPayloadChannelRequestHandlers.end()) - { - if (channelRequestHandler != nullptr) - { - this->mapChannelRequestHandlers.erase(id); - } - - MS_THROW_ERROR("PayloadChannel request handler with ID %s already exists", id.c_str()); - } - - this->mapPayloadChannelRequestHandlers[id] = payloadChannelRequestHandler; - } - - if (payloadChannelNotificationHandler != nullptr) + if (channelNotificationHandler != nullptr) { - if ( - this->mapPayloadChannelNotificationHandlers.find(id) != - this->mapPayloadChannelNotificationHandlers.end()) + if (this->mapChannelNotificationHandlers.find(id) != this->mapChannelNotificationHandlers.end()) { if (channelRequestHandler != nullptr) { this->mapChannelRequestHandlers.erase(id); } - if (payloadChannelRequestHandler != nullptr) - { - this->mapPayloadChannelRequestHandlers.erase(id); - } - - MS_THROW_ERROR("PayloadChannel notification handler with ID %s already exists", id.c_str()); + MS_THROW_ERROR("Channel notification handler with ID %s already exists", id.c_str()); } - this->mapPayloadChannelNotificationHandlers[id] = payloadChannelNotificationHandler; + this->mapChannelNotificationHandlers[id] = channelNotificationHandler; } } @@ -119,8 +81,7 @@ void ChannelMessageRegistrator::UnregisterHandler(const std::string& id) MS_TRACE(); this->mapChannelRequestHandlers.erase(id); - this->mapPayloadChannelRequestHandlers.erase(id); - this->mapPayloadChannelNotificationHandlers.erase(id); + this->mapChannelNotificationHandlers.erase(id); } Channel::ChannelSocket::RequestHandler* ChannelMessageRegistrator::GetChannelRequestHandler( @@ -140,31 +101,14 @@ Channel::ChannelSocket::RequestHandler* ChannelMessageRegistrator::GetChannelReq } } -PayloadChannel::PayloadChannelSocket::RequestHandler* ChannelMessageRegistrator::GetPayloadChannelRequestHandler( +Channel::ChannelSocket::NotificationHandler* ChannelMessageRegistrator::GetChannelNotificationHandler( const std::string& id) { MS_TRACE(); - auto it = this->mapPayloadChannelRequestHandlers.find(id); - - if (it != this->mapPayloadChannelRequestHandlers.end()) - { - return it->second; - } - else - { - return nullptr; - } -} - -PayloadChannel::PayloadChannelSocket::NotificationHandler* ChannelMessageRegistrator:: - GetPayloadChannelNotificationHandler(const std::string& id) -{ - MS_TRACE(); - - auto it = this->mapPayloadChannelNotificationHandlers.find(id); + auto it = this->mapChannelNotificationHandlers.find(id); - if (it != this->mapPayloadChannelNotificationHandlers.end()) + if (it != this->mapChannelNotificationHandlers.end()) { return it->second; } diff --git a/worker/src/DepLibSRTP.cpp b/worker/src/DepLibSRTP.cpp index ea888bf296..bb9fdde4d6 100644 --- a/worker/src/DepLibSRTP.cpp +++ b/worker/src/DepLibSRTP.cpp @@ -8,8 +8,8 @@ /* Static variables. */ -static std::mutex globalSyncMutex; -static size_t globalInstances = 0; +static std::mutex GlobalSyncMutex; +static size_t GlobalInstances = 0; // clang-format off std::vector DepLibSRTP::errors = @@ -50,19 +50,21 @@ void DepLibSRTP::ClassInit() MS_TRACE(); { - std::lock_guard lock(globalSyncMutex); + const std::lock_guard lock(GlobalSyncMutex); - if (globalInstances == 0) + if (GlobalInstances == 0) { MS_DEBUG_TAG(info, "libsrtp version: \"%s\"", srtp_get_version_string()); - srtp_err_status_t err = srtp_init(); + const srtp_err_status_t err = srtp_init(); if (DepLibSRTP::IsError(err)) + { MS_THROW_ERROR("srtp_init() failed: %s", DepLibSRTP::GetErrorString(err)); + } } - ++globalInstances; + ++GlobalInstances; } } @@ -71,10 +73,10 @@ void DepLibSRTP::ClassDestroy() MS_TRACE(); { - std::lock_guard lock(globalSyncMutex); - --globalInstances; + const std::lock_guard lock(GlobalSyncMutex); + --GlobalInstances; - if (globalInstances == 0) + if (GlobalInstances == 0) { srtp_shutdown(); } diff --git a/worker/src/DepLibUV.cpp b/worker/src/DepLibUV.cpp index ed6c7b4a9b..12af49ba59 100644 --- a/worker/src/DepLibUV.cpp +++ b/worker/src/DepLibUV.cpp @@ -3,7 +3,6 @@ #include "DepLibUV.hpp" #include "Logger.hpp" -#include // std::abort() /* Static variables. */ @@ -11,9 +10,9 @@ thread_local uv_loop_t* DepLibUV::loop{ nullptr }; /* Static methods for UV callbacks. */ -inline static void onClose(uv_handle_t* handle) +inline static void onCloseLoop(uv_handle_t* handle) { - delete handle; + delete reinterpret_cast(handle); } inline static void onWalk(uv_handle_t* handle, void* /*arg*/) @@ -27,7 +26,9 @@ inline static void onWalk(uv_handle_t* handle, void* /*arg*/) uv_has_ref(handle)); if (!uv_is_closing(handle)) - uv_close(handle, onClose); + { + uv_close(handle, onCloseLoop); + } } /* Static methods. */ @@ -38,10 +39,12 @@ void DepLibUV::ClassInit() DepLibUV::loop = new uv_loop_t; - int err = uv_loop_init(DepLibUV::loop); + const int err = uv_loop_init(DepLibUV::loop); if (err != 0) + { MS_ABORT("libuv loop initialization failed"); + } } void DepLibUV::ClassDestroy() @@ -63,13 +66,17 @@ void DepLibUV::ClassDestroy() err = uv_loop_close(DepLibUV::loop); if (err != UV_EBUSY) + { break; + } uv_run(DepLibUV::loop, UV_RUN_NOWAIT); } if (err != 0) + { MS_ERROR_STD("failed to close libuv loop: %s", uv_err_name(err)); + } delete DepLibUV::loop; } @@ -88,7 +95,7 @@ void DepLibUV::RunLoop() // This should never happen. MS_ASSERT(DepLibUV::loop != nullptr, "loop unset"); - int ret = uv_run(DepLibUV::loop, UV_RUN_DEFAULT); + const int ret = uv_run(DepLibUV::loop, UV_RUN_DEFAULT); MS_ASSERT(ret == 0, "uv_run() returned %s", uv_err_name(ret)); } diff --git a/worker/src/DepLibUring.cpp b/worker/src/DepLibUring.cpp new file mode 100644 index 0000000000..651a8a025d --- /dev/null +++ b/worker/src/DepLibUring.cpp @@ -0,0 +1,629 @@ +#define MS_CLASS "DepLibUring" +// #define MS_LOG_DEV_LEVEL 3 + +#include "DepLibUring.hpp" +#include "Logger.hpp" +#include "MediaSoupErrors.hpp" +#include "Settings.hpp" +#include "Utils.hpp" +#include +#include +#include + +/* Static variables. */ +thread_local bool DepLibUring::enabled{ false }; +// liburing instance per thread. +thread_local DepLibUring::LibUring* DepLibUring::liburing{ nullptr }; +// Completion queue entry array used to retrieve processes tasks. +thread_local struct io_uring_cqe* cqes[DepLibUring::QueueDepth]; + +/* Static methods for UV callbacks. */ + +inline static void onCloseFd(uv_handle_t* handle) +{ + delete reinterpret_cast(handle); +} + +inline static void onFdEvent(uv_poll_t* handle, int status, int events) +{ + auto* liburing = static_cast(handle->data); + auto count = io_uring_peek_batch_cqe(liburing->GetRing(), cqes, DepLibUring::QueueDepth); + + // libuv uses level triggering, so we need to read from the socket to reset + // the counter in order to avoid libuv calling this callback indefinitely. + eventfd_t v; + int err = eventfd_read(liburing->GetEventFd(), std::addressof(v)); + + if (err < 0) + { + // Get positive errno. + int error = -err; + + MS_ABORT("eventfd_read() failed: %s", std::strerror(error)); + }; + + for (unsigned int i{ 0 }; i < count; ++i) + { + struct io_uring_cqe* cqe = cqes[i]; + auto* userData = static_cast(io_uring_cqe_get_data(cqe)); + + if (liburing->IsZeroCopyEnabled()) + { + // CQE notification for a zero-copy submission. + if (cqe->flags & IORING_CQE_F_NOTIF) + { + // The send buffer is now in the network card, run the send callback. + if (userData->cb) + { + (*userData->cb)(true); + delete userData->cb; + userData->cb = nullptr; + } + + liburing->ReleaseUserDataEntry(userData->idx); + io_uring_cqe_seen(liburing->GetRing(), cqe); + + continue; + } + + // CQE for a zero-copy submission, a CQE notification will follow. + if (cqe->flags & IORING_CQE_F_MORE) + { + if (cqe->res < 0) + { + if (userData->cb) + { + (*userData->cb)(false); + delete userData->cb; + userData->cb = nullptr; + } + } + + // NOTE: Do not release the user data as it will be done upon reception + // of CQE notification. + io_uring_cqe_seen(liburing->GetRing(), cqe); + + continue; + } + } + + // Successfull SQE. + if (cqe->res >= 0) + { + if (userData->cb) + { + (*userData->cb)(true); + delete userData->cb; + userData->cb = nullptr; + } + } + // Failed SQE. + else + { + if (userData->cb) + { + (*userData->cb)(false); + delete userData->cb; + userData->cb = nullptr; + } + } + + liburing->ReleaseUserDataEntry(userData->idx); + io_uring_cqe_seen(liburing->GetRing(), cqe); + } +} + +/* Static class methods */ + +void DepLibUring::ClassInit() +{ + const auto mayor = io_uring_major_version(); + const auto minor = io_uring_minor_version(); + + MS_DEBUG_TAG(info, "io_uring version: \"%i.%i\"", mayor, minor); + + if (Settings::configuration.liburingDisabled) + { + MS_DEBUG_TAG(info, "io_uring disabled by user settings"); + + return; + } + + // This must be called first. + if (DepLibUring::CheckRuntimeSupport()) + { + try + { + DepLibUring::liburing = new LibUring(); + + MS_DEBUG_TAG(info, "io_uring enabled"); + + DepLibUring::enabled = true; + } + catch (const MediaSoupError& error) + { + MS_DEBUG_TAG(info, "io_uring initialization failed, io_uring not enabled"); + } + } + else + { + MS_DEBUG_TAG(info, "io_uring not enabled"); + } +} + +void DepLibUring::ClassDestroy() +{ + MS_TRACE(); + + delete DepLibUring::liburing; +} + +bool DepLibUring::CheckRuntimeSupport() +{ + // clang-format off + struct utsname buffer{}; + // clang-format on + + auto err = uname(std::addressof(buffer)); + + if (err != 0) + { + MS_THROW_ERROR("uname() failed: %s", std::strerror(err)); + } + + MS_DEBUG_TAG(info, "kernel version: %s", buffer.version); + + auto* kernelMayorCstr = buffer.release; + auto kernelMayorLong = strtol(kernelMayorCstr, &kernelMayorCstr, 10); + + // liburing `sento` capabilities are supported for kernel versions greather + // than or equal to 6. + if (kernelMayorLong < 6) + { + MS_DEBUG_TAG(info, "kernel doesn't support io_uring"); + + return false; + } + + return true; +} + +bool DepLibUring::IsEnabled() +{ + return DepLibUring::enabled; +} + +flatbuffers::Offset DepLibUring::FillBuffer(flatbuffers::FlatBufferBuilder& builder) +{ + MS_TRACE(); + + MS_ASSERT(DepLibUring::enabled, "io_uring not enabled"); + + return DepLibUring::liburing->FillBuffer(builder); +} + +void DepLibUring::StartPollingCQEs() +{ + MS_TRACE(); + + MS_ASSERT(DepLibUring::enabled, "io_uring not enabled"); + + DepLibUring::liburing->StartPollingCQEs(); +} + +void DepLibUring::StopPollingCQEs() +{ + MS_TRACE(); + + MS_ASSERT(DepLibUring::enabled, "io_uring not enabled"); + + DepLibUring::liburing->StopPollingCQEs(); +} + +uint8_t* DepLibUring::GetSendBuffer() +{ + MS_TRACE(); + + MS_ASSERT(DepLibUring::enabled, "io_uring not enabled"); + + return DepLibUring::liburing->GetSendBuffer(); +} + +bool DepLibUring::PrepareSend( + int sockfd, const uint8_t* data, size_t len, const struct sockaddr* addr, onSendCallback* cb) +{ + MS_TRACE(); + + MS_ASSERT(DepLibUring::enabled, "io_uring not enabled"); + + return DepLibUring::liburing->PrepareSend(sockfd, data, len, addr, cb); +} + +bool DepLibUring::PrepareWrite( + int sockfd, const uint8_t* data1, size_t len1, const uint8_t* data2, size_t len2, onSendCallback* cb) +{ + MS_TRACE(); + + MS_ASSERT(DepLibUring::enabled, "io_uring not enabled"); + + return DepLibUring::liburing->PrepareWrite(sockfd, data1, len1, data2, len2, cb); +} + +void DepLibUring::Submit() +{ + MS_TRACE(); + + MS_ASSERT(DepLibUring::enabled, "io_uring not enabled"); + + DepLibUring::liburing->Submit(); +} + +void DepLibUring::SetActive() +{ + MS_TRACE(); + + MS_ASSERT(DepLibUring::enabled, "io_uring not enabled"); + + DepLibUring::liburing->SetActive(); +} + +bool DepLibUring::IsActive() +{ + MS_TRACE(); + + MS_ASSERT(DepLibUring::enabled, "io_uring not enabled"); + + return DepLibUring::liburing->IsActive(); +} + +/* Instance methods. */ + +DepLibUring::LibUring::LibUring() +{ + MS_TRACE(); + + /** + * IORING_SETUP_SINGLE_ISSUER: A hint to the kernel that only a single task + * (or thread) will submit requests, which is used for internal optimisations. + */ + + unsigned int flags = IORING_SETUP_SINGLE_ISSUER; + + // Initialize io_uring. + auto err = io_uring_queue_init(DepLibUring::QueueDepth, std::addressof(this->ring), flags); + + if (err < 0) + { + // Get positive errno. + int error = -err; + + MS_THROW_ERROR("io_uring_queue_init() failed: %s", std::strerror(error)); + } + + // Create an eventfd instance. + this->efd = eventfd(0, 0); + + if (this->efd < 0) + { + MS_THROW_ERROR("eventfd() failed: %s", std::strerror(-this->efd)); + } + + err = io_uring_register_eventfd(std::addressof(this->ring), this->efd); + + if (err < 0) + { + // Get positive errno. + int error = -err; + + MS_THROW_ERROR("io_uring_register_eventfd() failed: %s", std::strerror(error)); + } + + // Initialize available UserData entries. + for (size_t i{ 0 }; i < DepLibUring::QueueDepth; ++i) + { + this->userDatas[i].store = this->sendBuffers[i]; + this->availableUserDataEntries.push(i); + } + + // Initialize iovecs. + for (size_t i{ 0 }; i < DepLibUring::QueueDepth; ++i) + { + this->iovecs[i].iov_base = this->sendBuffers[i]; + this->iovecs[i].iov_len = DepLibUring::SendBufferSize; + } + + err = io_uring_register_buffers(std::addressof(this->ring), this->iovecs, DepLibUring::QueueDepth); + + if (err < 0) + { + // Get positive errno. + int error = -err; + + if (error == ENOMEM) + { + this->zeroCopyEnabled = false; + + struct rlimit l = {}; + + if (getrlimit(RLIMIT_MEMLOCK, std::addressof(l)) == -1) + { + MS_WARN_TAG(info, "getrlimit() failed: %s", std::strerror(errno)); + MS_WARN_TAG( + info, + "io_uring_register_buffers() failed due to low RLIMIT_MEMLOCK, disabling zero copy: %s", + std::strerror(error)); + } + else + { + MS_WARN_TAG( + info, + "io_uring_register_buffers() failed due to low RLIMIT_MEMLOCK (soft:%lu, hard:%lu), disabling zero copy: %s", + l.rlim_cur, + l.rlim_max, + std::strerror(error)); + } + } + else + { + MS_THROW_ERROR("io_uring_register_buffers() failed: %s", std::strerror(error)); + } + } +} + +DepLibUring::LibUring::~LibUring() +{ + MS_TRACE(); + + // Close the event file descriptor. + const auto err = close(this->efd); + + if (err != 0) + { + // Get positive errno. + int error = -err; + + MS_ABORT("close() failed: %s", std::strerror(error)); + } + + // Close the ring. + io_uring_queue_exit(std::addressof(this->ring)); +} + +flatbuffers::Offset DepLibUring::LibUring::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const +{ + MS_TRACE(); + + return FBS::LibUring::CreateDump( + builder, this->sqeProcessCount, this->sqeMissCount, this->userDataMissCount); +} + +void DepLibUring::LibUring::StartPollingCQEs() +{ + MS_TRACE(); + + // Watch the event file descriptor for completions. + this->uvHandle = new uv_poll_t; + + auto err = uv_poll_init(DepLibUV::GetLoop(), this->uvHandle, this->efd); + + if (err != 0) + { + delete this->uvHandle; + + MS_THROW_ERROR("uv_poll_init() failed: %s", uv_strerror(err)); + } + + this->uvHandle->data = this; + + err = uv_poll_start(this->uvHandle, UV_READABLE, static_cast(onFdEvent)); + + if (err != 0) + { + MS_THROW_ERROR("uv_poll_start() failed: %s", uv_strerror(err)); + } +} + +void DepLibUring::LibUring::StopPollingCQEs() +{ + MS_TRACE(); + + this->uvHandle->data = nullptr; + + // Stop polling the event file descriptor. + auto err = uv_poll_stop(this->uvHandle); + + if (err != 0) + { + MS_ABORT("uv_poll_stop() failed: %s", uv_strerror(err)); + } + + // NOTE: Handles that wrap file descriptors are clossed immediately. + uv_close(reinterpret_cast(this->uvHandle), static_cast(onCloseFd)); +} + +uint8_t* DepLibUring::LibUring::GetSendBuffer() +{ + MS_TRACE(); + + if (this->availableUserDataEntries.empty()) + { + MS_DEBUG_DEV("no user data entry available"); + + return nullptr; + } + + auto idx = this->availableUserDataEntries.front(); + + return this->userDatas[idx].store; +} + +bool DepLibUring::LibUring::PrepareSend( + int sockfd, const uint8_t* data, size_t len, const struct sockaddr* addr, onSendCallback* cb) +{ + MS_TRACE(); + + auto* userData = this->GetUserData(); + + if (!userData) + { + MS_DEBUG_DEV("no user data entry available"); + + this->userDataMissCount++; + + return false; + } + + auto* sqe = io_uring_get_sqe(std::addressof(this->ring)); + + if (!sqe) + { + MS_DEBUG_DEV("no sqe available"); + + this->sqeMissCount++; + + return false; + } + + // The send data buffer belongs to us, no need to memcpy. + if (this->IsDataInSendBuffers(data)) + { + MS_ASSERT(data == userData->store, "send buffer does not match userData store"); + } + else + { + std::memcpy(userData->store, data, len); + } + + userData->cb = cb; + + io_uring_sqe_set_data(sqe, userData); + + socklen_t addrlen = Utils::IP::GetAddressLen(addr); + + if (this->zeroCopyEnabled) + { + auto iovec = this->iovecs[userData->idx]; + iovec.iov_len = len; + + io_uring_prep_send_zc(sqe, sockfd, iovec.iov_base, iovec.iov_len, 0, 0); + io_uring_prep_send_set_addr(sqe, addr, addrlen); + + // Tell io_uring that we are providing the already registered send buffer + // for zero copy. + sqe->ioprio |= IORING_RECVSEND_FIXED_BUF; + sqe->buf_index = userData->idx; + } + else + { + io_uring_prep_sendto(sqe, sockfd, userData->store, len, 0, addr, addrlen); + } + + this->sqeProcessCount++; + + return true; +} + +bool DepLibUring::LibUring::PrepareWrite( + int sockfd, const uint8_t* data1, size_t len1, const uint8_t* data2, size_t len2, onSendCallback* cb) +{ + MS_TRACE(); + + auto* userData = this->GetUserData(); + + if (!userData) + { + MS_DEBUG_DEV("no user data entry available"); + + this->userDataMissCount++; + + return false; + } + + auto* sqe = io_uring_get_sqe(std::addressof(this->ring)); + + if (!sqe) + { + MS_DEBUG_DEV("no sqe available"); + + this->sqeMissCount++; + + return false; + } + + // The send data buffer belongs to us, no need to memcpy. + // NOTE: data1 contains the TCP framing buffer and data2 the actual payload. + if (this->IsDataInSendBuffers(data2)) + { + MS_ASSERT(data2 == userData->store, "send buffer does not match userData store"); + + // Always memcpy the frame len as it resides in the stack memory. + std::memcpy(userData->frameLen, data1, len1); + + userData->iov[0].iov_base = userData->frameLen; + userData->iov[0].iov_len = len1; + userData->iov[1].iov_base = userData->store; + userData->iov[1].iov_len = len2; + } + else + { + std::memcpy(userData->store, data1, len1); + std::memcpy(userData->store + len1, data2, len2); + + userData->iov[0].iov_base = userData->store; + userData->iov[0].iov_len = len1; + userData->iov[1].iov_base = userData->store + len1; + userData->iov[1].iov_len = len2; + } + + userData->cb = cb; + + io_uring_sqe_set_data(sqe, userData); + + io_uring_prep_writev(sqe, sockfd, userData->iov, 2, 0); + + this->sqeProcessCount++; + + return true; +} + +void DepLibUring::LibUring::Submit() +{ + MS_TRACE(); + + // Unset active flag. + SetInactive(); + + auto err = io_uring_submit(std::addressof(this->ring)); + + if (err >= 0) + { + MS_DEBUG_DEV("%i submission queue entries submitted", err); + } + else + { + // Get positive errno. + int error = -err; + + MS_ERROR("io_uring_submit() failed: %s", std::strerror(error)); + } +} + +DepLibUring::UserData* DepLibUring::LibUring::GetUserData() +{ + MS_TRACE(); + + if (this->availableUserDataEntries.empty()) + { + return nullptr; + } + + auto idx = this->availableUserDataEntries.front(); + + this->availableUserDataEntries.pop(); + + auto* userData = std::addressof(this->userDatas[idx]); + userData->idx = idx; + + return userData; +} diff --git a/worker/src/DepLibWebRTC.cpp b/worker/src/DepLibWebRTC.cpp index c5edfa2999..dda51cc403 100644 --- a/worker/src/DepLibWebRTC.cpp +++ b/worker/src/DepLibWebRTC.cpp @@ -9,7 +9,7 @@ /* Static. */ -static std::once_flag globalInitOnce; +static std::once_flag GlobalInitOnce; /* Static methods. */ @@ -21,7 +21,7 @@ void DepLibWebRTC::ClassInit() info, "libwebrtc field trials: \"%s\"", Settings::configuration.libwebrtcFieldTrials.c_str()); std::call_once( - globalInitOnce, + GlobalInitOnce, [] { webrtc::field_trial::InitFieldTrialsFromString( diff --git a/worker/src/DepOpenSSL.cpp b/worker/src/DepOpenSSL.cpp index 7b50a83b78..1f75d46d6c 100644 --- a/worker/src/DepOpenSSL.cpp +++ b/worker/src/DepOpenSSL.cpp @@ -9,7 +9,7 @@ /* Static. */ -static std::once_flag globalInitOnce; +static std::once_flag GlobalInitOnce; /* Static methods. */ @@ -18,10 +18,11 @@ void DepOpenSSL::ClassInit() MS_TRACE(); std::call_once( - globalInitOnce, + GlobalInitOnce, [] { MS_DEBUG_TAG(info, "openssl version: \"%s\"", OpenSSL_version(OPENSSL_VERSION)); + MS_DEBUG_TAG(info, "openssl CPU info: \"%s\"", OpenSSL_version(OPENSSL_CPU_INFO)); // Initialize some crypto stuff. RAND_poll(); diff --git a/worker/src/DepUsrSCTP.cpp b/worker/src/DepUsrSCTP.cpp index 28ca0896a0..a833a5aefb 100644 --- a/worker/src/DepUsrSCTP.cpp +++ b/worker/src/DepUsrSCTP.cpp @@ -2,6 +2,9 @@ // #define MS_LOG_DEV_LEVEL 3 #include "DepUsrSCTP.hpp" +#ifdef MS_LIBURING_SUPPORTED +#include "DepLibUring.hpp" +#endif #include "DepLibUV.hpp" #include "Logger.hpp" #include @@ -66,7 +69,7 @@ void DepUsrSCTP::ClassInit() MS_DEBUG_TAG(info, "usrsctp"); - std::lock_guard lock(GlobalSyncMutex); + const std::lock_guard lock(GlobalSyncMutex); if (GlobalInstances == 0) { @@ -87,7 +90,7 @@ void DepUsrSCTP::ClassDestroy() { MS_TRACE(); - std::lock_guard lock(GlobalSyncMutex); + const std::lock_guard lock(GlobalSyncMutex); --GlobalInstances; if (GlobalInstances == 0) @@ -123,11 +126,13 @@ uintptr_t DepUsrSCTP::GetNextSctpAssociationId() { MS_TRACE(); - std::lock_guard lock(GlobalSyncMutex); + const std::lock_guard lock(GlobalSyncMutex); // NOTE: usrsctp_connect() fails with a value of 0. if (DepUsrSCTP::nextSctpAssociationId == 0u) + { ++DepUsrSCTP::nextSctpAssociationId; + } // In case we've wrapped around and need to find an empty spot from a removed // SctpAssociation. Assumes we'll never be full. @@ -137,7 +142,9 @@ uintptr_t DepUsrSCTP::GetNextSctpAssociationId() ++DepUsrSCTP::nextSctpAssociationId; if (DepUsrSCTP::nextSctpAssociationId == 0u) + { ++DepUsrSCTP::nextSctpAssociationId; + } } return DepUsrSCTP::nextSctpAssociationId++; @@ -147,7 +154,7 @@ void DepUsrSCTP::RegisterSctpAssociation(RTC::SctpAssociation* sctpAssociation) { MS_TRACE(); - std::lock_guard lock(GlobalSyncMutex); + const std::lock_guard lock(GlobalSyncMutex); MS_ASSERT(DepUsrSCTP::checker != nullptr, "Checker not created"); @@ -160,14 +167,16 @@ void DepUsrSCTP::RegisterSctpAssociation(RTC::SctpAssociation* sctpAssociation) DepUsrSCTP::mapIdSctpAssociation[sctpAssociation->id] = sctpAssociation; if (++DepUsrSCTP::numSctpAssociations == 1u) + { DepUsrSCTP::checker->Start(); + } } void DepUsrSCTP::DeregisterSctpAssociation(RTC::SctpAssociation* sctpAssociation) { MS_TRACE(); - std::lock_guard lock(GlobalSyncMutex); + const std::lock_guard lock(GlobalSyncMutex); MS_ASSERT(DepUsrSCTP::checker != nullptr, "Checker not created"); @@ -177,30 +186,32 @@ void DepUsrSCTP::DeregisterSctpAssociation(RTC::SctpAssociation* sctpAssociation MS_ASSERT(DepUsrSCTP::numSctpAssociations > 0u, "numSctpAssociations was not higher than 0"); if (--DepUsrSCTP::numSctpAssociations == 0u) + { DepUsrSCTP::checker->Stop(); + } } RTC::SctpAssociation* DepUsrSCTP::RetrieveSctpAssociation(uintptr_t id) { MS_TRACE(); - std::lock_guard lock(GlobalSyncMutex); + const std::lock_guard lock(GlobalSyncMutex); auto it = DepUsrSCTP::mapIdSctpAssociation.find(id); if (it == DepUsrSCTP::mapIdSctpAssociation.end()) + { return nullptr; + } return it->second; } /* DepUsrSCTP::Checker instance methods. */ -DepUsrSCTP::Checker::Checker() +DepUsrSCTP::Checker::Checker() : timer(new TimerHandle(this)) { MS_TRACE(); - - this->timer = new Timer(this); } DepUsrSCTP::Checker::~Checker() @@ -232,14 +243,34 @@ void DepUsrSCTP::Checker::Stop() this->timer->Stop(); } -void DepUsrSCTP::Checker::OnTimer(Timer* /*timer*/) +void DepUsrSCTP::Checker::OnTimer(TimerHandle* /*timer*/) { MS_TRACE(); - auto nowMs = DepLibUV::GetTimeMs(); - int elapsedMs = this->lastCalledAtMs ? static_cast(nowMs - this->lastCalledAtMs) : 0; + auto nowMs = DepLibUV::GetTimeMs(); + const int elapsedMs = this->lastCalledAtMs ? static_cast(nowMs - this->lastCalledAtMs) : 0; + +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + // Activate liburing usage. + // 'usrsctp_handle_timers()' will synchronously call the send/recv + // callbacks for the pending data. If there are multiple messages to be + // sent over the network then we will send those messages within a single + // system call. + DepLibUring::SetActive(); + } +#endif usrsctp_handle_timers(elapsedMs); +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + // Submit all prepared submission entries. + DepLibUring::Submit(); + } +#endif + this->lastCalledAtMs = nowMs; } diff --git a/worker/src/Logger.cpp b/worker/src/Logger.cpp index d7d972c9d7..6ec0c4181f 100644 --- a/worker/src/Logger.cpp +++ b/worker/src/Logger.cpp @@ -6,9 +6,9 @@ /* Class variables. */ -const uint64_t Logger::pid{ static_cast(uv_os_getpid()) }; +const uint64_t Logger::Pid{ static_cast(uv_os_getpid()) }; thread_local Channel::ChannelSocket* Logger::channel{ nullptr }; -thread_local char Logger::buffer[Logger::bufferSize]; +thread_local char Logger::buffer[Logger::BufferSize]; /* Class methods. */ diff --git a/worker/src/MediaSoupErrors.cpp b/worker/src/MediaSoupErrors.cpp index 29e3492f4a..d40b6d225b 100644 --- a/worker/src/MediaSoupErrors.cpp +++ b/worker/src/MediaSoupErrors.cpp @@ -2,4 +2,4 @@ #include "MediaSoupErrors.hpp" -thread_local char MediaSoupError::buffer[MediaSoupError::bufferSize]; +thread_local char MediaSoupError::buffer[MediaSoupError::BufferSize]; diff --git a/worker/src/PayloadChannel/PayloadChannelNotification.cpp b/worker/src/PayloadChannel/PayloadChannelNotification.cpp deleted file mode 100644 index 8805323d6d..0000000000 --- a/worker/src/PayloadChannel/PayloadChannelNotification.cpp +++ /dev/null @@ -1,80 +0,0 @@ -#define MS_CLASS "PayloadChannel::PayloadChannelNotification" -// #define MS_LOG_DEV_LEVEL 3 - -#include "PayloadChannel/PayloadChannelNotification.hpp" -#include "Logger.hpp" -#include "MediaSoupErrors.hpp" -#include "Utils.hpp" - -namespace PayloadChannel -{ - /* Class variables. */ - - // clang-format off - absl::flat_hash_map PayloadChannelNotification::string2EventId = - { - { "transport.sendRtcp", PayloadChannelNotification::EventId::TRANSPORT_SEND_RTCP }, - { "producer.send", PayloadChannelNotification::EventId::PRODUCER_SEND }, - { "dataProducer.send", PayloadChannelNotification::EventId::DATA_PRODUCER_SEND } - }; - // clang-format on - - /* Class methods. */ - - bool PayloadChannelNotification::IsNotification(const char* msg, size_t msgLen) - { - MS_TRACE(); - - return (msgLen > 2 && msg[0] == 'n' && msg[1] == ':'); - } - - /* Instance methods. */ - - PayloadChannelNotification::PayloadChannelNotification(const char* msg, size_t msgLen) - { - MS_TRACE(); - - auto info = Utils::String::Split(std::string(msg, msgLen), ':'); - - if (info.size() < 1) - MS_THROW_ERROR("too few arguments"); - - this->event = info[0]; - - auto eventIdIt = PayloadChannelNotification::string2EventId.find(this->event); - - if (eventIdIt == PayloadChannelNotification::string2EventId.end()) - MS_THROW_ERROR("unknown event '%s'", this->event.c_str()); - - this->eventId = eventIdIt->second; - - if (info.size() > 1) - { - auto& handlerId = info[1]; - - if (handlerId != "undefined") - this->handlerId = handlerId; - } - - if (info.size() > 2) - { - auto& data = info[2]; - - if (data != "undefined") - this->data = data; - } - } - - PayloadChannelNotification::~PayloadChannelNotification() - { - MS_TRACE(); - } - - void PayloadChannelNotification::SetPayload(const uint8_t* payload, size_t payloadLen) - { - MS_TRACE(); - - this->payload = payload; - this->payloadLen = payloadLen; - } -} // namespace PayloadChannel diff --git a/worker/src/PayloadChannel/PayloadChannelNotifier.cpp b/worker/src/PayloadChannel/PayloadChannelNotifier.cpp deleted file mode 100644 index 57090ff80d..0000000000 --- a/worker/src/PayloadChannel/PayloadChannelNotifier.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#define MS_CLASS "PayloadChannel::Notifier" -// #define MS_LOG_DEV_LEVEL 3 - -#include "PayloadChannel/PayloadChannelNotifier.hpp" -#include "Logger.hpp" - -namespace PayloadChannel -{ - PayloadChannelNotifier::PayloadChannelNotifier(PayloadChannel::PayloadChannelSocket* payloadChannel) - : payloadChannel(payloadChannel) - { - MS_TRACE(); - } - - void PayloadChannelNotifier::Emit( - const std::string& targetId, const char* event, const uint8_t* payload, size_t payloadLen) - { - MS_TRACE(); - - std::string notification("{\"targetId\":\""); - - notification.append(targetId); - notification.append("\",\"event\":\""); - notification.append(event); - notification.append("\"}"); - - this->payloadChannel->Send(notification, payload, payloadLen); - } - - void PayloadChannelNotifier::Emit( - const std::string& targetId, const char* event, json& data, const uint8_t* payload, size_t payloadLen) - { - MS_TRACE(); - - json jsonNotification = json::object(); - - jsonNotification["targetId"] = targetId; - jsonNotification["event"] = event; - jsonNotification["data"] = data; - - this->payloadChannel->Send(jsonNotification, payload, payloadLen); - } -} // namespace PayloadChannel diff --git a/worker/src/PayloadChannel/PayloadChannelRequest.cpp b/worker/src/PayloadChannel/PayloadChannelRequest.cpp deleted file mode 100644 index 9e074695d2..0000000000 --- a/worker/src/PayloadChannel/PayloadChannelRequest.cpp +++ /dev/null @@ -1,165 +0,0 @@ -#define MS_CLASS "PayloadChannel::PayloadChannelRequest" -// #define MS_LOG_DEV_LEVEL 3 - -#include "PayloadChannel/PayloadChannelRequest.hpp" -#include "Logger.hpp" -#include "MediaSoupErrors.hpp" -#include "Utils.hpp" -#include "PayloadChannel/PayloadChannelSocket.hpp" - -namespace PayloadChannel -{ - /* Class variables. */ - - // clang-format off - absl::flat_hash_map PayloadChannelRequest::string2MethodId = - { - { "dataConsumer.send", PayloadChannelRequest::MethodId::DATA_CONSUMER_SEND }, - }; - // clang-format on - - /* Class methods. */ - - bool PayloadChannelRequest::IsRequest(const char* msg, size_t msgLen) - { - MS_TRACE(); - - return (msgLen > 2 && msg[0] == 'r' && msg[1] == ':'); - } - - /* Instance methods. */ - - /** - * msg contains "id:method:handlerId:data" where: - * - id: The ID of the request. - * - handlerId: The ID of the target entity - * - data: JSON object. - */ - PayloadChannelRequest::PayloadChannelRequest( - PayloadChannel::PayloadChannelSocket* channel, char* msg, size_t msgLen) - : channel(channel) - { - MS_TRACE(); - - auto info = Utils::String::Split(std::string(msg, msgLen), ':', 3); - - if (info.size() < 2) - MS_THROW_ERROR("too few arguments"); - - this->id = std::stoul(info[0]); - this->method = info[1]; - - auto methodIdIt = PayloadChannelRequest::string2MethodId.find(this->method); - - if (methodIdIt == PayloadChannelRequest::string2MethodId.end()) - { - Error("unknown method"); - - MS_THROW_ERROR("unknown method '%s'", this->method.c_str()); - } - - this->methodId = methodIdIt->second; - - if (info.size() > 2) - { - auto& handlerId = info[2]; - - if (handlerId != "undefined") - this->handlerId = handlerId; - } - - if (info.size() > 3) - { - auto& data = info[3]; - - if (data != "undefined") - this->data = data; - } - } - - PayloadChannelRequest::~PayloadChannelRequest() - { - MS_TRACE(); - } - - void PayloadChannelRequest::Accept() - { - MS_TRACE(); - - MS_ASSERT(!this->replied, "request already replied"); - - this->replied = true; - - std::string response("{\"id\":"); - - response.append(std::to_string(this->id)); - response.append(",\"accepted\":true}"); - - this->channel->Send(response); - } - - void PayloadChannelRequest::Accept(json& data) - { - MS_TRACE(); - - MS_ASSERT(!this->replied, "request already replied"); - - this->replied = true; - - json jsonResponse = json::object(); - - jsonResponse["id"] = this->id; - jsonResponse["accepted"] = true; - - if (data.is_structured()) - jsonResponse["data"] = data; - - this->channel->Send(jsonResponse); - } - - void PayloadChannelRequest::Error(const char* reason) - { - MS_TRACE(); - - MS_ASSERT(!this->replied, "request already replied"); - - this->replied = true; - - json jsonResponse = json::object(); - - jsonResponse["id"] = this->id; - jsonResponse["error"] = "Error"; - - if (reason != nullptr) - jsonResponse["reason"] = reason; - - this->channel->Send(jsonResponse); - } - - void PayloadChannelRequest::TypeError(const char* reason) - { - MS_TRACE(); - - MS_ASSERT(!this->replied, "request already replied"); - - this->replied = true; - - json jsonResponse = json::object(); - - jsonResponse["id"] = this->id; - jsonResponse["error"] = "TypeError"; - - if (reason != nullptr) - jsonResponse["reason"] = reason; - - this->channel->Send(jsonResponse); - } - - void PayloadChannelRequest::SetPayload(const uint8_t* payload, size_t payloadLen) - { - MS_TRACE(); - - this->payload = payload; - this->payloadLen = payloadLen; - } -} // namespace PayloadChannel diff --git a/worker/src/PayloadChannel/PayloadChannelSocket.cpp b/worker/src/PayloadChannel/PayloadChannelSocket.cpp deleted file mode 100644 index 52ba754f06..0000000000 --- a/worker/src/PayloadChannel/PayloadChannelSocket.cpp +++ /dev/null @@ -1,538 +0,0 @@ -#define MS_CLASS "PayloadChannel::PayloadChannelSocket" -// #define MS_LOG_DEV_LEVEL 3 - -#include "PayloadChannel/PayloadChannelSocket.hpp" -#include "DepLibUV.hpp" -#include "Logger.hpp" -#include "MediaSoupErrors.hpp" -#include "PayloadChannel/PayloadChannelRequest.hpp" -#include // std::ceil() -#include // std::memcpy(), std::memmove() - -namespace PayloadChannel -{ - // Binary length for a 4194304 bytes payload. - static constexpr size_t MessageMaxLen{ 4194308 }; - static constexpr size_t PayloadMaxLen{ 4194304 }; - - /* Static methods for UV callbacks. */ - - inline static void onAsync(uv_handle_t* handle) - { - while (static_cast(handle->data)->CallbackRead()) - { - // Read while there are new messages. - } - } - - inline static void onClose(uv_handle_t* handle) - { - delete handle; - } - - /* Instance methods. */ - - PayloadChannelSocket::PayloadChannelSocket(int consumerFd, int producerFd) - : consumerSocket(new ConsumerSocket(consumerFd, MessageMaxLen, this)), - producerSocket(new ProducerSocket(producerFd, MessageMaxLen)), - writeBuffer(static_cast(std::malloc(MessageMaxLen))) - { - MS_TRACE(); - } - - PayloadChannelSocket::PayloadChannelSocket( - PayloadChannelReadFn payloadChannelReadFn, - PayloadChannelReadCtx payloadChannelReadCtx, - PayloadChannelWriteFn payloadChannelWriteFn, - PayloadChannelWriteCtx payloadChannelWriteCtx) - : payloadChannelReadFn(payloadChannelReadFn), payloadChannelReadCtx(payloadChannelReadCtx), - payloadChannelWriteFn(payloadChannelWriteFn), payloadChannelWriteCtx(payloadChannelWriteCtx) - { - MS_TRACE(); - - int err; - - this->uvReadHandle = new uv_async_t; - this->uvReadHandle->data = static_cast(this); - - err = - uv_async_init(DepLibUV::GetLoop(), this->uvReadHandle, reinterpret_cast(onAsync)); - - if (err != 0) - { - delete this->uvReadHandle; - this->uvReadHandle = nullptr; - - MS_THROW_ERROR("uv_async_init() failed: %s", uv_strerror(err)); - } - - err = uv_async_send(this->uvReadHandle); - - if (err != 0) - { - delete this->uvReadHandle; - this->uvReadHandle = nullptr; - - MS_THROW_ERROR("uv_async_send() failed: %s", uv_strerror(err)); - } - } - - PayloadChannelSocket::~PayloadChannelSocket() - { - MS_TRACE(); - - std::free(this->writeBuffer); - delete this->ongoingNotification; - - if (!this->closed) - Close(); - - delete this->consumerSocket; - delete this->producerSocket; - } - - void PayloadChannelSocket::Close() - { - MS_TRACE_STD(); - - if (this->closed) - return; - - this->closed = true; - - if (this->uvReadHandle) - { - uv_close(reinterpret_cast(this->uvReadHandle), static_cast(onClose)); - } - - if (this->consumerSocket) - { - this->consumerSocket->Close(); - } - - if (this->producerSocket) - { - this->producerSocket->Close(); - } - } - - void PayloadChannelSocket::SetListener(Listener* listener) - { - MS_TRACE(); - - this->listener = listener; - } - - void PayloadChannelSocket::Send(json& jsonMessage, const uint8_t* payload, size_t payloadLen) - { - MS_TRACE(); - - if (this->closed) - return; - - std::string message = jsonMessage.dump(); - - this->Send(message, payload, payloadLen); - } - - void PayloadChannelSocket::Send(const std::string& message, const uint8_t* payload, size_t payloadLen) - { - MS_TRACE(); - - if (this->closed) - return; - - if (message.length() > PayloadMaxLen) - { - MS_ERROR("message too big"); - - return; - } - else if (payloadLen > PayloadMaxLen) - { - MS_ERROR("payload too big"); - - return; - } - - SendImpl( - reinterpret_cast(message.c_str()), - static_cast(message.length()), - payload, - static_cast(payloadLen)); - } - - void PayloadChannelSocket::Send(json& jsonMessage) - { - MS_TRACE_STD(); - - if (this->closed) - return; - - std::string message = jsonMessage.dump(); - - if (message.length() > PayloadMaxLen) - { - MS_ERROR_STD("message too big"); - - return; - } - - SendImpl( - reinterpret_cast(message.c_str()), static_cast(message.length())); - } - - void PayloadChannelSocket::Send(const std::string& message) - { - MS_TRACE(); - - if (this->closed) - return; - - if (message.length() > PayloadMaxLen) - { - MS_ERROR("message too big"); - - return; - } - - SendImpl( - reinterpret_cast(message.c_str()), static_cast(message.length())); - } - - bool PayloadChannelSocket::CallbackRead() - { - MS_TRACE(); - - if (this->closed) - return false; - - uint8_t* message{ nullptr }; - uint32_t messageLen; - size_t messageCtx; - - uint8_t* payload{ nullptr }; - uint32_t payloadLen; - size_t payloadCapacity; - - // Try to read next message and payload using `payloadChannelReadFn`, message and payload, - // alongside their lengths and contexts will be stored in provided arguments. - auto free = this->payloadChannelReadFn( - &message, - &messageLen, - &messageCtx, - &payload, - &payloadLen, - &payloadCapacity, - this->uvReadHandle, - this->payloadChannelReadCtx); - - // Non-null free function pointer means message was successfully read above and will need to be - // freed later. - if (free) - { - try - { - char* charMessage{ reinterpret_cast(message) }; - - if (PayloadChannelRequest::IsRequest(charMessage, messageLen)) - { - try - { - auto* request = - new PayloadChannel::PayloadChannelRequest(this, charMessage + 2, messageLen - 2); - request->SetPayload(payload, payloadLen); - - // Notify the listener. - try - { - this->listener->HandleRequest(request); - } - catch (const MediaSoupTypeError& error) - { - request->TypeError(error.what()); - } - catch (const MediaSoupError& error) - { - request->Error(error.what()); - } - - // Delete the Request. - delete request; - } - catch (const json::parse_error& error) - { - MS_ERROR_STD("message parsing error: %s", error.what()); - } - catch (const MediaSoupError& error) - { - MS_ERROR("discarding wrong PayloadChannel request: %s", error.what()); - } - } - - else if (PayloadChannelNotification::IsNotification(charMessage, messageLen)) - { - try - { - auto* notification = - new PayloadChannel::PayloadChannelNotification(charMessage + 2, messageLen - 2); - notification->SetPayload(payload, payloadLen); - - // Notify the listener. - try - { - this->listener->HandleNotification(notification); - } - catch (const MediaSoupError& error) - { - MS_ERROR("notification failed: %s", error.what()); - } - - // Delete the Notification. - delete notification; - } - catch (const json::parse_error& error) - { - MS_ERROR_STD("message parsing error: %s", error.what()); - } - catch (const MediaSoupError& error) - { - MS_ERROR("discarding wrong PayloadChannel notification: %s", error.what()); - } - } - - else - { - MS_ERROR("discarding wrong PayloadChannel data"); - } - } - catch (const json::parse_error& error) - { - MS_ERROR("JSON parsing error: %s", error.what()); - } - catch (const MediaSoupError& error) - { - MS_ERROR("discarding wrong Channel request: %s", error.what()); - } - - // Message and payload need to be freed using stored function pointer. - free(message, messageLen, messageCtx); - free(payload, payloadLen, payloadCapacity); - } - - // Return `true` if something was processed. - return free != nullptr; - } - - inline void PayloadChannelSocket::SendImpl(const uint8_t* message, uint32_t messageLen) - { - MS_TRACE(); - - // Write using function call if provided. - if (this->payloadChannelWriteFn) - { - this->payloadChannelWriteFn(message, messageLen, nullptr, 0, this->payloadChannelWriteCtx); - } - else - { - std::memcpy(this->writeBuffer, &messageLen, sizeof(uint32_t)); - - if (messageLen != 0) - { - std::memcpy(this->writeBuffer + sizeof(uint32_t), message, messageLen); - } - - size_t len = sizeof(uint32_t) + messageLen; - - this->producerSocket->Write(this->writeBuffer, len); - } - } - - inline void PayloadChannelSocket::SendImpl( - const uint8_t* message, uint32_t messageLen, const uint8_t* payload, uint32_t payloadLen) - { - MS_TRACE(); - - // Write using function call if provided. - if (this->payloadChannelWriteFn) - { - this->payloadChannelWriteFn( - message, messageLen, payload, payloadLen, this->payloadChannelWriteCtx); - } - else - { - SendImpl(message, messageLen); - SendImpl(payload, payloadLen); - } - } - - void PayloadChannelSocket::OnConsumerSocketMessage( - ConsumerSocket* /*consumerSocket*/, char* msg, size_t msgLen) - { - MS_TRACE(); - - if (!this->ongoingNotification && !this->ongoingRequest) - { - if (PayloadChannelRequest::IsRequest(msg, msgLen)) - { - try - { - this->ongoingRequest = new PayloadChannel::PayloadChannelRequest(this, msg + 2, msgLen - 2); - } - catch (const json::parse_error& error) - { - MS_ERROR_STD("msg parsing error: %s", error.what()); - } - catch (const MediaSoupError& error) - { - MS_ERROR("discarding wrong PayloadChannel request: %s", error.what()); - } - } - else if (PayloadChannelNotification::IsNotification(msg, msgLen)) - { - try - { - this->ongoingNotification = - new PayloadChannel::PayloadChannelNotification(msg + 2, msgLen - 2); - } - catch (const json::parse_error& error) - { - MS_ERROR_STD("msg parsing error: %s", error.what()); - } - catch (const MediaSoupError& error) - { - MS_ERROR("discarding wrong PayloadChannel notification: %s", error.what()); - } - } - else - { - MS_ERROR("discarding wrong PayloadChannel data"); - } - } - else if (this->ongoingNotification) - { - this->ongoingNotification->SetPayload(reinterpret_cast(msg), msgLen); - - // Notify the listener. - try - { - this->listener->HandleNotification(this->ongoingNotification); - } - catch (const MediaSoupError& error) - { - MS_ERROR("notification failed: %s", error.what()); - } - - // Delete the Notification. - delete this->ongoingNotification; - this->ongoingNotification = nullptr; - } - else if (this->ongoingRequest) - { - this->ongoingRequest->SetPayload(reinterpret_cast(msg), msgLen); - - // Notify the listener. - try - { - this->listener->HandleRequest(this->ongoingRequest); - } - catch (const MediaSoupTypeError& error) - { - this->ongoingRequest->TypeError(error.what()); - } - catch (const MediaSoupError& error) - { - this->ongoingRequest->Error(error.what()); - } - - // Delete the Request. - delete this->ongoingRequest; - this->ongoingRequest = nullptr; - } - } - - void PayloadChannelSocket::OnConsumerSocketClosed(ConsumerSocket* /*consumerSocket*/) - { - MS_TRACE(); - - this->listener->OnPayloadChannelClosed(this); - } - - /* Instance methods. */ - - ConsumerSocket::ConsumerSocket(int fd, size_t bufferSize, Listener* listener) - : ::UnixStreamSocket(fd, bufferSize, ::UnixStreamSocket::Role::CONSUMER), listener(listener) - { - MS_TRACE(); - } - - ConsumerSocket::~ConsumerSocket() - { - MS_TRACE(); - } - - void ConsumerSocket::UserOnUnixStreamRead() - { - MS_TRACE(); - - size_t msgStart{ 0 }; - - // Be ready to parse more than a single message in a single chunk. - while (true) - { - if (IsClosed()) - return; - - size_t readLen = this->bufferDataLen - msgStart; - - if (readLen < sizeof(uint32_t)) - { - // Incomplete data. - break; - } - - uint32_t msgLen; - // Read message length. - std::memcpy(&msgLen, this->buffer + msgStart, sizeof(uint32_t)); - - if (readLen < sizeof(uint32_t) + static_cast(msgLen)) - { - // Incomplete data. - break; - } - - this->listener->OnConsumerSocketMessage( - this, - reinterpret_cast(this->buffer + msgStart + sizeof(uint32_t)), - static_cast(msgLen)); - - msgStart += sizeof(uint32_t) + static_cast(msgLen); - } - - if (msgStart != 0) - { - this->bufferDataLen = this->bufferDataLen - msgStart; - - if (this->bufferDataLen != 0) - { - std::memmove(this->buffer, this->buffer + msgStart, this->bufferDataLen); - } - } - } - - void ConsumerSocket::UserOnUnixStreamSocketClosed() - { - MS_TRACE(); - - // Notify the listener. - this->listener->OnConsumerSocketClosed(this); - } - - /* Instance methods. */ - - ProducerSocket::ProducerSocket(int fd, size_t bufferSize) - : ::UnixStreamSocket(fd, bufferSize, ::UnixStreamSocket::Role::PRODUCER) - { - MS_TRACE(); - } -} // namespace PayloadChannel diff --git a/worker/src/RTC/ActiveSpeakerObserver.cpp b/worker/src/RTC/ActiveSpeakerObserver.cpp index e86bdfcdb1..a858c84615 100644 --- a/worker/src/RTC/ActiveSpeakerObserver.cpp +++ b/worker/src/RTC/ActiveSpeakerObserver.cpp @@ -3,7 +3,6 @@ #include "RTC/ActiveSpeakerObserver.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" -#include "Utils.hpp" #include "RTC/RtpDictionaries.hpp" namespace RTC @@ -67,8 +66,8 @@ namespace RTC inline bool ComputeBigs( const std::vector& littles, std::vector& bigs, uint8_t threashold) { - uint32_t littleLen = littles.size(); - uint32_t bigLen = bigs.size(); + const uint32_t littleLen = littles.size(); + const uint32_t bigLen = bigs.size(); const uint32_t littleLenPerBig = littleLen / bigLen; bool changed{ false }; @@ -95,26 +94,24 @@ namespace RTC } ActiveSpeakerObserver::ActiveSpeakerObserver( - RTC::Shared* shared, const std::string& id, RTC::RtpObserver::Listener* listener, json& data) - : RTC::RtpObserver(shared, id, listener) + RTC::Shared* shared, + const std::string& id, + RTC::RtpObserver::Listener* listener, + const FBS::ActiveSpeakerObserver::ActiveSpeakerObserverOptions* options) + : RTC::RtpObserver(shared, id, listener), interval(options->interval()) { MS_TRACE(); - auto jsonIntervalIt = data.find("interval"); - - if (jsonIntervalIt == data.end() || !jsonIntervalIt->is_number()) - { - MS_THROW_TYPE_ERROR("missing interval"); - } - - this->interval = jsonIntervalIt->get(); - if (this->interval < 100) + { this->interval = 100; + } else if (this->interval > 5000) + { this->interval = 5000; + } - this->periodicTimer = new Timer(this); + this->periodicTimer = new TimerHandle(this); this->periodicTimer->Start(interval, interval); @@ -122,8 +119,7 @@ namespace RTC this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ nullptr, - /*payloadChannelNotificationHandler*/ nullptr); + /*channelNotificationHandler*/ nullptr); } ActiveSpeakerObserver::~ActiveSpeakerObserver() @@ -150,10 +146,14 @@ namespace RTC MS_TRACE(); if (producer->GetKind() != RTC::Media::Kind::AUDIO) + { MS_THROW_TYPE_ERROR("not an audio Producer"); + } if (this->mapProducerSpeakers.find(producer->id) != this->mapProducerSpeakers.end()) + { MS_THROW_ERROR("Producer already in map"); + } this->mapProducerSpeakers[producer->id] = new ProducerSpeaker(producer); } @@ -216,21 +216,26 @@ namespace RTC MS_TRACE(); if (IsPaused()) + { return; + } uint8_t level; bool voice; if (!packet->ReadSsrcAudioLevel(level, voice)) + { return; - uint8_t volume = 127 - level; + } + + const uint8_t volume = 127 - level; auto it = this->mapProducerSpeakers.find(producer->id); if (it != this->mapProducerSpeakers.end()) { auto* producerSpeaker = it->second; - uint64_t now = DepLibUV::GetTimeMs(); + const uint64_t now = DepLibUV::GetTimeMs(); producerSpeaker->speaker->LevelChanged(volume, now); } @@ -250,7 +255,7 @@ namespace RTC this->periodicTimer->Restart(); } - void ActiveSpeakerObserver::OnTimer(Timer* timer) + void ActiveSpeakerObserver::OnTimer(TimerHandle* /*timer*/) { MS_TRACE(); @@ -275,10 +280,14 @@ namespace RTC if (!this->mapProducerSpeakers.empty() && CalculateActiveSpeaker()) { - json data = json::object(); - data["producerId"] = this->dominantId; - - this->shared->channelNotifier->Emit(this->id, "dominantspeaker", data); + auto notification = FBS::ActiveSpeakerObserver::CreateDominantSpeakerNotificationDirect( + this->shared->channelNotifier->GetBufferBuilder(), this->dominantId.c_str()); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::ACTIVESPEAKEROBSERVER_DOMINANT_SPEAKER, + FBS::Notification::Body::ActiveSpeakerObserver_DominantSpeakerNotification, + notification); } } @@ -287,7 +296,7 @@ namespace RTC MS_TRACE(); std::string newDominantId; - int32_t speakerCount = this->mapProducerSpeakers.size(); + const int32_t speakerCount = this->mapProducerSpeakers.size(); if (speakerCount == 0) { @@ -324,7 +333,7 @@ namespace RTC { auto* producerSpeaker = kv.second; auto* speaker = producerSpeaker->speaker; - auto& id = producerSpeaker->producer->id; + const auto& id = producerSpeaker->producer->id; if (id == this->dominantId || speaker->paused) { @@ -340,9 +349,9 @@ namespace RTC speaker->GetActivityScore(interval) / dominantSpeaker->GetActivityScore(interval)); } - double c1 = this->relativeSpeachActivities[0]; - double c2 = this->relativeSpeachActivities[1]; - double c3 = this->relativeSpeachActivities[2]; + const double c1 = this->relativeSpeachActivities[0]; + const double c2 = this->relativeSpeachActivities[1]; + const double c3 = this->relativeSpeachActivities[2]; if ((c1 > C1) && (c2 > C2) && (c3 > C3) && (c2 > newDominantC2)) { @@ -370,8 +379,8 @@ namespace RTC { auto* producerSpeaker = kv.second; auto* speaker = producerSpeaker->speaker; - auto& id = producerSpeaker->producer->id; - uint64_t idle = now - speaker->lastLevelChangeTime; + const auto& id = producerSpeaker->producer->id; + const uint64_t idle = now - speaker->lastLevelChangeTime; if (SpeakerIdleTimeout < idle && (this->dominantId.empty() || id != this->dominantId)) { @@ -385,11 +394,10 @@ namespace RTC } ActiveSpeakerObserver::ProducerSpeaker::ProducerSpeaker(RTC::Producer* producer) - : producer(producer) + : producer(producer), speaker(new Speaker()) { MS_TRACE(); - this->speaker = new Speaker(); this->speaker->paused = producer->IsPaused(); } @@ -404,7 +412,7 @@ namespace RTC : immediateActivityScore(MinActivityScore), mediumActivityScore(MinActivityScore), longActivityScore(MinActivityScore), lastLevelChangeTime(DepLibUV::GetTimeMs()), minLevel(MinLevel), nextMinLevel(MinLevel), immediates(ImmediateBuffLen, 0), - mediums(MediumsBuffLen, 0), longs(LongsBuffLen, 0), levels(LevelsBuffLen, 0), nextLevelIndex(0) + mediums(MediumsBuffLen, 0), longs(LongsBuffLen, 0), levels(LevelsBuffLen, 0) { MS_TRACE(); } @@ -429,7 +437,7 @@ namespace RTC } } - double ActiveSpeakerObserver::Speaker::GetActivityScore(uint8_t interval) + double ActiveSpeakerObserver::Speaker::GetActivityScore(uint8_t interval) const { MS_TRACE(); @@ -474,7 +482,7 @@ namespace RTC // The algorithm expect to have an update every 20 milliseconds. If the // Producer is paused, using a different packetization time or using DTX // we need to update more than one sample when receiving an audio packet. - uint32_t intervalsUpdated = + const uint32_t intervalsUpdated = std::min(std::max(static_cast(elapsed / 20), 1U), LevelsBuffLen); for (uint32_t i{ 0u }; i < intervalsUpdated; ++i) diff --git a/worker/src/RTC/AudioLevelObserver.cpp b/worker/src/RTC/AudioLevelObserver.cpp index 5fece66d50..ef40fa26e1 100644 --- a/worker/src/RTC/AudioLevelObserver.cpp +++ b/worker/src/RTC/AudioLevelObserver.cpp @@ -4,61 +4,42 @@ #include "RTC/AudioLevelObserver.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" -#include "Utils.hpp" #include "RTC/RtpDictionaries.hpp" +#include #include // std::lround() -#include namespace RTC { /* Instance methods. */ AudioLevelObserver::AudioLevelObserver( - RTC::Shared* shared, const std::string& id, RTC::RtpObserver::Listener* listener, json& data) + RTC::Shared* shared, + const std::string& id, + RTC::RtpObserver::Listener* listener, + const FBS::AudioLevelObserver::AudioLevelObserverOptions* options) : RTC::RtpObserver(shared, id, listener) { MS_TRACE(); - auto jsonMaxEntriesIt = data.find("maxEntries"); + this->maxEntries = options->maxEntries(); + this->threshold = options->threshold(); + this->interval = options->interval(); - // clang-format off - if ( - jsonMaxEntriesIt == data.end() || - !Utils::Json::IsPositiveInteger(*jsonMaxEntriesIt) - ) - // clang-format on + if (this->threshold > 0) { - MS_THROW_TYPE_ERROR("missing maxEntries"); - } - - this->maxEntries = jsonMaxEntriesIt->get(); - - if (this->maxEntries < 1) - MS_THROW_TYPE_ERROR("invalid maxEntries value %" PRIu16, this->maxEntries); - - auto jsonThresholdIt = data.find("threshold"); - - if (jsonThresholdIt == data.end() || !jsonThresholdIt->is_number()) - MS_THROW_TYPE_ERROR("missing threshold"); - - this->threshold = jsonThresholdIt->get(); - - if (this->threshold < -127 || this->threshold > 0) MS_THROW_TYPE_ERROR("invalid threshold value %" PRIi8, this->threshold); - - auto jsonIntervalIt = data.find("interval"); - - if (jsonIntervalIt == data.end() || !jsonIntervalIt->is_number()) - MS_THROW_TYPE_ERROR("missing interval"); - - this->interval = jsonIntervalIt->get(); + } if (this->interval < 250) + { this->interval = 250; + } else if (this->interval > 5000) + { this->interval = 5000; + } - this->periodicTimer = new Timer(this); + this->periodicTimer = new TimerHandle(this); this->periodicTimer->Start(this->interval, this->interval); @@ -66,8 +47,7 @@ namespace RTC this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ nullptr, - /*payloadChannelNotificationHandler*/ nullptr); + /*channelNotificationHandler*/ nullptr); } AudioLevelObserver::~AudioLevelObserver() @@ -84,7 +64,9 @@ namespace RTC MS_TRACE(); if (producer->GetKind() != RTC::Media::Kind::AUDIO) + { MS_THROW_TYPE_ERROR("not an audio Producer"); + } // Insert into the map. this->mapProducerDBovs[producer]; @@ -103,13 +85,17 @@ namespace RTC MS_TRACE(); if (IsPaused()) + { return; + } uint8_t volume; bool voice; if (!packet->ReadSsrcAudioLevel(volume, voice)) + { return; + } auto& dBovs = this->mapProducerDBovs.at(producer); @@ -141,7 +127,8 @@ namespace RTC { this->silence = true; - this->shared->channelNotifier->Emit(this->id, "silence"); + this->shared->channelNotifier->Emit( + this->id, FBS::Notification::Event::AUDIOLEVELOBSERVER_SILENCE); } } @@ -156,7 +143,7 @@ namespace RTC { MS_TRACE(); - absl::btree_map mapDBovsProducer; + absl::btree_multimap mapDBovsProducer; for (auto& kv : this->mapProducerDBovs) { @@ -164,12 +151,16 @@ namespace RTC auto& dBovs = kv.second; if (dBovs.count < 10) + { continue; + } auto avgDBov = -1 * static_cast(std::lround(dBovs.totalSum / dBovs.count)); if (avgDBov >= this->threshold) - mapDBovsProducer[avgDBov] = producer; + { + mapDBovsProducer.insert({ avgDBov, producer }); + } } // Clear the map. @@ -180,26 +171,31 @@ namespace RTC this->silence = false; uint16_t idx{ 0 }; - auto rit = mapDBovsProducer.crbegin(); - json data = json::array(); + auto rit = mapDBovsProducer.crbegin(); + + std::vector> volumes; for (; idx < this->maxEntries && rit != mapDBovsProducer.crend(); ++idx, ++rit) { - data.emplace_back(json::value_t::object); - - auto& jsonEntry = data[idx]; - - jsonEntry["producerId"] = rit->second->id; - jsonEntry["volume"] = rit->first; + volumes.emplace_back(FBS::AudioLevelObserver::CreateVolumeDirect( + this->shared->channelNotifier->GetBufferBuilder(), rit->second->id.c_str(), rit->first)); } - this->shared->channelNotifier->Emit(this->id, "volumes", data); + auto notification = FBS::AudioLevelObserver::CreateVolumesNotificationDirect( + this->shared->channelNotifier->GetBufferBuilder(), &volumes); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::AUDIOLEVELOBSERVER_VOLUMES, + FBS::Notification::Body::AudioLevelObserver_VolumesNotification, + notification); } else if (!this->silence) { this->silence = true; - this->shared->channelNotifier->Emit(this->id, "silence"); + this->shared->channelNotifier->Emit( + this->id, FBS::Notification::Event::AUDIOLEVELOBSERVER_SILENCE); } } @@ -216,7 +212,7 @@ namespace RTC } } - inline void AudioLevelObserver::OnTimer(Timer* /*timer*/) + inline void AudioLevelObserver::OnTimer(TimerHandle* /*timer*/) { MS_TRACE(); diff --git a/worker/src/RTC/Codecs/H264.cpp b/worker/src/RTC/Codecs/H264.cpp index 8b894570f6..b2dd988e65 100644 --- a/worker/src/RTC/Codecs/H264.cpp +++ b/worker/src/RTC/Codecs/H264.cpp @@ -17,7 +17,11 @@ namespace RTC MS_TRACE(); if (len < 2) + { + MS_WARN_DEV("ignoring payload with length < 2"); + return nullptr; + } std::unique_ptr payloadDescriptor(new PayloadDescriptor()); @@ -48,7 +52,9 @@ namespace RTC // Detect key frame. if (frameMarking->start && frameMarking->independent) + { payloadDescriptor->isKeyFrame = true; + } } // NOTE: Unfortunately libwebrtc produces wrong Frame-Marking (without i=1 in @@ -59,7 +65,7 @@ namespace RTC // there is no frame-marking or if there is but keyframe was not detected above. if (!frameMarking || !payloadDescriptor->isKeyFrame) { - uint8_t nal = *data & 0x1F; + const uint8_t nal = *data & 0x1F; switch (nal) { @@ -83,8 +89,8 @@ namespace RTC // Iterate NAL units. while (len >= 3) { - auto naluSize = Utils::Byte::Get2Bytes(data, offset); - uint8_t subnal = *(data + offset + sizeof(naluSize)) & 0x1F; + auto naluSize = Utils::Byte::Get2Bytes(data, offset); + const uint8_t subnal = *(data + offset + sizeof(naluSize)) & 0x1F; if (subnal == 7) { @@ -95,7 +101,9 @@ namespace RTC // Check if there is room for the indicated NAL unit size. if (len < (naluSize + sizeof(naluSize))) + { break; + } offset += naluSize + sizeof(naluSize); len -= naluSize + sizeof(naluSize); @@ -109,11 +117,13 @@ namespace RTC case 28: case 29: { - uint8_t subnal = *(data + 1) & 0x1F; - uint8_t startBit = *(data + 1) & 0x80; + const uint8_t subnal = *(data + 1) & 0x1F; + const uint8_t startBit = *(data + 1) & 0x80; if (subnal == 7 && startBit == 128) + { payloadDescriptor->isKeyFrame = true; + } break; } @@ -138,7 +148,9 @@ namespace RTC PayloadDescriptor* payloadDescriptor = H264::Parse(data, len, frameMarking, frameMarkingLen); if (!payloadDescriptor) + { return; + } auto* payloadDescriptorHandler = new PayloadDescriptorHandler(payloadDescriptor); @@ -151,7 +163,7 @@ namespace RTC { MS_TRACE(); - MS_DUMP(""); + MS_DUMP(""); MS_DUMP( " s:%" PRIu8 "|e:%" PRIu8 "|i:%" PRIu8 "|d:%" PRIu8 "|b:%" PRIu8, this->s, @@ -160,13 +172,19 @@ namespace RTC this->d, this->b); if (this->hasTid) - MS_DUMP(" tid : %" PRIu8, this->tid); + { + MS_DUMP(" tid: %" PRIu8, this->tid); + } if (this->hasLid) - MS_DUMP(" lid : %" PRIu8, this->lid); + { + MS_DUMP(" lid: %" PRIu8, this->lid); + } if (this->hasTl0picidx) - MS_DUMP(" tl0picidx : %" PRIu8, this->tl0picidx); - MS_DUMP(" isKeyFrame : %s", this->isKeyFrame ? "true" : "false"); - MS_DUMP(""); + { + MS_DUMP(" tl0picidx: %" PRIu8, this->tl0picidx); + } + MS_DUMP(" isKeyFrame: %s", this->isKeyFrame ? "true" : "false"); + MS_DUMP(""); } H264::PayloadDescriptorHandler::PayloadDescriptorHandler(H264::PayloadDescriptor* payloadDescriptor) @@ -231,7 +249,9 @@ namespace RTC } if (context->GetCurrentTemporalLayer() > context->GetTargetTemporalLayer()) + { context->SetCurrentTemporalLayer(context->GetTargetTemporalLayer()); + } return true; } diff --git a/worker/src/RTC/Codecs/H264_SVC.cpp b/worker/src/RTC/Codecs/H264_SVC.cpp index a926006cba..9214094ffb 100644 --- a/worker/src/RTC/Codecs/H264_SVC.cpp +++ b/worker/src/RTC/Codecs/H264_SVC.cpp @@ -16,7 +16,11 @@ namespace RTC MS_TRACE(); if (len < 2) + { + MS_WARN_DEV("ignoring payload with length < 2"); + return nullptr; + } std::unique_ptr payloadDescriptor(new PayloadDescriptor()); @@ -47,18 +51,21 @@ namespace RTC // Detect key frame. if (frameMarking->start && frameMarking->independent) + { payloadDescriptor->isKeyFrame = true; + } } - // NOTE: Unfortunately libwebrtc produces wrong Frame-Marking (without i=1 in - // keyframes) when it uses H264 hardware encoder (at least in Mac): + // NOTE: Unfortunately libwebrtc produces wrong Frame-Marking (without i=1 + // in keyframes) when it uses H264 hardware encoder (at least in Mac): // https://bugs.chromium.org/p/webrtc/issues/detail?id=10746 // - // As a temporal workaround, always do payload parsing to detect keyframes if - // there is no frame-marking or if there is but keyframe was not detected above. + // As a temporal workaround, always do payload parsing to detect keyframes + // if there is no frame-marking or if there is but keyframe was not + // detected above. if (!frameMarking || !payloadDescriptor->isKeyFrame) { - uint8_t nal = *data & 0x1F; + const uint8_t nal = *data & 0x1F; switch (nal) { @@ -74,7 +81,11 @@ namespace RTC H264_SVC::ParseSingleNalu(data, len, std::move(payloadDescriptor), true); if (payloadDescriptor == nullptr) + { + MS_WARN_DEV("ignoring invalid payload (1)"); + return nullptr; + } break; } @@ -97,8 +108,13 @@ namespace RTC (len - sizeof(naluSize)), std::move(payloadDescriptor), true); + if (payloadDescriptor == nullptr) + { + MS_WARN_DEV("ignoring invalid payload (2)"); + return nullptr; + } if (payloadDescriptor->isKeyFrame) { @@ -107,7 +123,9 @@ namespace RTC // Check if there is room for the indicated NAL unit size. if (len < (naluSize + sizeof(naluSize))) + { break; + } offset += naluSize + sizeof(naluSize); len -= naluSize + sizeof(naluSize); @@ -121,15 +139,20 @@ namespace RTC case 28: case 29: { - uint8_t startBit = *(data + 1) & 0x80; + const uint8_t startBit = *(data + 1) & 0x80; if (startBit == 128) { payloadDescriptor = H264_SVC::ParseSingleNalu( (data + 1), (len - 1), std::move(payloadDescriptor), (startBit == 128 ? true : false)); } + if (payloadDescriptor == nullptr) + { + MS_WARN_DEV("ignoring invalid payload (3)"); + return nullptr; + } break; } @@ -145,14 +168,17 @@ namespace RTC std::unique_ptr payloadDescriptor, bool isStartBit) { - uint8_t nal = *data & 0x1F; + const uint8_t nal = *data & 0x1F; switch (nal) { // Single NAL unit packet. // IDR (instantaneous decoding picture). case 5: + { payloadDescriptor->isKeyFrame = true; + } + case 1: { payloadDescriptor->slIndex = 0; @@ -163,9 +189,15 @@ namespace RTC break; } + case 14: case 20: { + if (len <= 1) + { + return nullptr; + } + size_t offset{ 1 }; uint8_t byte = data[offset]; @@ -174,14 +206,18 @@ namespace RTC payloadDescriptor->isKeyFrame = (isStartBit && payloadDescriptor->idr) ? true : false; if (len < ++offset + 1) + { return nullptr; + } byte = data[offset]; payloadDescriptor->noIntLayerPredFlag = byte >> 7 & 0x01; payloadDescriptor->slIndex = byte >> 4 & 0x03; if (len < ++offset + 1) + { return nullptr; + } byte = data[offset]; @@ -192,6 +228,7 @@ namespace RTC break; } + case 7: { payloadDescriptor->isKeyFrame = isStartBit ? true : false; @@ -199,6 +236,7 @@ namespace RTC break; } } + return payloadDescriptor; } @@ -218,7 +256,9 @@ namespace RTC H264_SVC::Parse(data, len, frameMarking, frameMarkingLen); if (!payloadDescriptor) + { return; + } auto* payloadDescriptorHandler = new PayloadDescriptorHandler(payloadDescriptor); @@ -231,7 +271,7 @@ namespace RTC { MS_TRACE(); - MS_DUMP(""); + MS_DUMP(""); MS_DUMP( " s:%" PRIu8 "|e:%" PRIu8 "|i:%" PRIu8 "|d:%" PRIu8 "|b:%" PRIu8, this->s, @@ -239,12 +279,12 @@ namespace RTC this->i, this->d, this->b); - MS_DUMP(" hasSlIndex : %s", this->hasSlIndex ? "true" : "false"); - MS_DUMP(" hasTlIndex : %s", this->hasTlIndex ? "true" : "false"); - MS_DUMP(" tl0picidx : %" PRIu8, this->tl0picidx); - MS_DUMP(" noIntLayerPredFlag : %" PRIu8, this->noIntLayerPredFlag); - MS_DUMP(" isKeyFrame : %s", this->isKeyFrame ? "true" : "false"); - MS_DUMP(""); + MS_DUMP(" hasSlIndex: %s", this->hasSlIndex ? "true" : "false"); + MS_DUMP(" hasTlIndex: %s", this->hasTlIndex ? "true" : "false"); + MS_DUMP(" tl0picidx: %" PRIu8, this->tl0picidx); + MS_DUMP(" noIntLayerPredFlag: %" PRIu8, this->noIntLayerPredFlag); + MS_DUMP(" isKeyFrame: %s", this->isKeyFrame ? "true" : "false"); + MS_DUMP(""); } H264_SVC::PayloadDescriptorHandler::PayloadDescriptorHandler( @@ -286,7 +326,7 @@ namespace RTC } // clang-format off - bool isOldPacket = false; + const bool isOldPacket = false; // Upgrade current spatial layer if needed. if (context->GetTargetSpatialLayer() > context->GetCurrentSpatialLayer()) @@ -352,7 +392,9 @@ namespace RTC // Filter spatial layers higher than current one (unless old packet). if (packetSpatialLayer > tmpSpatialLayer && !isOldPacket) + { return false; + } // Check and handle temporal layer (unless old packet). if (!isOldPacket) @@ -402,20 +444,28 @@ namespace RTC // Filter temporal layers higher than current one. if (packetTemporalLayer > tmpTemporalLayer) + { return false; + } } // Set marker bit if needed. if (packetSpatialLayer == tmpSpatialLayer && this->payloadDescriptor->e) + { marker = true; + } // Update current spatial layer if needed. if (tmpSpatialLayer != context->GetCurrentSpatialLayer()) + { context->SetCurrentSpatialLayer(tmpSpatialLayer); + } // Update current temporal layer if needed. if (tmpTemporalLayer != context->GetCurrentTemporalLayer()) + { context->SetCurrentTemporalLayer(tmpTemporalLayer); + } return true; } diff --git a/worker/src/RTC/Codecs/Opus.cpp b/worker/src/RTC/Codecs/Opus.cpp index 90dd986d4d..0fa4dff95e 100644 --- a/worker/src/RTC/Codecs/Opus.cpp +++ b/worker/src/RTC/Codecs/Opus.cpp @@ -14,12 +14,72 @@ namespace RTC { MS_TRACE(); + if (len < 1) + { + MS_WARN_DEV("ignoring empty payload"); + + return nullptr; + } + std::unique_ptr payloadDescriptor(new PayloadDescriptor()); - // libopus generates a single byte payload (TOC, no frames) to generate DTX. - if (len == 1) + uint8_t byte = data[0]; + + payloadDescriptor->stereo = (byte >> 2) & 0x01; + payloadDescriptor->code = byte & 0x03; + + switch (payloadDescriptor->code) { - payloadDescriptor->isDtx = true; + case 0: + case 1: + { + // In code 0 and 1 packets, DTX is determined by total length = 1 (TOC + // byte only). + if (len == 1) + { + payloadDescriptor->isDtx = true; + } + + break; + } + + case 2: + { + // As per spec, a 1-byte code 2 packet is always invalid. + if (len == 1) + { + MS_WARN_DEV("ignoring invalid payload (1)"); + + return nullptr; + } + + // In code 2 packets, DTX is determined by total length = 2 (TOC byte + // only). Per spec, the only valid 2-byte code 2 packet is one where + // the length of both frames is zero. + if (len == 2) + { + payloadDescriptor->isDtx = true; + } + + break; + } + + case 3: + { + // As per spec, a 1-byte code 3 packet is always invalid. + if (len == 1) + { + MS_WARN_DEV("ignoring invalid payload (2)"); + + return nullptr; + } + + // A code 3 packet can never be DTX. + + break; + } + + default:; } return payloadDescriptor.release(); @@ -34,6 +94,11 @@ namespace RTC PayloadDescriptor* payloadDescriptor = Opus::Parse(data, len); + if (!payloadDescriptor) + { + return; + } + auto* payloadDescriptorHandler = new PayloadDescriptorHandler(payloadDescriptor); packet->SetPayloadDescriptorHandler(payloadDescriptorHandler); @@ -45,9 +110,11 @@ namespace RTC { MS_TRACE(); - MS_DUMP(""); - MS_DUMP(" isDtx : %s", this->isDtx ? "true" : "false"); - MS_DUMP(""); + MS_DUMP(""); + MS_DUMP(" stereo: %" PRIu8, this->stereo); + MS_DUMP(" code: %" PRIu8, this->code); + MS_DUMP(" isDtx: %s", this->isDtx ? "true" : "false"); + MS_DUMP(""); } Opus::PayloadDescriptorHandler::PayloadDescriptorHandler(Opus::PayloadDescriptor* payloadDescriptor) diff --git a/worker/src/RTC/Codecs/VP8.cpp b/worker/src/RTC/Codecs/VP8.cpp index 9136df0ab2..af2d3278d0 100644 --- a/worker/src/RTC/Codecs/VP8.cpp +++ b/worker/src/RTC/Codecs/VP8.cpp @@ -20,7 +20,11 @@ namespace RTC MS_TRACE(); if (len < 1) + { + MS_WARN_DEV("ignoring empty payload"); + return nullptr; + } std::unique_ptr payloadDescriptor(new PayloadDescriptor()); @@ -34,12 +38,18 @@ namespace RTC if (!payloadDescriptor->extended) { + MS_WARN_DEV("ignoring invalid payload (1)"); + return nullptr; } else { if (len < ++offset + 1) + { + MS_WARN_DEV("ignoring invalid payload (2)"); + return nullptr; + } byte = data[offset]; @@ -52,14 +62,22 @@ namespace RTC if (payloadDescriptor->i) { if (len < ++offset + 1) + { + MS_WARN_DEV("ignoring invalid payload (3)"); + return nullptr; + } byte = data[offset]; if ((byte >> 7) & 0x01) { if (len < ++offset + 1) + { + MS_WARN_DEV("ignoring invalid payload (4)"); + return nullptr; + } payloadDescriptor->hasTwoBytesPictureId = true; payloadDescriptor->pictureId = (byte & 0x7F) << 8; @@ -77,7 +95,11 @@ namespace RTC if (payloadDescriptor->l) { if (len < ++offset + 1) + { + MS_WARN_DEV("ignoring invalid payload (5)"); + return nullptr; + } payloadDescriptor->hasTl0PictureIndex = true; payloadDescriptor->tl0PictureIndex = data[offset]; @@ -86,7 +108,11 @@ namespace RTC if (payloadDescriptor->t || payloadDescriptor->k) { if (len < ++offset + 1) + { + MS_WARN_DEV("ignoring invalid payload (6)"); + return nullptr; + } byte = data[offset]; @@ -126,7 +152,9 @@ namespace RTC PayloadDescriptor* payloadDescriptor = VP8::Parse(data, len, frameMarking, frameMarkingLen); if (!payloadDescriptor) + { return; + } auto* payloadDescriptorHandler = new PayloadDescriptorHandler(payloadDescriptor); @@ -153,25 +181,25 @@ namespace RTC { MS_TRACE(); - MS_DUMP(""); + MS_DUMP(""); MS_DUMP( " i:%" PRIu8 "|l:%" PRIu8 "|t:%" PRIu8 "|k:%" PRIu8, this->i, this->l, this->t, this->k); - MS_DUMP(" extended : %" PRIu8, this->extended); - MS_DUMP(" nonReference : %" PRIu8, this->nonReference); - MS_DUMP(" start : %" PRIu8, this->start); - MS_DUMP(" partitionIndex : %" PRIu8, this->partitionIndex); - MS_DUMP(" pictureId : %" PRIu16, this->pictureId); - MS_DUMP(" tl0PictureIndex : %" PRIu8, this->tl0PictureIndex); - MS_DUMP(" tlIndex : %" PRIu8, this->tlIndex); - MS_DUMP(" y : %" PRIu8, this->y); - MS_DUMP(" keyIndex : %" PRIu8, this->keyIndex); - MS_DUMP(" isKeyFrame : %s", this->isKeyFrame ? "true" : "false"); - MS_DUMP(" hasPictureId : %s", this->hasPictureId ? "true" : "false"); - MS_DUMP(" hasOneBytePictureId : %s", this->hasOneBytePictureId ? "true" : "false"); - MS_DUMP(" hasTwoBytesPictureId : %s", this->hasTwoBytesPictureId ? "true" : "false"); - MS_DUMP(" hasTl0PictureIndex : %s", this->hasTl0PictureIndex ? "true" : "false"); - MS_DUMP(" hasTlIndex : %s", this->hasTlIndex ? "true" : "false"); - MS_DUMP(""); + MS_DUMP(" extended: %" PRIu8, this->extended); + MS_DUMP(" nonReference: %" PRIu8, this->nonReference); + MS_DUMP(" start: %" PRIu8, this->start); + MS_DUMP(" partitionIndex: %" PRIu8, this->partitionIndex); + MS_DUMP(" pictureId: %" PRIu16, this->pictureId); + MS_DUMP(" tl0PictureIndex: %" PRIu8, this->tl0PictureIndex); + MS_DUMP(" tlIndex: %" PRIu8, this->tlIndex); + MS_DUMP(" y: %" PRIu8, this->y); + MS_DUMP(" keyIndex: %" PRIu8, this->keyIndex); + MS_DUMP(" isKeyFrame: %s", this->isKeyFrame ? "true" : "false"); + MS_DUMP(" hasPictureId: %s", this->hasPictureId ? "true" : "false"); + MS_DUMP(" hasOneBytePictureId: %s", this->hasOneBytePictureId ? "true" : "false"); + MS_DUMP(" hasTwoBytesPictureId: %s", this->hasTwoBytesPictureId ? "true" : "false"); + MS_DUMP(" hasTl0PictureIndex: %s", this->hasTl0PictureIndex ? "true" : "false"); + MS_DUMP(" hasTlIndex: %s", this->hasTlIndex ? "true" : "false"); + MS_DUMP(""); } void VP8::PayloadDescriptor::Encode(uint8_t* data, uint16_t pictureId, uint8_t tl0PictureIndex) const @@ -180,7 +208,9 @@ namespace RTC // Nothing to do. if (!this->extended) + { return; + } data += 2; @@ -200,12 +230,16 @@ namespace RTC data++; if (pictureId > 127) + { MS_DEBUG_TAG(rtp, "casting pictureId value to one byte"); + } } } if (this->l) + { *data = tl0PictureIndex; + } } void VP8::PayloadDescriptor::Restore(uint8_t* data) const @@ -337,7 +371,9 @@ namespace RTC } if (context->GetCurrentTemporalLayer() > context->GetTargetTemporalLayer()) + { context->SetCurrentTemporalLayer(context->GetTargetTemporalLayer()); + } // Do not send tlIndex higher than current one. if (this->payloadDescriptor->tlIndex > context->GetCurrentTemporalLayer()) diff --git a/worker/src/RTC/Codecs/VP9.cpp b/worker/src/RTC/Codecs/VP9.cpp index 539308f1af..dea55d37c3 100644 --- a/worker/src/RTC/Codecs/VP9.cpp +++ b/worker/src/RTC/Codecs/VP9.cpp @@ -19,7 +19,11 @@ namespace RTC MS_TRACE(); if (len < 1) + { + MS_WARN_DEV("ignoring empty payload"); + return nullptr; + } std::unique_ptr payloadDescriptor(new PayloadDescriptor()); @@ -37,14 +41,22 @@ namespace RTC if (payloadDescriptor->i) { if (len < ++offset + 1) + { + MS_WARN_DEV("ignoring invalid payload (1)"); + return nullptr; + } byte = data[offset]; if (byte >> 7 & 0x01) { if (len < ++offset + 1) + { + MS_WARN_DEV("ignoring invalid payload (2)"); + return nullptr; + } payloadDescriptor->pictureId = (byte & 0x7F) << 8; payloadDescriptor->pictureId += data[offset]; @@ -62,7 +74,11 @@ namespace RTC if (payloadDescriptor->l) { if (len < ++offset + 1) + { + MS_WARN_DEV("ignoring invalid payload (3)"); + return nullptr; + } byte = data[offset]; @@ -74,7 +90,11 @@ namespace RTC payloadDescriptor->hasTlIndex = true; if (len < ++offset + 1) + { + MS_WARN_DEV("ignoring invalid payload (4)"); + return nullptr; + } // Read TL0PICIDX if flexible mode is unset. if (!payloadDescriptor->f) @@ -113,7 +133,9 @@ namespace RTC PayloadDescriptor* payloadDescriptor = VP9::Parse(data, len, frameMarking, frameMarkingLen); if (!payloadDescriptor) + { return; + } if (payloadDescriptor->isKeyFrame) { @@ -134,7 +156,7 @@ namespace RTC { MS_TRACE(); - MS_DUMP(""); + MS_DUMP(""); MS_DUMP( " i:%" PRIu8 "|p:%" PRIu8 "|l:%" PRIu8 "|f:%" PRIu8 "|b:%" PRIu8 "|e:%" PRIu8 "|v:%" PRIu8, this->i, @@ -144,20 +166,20 @@ namespace RTC this->b, this->e, this->v); - MS_DUMP(" pictureId : %" PRIu16, this->pictureId); - MS_DUMP(" slIndex : %" PRIu8, this->slIndex); - MS_DUMP(" tlIndex : %" PRIu8, this->tlIndex); - MS_DUMP(" tl0PictureIndex : %" PRIu8, this->tl0PictureIndex); - MS_DUMP(" interLayerDependency : %" PRIu8, this->interLayerDependency); - MS_DUMP(" switchingUpPoint : %" PRIu8, this->switchingUpPoint); - MS_DUMP(" isKeyFrame : %s", this->isKeyFrame ? "true" : "false"); - MS_DUMP(" hasPictureId : %s", this->hasPictureId ? "true" : "false"); - MS_DUMP(" hasOneBytePictureId : %s", this->hasOneBytePictureId ? "true" : "false"); - MS_DUMP(" hasTwoBytesPictureId : %s", this->hasTwoBytesPictureId ? "true" : "false"); - MS_DUMP(" hasTl0PictureIndex : %s", this->hasTl0PictureIndex ? "true" : "false"); - MS_DUMP(" hasSlIndex : %s", this->hasSlIndex ? "true" : "false"); - MS_DUMP(" hasTlIndex : %s", this->hasTlIndex ? "true" : "false"); - MS_DUMP(""); + MS_DUMP(" pictureId: %" PRIu16, this->pictureId); + MS_DUMP(" slIndex: %" PRIu8, this->slIndex); + MS_DUMP(" tlIndex: %" PRIu8, this->tlIndex); + MS_DUMP(" tl0PictureIndex: %" PRIu8, this->tl0PictureIndex); + MS_DUMP(" interLayerDependency: %" PRIu8, this->interLayerDependency); + MS_DUMP(" switchingUpPoint: %" PRIu8, this->switchingUpPoint); + MS_DUMP(" isKeyFrame: %s", this->isKeyFrame ? "true" : "false"); + MS_DUMP(" hasPictureId: %s", this->hasPictureId ? "true" : "false"); + MS_DUMP(" hasOneBytePictureId: %s", this->hasOneBytePictureId ? "true" : "false"); + MS_DUMP(" hasTwoBytesPictureId: %s", this->hasTwoBytesPictureId ? "true" : "false"); + MS_DUMP(" hasTl0PictureIndex: %s", this->hasTl0PictureIndex ? "true" : "false"); + MS_DUMP(" hasSlIndex: %s", this->hasSlIndex ? "true" : "false"); + MS_DUMP(" hasTlIndex: %s", this->hasTlIndex ? "true" : "false"); + MS_DUMP(""); } VP9::PayloadDescriptorHandler::PayloadDescriptorHandler(VP9::PayloadDescriptor* payloadDescriptor) @@ -211,7 +233,7 @@ namespace RTC } // clang-format off - bool isOldPacket = ( + const bool isOldPacket = ( this->payloadDescriptor->hasPictureId && RTC::SeqManager::IsSeqLowerThan( this->payloadDescriptor->pictureId, @@ -285,10 +307,15 @@ namespace RTC // * higher than current one // * different than the current one when KSVC is enabled and this is not a keyframe // (interframe p bit = 1) + // clang-format off if ( !isOldPacket && - (packetSpatialLayer > tmpSpatialLayer || - (context->IsKSvc() && this->payloadDescriptor->p && packetSpatialLayer != tmpSpatialLayer))) + ( + packetSpatialLayer > tmpSpatialLayer || + (context->IsKSvc() && this->payloadDescriptor->p && packetSpatialLayer != tmpSpatialLayer) + ) + ) + // clang-format on { return false; } @@ -345,12 +372,16 @@ namespace RTC // Filter temporal layers higher than current one. if (packetTemporalLayer > tmpTemporalLayer) + { return false; + } } // Set marker bit if needed. if (packetSpatialLayer == tmpSpatialLayer && this->payloadDescriptor->e) + { marker = true; + } // Update the pictureId manager. if (this->payloadDescriptor->hasPictureId) @@ -362,11 +393,15 @@ namespace RTC // Update current spatial layer if needed. if (tmpSpatialLayer != context->GetCurrentSpatialLayer()) + { context->SetCurrentSpatialLayer(tmpSpatialLayer); + } // Update current temporal layer if needed. if (tmpTemporalLayer != context->GetCurrentTemporalLayer()) + { context->SetCurrentTemporalLayer(tmpTemporalLayer); + } return true; } diff --git a/worker/src/RTC/Consumer.cpp b/worker/src/RTC/Consumer.cpp index 0f09842377..832b083e3c 100644 --- a/worker/src/RTC/Consumer.cpp +++ b/worker/src/RTC/Consumer.cpp @@ -5,8 +5,6 @@ #include "DepLibUV.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" -#include // std::ostream_iterator -#include // std::ostringstream namespace RTC { @@ -17,56 +15,44 @@ namespace RTC const std::string& id, const std::string& producerId, Listener* listener, - json& data, + const FBS::Transport::ConsumeRequest* data, RTC::RtpParameters::Type type) - : id(id), producerId(producerId), shared(shared), listener(listener), type(type) + : id(id), producerId(producerId), shared(shared), listener(listener), + kind(RTC::Media::Kind(data->kind())), type(type) { MS_TRACE(); - auto jsonKindIt = data.find("kind"); - - if (jsonKindIt == data.end() || !jsonKindIt->is_string()) - MS_THROW_TYPE_ERROR("missing kind"); - - // This may throw. - this->kind = RTC::Media::GetKind(jsonKindIt->get()); - - if (this->kind == RTC::Media::Kind::ALL) - MS_THROW_TYPE_ERROR("invalid empty kind"); - - auto jsonRtpParametersIt = data.find("rtpParameters"); - - if (jsonRtpParametersIt == data.end() || !jsonRtpParametersIt->is_object()) - MS_THROW_TYPE_ERROR("missing rtpParameters"); - // This may throw. - this->rtpParameters = RTC::RtpParameters(*jsonRtpParametersIt); + this->rtpParameters = RTC::RtpParameters(data->rtpParameters()); if (this->rtpParameters.encodings.empty()) + { MS_THROW_TYPE_ERROR("empty rtpParameters.encodings"); + } // All encodings must have SSRCs. for (auto& encoding : this->rtpParameters.encodings) { if (encoding.ssrc == 0) + { MS_THROW_TYPE_ERROR("invalid encoding in rtpParameters (missing ssrc)"); + } else if (encoding.hasRtx && encoding.rtx.ssrc == 0) + { MS_THROW_TYPE_ERROR("invalid encoding in rtpParameters (missing rtx.ssrc)"); + } } - auto jsonConsumableRtpEncodingsIt = data.find("consumableRtpEncodings"); - - if (jsonConsumableRtpEncodingsIt == data.end() || !jsonConsumableRtpEncodingsIt->is_array()) - MS_THROW_TYPE_ERROR("missing consumableRtpEncodings"); - - if (jsonConsumableRtpEncodingsIt->empty()) + if (data->consumableRtpEncodings()->size() == 0) + { MS_THROW_TYPE_ERROR("empty consumableRtpEncodings"); + } - this->consumableRtpEncodings.reserve(jsonConsumableRtpEncodingsIt->size()); + this->consumableRtpEncodings.reserve(data->consumableRtpEncodings()->size()); - for (size_t i{ 0 }; i < jsonConsumableRtpEncodingsIt->size(); ++i) + for (size_t i{ 0 }; i < data->consumableRtpEncodings()->size(); ++i) { - auto& entry = (*jsonConsumableRtpEncodingsIt)[i]; + const auto* entry = data->consumableRtpEncodings()->Get(i); // This may throw due the constructor of RTC::RtpEncodingParameters. this->consumableRtpEncodings.emplace_back(entry); @@ -75,7 +61,9 @@ namespace RTC auto& encoding = this->consumableRtpEncodings[i]; if (encoding.ssrc == 0u) + { MS_THROW_TYPE_ERROR("wrong encoding in consumableRtpEncodings (missing ssrc)"); + } } // Fill RTP header extension ids and their mapped values. @@ -83,7 +71,9 @@ namespace RTC for (auto& exten : this->rtpParameters.headerExtensions) { if (exten.id == 0u) + { MS_THROW_TYPE_ERROR("RTP extension id cannot be 0"); + } if (this->rtpHeaderExtensionIds.ssrcAudioLevel == 0u && exten.type == RTC::RtpHeaderExtensionUri::Type::SSRC_AUDIO_LEVEL) { @@ -95,6 +85,11 @@ namespace RTC this->rtpHeaderExtensionIds.videoOrientation = exten.id; } + if (this->rtpHeaderExtensionIds.playoutDelay == 0u && exten.type == RTC::RtpHeaderExtensionUri::Type::PLAYOUT_DELAY) + { + this->rtpHeaderExtensionIds.playoutDelay = exten.id; + } + if (this->rtpHeaderExtensionIds.absSendTime == 0u && exten.type == RTC::RtpHeaderExtensionUri::Type::ABS_SEND_TIME) { this->rtpHeaderExtensionIds.absSendTime = exten.id; @@ -121,16 +116,16 @@ namespace RTC } } - auto jsonPausedIt = data.find("paused"); - - if (jsonPausedIt != data.end() && jsonPausedIt->is_boolean()) - this->paused = jsonPausedIt->get(); + // paused is set to false by default. + this->paused = data->paused(); // Fill supported codec payload types. for (auto& codec : this->rtpParameters.codecs) { if (codec.mimeType.IsMediaCodec()) + { this->supportedCodecPayloadTypes[codec.payloadType] = true; + } } // Fill media SSRCs vector. @@ -143,14 +138,20 @@ namespace RTC for (auto& encoding : this->rtpParameters.encodings) { if (encoding.hasRtx) + { this->rtxSsrcs.push_back(encoding.rtx.ssrc); + } } // Set the RTCP report generation interval. if (this->kind == RTC::Media::Kind::AUDIO) + { this->maxRtcpInterval = RTC::RTCP::MaxAudioIntervalMs; + } else + { this->maxRtcpInterval = RTC::RTCP::MaxVideoIntervalMs; + } } Consumer::~Consumer() @@ -158,119 +159,96 @@ namespace RTC MS_TRACE(); } - void Consumer::FillJson(json& jsonObject) const + flatbuffers::Offset Consumer::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Add id. - jsonObject["id"] = this->id; - - // Add producerId. - jsonObject["producerId"] = this->producerId; - - // Add kind. - jsonObject["kind"] = RTC::Media::GetString(this->kind); - // Add rtpParameters. - this->rtpParameters.FillJson(jsonObject["rtpParameters"]); - - // Add type. - jsonObject["type"] = RTC::RtpParameters::GetTypeString(this->type); + auto rtpParameters = this->rtpParameters.FillBuffer(builder); // Add consumableRtpEncodings. - jsonObject["consumableRtpEncodings"] = json::array(); - auto jsonConsumableRtpEncodingsIt = jsonObject.find("consumableRtpEncodings"); + std::vector> consumableRtpEncodings; + consumableRtpEncodings.reserve(this->consumableRtpEncodings.size()); - for (size_t i{ 0 }; i < this->consumableRtpEncodings.size(); ++i) + for (const auto& encoding : this->consumableRtpEncodings) { - jsonConsumableRtpEncodingsIt->emplace_back(json::value_t::object); - - auto& jsonEntry = (*jsonConsumableRtpEncodingsIt)[i]; - const auto& encoding = this->consumableRtpEncodings[i]; - - encoding.FillJson(jsonEntry); + consumableRtpEncodings.emplace_back(encoding.FillBuffer(builder)); } // Add supportedCodecPayloadTypes. - jsonObject["supportedCodecPayloadTypes"] = json::array(); + std::vector supportedCodecPayloadTypes; for (auto i = 0; i < 128; ++i) { if (this->supportedCodecPayloadTypes[i]) - jsonObject["supportedCodecPayloadTypes"].push_back(i); + { + supportedCodecPayloadTypes.push_back(i); + } } - // Add paused. - jsonObject["paused"] = this->paused; - - // Add producerPaused. - jsonObject["producerPaused"] = this->producerPaused; - - // Add priority. - jsonObject["priority"] = this->priority; - // Add traceEventTypes. - std::vector traceEventTypes; - std::ostringstream traceEventTypesStream; + std::vector traceEventTypes; if (this->traceEventTypes.rtp) - traceEventTypes.emplace_back("rtp"); + { + traceEventTypes.emplace_back(FBS::Consumer::TraceEventType::RTP); + } if (this->traceEventTypes.keyframe) - traceEventTypes.emplace_back("keyframe"); + { + traceEventTypes.emplace_back(FBS::Consumer::TraceEventType::KEYFRAME); + } if (this->traceEventTypes.nack) - traceEventTypes.emplace_back("nack"); + { + traceEventTypes.emplace_back(FBS::Consumer::TraceEventType::NACK); + } if (this->traceEventTypes.pli) - traceEventTypes.emplace_back("pli"); + { + traceEventTypes.emplace_back(FBS::Consumer::TraceEventType::PLI); + } if (this->traceEventTypes.fir) - traceEventTypes.emplace_back("fir"); - - if (!traceEventTypes.empty()) { - std::copy( - traceEventTypes.begin(), - traceEventTypes.end() - 1, - std::ostream_iterator(traceEventTypesStream, ",")); - traceEventTypesStream << traceEventTypes.back(); + traceEventTypes.emplace_back(FBS::Consumer::TraceEventType::FIR); } - jsonObject["traceEventTypes"] = traceEventTypesStream.str(); + return FBS::Consumer::CreateBaseConsumerDumpDirect( + builder, + this->id.c_str(), + RTC::RtpParameters::TypeToFbs(this->type), + this->producerId.c_str(), + this->kind == RTC::Media::Kind::AUDIO ? FBS::RtpParameters::MediaKind::AUDIO + : FBS::RtpParameters::MediaKind::VIDEO, + rtpParameters, + &consumableRtpEncodings, + &supportedCodecPayloadTypes, + &traceEventTypes, + this->paused, + this->producerPaused, + this->priority); } void Consumer::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::CONSUMER_DUMP: + case Channel::ChannelRequest::Method::CONSUMER_GET_STATS: { - json data = json::object(); + auto responseOffset = FillBufferStats(request->GetBufferBuilder()); - FillJson(data); - - request->Accept(data); + request->Accept(FBS::Response::Body::Consumer_GetStatsResponse, responseOffset); break; } - case Channel::ChannelRequest::MethodId::CONSUMER_GET_STATS: - { - json data = json::array(); - - FillJsonStats(data); - - request->Accept(data); - - break; - } - - case Channel::ChannelRequest::MethodId::CONSUMER_PAUSE: + case Channel::ChannelRequest::Method::CONSUMER_PAUSE: { if (this->paused) { request->Accept(); - return; + break; } const bool wasActive = IsActive(); @@ -280,20 +258,22 @@ namespace RTC MS_DEBUG_DEV("Consumer paused [consumerId:%s]", this->id.c_str()); if (wasActive) + { UserOnPaused(); + } request->Accept(); break; } - case Channel::ChannelRequest::MethodId::CONSUMER_RESUME: + case Channel::ChannelRequest::Method::CONSUMER_RESUME: { if (!this->paused) { request->Accept(); - return; + break; } this->paused = false; @@ -301,64 +281,76 @@ namespace RTC MS_DEBUG_DEV("Consumer resumed [consumerId:%s]", this->id.c_str()); if (IsActive()) + { UserOnResumed(); + } request->Accept(); break; } - case Channel::ChannelRequest::MethodId::CONSUMER_SET_PRIORITY: + case Channel::ChannelRequest::Method::CONSUMER_SET_PRIORITY: { - auto jsonPriorityIt = request->data.find("priority"); - - if (jsonPriorityIt == request->data.end() || !jsonPriorityIt->is_number()) - MS_THROW_TYPE_ERROR("wrong priority (not a number)"); - - auto priority = jsonPriorityIt->get(); + const auto* body = request->data->body_as(); - if (priority < 1u) + if (body->priority() < 1u) + { MS_THROW_TYPE_ERROR("wrong priority (must be higher than 0)"); + } - this->priority = priority; - - json data = json::object(); + this->priority = body->priority(); - data["priority"] = this->priority; + auto responseOffset = + FBS::Consumer::CreateSetPriorityResponse(request->GetBufferBuilder(), this->priority); - request->Accept(data); + request->Accept(FBS::Response::Body::Consumer_SetPriorityResponse, responseOffset); break; } - case Channel::ChannelRequest::MethodId::CONSUMER_ENABLE_TRACE_EVENT: + case Channel::ChannelRequest::Method::CONSUMER_ENABLE_TRACE_EVENT: { - auto jsonTypesIt = request->data.find("types"); - - // Disable all if no entries. - if (jsonTypesIt == request->data.end() || !jsonTypesIt->is_array()) - MS_THROW_TYPE_ERROR("wrong types (not an array)"); + const auto* body = request->data->body_as(); // Reset traceEventTypes. struct TraceEventTypes newTraceEventTypes; - for (const auto& type : *jsonTypesIt) + for (const auto& type : *body->events()) { - if (!type.is_string()) - MS_THROW_TYPE_ERROR("wrong type (not a string)"); - - std::string typeStr = type.get(); - - if (typeStr == "rtp") - newTraceEventTypes.rtp = true; - else if (typeStr == "keyframe") - newTraceEventTypes.keyframe = true; - else if (typeStr == "nack") - newTraceEventTypes.nack = true; - else if (typeStr == "pli") - newTraceEventTypes.pli = true; - else if (typeStr == "fir") - newTraceEventTypes.fir = true; + switch (type) + { + case FBS::Consumer::TraceEventType::KEYFRAME: + { + newTraceEventTypes.keyframe = true; + + break; + } + case FBS::Consumer::TraceEventType::FIR: + { + newTraceEventTypes.fir = true; + + break; + } + case FBS::Consumer::TraceEventType::NACK: + { + newTraceEventTypes.nack = true; + + break; + } + case FBS::Consumer::TraceEventType::PLI: + { + newTraceEventTypes.pli = true; + + break; + } + case FBS::Consumer::TraceEventType::RTP: + { + newTraceEventTypes.rtp = true; + + break; + } + } } this->traceEventTypes = newTraceEventTypes; @@ -370,7 +362,7 @@ namespace RTC default: { - MS_THROW_ERROR("unknown method '%s'", request->method.c_str()); + MS_THROW_ERROR("unknown method '%s'", request->methodCStr); } } } @@ -380,7 +372,9 @@ namespace RTC MS_TRACE(); if (this->transportConnected) + { return; + } this->transportConnected = true; @@ -394,7 +388,9 @@ namespace RTC MS_TRACE(); if (!this->transportConnected) + { return; + } this->transportConnected = false; @@ -408,7 +404,9 @@ namespace RTC MS_TRACE(); if (this->producerPaused) + { return; + } const bool wasActive = IsActive(); @@ -417,9 +415,11 @@ namespace RTC MS_DEBUG_DEV("Producer paused [consumerId:%s]", this->id.c_str()); if (wasActive) + { UserOnPaused(); + } - this->shared->channelNotifier->Emit(this->id, "producerpause"); + this->shared->channelNotifier->Emit(this->id, FBS::Notification::Event::CONSUMER_PRODUCER_PAUSE); } void Consumer::ProducerResumed() @@ -427,16 +427,20 @@ namespace RTC MS_TRACE(); if (!this->producerPaused) + { return; + } this->producerPaused = false; MS_DEBUG_DEV("Producer resumed [consumerId:%s]", this->id.c_str()); if (IsActive()) + { UserOnResumed(); + } - this->shared->channelNotifier->Emit(this->id, "producerresume"); + this->shared->channelNotifier->Emit(this->id, FBS::Notification::Event::CONSUMER_PRODUCER_RESUME); } void Consumer::ProducerRtpStreamScores(const std::vector* scores) @@ -457,7 +461,7 @@ namespace RTC MS_DEBUG_DEV("Producer closed [consumerId:%s]", this->id.c_str()); - this->shared->channelNotifier->Emit(this->id, "producerclose"); + this->shared->channelNotifier->Emit(this->id, FBS::Notification::Event::CONSUMER_PRODUCER_CLOSE); this->listener->OnConsumerProducerClosed(this); } @@ -468,33 +472,35 @@ namespace RTC if (this->traceEventTypes.keyframe && packet->IsKeyFrame()) { - json data = json::object(); - - data["type"] = "keyframe"; - data["timestamp"] = DepLibUV::GetTimeMs(); - data["direction"] = "out"; - - packet->FillJson(data["info"]); - - if (isRtx) - data["info"]["isRtx"] = true; - - this->shared->channelNotifier->Emit(this->id, "trace", data); + auto rtpPacketDump = packet->FillBuffer(this->shared->channelNotifier->GetBufferBuilder()); + auto traceInfo = FBS::Consumer::CreateKeyFrameTraceInfo( + this->shared->channelNotifier->GetBufferBuilder(), rtpPacketDump, isRtx); + + auto notification = FBS::Consumer::CreateTraceNotification( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::Consumer::TraceEventType::KEYFRAME, + DepLibUV::GetTimeMs(), + FBS::Common::TraceDirection::DIRECTION_OUT, + FBS::Consumer::TraceInfo::KeyFrameTraceInfo, + traceInfo.Union()); + + EmitTraceEvent(notification); } else if (this->traceEventTypes.rtp) { - json data = json::object(); - - data["type"] = "rtp"; - data["timestamp"] = DepLibUV::GetTimeMs(); - data["direction"] = "out"; - - packet->FillJson(data["info"]); - - if (isRtx) - data["info"]["isRtx"] = true; - - this->shared->channelNotifier->Emit(this->id, "trace", data); + auto rtpPacketDump = packet->FillBuffer(this->shared->channelNotifier->GetBufferBuilder()); + auto traceInfo = FBS::Consumer::CreateRtpTraceInfo( + this->shared->channelNotifier->GetBufferBuilder(), rtpPacketDump, isRtx); + + auto notification = FBS::Consumer::CreateTraceNotification( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::Consumer::TraceEventType::RTP, + DepLibUV::GetTimeMs(), + FBS::Common::TraceDirection::DIRECTION_OUT, + FBS::Consumer::TraceInfo::RtpTraceInfo, + traceInfo.Union()); + + EmitTraceEvent(notification); } } @@ -503,16 +509,22 @@ namespace RTC MS_TRACE(); if (!this->traceEventTypes.pli) + { return; + } - json data = json::object(); + auto traceInfo = + FBS::Consumer::CreatePliTraceInfo(this->shared->channelNotifier->GetBufferBuilder(), ssrc); - data["type"] = "pli"; - data["timestamp"] = DepLibUV::GetTimeMs(); - data["direction"] = "in"; - data["info"]["ssrc"] = ssrc; + auto notification = FBS::Consumer::CreateTraceNotification( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::Consumer::TraceEventType::PLI, + DepLibUV::GetTimeMs(), + FBS::Common::TraceDirection::DIRECTION_IN, + FBS::Consumer::TraceInfo::PliTraceInfo, + traceInfo.Union()); - this->shared->channelNotifier->Emit(this->id, "trace", data); + EmitTraceEvent(notification); } void Consumer::EmitTraceEventFirType(uint32_t ssrc) const @@ -520,16 +532,22 @@ namespace RTC MS_TRACE(); if (!this->traceEventTypes.fir) + { return; + } - json data = json::object(); + auto traceInfo = + FBS::Consumer::CreateFirTraceInfo(this->shared->channelNotifier->GetBufferBuilder(), ssrc); - data["type"] = "fir"; - data["timestamp"] = DepLibUV::GetTimeMs(); - data["direction"] = "in"; - data["info"]["ssrc"] = ssrc; + auto notification = FBS::Consumer::CreateTraceNotification( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::Consumer::TraceEventType::FIR, + DepLibUV::GetTimeMs(), + FBS::Common::TraceDirection::DIRECTION_IN, + FBS::Consumer::TraceInfo::FirTraceInfo, + traceInfo.Union()); - this->shared->channelNotifier->Emit(this->id, "trace", data); + EmitTraceEvent(notification); } void Consumer::EmitTraceEventNackType() const @@ -537,15 +555,27 @@ namespace RTC MS_TRACE(); if (!this->traceEventTypes.nack) + { return; + } + + auto notification = FBS::Consumer::CreateTraceNotification( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::Consumer::TraceEventType::NACK, + DepLibUV::GetTimeMs(), + FBS::Common::TraceDirection::DIRECTION_IN); - json data = json::object(); + EmitTraceEvent(notification); + } - data["type"] = "nack"; - data["timestamp"] = DepLibUV::GetTimeMs(); - data["direction"] = "in"; - data["info"] = json::object(); + void Consumer::EmitTraceEvent(flatbuffers::Offset& notification) const + { + MS_TRACE(); - this->shared->channelNotifier->Emit(this->id, "trace", data); + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::CONSUMER_TRACE, + FBS::Notification::Body::Consumer_TraceNotification, + notification); } } // namespace RTC diff --git a/worker/src/RTC/DataConsumer.cpp b/worker/src/RTC/DataConsumer.cpp index 9add8cab3b..c9034cdf0f 100644 --- a/worker/src/RTC/DataConsumer.cpp +++ b/worker/src/RTC/DataConsumer.cpp @@ -5,9 +5,7 @@ #include "DepLibUV.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" -#include "Utils.hpp" #include "RTC/SctpAssociation.hpp" -#include namespace RTC { @@ -19,58 +17,67 @@ namespace RTC const std::string& dataProducerId, RTC::SctpAssociation* sctpAssociation, RTC::DataConsumer::Listener* listener, - json& data, + const FBS::Transport::ConsumeDataRequest* data, size_t maxMessageSize) : id(id), dataProducerId(dataProducerId), shared(shared), sctpAssociation(sctpAssociation), listener(listener), maxMessageSize(maxMessageSize) { MS_TRACE(); - auto jsonTypeIt = data.find("type"); - auto jsonSctpStreamParametersIt = data.find("sctpStreamParameters"); - auto jsonLabelIt = data.find("label"); - auto jsonProtocolIt = data.find("protocol"); - - if (jsonTypeIt == data.end() || !jsonTypeIt->is_string()) - MS_THROW_TYPE_ERROR("missing type"); + switch (data->type()) + { + case FBS::DataProducer::Type::SCTP: + { + this->type = DataConsumer::Type::SCTP; - this->typeString = jsonTypeIt->get(); + break; + } + case FBS::DataProducer::Type::DIRECT: + { + this->type = DataConsumer::Type::DIRECT; - if (this->typeString == "sctp") - this->type = DataConsumer::Type::SCTP; - else if (this->typeString == "direct") - this->type = DataConsumer::Type::DIRECT; - else - MS_THROW_TYPE_ERROR("invalid type"); + break; + } + } if (this->type == DataConsumer::Type::SCTP) { - // clang-format off - if ( - jsonSctpStreamParametersIt == data.end() || - !jsonSctpStreamParametersIt->is_object() - ) - // clang-format on + if (!flatbuffers::IsFieldPresent( + data, FBS::Transport::ConsumeDataRequest::VT_SCTPSTREAMPARAMETERS)) { MS_THROW_TYPE_ERROR("missing sctpStreamParameters"); } // This may throw. - this->sctpStreamParameters = RTC::SctpStreamParameters(*jsonSctpStreamParametersIt); + this->sctpStreamParameters = RTC::SctpStreamParameters(data->sctpStreamParameters()); } - if (jsonLabelIt != data.end() && jsonLabelIt->is_string()) - this->label = jsonLabelIt->get(); + if (flatbuffers::IsFieldPresent(data, FBS::Transport::ConsumeDataRequest::VT_LABEL)) + { + this->label = data->label()->str(); + } - if (jsonProtocolIt != data.end() && jsonProtocolIt->is_string()) - this->protocol = jsonProtocolIt->get(); + if (flatbuffers::IsFieldPresent(data, FBS::Transport::ConsumeDataRequest::VT_PROTOCOL)) + { + this->protocol = data->protocol()->str(); + } + + // paused is set to false by default. + this->paused = data->paused(); + + if (flatbuffers::IsFieldPresent(data, FBS::Transport::ConsumeDataRequest::VT_SUBCHANNELS)) + { + for (const auto subchannel : *data->subchannels()) + { + this->subchannels.insert(subchannel); + } + } // NOTE: This may throw. this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ this, - /*payloadChannelNotificationHandler*/ nullptr); + /*channelNotificationHandler*/ nullptr); } DataConsumer::~DataConsumer() @@ -80,93 +87,125 @@ namespace RTC this->shared->channelMessageRegistrator->UnregisterHandler(this->id); } - void DataConsumer::FillJson(json& jsonObject) const + flatbuffers::Offset DataConsumer::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Add id. - jsonObject["id"] = this->id; - - // Add type. - jsonObject["type"] = this->typeString; - - // Add dataProducerId. - jsonObject["dataProducerId"] = this->dataProducerId; + flatbuffers::Offset sctpStreamParameters; // Add sctpStreamParameters. if (this->type == DataConsumer::Type::SCTP) { - this->sctpStreamParameters.FillJson(jsonObject["sctpStreamParameters"]); + sctpStreamParameters = this->sctpStreamParameters.FillBuffer(builder); } - // Add label. - jsonObject["label"] = this->label; + std::vector subchannels; - // Add protocol. - jsonObject["protocol"] = this->protocol; + subchannels.reserve(this->subchannels.size()); - // Add bufferedAmountLowThreshold. - jsonObject["bufferedAmountLowThreshold"] = this->bufferedAmountLowThreshold; + for (auto subchannel : this->subchannels) + { + subchannels.emplace_back(subchannel); + } + + return FBS::DataConsumer::CreateDumpResponseDirect( + builder, + this->id.c_str(), + this->dataProducerId.c_str(), + this->type == DataConsumer::Type::SCTP ? FBS::DataProducer::Type::SCTP + : FBS::DataProducer::Type::DIRECT, + sctpStreamParameters, + this->label.c_str(), + this->protocol.c_str(), + this->bufferedAmountLowThreshold, + this->paused, + this->dataProducerPaused, + std::addressof(subchannels)); } - void DataConsumer::FillJsonStats(json& jsonArray) const + flatbuffers::Offset DataConsumer::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - jsonArray.emplace_back(json::value_t::object); - auto& jsonObject = jsonArray[0]; - - // Add type. - jsonObject["type"] = "data-consumer"; + return FBS::DataConsumer::CreateGetStatsResponseDirect( + builder, + // timestamp. + DepLibUV::GetTimeMs(), + // label. + this->label.c_str(), + // protocol. + this->protocol.c_str(), + // messagesSent. + this->messagesSent, + // bytesSent. + this->bytesSent, + // bufferedAmount. + this->bufferedAmount); + } - // Add timestamp. - jsonObject["timestamp"] = DepLibUV::GetTimeMs(); + void DataConsumer::HandleRequest(Channel::ChannelRequest* request) + { + MS_TRACE(); - // Add label. - jsonObject["label"] = this->label; + switch (request->method) + { + case Channel::ChannelRequest::Method::DATACONSUMER_DUMP: + { + auto dumpOffset = FillBuffer(request->GetBufferBuilder()); - // Add protocol. - jsonObject["protocol"] = this->protocol; + request->Accept(FBS::Response::Body::DataConsumer_DumpResponse, dumpOffset); - // Add messagesSent. - jsonObject["messagesSent"] = this->messagesSent; + break; + } - // Add bytesSent. - jsonObject["bytesSent"] = this->bytesSent; + case Channel::ChannelRequest::Method::DATACONSUMER_GET_STATS: + { + auto responseOffset = FillBufferStats(request->GetBufferBuilder()); - // Add bufferedAmount. - jsonObject["bufferedAmount"] = this->bufferedAmount; - } + request->Accept(FBS::Response::Body::DataConsumer_GetStatsResponse, responseOffset); - void DataConsumer::HandleRequest(Channel::ChannelRequest* request) - { - MS_TRACE(); + break; + } - switch (request->methodId) - { - case Channel::ChannelRequest::MethodId::DATA_CONSUMER_DUMP: + case Channel::ChannelRequest::Method::DATACONSUMER_PAUSE: { - json data = json::object(); + if (this->paused) + { + request->Accept(); - FillJson(data); + break; + } + + this->paused = true; + + MS_DEBUG_DEV("DataConsumer paused [dataConsumerId:%s]", this->id.c_str()); - request->Accept(data); + request->Accept(); break; } - case Channel::ChannelRequest::MethodId::DATA_CONSUMER_GET_STATS: + case Channel::ChannelRequest::Method::DATACONSUMER_RESUME: { - json data = json::array(); + if (!this->paused) + { + request->Accept(); - FillJsonStats(data); + break; + } - request->Accept(data); + this->paused = false; + + MS_DEBUG_DEV("DataConsumer resumed [dataConsumerId:%s]", this->id.c_str()); + + request->Accept(); break; } - case Channel::ChannelRequest::MethodId::DATA_CONSUMER_GET_BUFFERED_AMOUNT: + case Channel::ChannelRequest::Method::DATACONSUMER_GET_BUFFERED_AMOUNT: { if (this->GetType() != RTC::DataConsumer::Type::SCTP) { @@ -179,28 +218,25 @@ namespace RTC } // Create status response. - json data = json::object(); + auto responseOffset = FBS::DataConsumer::CreateGetBufferedAmountResponse( + request->GetBufferBuilder(), this->sctpAssociation->GetSctpBufferedAmount()); - data["bufferedAmount"] = this->sctpAssociation->GetSctpBufferedAmount(); - - request->Accept(data); + request->Accept(FBS::Response::Body::DataConsumer_GetBufferedAmountResponse, responseOffset); break; } - case Channel::ChannelRequest::MethodId::DATA_CONSUMER_SET_BUFFERED_AMOUNT_LOW_THRESHOLD: + case Channel::ChannelRequest::Method::DATACONSUMER_SET_BUFFERED_AMOUNT_LOW_THRESHOLD: { if (this->GetType() != DataConsumer::Type::SCTP) { MS_THROW_TYPE_ERROR("invalid DataConsumer type"); } - auto jsonThresholdIt = request->data.find("threshold"); - - if (jsonThresholdIt == request->data.end() || !jsonThresholdIt->is_number_unsigned()) - MS_THROW_TYPE_ERROR("wrong bufferedAmountThreshold (not an unsigned number)"); + const auto* body = + request->data->body_as(); - this->bufferedAmountLowThreshold = jsonThresholdIt->get(); + this->bufferedAmountLowThreshold = body->threshold(); request->Accept(); @@ -208,10 +244,15 @@ namespace RTC // Trigger 'bufferedamountlow' now. if (this->bufferedAmount <= this->bufferedAmountLowThreshold) { - std::string data(R"({"bufferedAmount":)"); - - data.append(std::to_string(this->bufferedAmount)); - data.append("}"); + // Notify the Node DataConsumer. + auto bufferedAmountLowOffset = FBS::DataConsumer::CreateBufferedAmountLowNotification( + this->shared->channelNotifier->GetBufferBuilder(), this->bufferedAmount); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::DATACONSUMER_BUFFERED_AMOUNT_LOW, + FBS::Notification::Body::DataConsumer_BufferedAmountLowNotification, + bufferedAmountLowOffset); } // Force the trigger of 'bufferedamountlow' once there is less or same // buffered data than the given threshold. @@ -223,20 +264,7 @@ namespace RTC break; } - default: - { - MS_THROW_ERROR("unknown method '%s'", request->method.c_str()); - } - } - } - - void DataConsumer::HandleRequest(PayloadChannel::PayloadChannelRequest* request) - { - MS_TRACE(); - - switch (request->methodId) - { - case PayloadChannel::PayloadChannelRequest::MethodId::DATA_CONSUMER_SEND: + case Channel::ChannelRequest::Method::DATACONSUMER_SEND: { if (this->GetType() != RTC::DataConsumer::Type::SCTP) { @@ -248,49 +276,118 @@ namespace RTC MS_THROW_ERROR("no SCTP association present"); } - int ppid; - - // This may throw. - // NOTE: If this throws we have to catch the error and throw a MediaSoupError - // intead, otherwise the process would crash. - try - { - ppid = std::stoi(request->data); - } - catch (const std::exception& error) - { - MS_THROW_TYPE_ERROR("invalid PPID value: %s", error.what()); - } - - const auto* msg = request->payload; - auto len = request->payloadLen; + const auto* body = request->data->body_as(); + const uint8_t* data = body->data()->Data(); + const size_t len = body->data()->size(); if (len > this->maxMessageSize) { MS_THROW_TYPE_ERROR( "given message exceeds maxMessageSize value [maxMessageSize:%zu, len:%zu]", - len, - this->maxMessageSize); + this->maxMessageSize, + len); } const auto* cb = new onQueuedCallback( [&request](bool queued, bool sctpSendBufferFull) { if (queued) + { request->Accept(); + } else - request->Error( - sctpSendBufferFull == true ? "sctpsendbufferfull" : "message send failed"); + { + request->Error(sctpSendBufferFull ? "sctpsendbufferfull" : "message send failed"); + } }); - SendMessage(ppid, msg, len, cb); + static std::vector emptySubchannels; + + SendMessage(data, len, body->ppid(), emptySubchannels, std::nullopt, cb); + + break; + } + + case Channel::ChannelRequest::Method::DATACONSUMER_SET_SUBCHANNELS: + { + const auto* body = request->data->body_as(); + + this->subchannels.clear(); + + for (const auto subchannel : *body->subchannels()) + { + this->subchannels.insert(subchannel); + } + + std::vector subchannels; + + subchannels.reserve(this->subchannels.size()); + + for (auto subchannel : this->subchannels) + { + subchannels.emplace_back(subchannel); + } + + // Create response. + auto responseOffset = FBS::DataConsumer::CreateSetSubchannelsResponseDirect( + request->GetBufferBuilder(), std::addressof(subchannels)); + + request->Accept(FBS::Response::Body::DataConsumer_SetSubchannelsResponse, responseOffset); + + break; + } + + case Channel::ChannelRequest::Method::DATACONSUMER_ADD_SUBCHANNEL: + { + const auto* body = request->data->body_as(); + + this->subchannels.insert(body->subchannel()); + + std::vector subchannels; + + subchannels.reserve(this->subchannels.size()); + + for (auto subchannel : this->subchannels) + { + subchannels.emplace_back(subchannel); + } + + // Create response. + auto responseOffset = FBS::DataConsumer::CreateAddSubchannelResponseDirect( + request->GetBufferBuilder(), std::addressof(subchannels)); + + request->Accept(FBS::Response::Body::DataConsumer_AddSubchannelResponse, responseOffset); + + break; + } + + case Channel::ChannelRequest::Method::DATACONSUMER_REMOVE_SUBCHANNEL: + { + const auto* body = request->data->body_as(); + + this->subchannels.erase(body->subchannel()); + + std::vector subchannels; + + subchannels.reserve(this->subchannels.size()); + + for (auto subchannel : this->subchannels) + { + subchannels.emplace_back(subchannel); + } + + // Create response. + auto responseOffset = FBS::DataConsumer::CreateRemoveSubchannelResponseDirect( + request->GetBufferBuilder(), std::addressof(subchannels)); + + request->Accept(FBS::Response::Body::DataConsumer_RemoveSubchannelResponse, responseOffset); break; } default: { - MS_THROW_ERROR("unknown method '%s'", request->method.c_str()); + MS_THROW_ERROR("unknown method '%s'", request->methodCStr); } } } @@ -313,6 +410,40 @@ namespace RTC MS_DEBUG_DEV("Transport disconnected [dataConsumerId:%s]", this->id.c_str()); } + void DataConsumer::DataProducerPaused() + { + MS_TRACE(); + + if (this->dataProducerPaused) + { + return; + } + + this->dataProducerPaused = true; + + MS_DEBUG_DEV("DataProducer paused [dataConsumerId:%s]", this->id.c_str()); + + this->shared->channelNotifier->Emit( + this->id, FBS::Notification::Event::DATACONSUMER_DATAPRODUCER_PAUSE); + } + + void DataConsumer::DataProducerResumed() + { + MS_TRACE(); + + if (!this->dataProducerPaused) + { + return; + } + + this->dataProducerPaused = false; + + MS_DEBUG_DEV("DataProducer resumed [dataConsumerId:%s]", this->id.c_str()); + + this->shared->channelNotifier->Emit( + this->id, FBS::Notification::Event::DATACONSUMER_DATAPRODUCER_RESUME); + } + void DataConsumer::SctpAssociationConnected() { MS_TRACE(); @@ -349,12 +480,14 @@ namespace RTC this->forceTriggerBufferedAmountLow = false; // Notify the Node DataConsumer. - std::string data(R"({"bufferedAmount":)"); - - data.append(std::to_string(this->bufferedAmount)); - data.append("}"); - - this->shared->channelNotifier->Emit(this->id, "bufferedamountlow", data); + auto bufferedAmountLowOffset = FBS::DataConsumer::CreateBufferedAmountLowNotification( + this->shared->channelNotifier->GetBufferBuilder(), this->bufferedAmount); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::DATACONSUMER_BUFFERED_AMOUNT_LOW, + FBS::Notification::Body::DataConsumer_BufferedAmountLowNotification, + bufferedAmountLowOffset); } } @@ -362,7 +495,8 @@ namespace RTC { MS_TRACE(); - this->shared->channelNotifier->Emit(this->id, "sctpsendbufferfull"); + this->shared->channelNotifier->Emit( + this->id, FBS::Notification::Event::DATACONSUMER_SCTP_SENDBUFFER_FULL); } // The caller (Router) is supposed to proceed with the deletion of this DataConsumer @@ -375,17 +509,75 @@ namespace RTC MS_DEBUG_DEV("DataProducer closed [dataConsumerId:%s]", this->id.c_str()); - this->shared->channelNotifier->Emit(this->id, "dataproducerclose"); + this->shared->channelNotifier->Emit( + this->id, FBS::Notification::Event::DATACONSUMER_DATAPRODUCER_CLOSE); this->listener->OnDataConsumerDataProducerClosed(this); } - void DataConsumer::SendMessage(uint32_t ppid, const uint8_t* msg, size_t len, onQueuedCallback* cb) + void DataConsumer::SendMessage( + const uint8_t* msg, + size_t len, + uint32_t ppid, + std::vector& subchannels, + std::optional requiredSubchannel, + onQueuedCallback* cb) { MS_TRACE(); if (!IsActive()) + { + if (cb) + { + (*cb)(false, false); + delete cb; + } + + return; + } + + // If a required subchannel is given, verify that this data consumer is + // subscribed to it. + if ( + requiredSubchannel.has_value() && + this->subchannels.find(requiredSubchannel.value()) == this->subchannels.end()) + { + if (cb) + { + (*cb)(false, false); + delete cb; + } + return; + } + + // If subchannels are given, verify that this data consumer is subscribed + // to at least one of them. + if (!subchannels.empty()) + { + bool subchannelMatched{ false }; + + for (const auto subchannel : subchannels) + { + if (this->subchannels.find(subchannel) != this->subchannels.end()) + { + subchannelMatched = true; + + break; + } + } + + if (!subchannelMatched) + { + if (cb) + { + (*cb)(false, false); + delete cb; + } + + return; + } + } if (len > this->maxMessageSize) { @@ -395,12 +587,18 @@ namespace RTC len, this->maxMessageSize); + if (cb) + { + (*cb)(false, false); + delete cb; + } + return; } this->messagesSent++; this->bytesSent += len; - this->listener->OnDataConsumerSendMessage(this, ppid, msg, len, cb); + this->listener->OnDataConsumerSendMessage(this, msg, len, ppid, cb); } } // namespace RTC diff --git a/worker/src/RTC/DataProducer.cpp b/worker/src/RTC/DataProducer.cpp index f0888c83ae..eebd66ebec 100644 --- a/worker/src/RTC/DataProducer.cpp +++ b/worker/src/RTC/DataProducer.cpp @@ -5,8 +5,7 @@ #include "DepLibUV.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" -#include "Utils.hpp" -#include +#include namespace RTC { @@ -17,56 +16,57 @@ namespace RTC const std::string& id, size_t maxMessageSize, RTC::DataProducer::Listener* listener, - json& data) + const FBS::Transport::ProduceDataRequest* data) : id(id), shared(shared), maxMessageSize(maxMessageSize), listener(listener) { MS_TRACE(); - auto jsonTypeIt = data.find("type"); - auto jsonSctpStreamParametersIt = data.find("sctpStreamParameters"); - auto jsonLabelIt = data.find("label"); - auto jsonProtocolIt = data.find("protocol"); + switch (data->type()) + { + case FBS::DataProducer::Type::SCTP: + { + this->type = DataProducer::Type::SCTP; - if (jsonTypeIt == data.end() || !jsonTypeIt->is_string()) - MS_THROW_TYPE_ERROR("missing type"); + break; + } - this->typeString = jsonTypeIt->get(); + case FBS::DataProducer::Type::DIRECT: + { + this->type = DataProducer::Type::DIRECT; - if (this->typeString == "sctp") - this->type = DataProducer::Type::SCTP; - else if (this->typeString == "direct") - this->type = DataProducer::Type::DIRECT; - else - MS_THROW_TYPE_ERROR("invalid type"); + break; + } + } if (this->type == DataProducer::Type::SCTP) { - // clang-format off - if ( - jsonSctpStreamParametersIt == data.end() || - !jsonSctpStreamParametersIt->is_object() - ) - // clang-format on + if (!flatbuffers::IsFieldPresent( + data, FBS::Transport::ProduceDataRequest::VT_SCTPSTREAMPARAMETERS)) { MS_THROW_TYPE_ERROR("missing sctpStreamParameters"); } // This may throw. - this->sctpStreamParameters = RTC::SctpStreamParameters(*jsonSctpStreamParametersIt); + this->sctpStreamParameters = RTC::SctpStreamParameters(data->sctpStreamParameters()); } - if (jsonLabelIt != data.end() && jsonLabelIt->is_string()) - this->label = jsonLabelIt->get(); + if (flatbuffers::IsFieldPresent(data, FBS::Transport::ProduceDataRequest::VT_LABEL)) + { + this->label = data->label()->str(); + } - if (jsonProtocolIt != data.end() && jsonProtocolIt->is_string()) - this->protocol = jsonProtocolIt->get(); + if (flatbuffers::IsFieldPresent(data, FBS::Transport::ProduceDataRequest::VT_PROTOCOL)) + { + this->protocol = data->protocol()->str(); + } + + this->paused = data->paused(); // NOTE: This may throw. this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ nullptr, - /*payloadChannelNotificationHandler*/ this); + /*channelNotificationHandler*/ this); } DataProducer::~DataProducer() @@ -76,124 +76,160 @@ namespace RTC this->shared->channelMessageRegistrator->UnregisterHandler(this->id); } - void DataProducer::FillJson(json& jsonObject) const + flatbuffers::Offset DataProducer::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Add id. - jsonObject["id"] = this->id; - - // Add type. - jsonObject["type"] = this->typeString; + flatbuffers::Offset sctpStreamParametersOffset; // Add sctpStreamParameters. if (this->type == DataProducer::Type::SCTP) { - this->sctpStreamParameters.FillJson(jsonObject["sctpStreamParameters"]); + sctpStreamParametersOffset = this->sctpStreamParameters.FillBuffer(builder); } - // Add label. - jsonObject["label"] = this->label; - - // Add protocol. - jsonObject["protocol"] = this->protocol; + return FBS::DataProducer::CreateDumpResponseDirect( + builder, + this->id.c_str(), + this->type == DataProducer::Type::SCTP ? FBS::DataProducer::Type::SCTP + : FBS::DataProducer::Type::DIRECT, + sctpStreamParametersOffset, + this->label.c_str(), + this->protocol.c_str(), + this->paused); } - void DataProducer::FillJsonStats(json& jsonArray) const + flatbuffers::Offset DataProducer::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - jsonArray.emplace_back(json::value_t::object); - auto& jsonObject = jsonArray[0]; + return FBS::DataProducer::CreateGetStatsResponseDirect( + builder, + // timestamp. + DepLibUV::GetTimeMs(), + // label. + this->label.c_str(), + // protocol. + this->protocol.c_str(), + // messagesReceived. + this->messagesReceived, + // bytesReceived. + this->bytesReceived); + } - // Add type. - jsonObject["type"] = "data-producer"; + void DataProducer::HandleRequest(Channel::ChannelRequest* request) + { + MS_TRACE(); - // Add timestamp. - jsonObject["timestamp"] = DepLibUV::GetTimeMs(); + switch (request->method) + { + case Channel::ChannelRequest::Method::DATAPRODUCER_DUMP: + { + auto dumpOffset = FillBuffer(request->GetBufferBuilder()); - // Add label. - jsonObject["label"] = this->label; + request->Accept(FBS::Response::Body::DataProducer_DumpResponse, dumpOffset); - // Add protocol. - jsonObject["protocol"] = this->protocol; + break; + } - // Add messagesReceived. - jsonObject["messagesReceived"] = this->messagesReceived; + case Channel::ChannelRequest::Method::DATAPRODUCER_GET_STATS: + { + auto responseOffset = FillBufferStats(request->GetBufferBuilder()); - // Add bytesReceived. - jsonObject["bytesReceived"] = this->bytesReceived; - } + request->Accept(FBS::Response::Body::DataProducer_GetStatsResponse, responseOffset); - void DataProducer::HandleRequest(Channel::ChannelRequest* request) - { - MS_TRACE(); + break; + } - switch (request->methodId) - { - case Channel::ChannelRequest::MethodId::DATA_PRODUCER_DUMP: + case Channel::ChannelRequest::Method::DATAPRODUCER_PAUSE: { - json data = json::object(); + if (this->paused) + { + request->Accept(); + + break; + } - FillJson(data); + this->paused = true; - request->Accept(data); + MS_DEBUG_DEV("DataProducer paused [dataProducerId:%s]", this->id.c_str()); + + this->listener->OnDataProducerPaused(this); + + request->Accept(); break; } - case Channel::ChannelRequest::MethodId::DATA_PRODUCER_GET_STATS: + case Channel::ChannelRequest::Method::DATAPRODUCER_RESUME: { - json data = json::array(); + if (!this->paused) + { + request->Accept(); + + break; + } + + this->paused = false; - FillJsonStats(data); + MS_DEBUG_DEV("DataProducer resumed [dataProducerId:%s]", this->id.c_str()); - request->Accept(data); + this->listener->OnDataProducerResumed(this); + + request->Accept(); break; } default: { - MS_THROW_ERROR("unknown method '%s'", request->method.c_str()); + MS_THROW_ERROR("unknown method '%s'", request->methodCStr); } } } - void DataProducer::HandleNotification(PayloadChannel::PayloadChannelNotification* notification) + void DataProducer::HandleNotification(Channel::ChannelNotification* notification) { MS_TRACE(); - switch (notification->eventId) + switch (notification->event) { - case PayloadChannel::PayloadChannelNotification::EventId::DATA_PRODUCER_SEND: + case Channel::ChannelNotification::Event::DATAPRODUCER_SEND: { - int ppid; + const auto* body = notification->data->body_as(); + const uint8_t* data = body->data()->Data(); + const size_t len = body->data()->size(); - // This may throw. - // NOTE: If this throws we have to catch the error and throw a MediaSoupError - // intead, otherwise the process would crash. - try + if (len > this->maxMessageSize) { - ppid = std::stoi(notification->data); + MS_THROW_TYPE_ERROR( + "given message exceeds maxMessageSize value [maxMessageSize:%zu, len:%zu]", + this->maxMessageSize, + len); } - catch (const std::exception& error) + + std::vector subchannels; + + if (flatbuffers::IsFieldPresent(body, FBS::DataProducer::SendNotification::VT_SUBCHANNELS)) { - MS_THROW_TYPE_ERROR("invalid PPID value: %s", error.what()); + subchannels.reserve(body->subchannels()->size()); + + for (const auto subchannel : *body->subchannels()) + { + subchannels.emplace_back(subchannel); + } } - const auto* msg = notification->payload; - auto len = notification->payloadLen; + std::optional requiredSubchannel{ std::nullopt }; - if (len > this->maxMessageSize) + if (body->requiredSubchannel().has_value()) { - MS_THROW_TYPE_ERROR( - "given message exceeds maxMessageSize value [maxMessageSize:%zu, len:%zu]", - len, - this->maxMessageSize); + requiredSubchannel = body->requiredSubchannel().value(); } - this->ReceiveMessage(ppid, msg, len); + ReceiveMessage(data, len, body->ppid(), subchannels, requiredSubchannel); // Increase receive transmission. this->listener->OnDataProducerReceiveData(this, len); @@ -203,18 +239,30 @@ namespace RTC default: { - MS_ERROR("unknown event '%s'", notification->event.c_str()); + MS_ERROR("unknown event '%s'", notification->eventCStr); } } } - void DataProducer::ReceiveMessage(uint32_t ppid, const uint8_t* msg, size_t len) + void DataProducer::ReceiveMessage( + const uint8_t* msg, + size_t len, + uint32_t ppid, + std::vector& subchannels, + std::optional requiredSubchannel) { MS_TRACE(); this->messagesReceived++; this->bytesReceived += len; - this->listener->OnDataProducerMessageReceived(this, ppid, msg, len); + // If paused stop here. + if (this->paused) + { + return; + } + + this->listener->OnDataProducerMessageReceived( + this, msg, len, ppid, subchannels, requiredSubchannel); } } // namespace RTC diff --git a/worker/src/RTC/DirectTransport.cpp b/worker/src/RTC/DirectTransport.cpp index bc12150a39..f3af041e0f 100644 --- a/worker/src/RTC/DirectTransport.cpp +++ b/worker/src/RTC/DirectTransport.cpp @@ -3,7 +3,6 @@ #include "RTC/DirectTransport.hpp" #include "Logger.hpp" -#include "MediaSoupErrors.hpp" namespace RTC { @@ -11,8 +10,11 @@ namespace RTC // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init) DirectTransport::DirectTransport( - RTC::Shared* shared, const std::string& id, RTC::Transport::Listener* listener, json& data) - : RTC::Transport::Transport(shared, id, listener, data) + RTC::Shared* shared, + const std::string& id, + RTC::Transport::Listener* listener, + const FBS::DirectTransport::DirectTransportOptions* options) + : RTC::Transport::Transport(shared, id, listener, options->base()) { MS_TRACE(); @@ -20,68 +22,94 @@ namespace RTC this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ this, - /*payloadChannelNotificationHandler*/ this); + /*channelNotificationHandler*/ this); } DirectTransport::~DirectTransport() { MS_TRACE(); + // Tell the Transport parent class that we are about to destroy + // the class instance. + Destroying(); + this->shared->channelMessageRegistrator->UnregisterHandler(this->id); } - void DirectTransport::FillJson(json& jsonObject) const + flatbuffers::Offset DirectTransport::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { - MS_TRACE(); + // Add base transport dump. + auto base = Transport::FillBuffer(builder); - // Call the parent method. - RTC::Transport::FillJson(jsonObject); + return FBS::DirectTransport::CreateDumpResponse(builder, base); } - void DirectTransport::FillJsonStats(json& jsonArray) + flatbuffers::Offset DirectTransport::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) { MS_TRACE(); - // Call the parent method. - RTC::Transport::FillJsonStats(jsonArray); - - auto& jsonObject = jsonArray[0]; + // Base Transport stats. + auto base = Transport::FillBufferStats(builder); - // Add type. - jsonObject["type"] = "direct-transport"; + return FBS::DirectTransport::CreateGetStatsResponse(builder, base); } void DirectTransport::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); - // Pass it to the parent class. - RTC::Transport::HandleRequest(request); + switch (request->method) + { + case Channel::ChannelRequest::Method::TRANSPORT_GET_STATS: + { + auto responseOffset = FillBufferStats(request->GetBufferBuilder()); + + request->Accept(FBS::Response::Body::DirectTransport_GetStatsResponse, responseOffset); + + break; + } + + case Channel::ChannelRequest::Method::TRANSPORT_DUMP: + { + auto dumpOffset = FillBuffer(request->GetBufferBuilder()); + + request->Accept(FBS::Response::Body::DirectTransport_DumpResponse, dumpOffset); + + break; + } + + default: + { + // Pass it to the parent class. + RTC::Transport::HandleRequest(request); + } + } } - void DirectTransport::HandleNotification(PayloadChannel::PayloadChannelNotification* notification) + void DirectTransport::HandleNotification(Channel::ChannelNotification* notification) { MS_TRACE(); - switch (notification->eventId) + switch (notification->event) { - case PayloadChannel::PayloadChannelNotification::EventId::TRANSPORT_SEND_RTCP: + case Channel::ChannelNotification::Event::TRANSPORT_SEND_RTCP: { - const auto* data = notification->payload; - auto len = notification->payloadLen; + const auto* body = notification->data->body_as(); + auto len = body->data()->size(); // Increase receive transmission. RTC::Transport::DataReceived(len); if (len > RTC::MtuSize + 100) { - MS_WARN_TAG(rtp, "given RTCP packet exceeds maximum size [len:%zu]", len); + MS_WARN_TAG(rtp, "given RTCP packet exceeds maximum size [len:%i]", len); return; } - RTC::RTCP::Packet* packet = RTC::RTCP::Packet::Parse(data, len); + RTC::RTCP::Packet* packet = RTC::RTCP::Packet::Parse(body->data()->data(), len); if (!packet) { @@ -118,14 +146,26 @@ namespace RTC { MS_WARN_TAG(rtp, "cannot send RTP packet not associated to a Consumer"); + if (cb) + { + (*cb)(false); + delete cb; + } + return; } - const uint8_t* data = packet->GetData(); - size_t len = packet->GetSize(); + const auto data = this->shared->channelNotifier->GetBufferBuilder().CreateVector( + packet->GetData(), packet->GetSize()); - // Notify the Node DirectTransport. - this->shared->payloadChannelNotifier->Emit(consumer->id, "rtp", data, len); + auto notification = + FBS::Consumer::CreateRtpNotification(this->shared->channelNotifier->GetBufferBuilder(), data); + + this->shared->channelNotifier->Emit( + consumer->id, + FBS::Notification::Event::CONSUMER_RTP, + FBS::Notification::Body::Consumer_RtpNotification, + notification); if (cb) { @@ -134,21 +174,28 @@ namespace RTC } // Increase send transmission. - RTC::Transport::DataSent(len); + RTC::Transport::DataSent(packet->GetSize()); } void DirectTransport::SendRtcpPacket(RTC::RTCP::Packet* packet) { MS_TRACE(); - const uint8_t* data = packet->GetData(); - size_t len = packet->GetSize(); - // Notify the Node DirectTransport. - this->shared->payloadChannelNotifier->Emit(this->id, "rtcp", data, len); + const auto data = this->shared->channelNotifier->GetBufferBuilder().CreateVector( + packet->GetData(), packet->GetSize()); + + auto notification = FBS::DirectTransport::CreateRtcpNotification( + this->shared->channelNotifier->GetBufferBuilder(), data); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::DIRECTTRANSPORT_RTCP, + FBS::Notification::Body::DirectTransport_RtcpNotification, + notification); // Increase send transmission. - RTC::Transport::DataSent(len); + RTC::Transport::DataSent(packet->GetSize()); } void DirectTransport::SendRtcpCompoundPacket(RTC::RTCP::CompoundPacket* packet) @@ -157,27 +204,41 @@ namespace RTC packet->Serialize(RTC::RTCP::Buffer); - const uint8_t* data = packet->GetData(); - size_t len = packet->GetSize(); + const auto data = this->shared->channelNotifier->GetBufferBuilder().CreateVector( + packet->GetData(), packet->GetSize()); - // Notify the Node DirectTransport. - this->shared->payloadChannelNotifier->Emit(this->id, "rtcp", data, len); + auto notification = FBS::DirectTransport::CreateRtcpNotification( + this->shared->channelNotifier->GetBufferBuilder(), data); - // Increase send transmission. - RTC::Transport::DataSent(len); + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::DIRECTTRANSPORT_RTCP, + FBS::Notification::Body::DirectTransport_RtcpNotification, + notification); } void DirectTransport::SendMessage( - RTC::DataConsumer* dataConsumer, uint32_t ppid, const uint8_t* msg, size_t len, onQueuedCallback* cb) + RTC::DataConsumer* dataConsumer, const uint8_t* msg, size_t len, uint32_t ppid, onQueuedCallback* cb) { MS_TRACE(); // Notify the Node DirectTransport. - json data = json::object(); + auto data = this->shared->channelNotifier->GetBufferBuilder().CreateVector(msg, len); - data["ppid"] = ppid; + auto notification = FBS::DataConsumer::CreateMessageNotification( + this->shared->channelNotifier->GetBufferBuilder(), ppid, data); - this->shared->payloadChannelNotifier->Emit(dataConsumer->id, "message", data, msg, len); + this->shared->channelNotifier->Emit( + dataConsumer->id, + FBS::Notification::Event::DATACONSUMER_MESSAGE, + FBS::Notification::Body::DataConsumer_MessageNotification, + notification); + + if (cb) + { + (*cb)(true, false); + delete cb; + } // Increase send transmission. RTC::Transport::DataSent(len); diff --git a/worker/src/RTC/DtlsTransport.cpp b/worker/src/RTC/DtlsTransport.cpp index d639f64767..279f7e7ff8 100644 --- a/worker/src/RTC/DtlsTransport.cpp +++ b/worker/src/RTC/DtlsTransport.cpp @@ -12,21 +12,25 @@ #include // std::snprintf(), std::fopen() #include // std::memcpy(), std::strcmp() -#define LOG_OPENSSL_ERROR(desc) \ - do \ - { \ - if (ERR_peek_error() == 0) \ - MS_ERROR("OpenSSL error [desc:'%s']", desc); \ - else \ - { \ - int64_t err; \ - while ((err = ERR_get_error()) != 0) \ - { \ - MS_ERROR("OpenSSL error [desc:'%s', error:'%s']", desc, ERR_error_string(err, nullptr)); \ - } \ - ERR_clear_error(); \ - } \ +// clang-format off +#define LOG_OPENSSL_ERROR(desc) \ + do \ + { \ + if (ERR_peek_error() == 0) \ + { \ + MS_ERROR("OpenSSL error [desc:'%s']", desc); \ + } \ + else \ + { \ + int64_t err; \ + while ((err = ERR_get_error()) != 0) \ + { \ + MS_ERROR("OpenSSL error [desc:'%s', error:'%s']", desc, ERR_error_string(err, nullptr)); \ + } \ + ERR_clear_error(); \ + } \ } while (false) +// clang-format on /* Static methods for OpenSSL callbacks. */ @@ -43,14 +47,54 @@ inline static void onSslInfo(const SSL* ssl, int where, int ret) static_cast(SSL_get_ex_data(ssl, 0))->OnSslInfo(where, ret); } +/** + * This callback is called by OpenSSL when it wants to send DTLS data to the + * endpoint. Such a data could be a full DTLS message, various DTLS messages, + * a DTLS message fragment, various DTLS message fragments or a combination of + * these. It's guaranteed (by observation) that |len| argument corresponds to + * the entire content of our BIO mem buffer |this->sslBioToNetwork| and it + * never exceeds our |DtlsMtu| limit. + */ +inline static long onSslBioOut( + BIO* bio, + int operationType, + const char* argp, + size_t len, + int /*argi*/, + long /*argl*/, + int ret, + size_t* /*processed*/) +{ + long resultOfcallback = (operationType == BIO_CB_RETURN) ? static_cast(ret) : 1; + + // This callback is called twice for write operations: + // - First one with operationType = BIO_CB_WRITE. + // - Second one with operationType = BIO_CB_RETURN | BIO_CB_WRITE. + // We only care about the former. + if ((operationType == BIO_CB_WRITE) && argp && len > 0) + { + auto* dtlsTransport = reinterpret_cast(BIO_get_callback_arg(bio)); + + dtlsTransport->SendDtlsData(reinterpret_cast(argp), len); + } + + return resultOfcallback; +} + inline static unsigned int onSslDtlsTimer(SSL* /*ssl*/, unsigned int timerUs) { - if (timerUs == 0) - return 100000; - else if (timerUs >= 4000000) - return 4000000; + if (timerUs == 0u) + { + return 100000u; + } + else if (timerUs >= 4000000u) + { + return 4000000u; + } else + { return 2 * timerUs; + } } namespace RTC @@ -61,15 +105,15 @@ namespace RTC static constexpr int DtlsMtu{ 1350 }; static constexpr int SslReadBufferSize{ 65536 }; // AES-HMAC: http://tools.ietf.org/html/rfc3711 - static constexpr size_t SrtpMasterKeyLength{ 16 }; - static constexpr size_t SrtpMasterSaltLength{ 14 }; + static constexpr size_t SrtpMasterKeyLength{ 16u }; + static constexpr size_t SrtpMasterSaltLength{ 14u }; static constexpr size_t SrtpMasterLength{ SrtpMasterKeyLength + SrtpMasterSaltLength }; // AES-GCM: http://tools.ietf.org/html/rfc7714 - static constexpr size_t SrtpAesGcm256MasterKeyLength{ 32 }; - static constexpr size_t SrtpAesGcm256MasterSaltLength{ 12 }; + static constexpr size_t SrtpAesGcm256MasterKeyLength{ 32u }; + static constexpr size_t SrtpAesGcm256MasterSaltLength{ 12u }; static constexpr size_t SrtpAesGcm256MasterLength{ SrtpAesGcm256MasterKeyLength + SrtpAesGcm256MasterSaltLength }; - static constexpr size_t SrtpAesGcm128MasterKeyLength{ 16 }; - static constexpr size_t SrtpAesGcm128MasterSaltLength{ 12 }; + static constexpr size_t SrtpAesGcm128MasterKeyLength{ 16u }; + static constexpr size_t SrtpAesGcm128MasterSaltLength{ 12u }; static constexpr size_t SrtpAesGcm128MasterLength{ SrtpAesGcm128MasterKeyLength + SrtpAesGcm128MasterSaltLength }; // clang-format on @@ -142,11 +186,156 @@ namespace RTC MS_TRACE(); if (DtlsTransport::privateKey) + { EVP_PKEY_free(DtlsTransport::privateKey); + } + if (DtlsTransport::certificate) + { X509_free(DtlsTransport::certificate); + } + if (DtlsTransport::sslCtx) + { SSL_CTX_free(DtlsTransport::sslCtx); + } + } + + DtlsTransport::Role DtlsTransport::RoleFromFbs(FBS::WebRtcTransport::DtlsRole role) + { + switch (role) + { + case FBS::WebRtcTransport::DtlsRole::AUTO: + { + return DtlsTransport::Role::AUTO; + } + + case FBS::WebRtcTransport::DtlsRole::CLIENT: + { + return DtlsTransport::Role::CLIENT; + } + + case FBS::WebRtcTransport::DtlsRole::SERVER: + { + return DtlsTransport::Role::SERVER; + } + } + } + + FBS::WebRtcTransport::DtlsRole DtlsTransport::RoleToFbs(DtlsTransport::Role role) + { + switch (role) + { + case DtlsTransport::Role::AUTO: + { + return FBS::WebRtcTransport::DtlsRole::AUTO; + } + + case DtlsTransport::Role::CLIENT: + { + return FBS::WebRtcTransport::DtlsRole::CLIENT; + } + + case DtlsTransport::Role::SERVER: + { + return FBS::WebRtcTransport::DtlsRole::SERVER; + } + } + } + + FBS::WebRtcTransport::DtlsState DtlsTransport::StateToFbs(DtlsTransport::DtlsState state) + { + switch (state) + { + case DtlsTransport::DtlsState::NEW: + { + return FBS::WebRtcTransport::DtlsState::NEW; + } + + case DtlsTransport::DtlsState::CONNECTING: + { + return FBS::WebRtcTransport::DtlsState::CONNECTING; + } + + case DtlsTransport::DtlsState::CONNECTED: + { + return FBS::WebRtcTransport::DtlsState::CONNECTED; + } + + case DtlsTransport::DtlsState::FAILED: + { + return FBS::WebRtcTransport::DtlsState::FAILED; + } + + case DtlsTransport::DtlsState::CLOSED: + { + return FBS::WebRtcTransport::DtlsState::CLOSED; + } + } + } + + DtlsTransport::FingerprintAlgorithm DtlsTransport::AlgorithmFromFbs( + FBS::WebRtcTransport::FingerprintAlgorithm algorithm) + { + switch (algorithm) + { + case FBS::WebRtcTransport::FingerprintAlgorithm::SHA1: + { + return DtlsTransport::FingerprintAlgorithm::SHA1; + } + + case FBS::WebRtcTransport::FingerprintAlgorithm::SHA224: + { + return DtlsTransport::FingerprintAlgorithm::SHA224; + } + + case FBS::WebRtcTransport::FingerprintAlgorithm::SHA256: + { + return DtlsTransport::FingerprintAlgorithm::SHA256; + } + + case FBS::WebRtcTransport::FingerprintAlgorithm::SHA384: + { + return DtlsTransport::FingerprintAlgorithm::SHA384; + } + + case FBS::WebRtcTransport::FingerprintAlgorithm::SHA512: + { + return DtlsTransport::FingerprintAlgorithm::SHA512; + } + } + } + + FBS::WebRtcTransport::FingerprintAlgorithm DtlsTransport::AlgorithmToFbs( + DtlsTransport::FingerprintAlgorithm algorithm) + { + switch (algorithm) + { + case DtlsTransport::FingerprintAlgorithm::SHA1: + { + return FBS::WebRtcTransport::FingerprintAlgorithm::SHA1; + } + + case DtlsTransport::FingerprintAlgorithm::SHA224: + { + return FBS::WebRtcTransport::FingerprintAlgorithm::SHA224; + } + + case DtlsTransport::FingerprintAlgorithm::SHA256: + { + return FBS::WebRtcTransport::FingerprintAlgorithm::SHA256; + } + + case DtlsTransport::FingerprintAlgorithm::SHA384: + { + return FBS::WebRtcTransport::FingerprintAlgorithm::SHA384; + } + + case DtlsTransport::FingerprintAlgorithm::SHA512: + { + return FBS::WebRtcTransport::FingerprintAlgorithm::SHA512; + } + } } void DtlsTransport::GenerateCertificateAndPrivateKey() @@ -155,7 +344,7 @@ namespace RTC int ret{ 0 }; X509_NAME* certName{ nullptr }; - std::string subject = + const std::string subject = std::string("mediasoup") + std::to_string(Utils::Crypto::GetRandomUInt(100000, 999999)); // Create key with curve. @@ -226,7 +415,7 @@ namespace RTC } // Sign the certificate with its own private key. - ret = X509_sign(DtlsTransport::certificate, DtlsTransport::privateKey, EVP_sha1()); + ret = X509_sign(DtlsTransport::certificate, DtlsTransport::privateKey, EVP_sha256()); if (ret == 0) { @@ -240,10 +429,14 @@ namespace RTC error: if (DtlsTransport::privateKey) + { EVP_PKEY_free(DtlsTransport::privateKey); + } if (DtlsTransport::certificate) + { X509_free(DtlsTransport::certificate); + } MS_THROW_ERROR("DTLS certificate and private key generation failed"); } @@ -356,11 +549,6 @@ namespace RTC // Don't use sessions cache. SSL_CTX_set_session_cache_mode(DtlsTransport::sslCtx, SSL_SESS_CACHE_OFF); - // Read always as much into the buffer as possible. - // NOTE: This is the default for DTLS, but a bug in non latest OpenSSL - // versions makes this call required. - SSL_CTX_set_read_ahead(DtlsTransport::sslCtx, 1); - SSL_CTX_set_verify_depth(DtlsTransport::sslCtx, 4); // Require certificate from peer. @@ -395,7 +583,9 @@ namespace RTC ++it) { if (it != DtlsTransport::srtpCryptoSuites.begin()) + { dtlsSrtpCryptoSuites += ":"; + } SrtpCryptoSuiteMapEntry* cryptoSuiteEntry = std::addressof(*it); dtlsSrtpCryptoSuites += cryptoSuiteEntry->name; @@ -434,8 +624,8 @@ namespace RTC for (auto& kv : DtlsTransport::string2FingerprintAlgorithm) { - const std::string& algorithmString = kv.first; - FingerprintAlgorithm algorithm = kv.second; + const std::string& algorithmString = kv.first; + const FingerprintAlgorithm algorithm = kv.second; uint8_t binaryFingerprint[EVP_MAX_MD_SIZE]; unsigned int size{ 0 }; char hexFingerprint[(EVP_MAX_MD_SIZE * 3) + 1]; @@ -445,27 +635,39 @@ namespace RTC switch (algorithm) { case FingerprintAlgorithm::SHA1: + { hashFunction = EVP_sha1(); break; + } case FingerprintAlgorithm::SHA224: + { hashFunction = EVP_sha224(); break; + } case FingerprintAlgorithm::SHA256: + { hashFunction = EVP_sha256(); break; + } case FingerprintAlgorithm::SHA384: + { hashFunction = EVP_sha384(); break; + } case FingerprintAlgorithm::SHA512: + { hashFunction = EVP_sha512(); break; + } default: + { MS_THROW_ERROR("unknown algorithm"); + } } ret = X509_digest(DtlsTransport::certificate, hashFunction, binaryFingerprint, &size); @@ -488,7 +690,7 @@ namespace RTC // Store it in the vector. DtlsTransport::Fingerprint fingerprint; - fingerprint.algorithm = DtlsTransport::GetFingerprintAlgorithm(algorithmString); + fingerprint.algorithm = algorithm; fingerprint.value = hexFingerprint; DtlsTransport::localFingerprints.push_back(fingerprint); @@ -497,14 +699,11 @@ namespace RTC /* Instance methods. */ - DtlsTransport::DtlsTransport(Listener* listener) : listener(listener) + DtlsTransport::DtlsTransport(Listener* listener) + : listener(listener), ssl(SSL_new(DtlsTransport::sslCtx)) { MS_TRACE(); - /* Set SSL. */ - - this->ssl = SSL_new(DtlsTransport::sslCtx); - if (!this->ssl) { LOG_OPENSSL_ERROR("SSL_new() failed"); @@ -538,17 +737,24 @@ namespace RTC goto error; } - SSL_set_bio(this->ssl, this->sslBioFromNetwork, this->sslBioToNetwork); - - // Set the MTU so that we don't send packets that are too large with no fragmentation. + // Set the MTU so that we don't send packets that are too large with no + // fragmentation. SSL_set_mtu(this->ssl, DtlsMtu); DTLS_set_link_mtu(this->ssl, DtlsMtu); + // We want to monitor OpenSSL write operations into our |sslBioToNetwork| + // buffer so we can immediately send those DTLS bytes (containing full DTLS + // messages, or valid DTLS fragment messages, or combination of them) to + // the endpoint, and hence we honor the configured DTLS MTU. + BIO_set_callback_ex(this->sslBioToNetwork, onSslBioOut); + BIO_set_callback_arg(this->sslBioToNetwork, reinterpret_cast(this)); + SSL_set_bio(this->ssl, this->sslBioFromNetwork, this->sslBioToNetwork); + // Set callback handler for setting DTLS timer interval. DTLS_set_timer_cb(this->ssl, onSslDtlsTimer); // Set the DTLS timer. - this->timer = new Timer(this); + this->timer = new TimerHandle(this); return; @@ -557,13 +763,19 @@ namespace RTC // NOTE: At this point SSL_set_bio() was not called so we must free BIOs as // well. if (this->sslBioFromNetwork) + { BIO_free(this->sslBioFromNetwork); + } if (this->sslBioToNetwork) + { BIO_free(this->sslBioToNetwork); + } if (this->ssl) + { SSL_free(this->ssl); + } // NOTE: If this is not catched by the caller the program will abort, but // this should never happen. @@ -578,7 +790,6 @@ namespace RTC { // Send close alert to the peer. SSL_shutdown(this->ssl); - SendPendingOutgoingDtlsData(); } if (this->ssl) @@ -604,38 +815,60 @@ namespace RTC switch (this->state) { case DtlsState::CONNECTING: + { state = "connecting"; break; + } + case DtlsState::CONNECTED: + { state = "connected"; break; + } + case DtlsState::FAILED: + { state = "failed"; break; + } + case DtlsState::CLOSED: + { state = "closed"; break; + } + default:; } - switch (this->localRole) + if (this->localRole.has_value()) { - case Role::AUTO: - role = "auto"; - break; - case Role::SERVER: - role = "server"; - break; - case Role::CLIENT: - role = "client"; - break; - default:; + switch (this->localRole.value()) + { + case Role::AUTO: + { + role = "auto"; + break; + } + + case Role::SERVER: + { + role = "server"; + break; + } + + case Role::CLIENT: + { + role = "client"; + break; + } + } } MS_DUMP(""); - MS_DUMP(" state : %s", state.c_str()); - MS_DUMP(" role : %s", role.c_str()); - MS_DUMP(" handshake done: : %s", this->handshakeDone ? "yes" : "no"); + MS_DUMP(" state: %s", state.c_str()); + MS_DUMP(" role: %s", role.c_str()); + MS_DUMP(" handshake done: %s", this->handshakeDone ? "yes" : "no"); MS_DUMP(""); } @@ -647,9 +880,7 @@ namespace RTC localRole == Role::CLIENT || localRole == Role::SERVER, "local DTLS role must be 'client' or 'server'"); - const Role previousLocalRole = this->localRole; - - if (localRole == previousLocalRole) + if (this->localRole.has_value() && localRole == this->localRole.value()) { MS_ERROR("same local DTLS role provided, doing nothing"); @@ -657,7 +888,9 @@ namespace RTC } // If the previous local DTLS role was 'client' or 'server' do reset. - if (previousLocalRole == Role::CLIENT || previousLocalRole == Role::SERVER) + if ( + this->localRole.has_value() && + (this->localRole.value() == Role::CLIENT || this->localRole.value() == Role::SERVER)) { MS_DEBUG_TAG(dtls, "resetting DTLS due to local role change"); @@ -671,7 +904,7 @@ namespace RTC this->state = DtlsState::CONNECTING; this->listener->OnDtlsTransportConnecting(this); - switch (this->localRole) + switch (this->localRole.value()) { case Role::CLIENT: { @@ -679,7 +912,6 @@ namespace RTC SSL_set_connect_state(this->ssl); SSL_do_handshake(this->ssl); - SendPendingOutgoingDtlsData(); SetTimeout(); break; @@ -706,9 +938,6 @@ namespace RTC { MS_TRACE(); - MS_ASSERT( - fingerprint.algorithm != FingerprintAlgorithm::NONE, "no fingerprint algorithm provided"); - this->remoteFingerprint = fingerprint; // The remote fingerpring may have been set after DTLS handshake was done, @@ -753,21 +982,23 @@ namespace RTC // Must call SSL_read() to process received DTLS data. read = SSL_read(this->ssl, static_cast(DtlsTransport::sslReadBuffer), SslReadBufferSize); - // Send data if it's ready. - SendPendingOutgoingDtlsData(); - // Check SSL status and return if it is bad/closed. if (!CheckStatus(read)) + { return; + } // Set/update the DTLS timeout. if (!SetTimeout()) + { return; + } // Application data received. Notify to the listener. if (read > 0) { - // It is allowed to receive DTLS data even before validating remote fingerprint. + // It is allowed to receive DTLS data even before validating remote + // fingerprint. if (!this->handshakeDone) { MS_WARN_TAG(dtls, "ignoring application data received while DTLS handshake not done"); @@ -785,7 +1016,8 @@ namespace RTC { MS_TRACE(); - // We cannot send data to the peer if its remote fingerprint is not validated. + // We cannot send data to the peer if its remote fingerprint is not + // validated. if (this->state != DtlsState::CONNECTED) { MS_WARN_TAG(dtls, "cannot send application data while DTLS is not fully connected"); @@ -809,16 +1041,40 @@ namespace RTC LOG_OPENSSL_ERROR("SSL_write() failed"); if (!CheckStatus(written)) + { return; + } } else if (written != static_cast(len)) { MS_WARN_TAG( dtls, "OpenSSL SSL_write() wrote less (%d bytes) than given data (%zu bytes)", written, len); } + } + + /** + * This method is called within our |onSslBioOut| callback above. As told + * there, it's guaranteed that OpenSSL invokes that callback with all the + * bytes currently written in our BIO mem buffer |this->sslBioToNetwork| so + * we can safely reset/clear that buffer once we have sent the data to the + * endpoint. + */ + void DtlsTransport::SendDtlsData(const uint8_t* data, size_t len) + { + MS_TRACE(); + + MS_DEBUG_DEV("%zu bytes of DTLS data ready to be sent", len); + + // Notify the listener. + this->listener->OnDtlsTransportSendData(this, data, len); + + // Clear the BIO buffer. + auto ret = BIO_reset(this->sslBioToNetwork); - // Send data. - SendPendingOutgoingDtlsData(); + if (ret != 1) + { + MS_ERROR("BIO_reset() failed [ret:%d]", ret); + } } void DtlsTransport::Reset() @@ -828,19 +1084,22 @@ namespace RTC int ret; if (!IsRunning()) + { return; + } MS_WARN_TAG(dtls, "resetting DTLS transport"); // Stop the DTLS timer. this->timer->Stop(); - // We need to reset the SSL instance so we need to "shutdown" it, but we - // don't want to send a Close Alert to the peer, so just don't call - // SendPendingOutgoingDTLSData(). + // NOTE: We need to reset the SSL instance so we need to "shutdown" it, but + // we don't want to send a DTLS Close Alert to the peer. However this is + // gonna happen since SSL_shutdown() will trigger a DTLS Close Alert and + // we'll have our onSslBioOut() callback called to deliver it. SSL_shutdown(this->ssl); - this->localRole = Role::NONE; + this->localRole.reset(); this->state = DtlsState::NEW; this->handshakeDone = false; this->handshakeDoneNow = false; @@ -852,55 +1111,76 @@ namespace RTC ret = SSL_clear(this->ssl); if (ret == 0) + { ERR_clear_error(); + } } - inline bool DtlsTransport::CheckStatus(int returnCode) + bool DtlsTransport::CheckStatus(int returnCode) { MS_TRACE(); - int err; const bool wasHandshakeDone = this->handshakeDone; - err = SSL_get_error(this->ssl, returnCode); + int err = SSL_get_error(this->ssl, returnCode); switch (err) { case SSL_ERROR_NONE: + { break; + } case SSL_ERROR_SSL: + { LOG_OPENSSL_ERROR("SSL status: SSL_ERROR_SSL"); break; + } case SSL_ERROR_WANT_READ: + { break; + } case SSL_ERROR_WANT_WRITE: + { MS_WARN_TAG(dtls, "SSL status: SSL_ERROR_WANT_WRITE"); break; + } case SSL_ERROR_WANT_X509_LOOKUP: + { MS_DEBUG_TAG(dtls, "SSL status: SSL_ERROR_WANT_X509_LOOKUP"); break; + } case SSL_ERROR_SYSCALL: + { LOG_OPENSSL_ERROR("SSL status: SSL_ERROR_SYSCALL"); break; + } case SSL_ERROR_ZERO_RETURN: + { break; + } case SSL_ERROR_WANT_CONNECT: + { MS_WARN_TAG(dtls, "SSL status: SSL_ERROR_WANT_CONNECT"); break; + } case SSL_ERROR_WANT_ACCEPT: + { MS_WARN_TAG(dtls, "SSL status: SSL_ERROR_WANT_ACCEPT"); break; + } default: + { MS_WARN_TAG(dtls, "SSL status: unknown error"); + } } // Check if the handshake (or re-handshake) has been done right now. @@ -913,8 +1193,10 @@ namespace RTC this->timer->Stop(); // Process the handshake just once (ignore if DTLS renegotiation). - if (!wasHandshakeDone && this->remoteFingerprint.algorithm != FingerprintAlgorithm::NONE) + if (!wasHandshakeDone && this->remoteFingerprint.has_value()) + { return ProcessHandshake(); + } return true; } @@ -950,33 +1232,7 @@ namespace RTC } } - inline void DtlsTransport::SendPendingOutgoingDtlsData() - { - MS_TRACE(); - - if (BIO_eof(this->sslBioToNetwork)) - return; - - int64_t read; - char* data{ nullptr }; - - read = BIO_get_mem_data(this->sslBioToNetwork, &data); // NOLINT - - if (read <= 0) - return; - - MS_DEBUG_DEV("%" PRIu64 " bytes of DTLS data ready to sent to the peer", read); - - // Notify the listener. - this->listener->OnDtlsTransportSendData( - this, reinterpret_cast(data), static_cast(read)); - - // Clear the BIO buffer. - // NOTE: the (void) avoids the -Wunused-value warning. - (void)BIO_reset(this->sslBioToNetwork); - } - - inline bool DtlsTransport::SetTimeout() + bool DtlsTransport::SetTimeout() { MS_TRACE(); @@ -1011,7 +1267,8 @@ namespace RTC return true; } - // NOTE: Don't start the timer again if the timeout is greater than 30 seconds. + // NOTE: Don't start the timer again if the timeout is greater than 30 + // seconds. else { MS_WARN_TAG(dtls, "DTLS timeout too high (%" PRIu64 "ms), resetting DLTS", timeoutMs); @@ -1026,13 +1283,11 @@ namespace RTC } } - inline bool DtlsTransport::ProcessHandshake() + bool DtlsTransport::ProcessHandshake() { MS_TRACE(); MS_ASSERT(this->handshakeDone, "handshake not done yet"); - MS_ASSERT( - this->remoteFingerprint.algorithm != FingerprintAlgorithm::NONE, "remote fingerprint not set"); // Validate the remote fingerprint. if (!CheckRemoteFingerprint()) @@ -1047,12 +1302,12 @@ namespace RTC } // Get the negotiated SRTP crypto suite. - RTC::SrtpSession::CryptoSuite srtpCryptoSuite = GetNegotiatedSrtpCryptoSuite(); + auto srtpCryptoSuite = GetNegotiatedSrtpCryptoSuite(); - if (srtpCryptoSuite != RTC::SrtpSession::CryptoSuite::NONE) + if (srtpCryptoSuite) { // Extract the SRTP keys (will notify the listener with them). - ExtractSrtpKeys(srtpCryptoSuite); + ExtractSrtpKeys(srtpCryptoSuite.value()); return true; } @@ -1070,12 +1325,11 @@ namespace RTC return false; } - inline bool DtlsTransport::CheckRemoteFingerprint() + bool DtlsTransport::CheckRemoteFingerprint() { MS_TRACE(); - MS_ASSERT( - this->remoteFingerprint.algorithm != FingerprintAlgorithm::NONE, "remote fingerprint not set"); + MS_ASSERT(this->remoteFingerprint.has_value(), "remote fingerprint not set"); X509* certificate; uint8_t binaryFingerprint[EVP_MAX_MD_SIZE]; @@ -1093,30 +1347,37 @@ namespace RTC return false; } - switch (this->remoteFingerprint.algorithm) + switch (this->remoteFingerprint->algorithm) { case FingerprintAlgorithm::SHA1: + { hashFunction = EVP_sha1(); break; + } case FingerprintAlgorithm::SHA224: + { hashFunction = EVP_sha224(); break; + } case FingerprintAlgorithm::SHA256: + { hashFunction = EVP_sha256(); break; + } case FingerprintAlgorithm::SHA384: + { hashFunction = EVP_sha384(); break; + } case FingerprintAlgorithm::SHA512: + { hashFunction = EVP_sha512(); break; - - default: - MS_ABORT("unknown algorithm"); + } } // Compare the remote fingerprint with the value given via signaling. @@ -1138,13 +1399,13 @@ namespace RTC } hexFingerprint[(size * 3) - 1] = '\0'; - if (this->remoteFingerprint.value != hexFingerprint) + if (this->remoteFingerprint->value != hexFingerprint) { MS_WARN_TAG( dtls, "fingerprint in the remote certificate (%s) does not match the announced one (%s)", hexFingerprint, - this->remoteFingerprint.value.c_str()); + this->remoteFingerprint->value.c_str()); X509_free(certificate); @@ -1158,8 +1419,8 @@ namespace RTC BIO* bio = BIO_new(BIO_s_mem()); // Ensure the underlying BUF_MEM structure is also freed. - // NOTE: Avoid stupid "warning: value computed is not used [-Wunused-value]" since - // BIO_set_close() always returns 1. + // NOTE: Avoid stupid "warning: value computed is not used [-Wunused-value]" + // since BIO_set_close() always returns 1. (void)BIO_set_close(bio, BIO_CLOSE); ret = PEM_write_bio_X509(bio, certificate); @@ -1196,7 +1457,7 @@ namespace RTC return true; } - inline void DtlsTransport::ExtractSrtpKeys(RTC::SrtpSession::CryptoSuite srtpCryptoSuite) + void DtlsTransport::ExtractSrtpKeys(RTC::SrtpSession::CryptoSuite srtpCryptoSuite) { MS_TRACE(); @@ -1233,11 +1494,6 @@ namespace RTC break; } - - default: - { - MS_ABORT("unknown SRTP crypto suite"); - } } auto* srtpMaterial = new uint8_t[srtpMasterLength * 2]; @@ -1253,8 +1509,9 @@ namespace RTC this->ssl, srtpMaterial, srtpMasterLength * 2, "EXTRACTOR-dtls_srtp", 19, nullptr, 0, 0); MS_ASSERT(ret != 0, "SSL_export_keying_material() failed"); + MS_ASSERT(this->localRole.has_value(), "no DTLS role set"); - switch (this->localRole) + switch (this->localRole.value()) { case Role::SERVER: { @@ -1305,18 +1562,20 @@ namespace RTC delete[] srtpRemoteMasterKey; } - inline RTC::SrtpSession::CryptoSuite DtlsTransport::GetNegotiatedSrtpCryptoSuite() + std::optional DtlsTransport::GetNegotiatedSrtpCryptoSuite() { MS_TRACE(); - RTC::SrtpSession::CryptoSuite negotiatedSrtpCryptoSuite = RTC::SrtpSession::CryptoSuite::NONE; + std::optional negotiatedSrtpCryptoSuite; // Ensure that the SRTP crypto suite has been negotiated. // NOTE: This is a OpenSSL type. SRTP_PROTECTION_PROFILE* sslSrtpCryptoSuite = SSL_get_selected_srtp_profile(this->ssl); if (!sslSrtpCryptoSuite) + { return negotiatedSrtpCryptoSuite; + } // Get the negotiated SRTP crypto suite. for (auto& srtpCryptoSuite : DtlsTransport::srtpCryptoSuites) @@ -1332,13 +1591,12 @@ namespace RTC } MS_ASSERT( - negotiatedSrtpCryptoSuite != RTC::SrtpSession::CryptoSuite::NONE, - "chosen SRTP crypto suite is not an available one"); + negotiatedSrtpCryptoSuite.has_value(), "chosen SRTP crypto suite is not an available one"); return negotiatedSrtpCryptoSuite; } - inline void DtlsTransport::OnSslInfo(int where, int ret) + void DtlsTransport::OnSslInfo(int where, int ret) { MS_TRACE(); @@ -1346,11 +1604,17 @@ namespace RTC const char* role; if ((w & SSL_ST_CONNECT) != 0) + { role = "client"; + } else if ((w & SSL_ST_ACCEPT) != 0) + { role = "server"; + } else + { role = "undefined"; + } if ((where & SSL_CB_LOOP) != 0) { @@ -1363,15 +1627,21 @@ namespace RTC switch (*SSL_alert_type_string(ret)) { case 'W': + { alertType = "warning"; break; + } case 'F': + { alertType = "fatal"; break; + } default: + { alertType = "undefined"; + } } if ((where & SSL_CB_READ) != 0) @@ -1390,9 +1660,13 @@ namespace RTC else if ((where & SSL_CB_EXIT) != 0) { if (ret == 0) + { MS_DEBUG_TAG(dtls, "[role:%s, failed:'%s']", role, SSL_state_string_long(this->ssl)); + } else if (ret < 0) + { MS_DEBUG_TAG(dtls, "role: %s, waiting:'%s']", role, SSL_state_string_long(this->ssl)); + } } else if ((where & SSL_CB_HANDSHAKE_START) != 0) { @@ -1405,11 +1679,12 @@ namespace RTC this->handshakeDoneNow = true; } - // NOTE: checking SSL_get_shutdown(this->ssl) & SSL_RECEIVED_SHUTDOWN here upon - // receipt of a close alert does not work (the flag is set after this callback). + // NOTE: checking SSL_get_shutdown(this->ssl) & SSL_RECEIVED_SHUTDOWN here + // upon receipt of a close alert does not work (the flag is set after this + // callback). } - inline void DtlsTransport::OnTimer(Timer* /*timer*/) + void DtlsTransport::OnTimer(TimerHandle* /*timer*/) { MS_TRACE(); @@ -1429,9 +1704,6 @@ namespace RTC if (ret == 1) { - // If required, send DTLS data. - SendPendingOutgoingDtlsData(); - // Set the DTLS timer again. SetTimeout(); } diff --git a/worker/src/RTC/IceCandidate.cpp b/worker/src/RTC/IceCandidate.cpp index 4d756c3a64..07e603d79a 100644 --- a/worker/src/RTC/IceCandidate.cpp +++ b/worker/src/RTC/IceCandidate.cpp @@ -6,53 +6,78 @@ namespace RTC { - /* Instance methods. */ + /* Class methods. */ - void IceCandidate::FillJson(json& jsonObject) const + IceCandidate::CandidateType IceCandidate::CandidateTypeFromFbs( + FBS::WebRtcTransport::IceCandidateType type) { - MS_TRACE(); - - // Add foundation. - jsonObject["foundation"] = this->foundation; - - // Add priority. - jsonObject["priority"] = this->priority; - - // Add ip. - jsonObject["ip"] = this->ip; - - // Add protocol. - switch (this->protocol) + switch (type) { - case Protocol::UDP: - jsonObject["protocol"] = "udp"; - break; + case FBS::WebRtcTransport::IceCandidateType::HOST: + return IceCandidate::CandidateType::HOST; + } + } - case Protocol::TCP: - jsonObject["protocol"] = "tcp"; - break; + FBS::WebRtcTransport::IceCandidateType IceCandidate::CandidateTypeToFbs(IceCandidate::CandidateType type) + { + switch (type) + { + case IceCandidate::CandidateType::HOST: + return FBS::WebRtcTransport::IceCandidateType::HOST; } + } - // Add port. - jsonObject["port"] = this->port; + IceCandidate::TcpCandidateType IceCandidate::TcpCandidateTypeFromFbs( + FBS::WebRtcTransport::IceCandidateTcpType type) + { + switch (type) + { + case FBS::WebRtcTransport::IceCandidateTcpType::PASSIVE: + return IceCandidate::TcpCandidateType::PASSIVE; + } + } - // Add type. - switch (this->type) + FBS::WebRtcTransport::IceCandidateTcpType IceCandidate::TcpCandidateTypeToFbs( + IceCandidate::TcpCandidateType type) + { + switch (type) { - case CandidateType::HOST: - jsonObject["type"] = "host"; - break; + case IceCandidate::TcpCandidateType::PASSIVE: + return FBS::WebRtcTransport::IceCandidateTcpType::PASSIVE; } + } + + /* Instance methods. */ + + flatbuffers::Offset IceCandidate::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const + { + MS_TRACE(); + + auto protocol = TransportTuple::ProtocolToFbs(this->protocol); + auto type = CandidateTypeToFbs(this->type); + flatbuffers::Optional tcpType; - // Add tcpType. if (this->protocol == Protocol::TCP) { - switch (this->tcpType) - { - case TcpCandidateType::PASSIVE: - jsonObject["tcpType"] = "passive"; - break; - } + tcpType.emplace(TcpCandidateTypeToFbs(this->tcpType)); } + + return FBS::WebRtcTransport::CreateIceCandidateDirect( + builder, + // foundation. + this->foundation.c_str(), + // priority. + this->priority, + // address. + this->address.c_str(), + // protocol. + protocol, + // port. + this->port, + // type. + type, + // tcpType. + tcpType); } } // namespace RTC diff --git a/worker/src/RTC/IceServer.cpp b/worker/src/RTC/IceServer.cpp index fde3d25a1b..f4754fb0a8 100644 --- a/worker/src/RTC/IceServer.cpp +++ b/worker/src/RTC/IceServer.cpp @@ -1,10 +1,9 @@ #define MS_CLASS "RTC::IceServer" // #define MS_LOG_DEV_LEVEL 3 -#include - -#include "Logger.hpp" #include "RTC/IceServer.hpp" +#include "DepLibUV.hpp" +#include "Logger.hpp" namespace RTC { @@ -13,14 +12,98 @@ namespace RTC static constexpr size_t StunSerializeBufferSize{ 65536 }; thread_local static uint8_t StunSerializeBuffer[StunSerializeBufferSize]; static constexpr size_t MaxTuples{ 8 }; + static constexpr uint8_t ConsentCheckMinTimeoutSec{ 10u }; + static constexpr uint8_t ConsentCheckMaxTimeoutSec{ 60u }; + + /* Class methods. */ + IceServer::IceState IceStateFromFbs(FBS::WebRtcTransport::IceState state) + { + switch (state) + { + case FBS::WebRtcTransport::IceState::NEW: + { + return IceServer::IceState::NEW; + } + + case FBS::WebRtcTransport::IceState::CONNECTED: + { + return IceServer::IceState::CONNECTED; + } + + case FBS::WebRtcTransport::IceState::COMPLETED: + { + return IceServer::IceState::COMPLETED; + } + + case FBS::WebRtcTransport::IceState::DISCONNECTED: + { + return IceServer::IceState::DISCONNECTED; + } + } + } + + FBS::WebRtcTransport::IceState IceServer::IceStateToFbs(IceServer::IceState state) + { + switch (state) + { + case IceServer::IceState::NEW: + { + return FBS::WebRtcTransport::IceState::NEW; + } + + case IceServer::IceState::CONNECTED: + { + return FBS::WebRtcTransport::IceState::CONNECTED; + } + + case IceServer::IceState::COMPLETED: + { + return FBS::WebRtcTransport::IceState::COMPLETED; + } + + case IceServer::IceState::DISCONNECTED: + { + return FBS::WebRtcTransport::IceState::DISCONNECTED; + } + } + } /* Instance methods. */ - IceServer::IceServer(Listener* listener, const std::string& usernameFragment, const std::string& password) + IceServer::IceServer( + Listener* listener, + const std::string& usernameFragment, + const std::string& password, + uint8_t consentTimeoutSec) : listener(listener), usernameFragment(usernameFragment), password(password) { MS_TRACE(); + if (consentTimeoutSec == 0u) + { + // 0 means disabled so it's a valid value. + } + else if (consentTimeoutSec < ConsentCheckMinTimeoutSec) + { + MS_WARN_TAG( + ice, + "consentTimeoutSec cannot be lower than %" PRIu8 " seconds, fixing it", + ConsentCheckMinTimeoutSec); + + consentTimeoutSec = ConsentCheckMinTimeoutSec; + } + else if (consentTimeoutSec > ConsentCheckMaxTimeoutSec) + { + MS_WARN_TAG( + ice, + "consentTimeoutSec cannot be higher than %" PRIu8 " seconds, fixing it", + ConsentCheckMaxTimeoutSec); + + consentTimeoutSec = ConsentCheckMaxTimeoutSec; + } + + this->consentTimeoutMs = consentTimeoutSec * 1000; + // Notify the listener. this->listener->OnIceServerLocalUsernameFragmentAdded(this, usernameFragment); } @@ -39,6 +122,9 @@ namespace RTC this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment); } + // Clear all tuples. + this->isRemovingTuples = true; + for (const auto& it : this->tuples) { auto* storedTuple = const_cast(std::addressof(it)); @@ -47,298 +133,416 @@ namespace RTC this->listener->OnIceServerTupleRemoved(this, storedTuple); } + this->isRemovingTuples = false; + + // Clear all tuples. + // NOTE: Do it after notifying the listener since the listener may need to + // use/read the tuple being removed so we cannot free it yet. this->tuples.clear(); + + // Unset selected tuple. + this->selectedTuple = nullptr; + + // Delete the ICE consent check timer. + delete this->consentCheckTimer; + this->consentCheckTimer = nullptr; } void IceServer::ProcessStunPacket(RTC::StunPacket* packet, RTC::TransportTuple* tuple) { MS_TRACE(); - // Must be a Binding method. - if (packet->GetMethod() != RTC::StunPacket::Method::BINDING) + switch (packet->GetClass()) { - if (packet->GetClass() == RTC::StunPacket::Class::REQUEST) + case RTC::StunPacket::Class::REQUEST: { - MS_WARN_TAG( - ice, - "unknown method %#.3x in STUN Request => 400", - static_cast(packet->GetMethod())); + ProcessStunRequest(packet, tuple); - // Reply 400. - RTC::StunPacket* response = packet->CreateErrorResponse(400); + break; + } - response->Serialize(StunSerializeBuffer); - this->listener->OnIceServerSendStunPacket(this, response, tuple); + case RTC::StunPacket::Class::INDICATION: + { + ProcessStunIndication(packet); - delete response; + break; } - else + + case RTC::StunPacket::Class::SUCCESS_RESPONSE: + case RTC::StunPacket::Class::ERROR_RESPONSE: + { + ProcessStunResponse(packet); + + break; + } + + default: { MS_WARN_TAG( - ice, - "ignoring STUN Indication or Response with unknown method %#.3x", - static_cast(packet->GetMethod())); + ice, "unknown STUN class %" PRIu16 ", discarded", static_cast(packet->GetClass())); } + } + } - return; + void IceServer::RestartIce(const std::string& usernameFragment, const std::string& password) + { + MS_TRACE(); + + if (!this->oldUsernameFragment.empty()) + { + this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment); } - // Must use FINGERPRINT (optional for ICE STUN indications). - if (!packet->HasFingerprint() && packet->GetClass() != RTC::StunPacket::Class::INDICATION) + this->oldUsernameFragment = this->usernameFragment; + this->usernameFragment = usernameFragment; + + this->oldPassword = this->password; + this->password = password; + + this->remoteNomination = 0u; + + // Notify the listener. + this->listener->OnIceServerLocalUsernameFragmentAdded(this, usernameFragment); + + // NOTE: Do not call listener->OnIceServerLocalUsernameFragmentRemoved() + // yet with old usernameFragment. Wait until we receive a STUN packet + // with the new one. + + // Restart ICE consent check (if running) to give some time to the + // client to establish ICE again. + if (IsConsentCheckSupported() && IsConsentCheckRunning()) { - if (packet->GetClass() == RTC::StunPacket::Class::REQUEST) - { - MS_WARN_TAG(ice, "STUN Binding Request without FINGERPRINT => 400"); + RestartConsentCheck(); + } + } - // Reply 400. - RTC::StunPacket* response = packet->CreateErrorResponse(400); + bool IceServer::IsValidTuple(const RTC::TransportTuple* tuple) const + { + MS_TRACE(); - response->Serialize(StunSerializeBuffer); - this->listener->OnIceServerSendStunPacket(this, response, tuple); + return HasTuple(tuple) != nullptr; + } - delete response; - } - else - { - MS_WARN_TAG(ice, "ignoring STUN Binding Response without FINGERPRINT"); - } + void IceServer::RemoveTuple(RTC::TransportTuple* tuple) + { + MS_TRACE(); + // If IceServer is removing a tuple or all tuples (for instance in the + // destructor), the OnIceServerTupleRemoved() callback may end triggering + // new calls to RemoveTuple(). We must ignore it to avoid double-free issues. + if (this->isRemovingTuples) + { return; } - switch (packet->GetClass()) + RTC::TransportTuple* removedTuple{ nullptr }; + + // Find the removed tuple. + auto it = this->tuples.begin(); + + for (; it != this->tuples.end(); ++it) { - case RTC::StunPacket::Class::REQUEST: + RTC::TransportTuple* storedTuple = std::addressof(*it); + + if (storedTuple->Compare(tuple)) { - // USERNAME, MESSAGE-INTEGRITY and PRIORITY are required. - if (!packet->HasMessageIntegrity() || (packet->GetPriority() == 0u) || packet->GetUsername().empty()) - { - MS_WARN_TAG(ice, "mising required attributes in STUN Binding Request => 400"); + removedTuple = storedTuple; + + break; + } + } - // Reply 400. - RTC::StunPacket* response = packet->CreateErrorResponse(400); + // If not found, ignore. + if (!removedTuple) + { + return; + } - response->Serialize(StunSerializeBuffer); - this->listener->OnIceServerSendStunPacket(this, response, tuple); + // Notify the listener. + this->isRemovingTuples = true; + this->listener->OnIceServerTupleRemoved(this, removedTuple); + this->isRemovingTuples = false; - delete response; + // Remove it from the list of tuples. + // NOTE: Do it after notifying the listener since the listener may need to + // use/read the tuple being removed so we cannot free it yet. + this->tuples.erase(it); - return; + // If this is the selected tuple, do things. + if (removedTuple == this->selectedTuple) + { + this->selectedTuple = nullptr; + + // Mark the first tuple as selected tuple (if any) but only if state was + // 'connected' or 'completed'. + if ( + (this->state == IceState::CONNECTED || this->state == IceState::COMPLETED) && + this->tuples.begin() != this->tuples.end()) + { + SetSelectedTuple(std::addressof(*this->tuples.begin())); + + // Restart ICE consent check to let the client send new consent requests + // on the new selected tuple. + if (IsConsentCheckSupported()) + { + RestartConsentCheck(); } + } + // Or just emit 'disconnected'. + else + { + // Update state. + this->state = IceState::DISCONNECTED; - // Check authentication. - switch (packet->CheckAuthentication(this->usernameFragment, this->password)) + // Reset remote nomination. + this->remoteNomination = 0u; + + // Notify the listener. + this->listener->OnIceServerDisconnected(this); + + if (IsConsentCheckSupported() && IsConsentCheckRunning()) { - case RTC::StunPacket::Authentication::OK: - { - if (!this->oldUsernameFragment.empty() && !this->oldPassword.empty()) - { - MS_DEBUG_TAG(ice, "new ICE credentials applied"); + StopConsentCheck(); + } + } + } + } - // Notify the listener. - this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment); + void IceServer::ProcessStunRequest(RTC::StunPacket* request, RTC::TransportTuple* tuple) + { + MS_TRACE(); - this->oldUsernameFragment.clear(); - this->oldPassword.clear(); - } + MS_DEBUG_DEV("processing STUN request"); - break; - } + // Must be a Binding method. + if (request->GetMethod() != RTC::StunPacket::Method::BINDING) + { + MS_WARN_TAG( + ice, + "STUN request with unknown method %#.3x => 400", + static_cast(request->GetMethod())); - case RTC::StunPacket::Authentication::UNAUTHORIZED: - { - // We may have changed our usernameFragment and password, so check - // the old ones. - // clang-format off - if ( - !this->oldUsernameFragment.empty() && - !this->oldPassword.empty() && - packet->CheckAuthentication(this->oldUsernameFragment, this->oldPassword) == RTC::StunPacket::Authentication::OK - ) - // clang-format on - { - MS_DEBUG_TAG(ice, "using old ICE credentials"); + // Reply 400. + RTC::StunPacket* response = request->CreateErrorResponse(400); - break; - } + response->Serialize(StunSerializeBuffer); + this->listener->OnIceServerSendStunPacket(this, response, tuple); - MS_WARN_TAG(ice, "wrong authentication in STUN Binding Request => 401"); + delete response; - // Reply 401. - RTC::StunPacket* response = packet->CreateErrorResponse(401); + return; + } - response->Serialize(StunSerializeBuffer); - this->listener->OnIceServerSendStunPacket(this, response, tuple); + // Must have FINGERPRINT attribute. + if (!request->HasFingerprint()) + { + MS_WARN_TAG(ice, "STUN Binding request without FINGERPRINT attribute => 400"); - delete response; + // Reply 400. + RTC::StunPacket* response = request->CreateErrorResponse(400); - return; - } + response->Serialize(StunSerializeBuffer); + this->listener->OnIceServerSendStunPacket(this, response, tuple); - case RTC::StunPacket::Authentication::BAD_REQUEST: - { - MS_WARN_TAG(ice, "cannot check authentication in STUN Binding Request => 400"); + delete response; - // Reply 400. - RTC::StunPacket* response = packet->CreateErrorResponse(400); + return; + } - response->Serialize(StunSerializeBuffer); - this->listener->OnIceServerSendStunPacket(this, response, tuple); + // PRIORITY attribute is required. + if (request->GetPriority() == 0u) + { + MS_WARN_TAG(ice, "STUN Binding request without PRIORITY attribute => 400"); - delete response; + // Reply 400. + RTC::StunPacket* response = request->CreateErrorResponse(400); - return; - } - } + response->Serialize(StunSerializeBuffer); + this->listener->OnIceServerSendStunPacket(this, response, tuple); - // The remote peer must be ICE controlling. - if (packet->GetIceControlled()) - { - MS_WARN_TAG(ice, "peer indicates ICE-CONTROLLED in STUN Binding Request => 487"); + delete response; - // Reply 487 (Role Conflict). - RTC::StunPacket* response = packet->CreateErrorResponse(487); + return; + } - response->Serialize(StunSerializeBuffer); - this->listener->OnIceServerSendStunPacket(this, response, tuple); + // Check authentication. + switch (request->CheckAuthentication(this->usernameFragment, this->password)) + { + case RTC::StunPacket::Authentication::OK: + { + if (!this->oldUsernameFragment.empty() && !this->oldPassword.empty()) + { + MS_DEBUG_TAG(ice, "new ICE credentials applied"); - delete response; + // Notify the listener. + this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment); - return; + this->oldUsernameFragment.clear(); + this->oldPassword.clear(); } - MS_DEBUG_DEV( - "processing STUN Binding Request [Priority:%" PRIu32 ", UseCandidate:%s]", - static_cast(packet->GetPriority()), - packet->HasUseCandidate() ? "true" : "false"); + break; + } + + case RTC::StunPacket::Authentication::UNAUTHORIZED: + { + // We may have changed our usernameFragment and password, so check the + // old ones. + // clang-format off + if ( + !this->oldUsernameFragment.empty() && + !this->oldPassword.empty() && + request->CheckAuthentication( + this->oldUsernameFragment, this->oldPassword + ) == RTC::StunPacket::Authentication::OK + ) + // clang-format on + { + MS_DEBUG_TAG(ice, "using old ICE credentials"); - // Create a success response. - RTC::StunPacket* response = packet->CreateSuccessResponse(); + break; + } - // Add XOR-MAPPED-ADDRESS. - response->SetXorMappedAddress(tuple->GetRemoteAddress()); + MS_WARN_TAG(ice, "wrong authentication in STUN Binding request => 401"); - // Authenticate the response. - if (this->oldPassword.empty()) - response->Authenticate(this->password); - else - response->Authenticate(this->oldPassword); + // Reply 401. + RTC::StunPacket* response = request->CreateErrorResponse(401); - // Send back. response->Serialize(StunSerializeBuffer); this->listener->OnIceServerSendStunPacket(this, response, tuple); delete response; - uint32_t nomination{ 0u }; - - if (packet->HasNomination()) - nomination = packet->GetNomination(); - - // Handle the tuple. - HandleTuple(tuple, packet->HasUseCandidate(), packet->HasNomination(), nomination); - - break; + return; } - case RTC::StunPacket::Class::INDICATION: + case RTC::StunPacket::Authentication::BAD_MESSAGE: { - MS_DEBUG_TAG(ice, "STUN Binding Indication processed"); + MS_WARN_TAG(ice, "cannot check authentication in STUN Binding request => 400"); - break; - } - - case RTC::StunPacket::Class::SUCCESS_RESPONSE: - { - MS_DEBUG_TAG(ice, "STUN Binding Success Response processed"); + // Reply 400. + RTC::StunPacket* response = request->CreateErrorResponse(400); - break; - } + response->Serialize(StunSerializeBuffer); + this->listener->OnIceServerSendStunPacket(this, response, tuple); - case RTC::StunPacket::Class::ERROR_RESPONSE: - { - MS_DEBUG_TAG(ice, "STUN Binding Error Response processed"); + delete response; - break; + return; } } - } - bool IceServer::IsValidTuple(const RTC::TransportTuple* tuple) const - { - MS_TRACE(); + // The remote peer must be ICE controlling. + if (request->GetIceControlled()) + { + MS_WARN_TAG(ice, "peer indicates ICE-CONTROLLED in STUN Binding request => 487"); - return HasTuple(tuple) != nullptr; - } + // Reply 487 (Role Conflict). + RTC::StunPacket* response = request->CreateErrorResponse(487); - void IceServer::RemoveTuple(RTC::TransportTuple* tuple) - { - MS_TRACE(); + response->Serialize(StunSerializeBuffer); + this->listener->OnIceServerSendStunPacket(this, response, tuple); - RTC::TransportTuple* removedTuple{ nullptr }; + delete response; - // Find the removed tuple. - auto it = this->tuples.begin(); + return; + } - for (; it != this->tuples.end(); ++it) - { - RTC::TransportTuple* storedTuple = std::addressof(*it); + MS_DEBUG_DEV( + "valid STUN Binding request [priority:%" PRIu32 ", useCandidate:%s]", + static_cast(request->GetPriority()), + request->HasUseCandidate() ? "true" : "false"); - if (storedTuple->Compare(tuple)) - { - removedTuple = storedTuple; + // Create a success response. + RTC::StunPacket* response = request->CreateSuccessResponse(); - break; - } + // Add XOR-MAPPED-ADDRESS. + response->SetXorMappedAddress(tuple->GetRemoteAddress()); + + // Authenticate the response. + if (this->oldPassword.empty()) + { + response->SetPassword(this->password); + } + else + { + response->SetPassword(this->oldPassword); } - // If not found, ignore. - if (!removedTuple) - return; + // Send back. + response->Serialize(StunSerializeBuffer); + this->listener->OnIceServerSendStunPacket(this, response, tuple); - // Notify the listener. - this->listener->OnIceServerTupleRemoved(this, removedTuple); + delete response; - // Remove it from the list of tuples. - // NOTE: Do it after notifying the listener since the listener may need to - // use/read the tuple being removed so we cannot free it yet. - this->tuples.erase(it); + uint32_t nomination{ 0u }; - // If this is the selected tuple, do things. - if (removedTuple == this->selectedTuple) + if (request->HasNomination()) { - this->selectedTuple = nullptr; + nomination = request->GetNomination(); + } + + // Handle the tuple. + HandleTuple(tuple, request->HasUseCandidate(), request->HasNomination(), nomination); - // Mark the first tuple as selected tuple (if any). - if (this->tuples.begin() != this->tuples.end()) + // If state is 'connected' or 'completed' after handling the tuple, then + // start or restart ICE consent check (if supported). + if (IsConsentCheckSupported() && (this->state == IceState::CONNECTED || this->state == IceState::COMPLETED)) + { + if (IsConsentCheckRunning()) { - SetSelectedTuple(std::addressof(*this->tuples.begin())); + RestartConsentCheck(); } - // Or just emit 'disconnected'. else { - // Update state. - this->state = IceState::DISCONNECTED; - // Notify the listener. - this->listener->OnIceServerDisconnected(this); + StartConsentCheck(); } } } - void IceServer::ForceSelectedTuple(const RTC::TransportTuple* tuple) + void IceServer::ProcessStunIndication(RTC::StunPacket* indication) + { + MS_TRACE(); + + MS_DEBUG_DEV("STUN indication received, discarded"); + + // Nothig else to do. We just discard STUN indications. + } + + void IceServer::ProcessStunResponse(RTC::StunPacket* response) + { + MS_TRACE(); + + const std::string responseType = response->GetClass() == RTC::StunPacket::Class::SUCCESS_RESPONSE + ? "success" + : std::to_string(response->GetErrorCode()) + " error"; + + MS_DEBUG_DEV("processing STUN %s response received, discarded", responseType.c_str()); + + // Nothig else to do. We just discard STUN responses because we do not + // generate STUN requests. + } + + void IceServer::MayForceSelectedTuple(const RTC::TransportTuple* tuple) { MS_TRACE(); - MS_ASSERT( - this->selectedTuple, "cannot force the selected tuple if there was not a selected tuple"); + if (this->state != IceState::CONNECTED && this->state != IceState::COMPLETED) + { + MS_WARN_TAG(ice, "cannot force selected tuple if not in state 'connected' or 'completed'"); + + return; + } auto* storedTuple = HasTuple(tuple); - MS_ASSERT( - storedTuple, - "cannot force the selected tuple if the given tuple was not already a valid tuple"); + if (!storedTuple) + { + MS_WARN_TAG(ice, "cannot force selected tuple if the given tuple was not already a valid one"); + + return; + } - // Mark it as selected tuple. SetSelectedTuple(storedTuple); } @@ -351,10 +555,6 @@ namespace RTC { case IceState::NEW: { - // There should be no tuples. - MS_ASSERT( - this->tuples.empty(), "state is 'new' but there are %zu tuples", this->tuples.size()); - // There shouldn't be a selected tuple. MS_ASSERT(!this->selectedTuple, "state is 'new' but there is selected tuple"); @@ -371,10 +571,12 @@ namespace RTC // Store the tuple. auto* storedTuple = AddTuple(tuple); - // Mark it as selected tuple. - SetSelectedTuple(storedTuple); // Update state. this->state = IceState::CONNECTED; + + // Mark it as selected tuple. + SetSelectedTuple(storedTuple); + // Notify the listener. this->listener->OnIceServerConnected(this); } @@ -393,13 +595,18 @@ namespace RTC hasNomination ? "true" : "false", nomination); - // Mark it as selected tuple. - SetSelectedTuple(storedTuple); // Update state. this->state = IceState::COMPLETED; + + // Mark it as selected tuple. + SetSelectedTuple(storedTuple); + // Update nomination. if (hasNomination && nomination > this->remoteNomination) + { this->remoteNomination = nomination; + } + // Notify the listener. this->listener->OnIceServerCompleted(this); } @@ -410,12 +617,6 @@ namespace RTC case IceState::DISCONNECTED: { - // There should be no tuples. - MS_ASSERT( - this->tuples.empty(), - "state is 'disconnected' but there are %zu tuples", - this->tuples.size()); - // There shouldn't be a selected tuple. MS_ASSERT(!this->selectedTuple, "state is 'disconnected' but there is selected tuple"); @@ -432,10 +633,12 @@ namespace RTC // Store the tuple. auto* storedTuple = AddTuple(tuple); - // Mark it as selected tuple. - SetSelectedTuple(storedTuple); // Update state. this->state = IceState::CONNECTED; + + // Mark it as selected tuple. + SetSelectedTuple(storedTuple); + // Notify the listener. this->listener->OnIceServerConnected(this); } @@ -454,13 +657,18 @@ namespace RTC hasNomination ? "true" : "false", nomination); - // Mark it as selected tuple. - SetSelectedTuple(storedTuple); // Update state. this->state = IceState::COMPLETED; + + // Mark it as selected tuple. + SetSelectedTuple(storedTuple); + // Update nomination. if (hasNomination && nomination > this->remoteNomination) + { this->remoteNomination = nomination; + } + // Notify the listener. this->listener->OnIceServerCompleted(this); } @@ -479,9 +687,8 @@ namespace RTC if (!hasUseCandidate && !hasNomination) { - // If a new tuple store it. - if (!HasTuple(tuple)) - AddTuple(tuple); + // Store the tuple. + AddTuple(tuple); } else { @@ -493,21 +700,23 @@ namespace RTC hasNomination ? "true" : "false", nomination); - auto* storedTuple = HasTuple(tuple); - - // If a new tuple store it. - if (!storedTuple) - storedTuple = AddTuple(tuple); + // Store the tuple. + auto* storedTuple = AddTuple(tuple); if ((hasNomination && nomination > this->remoteNomination) || !hasNomination) { - // Mark it as selected tuple. - SetSelectedTuple(storedTuple); // Update state. this->state = IceState::COMPLETED; + + // Mark it as selected tuple. + SetSelectedTuple(storedTuple); + // Update nomination. if (hasNomination && nomination > this->remoteNomination) + { this->remoteNomination = nomination; + } + // Notify the listener. this->listener->OnIceServerCompleted(this); } @@ -526,25 +735,24 @@ namespace RTC if (!hasUseCandidate && !hasNomination) { - // If a new tuple store it. - if (!HasTuple(tuple)) - AddTuple(tuple); + // Store the tuple. + AddTuple(tuple); } else { - auto* storedTuple = HasTuple(tuple); - - // If a new tuple store it. - if (!storedTuple) - storedTuple = AddTuple(tuple); + // Store the tuple. + auto* storedTuple = AddTuple(tuple); if ((hasNomination && nomination > this->remoteNomination) || !hasNomination) { // Mark it as selected tuple. SetSelectedTuple(storedTuple); + // Update nomination. if (hasNomination && nomination > this->remoteNomination) + { this->remoteNomination = nomination; + } } } @@ -553,19 +761,30 @@ namespace RTC } } - inline RTC::TransportTuple* IceServer::AddTuple(RTC::TransportTuple* tuple) + RTC::TransportTuple* IceServer::AddTuple(RTC::TransportTuple* tuple) { MS_TRACE(); + auto* storedTuple = HasTuple(tuple); + + if (storedTuple) + { + MS_DEBUG_DEV("tuple already exists"); + + return storedTuple; + } + // Add the new tuple at the beginning of the list. this->tuples.push_front(*tuple); - auto* storedTuple = std::addressof(*this->tuples.begin()); + storedTuple = std::addressof(*this->tuples.begin()); // If it is UDP then we must store the remote address (until now it is // just a pointer that will be freed soon). if (storedTuple->GetProtocol() == TransportTuple::Protocol::UDP) + { storedTuple->StoreUdpRemoteAddress(); + } // Notify the listener. this->listener->OnIceServerTupleAdded(this, storedTuple); @@ -596,7 +815,9 @@ namespace RTC MS_ASSERT(removedTuple, "couldn't find any tuple to be removed"); // Notify the listener. + this->isRemovingTuples = true; this->listener->OnIceServerTupleRemoved(this, removedTuple); + this->isRemovingTuples = false; // Remove it from the list of tuples. // NOTE: Do it after notifying the listener since the listener may need to @@ -610,18 +831,15 @@ namespace RTC return storedTuple; } - inline RTC::TransportTuple* IceServer::HasTuple(const RTC::TransportTuple* tuple) const + RTC::TransportTuple* IceServer::HasTuple(const RTC::TransportTuple* tuple) const { MS_TRACE(); - // If there is no selected tuple yet then we know that the tuples list - // is empty. - if (!this->selectedTuple) - return nullptr; - - // Check the current selected tuple. - if (this->selectedTuple->Compare(tuple)) + // Check the current selected tuple (if any). + if (this->selectedTuple && this->selectedTuple->Compare(tuple)) + { return this->selectedTuple; + } // Otherwise check other stored tuples. for (const auto& it : this->tuples) @@ -629,23 +847,115 @@ namespace RTC auto* storedTuple = const_cast(std::addressof(it)); if (storedTuple->Compare(tuple)) + { return storedTuple; + } } return nullptr; } - inline void IceServer::SetSelectedTuple(RTC::TransportTuple* storedTuple) + void IceServer::SetSelectedTuple(RTC::TransportTuple* storedTuple) { MS_TRACE(); // If already the selected tuple do nothing. if (storedTuple == this->selectedTuple) + { return; + } this->selectedTuple = storedTuple; // Notify the listener. this->listener->OnIceServerSelectedTuple(this, this->selectedTuple); } + + void IceServer::StartConsentCheck() + { + MS_TRACE(); + + MS_ASSERT(IsConsentCheckSupported(), "ICE consent check not supported"); + MS_ASSERT(!IsConsentCheckRunning(), "ICE consent check already running"); + MS_ASSERT(this->selectedTuple, "no selected tuple"); + + // Create the ICE consent check timer if it doesn't exist. + if (!this->consentCheckTimer) + { + this->consentCheckTimer = new TimerHandle(this); + } + + this->consentCheckTimer->Start(this->consentTimeoutMs); + } + + void IceServer::RestartConsentCheck() + { + MS_TRACE(); + + MS_ASSERT(IsConsentCheckSupported(), "ICE consent check not supported"); + MS_ASSERT(IsConsentCheckRunning(), "ICE consent check not running"); + MS_ASSERT(this->selectedTuple, "no selected tuple"); + + this->consentCheckTimer->Restart(); + } + + void IceServer::StopConsentCheck() + { + MS_TRACE(); + + MS_ASSERT(IsConsentCheckSupported(), "ICE consent check not supported"); + MS_ASSERT(IsConsentCheckRunning(), "ICE consent check not running"); + + this->consentCheckTimer->Stop(); + } + + inline void IceServer::OnTimer(TimerHandle* timer) + { + MS_TRACE(); + + if (timer == this->consentCheckTimer) + { + MS_ASSERT(IsConsentCheckSupported(), "ICE consent check not supported"); + + // State must be 'connected' or 'completed'. + MS_ASSERT( + this->state == IceState::COMPLETED || this->state == IceState::CONNECTED, + "ICE consent check timer fired but state is neither 'completed' nor 'connected'"); + + // There should be a selected tuple. + MS_ASSERT(this->selectedTuple, "ICE consent check timer fired but there is not selected tuple"); + + MS_WARN_TAG(ice, "ICE consent expired due to timeout, moving to 'disconnected' state"); + + // Update state. + this->state = IceState::DISCONNECTED; + + // Reset remote nomination. + this->remoteNomination = 0u; + + // Clear all tuples. + this->isRemovingTuples = true; + + for (const auto& it : this->tuples) + { + auto* storedTuple = const_cast(std::addressof(it)); + + // Notify the listener. + this->listener->OnIceServerTupleRemoved(this, storedTuple); + } + + this->isRemovingTuples = false; + + // Clear all tuples. + // NOTE: Do it after notifying the listener since the listener may need to + // use/read the tuple being removed so we cannot free it yet. + this->tuples.clear(); + + // Unset selected tuple. + this->selectedTuple = nullptr; + + // Notify the listener. + this->listener->OnIceServerDisconnected(this); + } + } } // namespace RTC diff --git a/worker/src/RTC/KeyFrameRequestManager.cpp b/worker/src/RTC/KeyFrameRequestManager.cpp index aae45b0f70..540414ae0c 100644 --- a/worker/src/RTC/KeyFrameRequestManager.cpp +++ b/worker/src/RTC/KeyFrameRequestManager.cpp @@ -4,16 +4,15 @@ #include "RTC/KeyFrameRequestManager.hpp" #include "Logger.hpp" -static constexpr uint32_t KeyFrameRetransmissionWaitTime{ 1000 }; +static constexpr uint32_t KeyFrameRetransmissionWaitTime{ 1000u }; /* PendingKeyFrameInfo methods. */ RTC::PendingKeyFrameInfo::PendingKeyFrameInfo(PendingKeyFrameInfo::Listener* listener, uint32_t ssrc) - : listener(listener), ssrc(ssrc) + : listener(listener), ssrc(ssrc), timer(new TimerHandle(this)) { MS_TRACE(); - this->timer = new Timer(this); this->timer->Start(KeyFrameRetransmissionWaitTime); } @@ -25,23 +24,24 @@ RTC::PendingKeyFrameInfo::~PendingKeyFrameInfo() delete this->timer; } -inline void RTC::PendingKeyFrameInfo::OnTimer(Timer* timer) +inline void RTC::PendingKeyFrameInfo::OnTimer(TimerHandle* timer) { MS_TRACE(); if (timer == this->timer) + { this->listener->OnKeyFrameRequestTimeout(this); + } } /* KeyFrameRequestDelayer methods. */ RTC::KeyFrameRequestDelayer::KeyFrameRequestDelayer( KeyFrameRequestDelayer::Listener* listener, uint32_t ssrc, uint32_t delay) - : listener(listener), ssrc(ssrc) + : listener(listener), ssrc(ssrc), timer(new TimerHandle(this)) { MS_TRACE(); - this->timer = new Timer(this); this->timer->Start(delay); } @@ -53,12 +53,14 @@ RTC::KeyFrameRequestDelayer::~KeyFrameRequestDelayer() delete this->timer; } -inline void RTC::KeyFrameRequestDelayer::OnTimer(Timer* timer) +inline void RTC::KeyFrameRequestDelayer::OnTimer(TimerHandle* timer) { MS_TRACE(); if (timer == this->timer) + { this->listener->OnKeyFrameDelayTimeout(this); + } } /* KeyFrameRequestManager methods. */ @@ -185,7 +187,9 @@ void RTC::KeyFrameRequestManager::KeyFrameReceived(uint32_t ssrc) // There is no pending key frame for the given ssrc. if (it == this->mapSsrcPendingKeyFrameInfo.end()) + { return; + } auto* pendingKeyFrameInfo = it->second; diff --git a/worker/src/RTC/NackGenerator.cpp b/worker/src/RTC/NackGenerator.cpp index 48fe2e8512..f77c1f3c6b 100644 --- a/worker/src/RTC/NackGenerator.cpp +++ b/worker/src/RTC/NackGenerator.cpp @@ -21,12 +21,10 @@ namespace RTC /* Instance methods. */ NackGenerator::NackGenerator(Listener* listener, unsigned int sendNackDelayMs) - : listener(listener), sendNackDelayMs(sendNackDelayMs), rtt(DefaultRtt) + : listener(listener), sendNackDelayMs(sendNackDelayMs), timer(new TimerHandle(this)), + rtt(DefaultRtt) { MS_TRACE(); - - // Set the timer. - this->timer = new Timer(this); } NackGenerator::~NackGenerator() @@ -42,7 +40,7 @@ namespace RTC { MS_TRACE(); - uint16_t seq = packet->GetSequenceNumber(); + const uint16_t seq = packet->GetSequenceNumber(); const bool isKeyFrame = packet->IsKeyFrame(); if (!this->started) @@ -51,14 +49,18 @@ namespace RTC this->lastSeq = seq; if (isKeyFrame) + { this->keyFrameList.insert(seq); + } return false; } // Obviously never nacked, so ignore. if (seq == this->lastSeq) + { return false; + } // May be an out of order packet, or already handled retransmitted packet, // or a retransmitted packet. @@ -79,10 +81,7 @@ namespace RTC this->nackList.erase(it); - if (retries != 0) - return true; - else - return false; + return retries != 0; } // Out of order packet or already handled NACKed packet. @@ -101,14 +100,18 @@ namespace RTC // newer than the latest seq seen. if (isKeyFrame) + { this->keyFrameList.insert(seq); + } // Remove old keyframes. { auto it = this->keyFrameList.lower_bound(seq - MaxPacketAge); if (it != this->keyFrameList.begin()) + { this->keyFrameList.erase(this->keyFrameList.begin(), it); + } } if (isRecovered) @@ -119,7 +122,9 @@ namespace RTC auto it = this->recoveredList.lower_bound(seq - MaxPacketAge); if (it != this->recoveredList.begin()) + { this->recoveredList.erase(this->recoveredList.begin(), it); + } // Do not let a packet pass if it's newer than last seen seq and came via // RTX. @@ -131,15 +136,19 @@ namespace RTC this->lastSeq = seq; // Check if there are any nacks that are waiting for this seq number. - std::vector nackBatch = GetNackBatch(NackFilter::SEQ); + const std::vector nackBatch = GetNackBatch(NackFilter::SEQ); if (!nackBatch.empty()) + { this->listener->OnNackGeneratorNackRequired(nackBatch); + } // This is important. Otherwise the running timer (filter:TIME) would be // interrupted and NACKs would never been sent more than once for each seq. if (!this->timer->IsActive()) + { MayRunTimer(); + } return false; } @@ -187,7 +196,9 @@ namespace RTC // Do not send NACK for packets that are already recovered by RTX. if (this->recoveredList.find(seq) != this->recoveredList.end()) + { continue; + } this->nackList.emplace(std::make_pair( seq, @@ -236,7 +247,7 @@ namespace RTC while (it != this->nackList.end()) { NackInfo& nackInfo = it->second; - uint16_t seq = nackInfo.seq; + const uint16_t seq = nackInfo.seq; if (this->sendNackDelayMs > 0 && nowMs - nackInfo.createdAtMs < this->sendNackDelayMs) { @@ -277,7 +288,10 @@ namespace RTC continue; } - if (filter == NackFilter::TIME && (nackInfo.sentAtMs == 0 || nowMs - nackInfo.sentAtMs >= this->rtt)) + if ( + filter == NackFilter::TIME && + (nackInfo.sentAtMs == 0 || + nowMs - nackInfo.sentAtMs >= (this->rtt > 0u ? this->rtt : DefaultRtt))) { nackBatch.emplace_back(seq); nackInfo.retries++; @@ -313,9 +327,13 @@ namespace RTC seqsStream << nackBatch.back(); if (filter == NackFilter::SEQ) + { MS_DEBUG_DEV("[filter:SEQ, asking seqs:%s]", seqsStream.str().c_str()); + } else + { MS_DEBUG_DEV("[filter:TIME, asking seqs:%s]", seqsStream.str().c_str()); + } } #endif @@ -329,25 +347,32 @@ namespace RTC this->nackList.clear(); this->keyFrameList.clear(); this->recoveredList.clear(); - this->started = false; this->lastSeq = 0u; } inline void NackGenerator::MayRunTimer() const { - if (!this->nackList.empty()) + if (this->nackList.empty()) + { + this->timer->Stop(); + } + else + { this->timer->Start(TimerInterval); + } } - inline void NackGenerator::OnTimer(Timer* /*timer*/) + inline void NackGenerator::OnTimer(TimerHandle* /*timer*/) { MS_TRACE(); - std::vector nackBatch = GetNackBatch(NackFilter::TIME); + const std::vector nackBatch = GetNackBatch(NackFilter::TIME); if (!nackBatch.empty()) + { this->listener->OnNackGeneratorNackRequired(nackBatch); + } MayRunTimer(); } diff --git a/worker/src/RTC/PipeConsumer.cpp b/worker/src/RTC/PipeConsumer.cpp index 5cbb7e72b6..8baedfefa5 100644 --- a/worker/src/RTC/PipeConsumer.cpp +++ b/worker/src/RTC/PipeConsumer.cpp @@ -2,9 +2,9 @@ // #define MS_LOG_DEV_LEVEL 3 #include "RTC/PipeConsumer.hpp" -#include "DepLibUV.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" +#include "Utils.hpp" #include "RTC/Codecs/Tools.hpp" namespace RTC @@ -16,17 +16,19 @@ namespace RTC const std::string& id, const std::string& producerId, RTC::Consumer::Listener* listener, - json& data) + const FBS::Transport::ConsumeRequest* data) : RTC::Consumer::Consumer(shared, id, producerId, listener, data, RTC::RtpParameters::Type::PIPE) { MS_TRACE(); // Ensure there are as many encodings as consumable encodings. if (this->rtpParameters.encodings.size() != this->consumableRtpEncodings.size()) + { MS_THROW_TYPE_ERROR("number of rtpParameters.encodings and consumableRtpEncodings do not match"); + } - auto& encoding = this->rtpParameters.encodings[0]; - auto* mediaCodec = this->rtpParameters.GetCodecForEncoding(encoding); + auto& encoding = this->rtpParameters.encodings[0]; + const auto* mediaCodec = this->rtpParameters.GetCodecForEncoding(encoding); this->keyFrameSupported = RTC::Codecs::Tools::CanBeKeyFrame(mediaCodec->mimeType); @@ -37,8 +39,7 @@ namespace RTC this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ nullptr, - /*payloadChannelNotificationHandler*/ nullptr); + /*channelNotificationHandler*/ nullptr); } PipeConsumer::~PipeConsumer() @@ -56,75 +57,91 @@ namespace RTC this->mapSsrcRtpStream.clear(); } - void PipeConsumer::FillJson(json& jsonObject) const + flatbuffers::Offset PipeConsumer::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); // Call the parent method. - RTC::Consumer::FillJson(jsonObject); + auto base = RTC::Consumer::FillBuffer(builder); // Add rtpStreams. - jsonObject["rtpStreams"] = json::array(); - auto jsonRtpStreamsIt = jsonObject.find("rtpStreams"); + std::vector> rtpStreams; + rtpStreams.reserve(this->rtpStreams.size()); - for (auto* rtpStream : this->rtpStreams) + for (const auto* rtpStream : this->rtpStreams) { - jsonRtpStreamsIt->emplace_back(json::value_t::object); + rtpStreams.emplace_back(rtpStream->FillBuffer(builder)); + } - auto& jsonEntry = (*jsonRtpStreamsIt)[jsonRtpStreamsIt->size() - 1]; + auto dump = FBS::Consumer::CreateConsumerDumpDirect(builder, base, &rtpStreams); - rtpStream->FillJson(jsonEntry); - } + return FBS::Consumer::CreateDumpResponse(builder, dump); } - void PipeConsumer::FillJsonStats(json& jsonArray) const + flatbuffers::Offset PipeConsumer::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) { MS_TRACE(); + std::vector> rtpStreams; + rtpStreams.reserve(this->rtpStreams.size()); + // Add stats of our send streams. for (auto* rtpStream : this->rtpStreams) { - jsonArray.emplace_back(json::value_t::object); - - auto& jsonEntry = jsonArray[jsonArray.size() - 1]; - - rtpStream->FillJsonStats(jsonEntry); + rtpStreams.emplace_back(rtpStream->FillBufferStats(builder)); } + + return FBS::Consumer::CreateGetStatsResponseDirect(builder, &rtpStreams); } - void PipeConsumer::FillJsonScore(json& jsonObject) const + flatbuffers::Offset PipeConsumer::FillBufferScore( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); MS_ASSERT(this->producerRtpStreamScores, "producerRtpStreamScores not set"); // NOTE: Hardcoded values in PipeTransport. - jsonObject["score"] = 10; - jsonObject["producerScore"] = 10; - jsonObject["producerScores"] = *this->producerRtpStreamScores; + return FBS::Consumer::CreateConsumerScoreDirect(builder, 10, 10, this->producerRtpStreamScores); } void PipeConsumer::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::CONSUMER_REQUEST_KEY_FRAME: + case Channel::ChannelRequest::Method::CONSUMER_DUMP: + { + auto dumpOffset = FillBuffer(request->GetBufferBuilder()); + + request->Accept(FBS::Response::Body::Consumer_DumpResponse, dumpOffset); + + break; + } + + case Channel::ChannelRequest::Method::CONSUMER_REQUEST_KEY_FRAME: { if (IsActive()) + { RequestKeyFrame(); + } request->Accept(); break; } - case Channel::ChannelRequest::MethodId::CONSUMER_SET_PREFERRED_LAYERS: + case Channel::ChannelRequest::Method::CONSUMER_SET_PREFERRED_LAYERS: { - // Do nothing. + // Accept with empty preferred layers object. - request->Accept(); + auto responseOffset = + FBS::Consumer::CreateSetPreferredLayersResponse(request->GetBufferBuilder()); + + request->Accept(FBS::Response::Body::Consumer_SetPreferredLayersResponse, responseOffset); break; } @@ -137,14 +154,14 @@ namespace RTC } } - void PipeConsumer::ProducerRtpStream(RTC::RtpStream* /*rtpStream*/, uint32_t /*mappedSsrc*/) + void PipeConsumer::ProducerRtpStream(RTC::RtpStreamRecv* /*rtpStream*/, uint32_t /*mappedSsrc*/) { MS_TRACE(); // Do nothing. } - void PipeConsumer::ProducerNewRtpStream(RTC::RtpStream* /*rtpStream*/, uint32_t /*mappedSsrc*/) + void PipeConsumer::ProducerNewRtpStream(RTC::RtpStreamRecv* /*rtpStream*/, uint32_t /*mappedSsrc*/) { MS_TRACE(); @@ -152,14 +169,14 @@ namespace RTC } void PipeConsumer::ProducerRtpStreamScore( - RTC::RtpStream* /*rtpStream*/, uint8_t /*score*/, uint8_t /*previousScore*/) + RTC::RtpStreamRecv* /*rtpStream*/, uint8_t /*score*/, uint8_t /*previousScore*/) { MS_TRACE(); // Do nothing. } - void PipeConsumer::ProducerRtcpSenderReport(RTC::RtpStream* /*rtpStream*/, bool /*first*/) + void PipeConsumer::ProducerRtcpSenderReport(RTC::RtpStreamRecv* /*rtpStream*/, bool /*first*/) { MS_TRACE(); @@ -201,8 +218,18 @@ namespace RTC { MS_TRACE(); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.consumerId = this->id; +#endif + if (!IsActive()) + { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::CONSUMER_INACTIVE); +#endif + return; + } auto payloadType = packet->GetPayloadType(); @@ -212,6 +239,10 @@ namespace RTC { MS_DEBUG_DEV("payload type not supported [payloadType:%" PRIu8 "]", payloadType); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::UNSUPPORTED_PAYLOAD_TYPE); +#endif + return; } @@ -223,18 +254,38 @@ namespace RTC // If we need to sync, support key frames and this is not a key frame, ignore // the packet. if (syncRequired && this->keyFrameSupported && !packet->IsKeyFrame()) + { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::NOT_A_KEYFRAME); +#endif + + return; + } + + // Packets with only padding are not forwarded. + if (packet->GetPayloadLength() == 0) + { + rtpSeqManager->Drop(packet->GetSequenceNumber()); + +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::EMPTY_PAYLOAD); +#endif + return; + } // Whether this is the first packet after re-sync. - bool isSyncPacket = syncRequired; + const bool isSyncPacket = syncRequired; // Sync sequence number and timestamp if required. if (isSyncPacket) { if (packet->IsKeyFrame()) + { MS_DEBUG_TAG(rtp, "sync key frame received"); + } - rtpSeqManager.Sync(packet->GetSequenceNumber() - 1); + rtpSeqManager->Sync(packet->GetSequenceNumber() - 1); syncRequired = false; } @@ -242,7 +293,7 @@ namespace RTC // Update RTP seq number and timestamp. uint16_t seq; - rtpSeqManager.Input(packet->GetSequenceNumber(), seq); + rtpSeqManager->Input(packet->GetSequenceNumber(), seq); // Save original packet fields. auto origSsrc = packet->GetSsrc(); @@ -252,6 +303,11 @@ namespace RTC packet->SetSsrc(ssrc); packet->SetSequenceNumber(seq); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.sendRtpTimestamp = packet->GetTimestamp(); + packet->logger.sendSeqNumber = seq; +#endif + if (isSyncPacket) { MS_DEBUG_TAG( @@ -310,14 +366,16 @@ namespace RTC std::vector senderReports; std::vector sdesChunks; - std::vector xrReports; + std::vector delaySinceLastRrSsrcInfos; for (auto* rtpStream : this->rtpStreams) { auto* report = rtpStream->GetRtcpSenderReport(nowMs); if (!report) + { continue; + } senderReports.push_back(report); @@ -325,20 +383,19 @@ namespace RTC auto* sdesChunk = rtpStream->GetRtcpSdesChunk(); sdesChunks.push_back(sdesChunk); - auto* dlrr = rtpStream->GetRtcpXrDelaySinceLastRr(nowMs); + auto* delaySinceLastRrSsrcInfo = rtpStream->GetRtcpXrDelaySinceLastRrSsrcInfo(nowMs); - if (dlrr) + if (delaySinceLastRrSsrcInfo) { - auto* report = new RTC::RTCP::DelaySinceLastRr(); - report->AddSsrcInfo(dlrr); - - xrReports.push_back(report); + delaySinceLastRrSsrcInfos.push_back(delaySinceLastRrSsrcInfo); } } // RTCP Compound packet buffer cannot hold the data. - if (!packet->Add(senderReports, sdesChunks, xrReports)) + if (!packet->Add(senderReports, sdesChunks, delaySinceLastRrSsrcInfos)) + { return false; + } this->lastRtcpSentTime = nowMs; @@ -350,7 +407,9 @@ namespace RTC MS_TRACE(); if (!IsActive()) + { return; + } for (auto* rtpStream : this->rtpStreams) { @@ -358,7 +417,9 @@ namespace RTC // If our fraction lost is worse than the given one, update it. if (fractionLost > worstRemoteFractionLost) + { worstRemoteFractionLost = fractionLost; + } } } @@ -367,7 +428,9 @@ namespace RTC MS_TRACE(); if (!IsActive()) + { return; + } // May emit 'trace' event. EmitTraceEventNackType(); @@ -406,7 +469,9 @@ namespace RTC rtpStream->ReceiveKeyFrameRequest(messageType); if (IsActive()) + { RequestKeyFrame(); + } } void PipeConsumer::ReceiveRtcpReceiverReport(RTC::RTCP::ReceiverReport* report) @@ -433,7 +498,9 @@ namespace RTC MS_TRACE(); if (!IsActive()) + { return 0u; + } uint32_t rate{ 0u }; @@ -454,7 +521,9 @@ namespace RTC for (auto* rtpStream : this->rtpStreams) { if (rtpStream->GetRtt() > rtt) + { rtt = rtpStream->GetRtt(); + } } return rtt; @@ -597,18 +666,30 @@ namespace RTC // If the Consumer is paused, tell the RtpStreamSend. if (IsPaused() || IsProducerPaused()) + { rtpStream->Pause(); + } const auto* rtxCodec = this->rtpParameters.GetRtxCodecForEncoding(encoding); if (rtxCodec && encoding.hasRtx) + { rtpStream->SetRtx(rtxCodec->payloadType, encoding.rtx.ssrc); + } this->rtpStreams.push_back(rtpStream); this->mapMappedSsrcSsrc[consumableEncoding.ssrc] = encoding.ssrc; this->mapSsrcRtpStream[encoding.ssrc] = rtpStream; this->mapRtpStreamSyncRequired[rtpStream] = false; - this->mapRtpStreamRtpSeqManager[rtpStream]; + + // Let's choose an initial output seq number between 1000 and 32768 to avoid + // libsrtp bug: + // https://github.com/versatica/mediasoup/issues/1437 + const uint16_t initialOutputSeq = + Utils::Crypto::GetRandomUInt(1000u, std::numeric_limits::max() / 2); + + this->mapRtpStreamRtpSeqManager[rtpStream].reset( + new RTC::SeqManager(initialOutputSeq)); } } @@ -617,7 +698,9 @@ namespace RTC MS_TRACE(); if (this->kind != RTC::Media::Kind::VIDEO) + { return; + } for (auto& consumableRtpEncoding : this->consumableRtpEncodings) { diff --git a/worker/src/RTC/PipeTransport.cpp b/worker/src/RTC/PipeTransport.cpp index 39bc2a0ead..257b328ef1 100644 --- a/worker/src/RTC/PipeTransport.cpp +++ b/worker/src/RTC/PipeTransport.cpp @@ -15,7 +15,6 @@ namespace RTC RTC::SrtpSession::CryptoSuite PipeTransport::srtpCryptoSuite{ RTC::SrtpSession::CryptoSuite::AEAD_AES_256_GCM }; - std::string PipeTransport::srtpCryptoSuiteString{ "AEAD_AES_256_GCM" }; // MAster length of AEAD_AES_256_GCM. size_t PipeTransport::srtpMasterLength{ 44 }; @@ -23,65 +22,47 @@ namespace RTC // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init) PipeTransport::PipeTransport( - RTC::Shared* shared, const std::string& id, RTC::Transport::Listener* listener, json& data) - : RTC::Transport::Transport(shared, id, listener, data) + RTC::Shared* shared, + const std::string& id, + RTC::Transport::Listener* listener, + const FBS::PipeTransport::PipeTransportOptions* options) + : RTC::Transport::Transport(shared, id, listener, options->base()) { MS_TRACE(); - auto jsonListenIpIt = data.find("listenIp"); - - if (jsonListenIpIt == data.end()) - MS_THROW_TYPE_ERROR("missing listenIp"); - else if (!jsonListenIpIt->is_object()) - MS_THROW_TYPE_ERROR("wrong listenIp (not an object)"); - - auto jsonIpIt = jsonListenIpIt->find("ip"); - - if (jsonIpIt == jsonListenIpIt->end()) - MS_THROW_TYPE_ERROR("missing listenIp.ip"); - else if (!jsonIpIt->is_string()) - MS_THROW_TYPE_ERROR("wrong listenIp.ip (not an string)"); + if (options->listenInfo()->protocol() != FBS::Transport::Protocol::UDP) + { + MS_THROW_TYPE_ERROR("unsupported listen protocol"); + } - this->listenIp.ip.assign(jsonIpIt->get()); + this->listenInfo.ip.assign(options->listenInfo()->ip()->str()); // This may throw. - Utils::IP::NormalizeIp(this->listenIp.ip); - - auto jsonAnnouncedIpIt = jsonListenIpIt->find("announcedIp"); + Utils::IP::NormalizeIp(this->listenInfo.ip); - if (jsonAnnouncedIpIt != jsonListenIpIt->end()) + if (flatbuffers::IsFieldPresent( + options->listenInfo(), FBS::Transport::ListenInfo::VT_ANNOUNCEDADDRESS)) { - if (!jsonAnnouncedIpIt->is_string()) - MS_THROW_TYPE_ERROR("wrong listenIp.announcedIp (not an string"); - - this->listenIp.announcedIp.assign(jsonAnnouncedIpIt->get()); + this->listenInfo.announcedAddress.assign(options->listenInfo()->announcedAddress()->str()); } - uint16_t port{ 0 }; - auto jsonPortIt = data.find("port"); - - if (jsonPortIt != data.end()) + if (flatbuffers::IsFieldPresent( + options->listenInfo(), FBS::Transport::ListenInfo::VT_ANNOUNCEDADDRESS)) { - if (!(jsonPortIt->is_number() && Utils::Json::IsPositiveInteger(*jsonPortIt))) - MS_THROW_TYPE_ERROR("wrong port (not a positive number)"); - - port = jsonPortIt->get(); + this->listenInfo.announcedAddress.assign(options->listenInfo()->announcedAddress()->str()); } - auto jsonEnableRtxIt = data.find("enableRtx"); + this->listenInfo.port = options->listenInfo()->port(); + this->listenInfo.portRange.min = options->listenInfo()->portRange()->min(); + this->listenInfo.portRange.max = options->listenInfo()->portRange()->max(); + this->listenInfo.sendBufferSize = options->listenInfo()->sendBufferSize(); + this->listenInfo.recvBufferSize = options->listenInfo()->recvBufferSize(); + this->listenInfo.flags.ipv6Only = options->listenInfo()->flags()->ipv6Only(); + this->listenInfo.flags.udpReusePort = options->listenInfo()->flags()->udpReusePort(); - if (jsonEnableRtxIt != data.end() && jsonEnableRtxIt->is_boolean()) - this->rtx = jsonEnableRtxIt->get(); + this->rtx = options->enableRtx(); - auto jsonEnableSrtpIt = data.find("enableSrtp"); - - // clang-format off - if ( - jsonEnableSrtpIt != data.end() && - jsonEnableSrtpIt->is_boolean() && - jsonEnableSrtpIt->get() - ) - // clang-format on + if (options->enableSrtp()) { this->srtpKey = Utils::Crypto::GetRandomString(PipeTransport::srtpMasterLength); this->srtpKeyBase64 = Utils::String::Base64Encode(this->srtpKey); @@ -89,18 +70,62 @@ namespace RTC try { - // This may throw. - if (port != 0) - this->udpSocket = new RTC::UdpSocket(this, this->listenIp.ip, port); + if (this->listenInfo.portRange.min != 0 && this->listenInfo.portRange.max != 0) + { + uint64_t portRangeHash{ 0u }; + + this->udpSocket = new RTC::UdpSocket( + this, + this->listenInfo.ip, + this->listenInfo.portRange.min, + this->listenInfo.portRange.max, + this->listenInfo.flags, + portRangeHash); + } + else if (this->listenInfo.port != 0) + { + this->udpSocket = new RTC::UdpSocket( + this, this->listenInfo.ip, this->listenInfo.port, this->listenInfo.flags); + } + // NOTE: This is temporal to allow deprecated usage of worker port range. + // In the future this should throw since |port| or |portRange| will be + // required. else - this->udpSocket = new RTC::UdpSocket(this, this->listenIp.ip); + { + uint64_t portRangeHash{ 0u }; + + this->udpSocket = new RTC::UdpSocket( + this, + this->listenInfo.ip, + Settings::configuration.rtcMinPort, + Settings::configuration.rtcMaxPort, + this->listenInfo.flags, + portRangeHash); + } + + if (this->listenInfo.sendBufferSize != 0) + { + // NOTE: This may throw. + this->udpSocket->SetSendBufferSize(this->listenInfo.sendBufferSize); + } + + if (this->listenInfo.recvBufferSize != 0) + { + // NOTE: This may throw. + this->udpSocket->SetRecvBufferSize(this->listenInfo.recvBufferSize); + } + + MS_DEBUG_TAG( + info, + "UDP socket buffer sizes [send:%" PRIu32 ", recv:%" PRIu32 "]", + udpSocket->GetSendBufferSize(), + udpSocket->GetRecvBufferSize()); // NOTE: This may throw. this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ this, - /*payloadChannelNotificationHandler*/ this); + /*channelNotificationHandler*/ this); } catch (const MediaSoupError& error) { @@ -117,6 +142,10 @@ namespace RTC { MS_TRACE(); + // Tell the Transport parent class that we are about to destroy + // the class instance. + Destroying(); + this->shared->channelMessageRegistrator->UnregisterHandler(this->id); delete this->udpSocket; @@ -132,89 +161,133 @@ namespace RTC this->srtpRecvSession = nullptr; } - void PipeTransport::FillJson(json& jsonObject) const + flatbuffers::Offset PipeTransport::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Call the parent method. - RTC::Transport::FillJson(jsonObject); - // Add tuple. + flatbuffers::Offset tuple; + if (this->tuple) { - this->tuple->FillJson(jsonObject["tuple"]); + tuple = this->tuple->FillBuffer(builder); } else { - jsonObject["tuple"] = json::object(); - auto jsonTupleIt = jsonObject.find("tuple"); + std::string localIp; - if (this->listenIp.announcedIp.empty()) - (*jsonTupleIt)["localIp"] = this->udpSocket->GetLocalIp(); + if (this->listenInfo.announcedAddress.empty()) + { + localIp = this->udpSocket->GetLocalIp(); + } else - (*jsonTupleIt)["localIp"] = this->listenIp.announcedIp; + { + localIp = this->listenInfo.announcedAddress; + } - (*jsonTupleIt)["localPort"] = this->udpSocket->GetLocalPort(); - (*jsonTupleIt)["protocol"] = "udp"; + tuple = FBS::Transport::CreateTupleDirect( + builder, + localIp.c_str(), + this->udpSocket->GetLocalPort(), + nullptr, + 0, + FBS::Transport::Protocol::UDP); } - // Add rtx. - jsonObject["rtx"] = this->rtx; - // Add srtpParameters. + flatbuffers::Offset srtpParameters; + if (HasSrtp()) { - jsonObject["srtpParameters"] = json::object(); - auto jsonSrtpParametersIt = jsonObject.find("srtpParameters"); - - (*jsonSrtpParametersIt)["cryptoSuite"] = PipeTransport::srtpCryptoSuiteString; - (*jsonSrtpParametersIt)["keyBase64"] = this->srtpKeyBase64; + srtpParameters = FBS::SrtpParameters::CreateSrtpParametersDirect( + builder, + SrtpSession::CryptoSuiteToFbs(PipeTransport::srtpCryptoSuite), + this->srtpKeyBase64.c_str()); } + + // Add base transport dump. + auto base = Transport::FillBuffer(builder); + + return FBS::PipeTransport::CreateDumpResponse(builder, base, tuple, this->rtx, srtpParameters); } - void PipeTransport::FillJsonStats(json& jsonArray) + flatbuffers::Offset PipeTransport::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) { MS_TRACE(); - // Call the parent method. - RTC::Transport::FillJsonStats(jsonArray); - - auto& jsonObject = jsonArray[0]; - - // Add type. - jsonObject["type"] = "pipe-transport"; + // Add tuple. + flatbuffers::Offset tuple; if (this->tuple) { - this->tuple->FillJson(jsonObject["tuple"]); + tuple = this->tuple->FillBuffer(builder); } else { - // Add tuple. - jsonObject["tuple"] = json::object(); - auto jsonTupleIt = jsonObject.find("tuple"); + std::string localIp; - if (this->listenIp.announcedIp.empty()) - (*jsonTupleIt)["localIp"] = this->udpSocket->GetLocalIp(); + if (this->listenInfo.announcedAddress.empty()) + { + localIp = this->udpSocket->GetLocalIp(); + } else - (*jsonTupleIt)["localIp"] = this->listenIp.announcedIp; + { + localIp = this->listenInfo.announcedAddress; + } - (*jsonTupleIt)["localPort"] = this->udpSocket->GetLocalPort(); - (*jsonTupleIt)["protocol"] = "udp"; + tuple = FBS::Transport::CreateTupleDirect( + builder, + // localIp. + localIp.c_str(), + // localPort, + this->udpSocket->GetLocalPort(), + // remoteIp. + nullptr, + // remotePort. + 0, + // protocol. + FBS::Transport::Protocol::UDP); } + + // Base Transport stats. + auto base = Transport::FillBufferStats(builder); + + return FBS::PipeTransport::CreateGetStatsResponse(builder, base, tuple); } void PipeTransport::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::TRANSPORT_CONNECT: + case Channel::ChannelRequest::Method::TRANSPORT_GET_STATS: + { + auto responseOffset = FillBufferStats(request->GetBufferBuilder()); + + request->Accept(FBS::Response::Body::PipeTransport_GetStatsResponse, responseOffset); + + break; + } + + case Channel::ChannelRequest::Method::TRANSPORT_DUMP: + { + auto dumpOffset = FillBuffer(request->GetBufferBuilder()); + + request->Accept(FBS::Response::Body::PipeTransport_DumpResponse, dumpOffset); + + break; + } + + case Channel::ChannelRequest::Method::PIPETRANSPORT_CONNECT: { // Ensure this method is not called twice. if (this->tuple) + { MS_THROW_ERROR("connect() already called"); + } try { @@ -222,63 +295,41 @@ namespace RTC uint16_t port{ 0u }; std::string srtpKeyBase64; - auto jsonSrtpParametersIt = request->data.find("srtpParameters"); + const auto* body = request->data->body_as(); + + auto srtpParametersPresent = + flatbuffers::IsFieldPresent(body, FBS::PipeTransport::ConnectRequest::VT_SRTPPARAMETERS); - if (!HasSrtp() && jsonSrtpParametersIt != request->data.end()) + if (!HasSrtp() && srtpParametersPresent) { MS_THROW_TYPE_ERROR("invalid srtpParameters (SRTP not enabled)"); } else if (HasSrtp()) { - // clang-format off - if ( - jsonSrtpParametersIt == request->data.end() || - !jsonSrtpParametersIt->is_object() - ) - // clang-format on + if (!srtpParametersPresent) { MS_THROW_TYPE_ERROR("missing srtpParameters (SRTP enabled)"); } - auto jsonCryptoSuiteIt = jsonSrtpParametersIt->find("cryptoSuite"); - - // clang-format off - if ( - jsonCryptoSuiteIt == jsonSrtpParametersIt->end() || - !jsonCryptoSuiteIt->is_string() - ) - // clang-format on - { - MS_THROW_TYPE_ERROR("missing srtpParameters.cryptoSuite)"); - } + const auto* srtpParameters = body->srtpParameters(); // NOTE: We just use AEAD_AES_256_GCM as SRTP crypto suite in // PipeTransport. - if (jsonCryptoSuiteIt->get() != PipeTransport::srtpCryptoSuiteString) + if (srtpParameters->cryptoSuite() != FBS::SrtpParameters::SrtpCryptoSuite::AEAD_AES_256_GCM) { MS_THROW_TYPE_ERROR("invalid/unsupported srtpParameters.cryptoSuite"); } - auto jsonKeyBase64It = jsonSrtpParametersIt->find("keyBase64"); - - // clang-format off - if ( - jsonKeyBase64It == jsonSrtpParametersIt->end() || - !jsonKeyBase64It->is_string() - ) - // clang-format on - { - MS_THROW_TYPE_ERROR("missing srtpParameters.keyBase64)"); - } - - srtpKeyBase64 = jsonKeyBase64It->get(); + srtpKeyBase64 = srtpParameters->keyBase64()->str(); size_t outLen; // This may throw. auto* srtpKey = Utils::String::Base64Decode(srtpKeyBase64, outLen); if (outLen != PipeTransport::srtpMasterLength) + { MS_THROW_TYPE_ERROR("invalid decoded SRTP key length"); + } auto* srtpLocalKey = new uint8_t[PipeTransport::srtpMasterLength]; auto* srtpRemoteKey = new uint8_t[PipeTransport::srtpMasterLength]; @@ -322,29 +373,22 @@ namespace RTC delete[] srtpRemoteKey; } - auto jsonIpIt = request->data.find("ip"); - - if (jsonIpIt == request->data.end() || !jsonIpIt->is_string()) + if (!flatbuffers::IsFieldPresent(body, FBS::PipeTransport::ConnectRequest::VT_IP)) + { MS_THROW_TYPE_ERROR("missing ip"); + } - ip = jsonIpIt->get(); + ip = body->ip()->str(); // This may throw. Utils::IP::NormalizeIp(ip); - auto jsonPortIt = request->data.find("port"); - - // clang-format off - if ( - jsonPortIt == request->data.end() || - !Utils::Json::IsPositiveInteger(*jsonPortIt) - ) - // clang-format on + if (!body->port().has_value()) { MS_THROW_TYPE_ERROR("missing port"); } - port = jsonPortIt->get(); + port = body->port().value(); int err; @@ -358,7 +402,9 @@ namespace RTC reinterpret_cast(&this->remoteAddrStorage)); if (err != 0) + { MS_THROW_ERROR("uv_ip4_addr() failed: %s", uv_strerror(err)); + } break; } @@ -371,7 +417,9 @@ namespace RTC reinterpret_cast(&this->remoteAddrStorage)); if (err != 0) + { MS_THROW_ERROR("uv_ip6_addr() failed: %s", uv_strerror(err)); + } break; } @@ -386,8 +434,10 @@ namespace RTC this->tuple = new RTC::TransportTuple( this->udpSocket, reinterpret_cast(&this->remoteAddrStorage)); - if (!this->listenIp.announcedIp.empty()) - this->tuple->SetLocalAnnouncedIp(this->listenIp.announcedIp); + if (!this->listenInfo.announcedAddress.empty()) + { + this->tuple->SetLocalAnnouncedAddress(this->listenInfo.announcedAddress); + } } catch (const MediaSoupError& error) { @@ -403,12 +453,12 @@ namespace RTC throw; } - // Tell the caller about the selected local DTLS role. - json data = json::object(); + auto tupleOffset = this->tuple->FillBuffer(request->GetBufferBuilder()); - this->tuple->FillJson(data["tuple"]); + auto responseOffset = + FBS::PipeTransport::CreateConnectResponse(request->GetBufferBuilder(), tupleOffset); - request->Accept(data); + request->Accept(FBS::Response::Body::PipeTransport_ConnectResponse, responseOffset); // Assume we are connected (there is no much more we can do to know it) // and tell the parent class. @@ -425,7 +475,7 @@ namespace RTC } } - void PipeTransport::HandleNotification(PayloadChannel::PayloadChannelNotification* notification) + void PipeTransport::HandleNotification(Channel::ChannelNotification* notification) { MS_TRACE(); @@ -460,9 +510,9 @@ namespace RTC } const uint8_t* data = packet->GetData(); - auto intLen = static_cast(packet->GetSize()); + auto len = packet->GetSize(); - if (HasSrtp() && !this->srtpSendSession->EncryptRtp(&data, &intLen)) + if (HasSrtp() && !this->srtpSendSession->EncryptRtp(&data, &len)) { if (cb) { @@ -473,8 +523,6 @@ namespace RTC return; } - auto len = static_cast(intLen); - this->tuple->Send(data, len, cb); // Increase send transmission. @@ -486,15 +534,17 @@ namespace RTC MS_TRACE(); if (!IsConnected()) + { return; + } const uint8_t* data = packet->GetData(); - auto intLen = static_cast(packet->GetSize()); + auto len = packet->GetSize(); - if (HasSrtp() && !this->srtpSendSession->EncryptRtcp(&data, &intLen)) + if (HasSrtp() && !this->srtpSendSession->EncryptRtcp(&data, &len)) + { return; - - auto len = static_cast(intLen); + } this->tuple->Send(data, len); @@ -507,17 +557,19 @@ namespace RTC MS_TRACE(); if (!IsConnected()) + { return; + } packet->Serialize(RTC::RTCP::Buffer); const uint8_t* data = packet->GetData(); - auto intLen = static_cast(packet->GetSize()); + auto len = packet->GetSize(); - if (HasSrtp() && !this->srtpSendSession->EncryptRtcp(&data, &intLen)) + if (HasSrtp() && !this->srtpSendSession->EncryptRtcp(&data, &len)) + { return; - - auto len = static_cast(intLen); + } this->tuple->Send(data, len); @@ -526,11 +578,11 @@ namespace RTC } void PipeTransport::SendMessage( - RTC::DataConsumer* dataConsumer, uint32_t ppid, const uint8_t* msg, size_t len, onQueuedCallback* cb) + RTC::DataConsumer* dataConsumer, const uint8_t* msg, size_t len, uint32_t ppid, onQueuedCallback* cb) { MS_TRACE(); - this->sctpAssociation->SendSctpMessage(dataConsumer, ppid, msg, len, cb); + this->sctpAssociation->SendSctpMessage(dataConsumer, msg, len, ppid, cb); } void PipeTransport::SendSctpData(const uint8_t* data, size_t len) @@ -538,7 +590,9 @@ namespace RTC MS_TRACE(); if (!IsConnected()) + { return; + } this->tuple->Send(data, len); @@ -599,14 +653,14 @@ namespace RTC MS_TRACE(); if (!IsConnected()) + { return; + } // Decrypt the SRTP packet. - auto intLen = static_cast(len); - - if (HasSrtp() && !this->srtpRecvSession->DecryptSrtp(const_cast(data), &intLen)) + if (HasSrtp() && !this->srtpRecvSession->DecryptSrtp(const_cast(data), &len)) { - RTC::RtpPacket* packet = RTC::RtpPacket::Parse(data, static_cast(intLen)); + RTC::RtpPacket* packet = RTC::RtpPacket::Parse(data, len); if (!packet) { @@ -627,7 +681,7 @@ namespace RTC return; } - RTC::RtpPacket* packet = RTC::RtpPacket::Parse(data, static_cast(intLen)); + RTC::RtpPacket* packet = RTC::RtpPacket::Parse(data, len); if (!packet) { @@ -659,12 +713,12 @@ namespace RTC MS_TRACE(); if (!IsConnected()) + { return; + } // Decrypt the SRTCP packet. - auto intLen = static_cast(len); - - if (HasSrtp() && !this->srtpRecvSession->DecryptSrtcp(const_cast(data), &intLen)) + if (HasSrtp() && !this->srtpRecvSession->DecryptSrtcp(const_cast(data), &len)) { return; } @@ -677,7 +731,7 @@ namespace RTC return; } - RTC::RTCP::Packet* packet = RTC::RTCP::Packet::Parse(data, static_cast(intLen)); + RTC::RTCP::Packet* packet = RTC::RTCP::Packet::Parse(data, len); if (!packet) { @@ -696,7 +750,9 @@ namespace RTC MS_TRACE(); if (!IsConnected()) + { return; + } // Verify that the packet's tuple matches our tuple. if (!this->tuple->Compare(tuple)) diff --git a/worker/src/RTC/PlainTransport.cpp b/worker/src/RTC/PlainTransport.cpp index a90ecdd8dc..3cb5a5b676 100644 --- a/worker/src/RTC/PlainTransport.cpp +++ b/worker/src/RTC/PlainTransport.cpp @@ -4,6 +4,7 @@ #include "RTC/PlainTransport.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" +#include "Settings.hpp" #include "Utils.hpp" namespace RTC @@ -26,114 +27,90 @@ namespace RTC /* Class variables. */ - // clang-format off - absl::flat_hash_map PlainTransport::string2SrtpCryptoSuite = - { - { "AEAD_AES_256_GCM", RTC::SrtpSession::CryptoSuite::AEAD_AES_256_GCM }, - { "AEAD_AES_128_GCM", RTC::SrtpSession::CryptoSuite::AEAD_AES_128_GCM }, - { "AES_CM_128_HMAC_SHA1_80", RTC::SrtpSession::CryptoSuite::AES_CM_128_HMAC_SHA1_80 }, - { "AES_CM_128_HMAC_SHA1_32", RTC::SrtpSession::CryptoSuite::AES_CM_128_HMAC_SHA1_32 } - }; - absl::flat_hash_map PlainTransport::srtpCryptoSuite2String = - { - { RTC::SrtpSession::CryptoSuite::AEAD_AES_256_GCM, "AEAD_AES_256_GCM" }, - { RTC::SrtpSession::CryptoSuite::AEAD_AES_128_GCM, "AEAD_AES_128_GCM" }, - { RTC::SrtpSession::CryptoSuite::AES_CM_128_HMAC_SHA1_80, "AES_CM_128_HMAC_SHA1_80" }, - { RTC::SrtpSession::CryptoSuite::AES_CM_128_HMAC_SHA1_32, "AES_CM_128_HMAC_SHA1_32" } - }; - /* Instance methods. */ - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init) - PlainTransport::PlainTransport(RTC::Shared* shared, const std::string& id, RTC::Transport::Listener* listener, json& data) - : RTC::Transport::Transport(shared, id, listener, data) + PlainTransport::PlainTransport( + RTC::Shared* shared, + const std::string& id, + RTC::Transport::Listener* listener, + const FBS::PlainTransport::PlainTransportOptions* options) + : RTC::Transport::Transport(shared, id, listener, options->base()) { MS_TRACE(); - auto jsonListenIpIt = data.find("listenIp"); - - if (jsonListenIpIt == data.end()) - MS_THROW_TYPE_ERROR("missing listenIp"); - else if (!jsonListenIpIt->is_object()) - MS_THROW_TYPE_ERROR("wrong listenIp (not an object)"); - - auto jsonIpIt = jsonListenIpIt->find("ip"); - - if (jsonIpIt == jsonListenIpIt->end()) - MS_THROW_TYPE_ERROR("missing listenIp.ip"); - else if (!jsonIpIt->is_string()) - MS_THROW_TYPE_ERROR("wrong listenIp.ip (not an string)"); + if (options->listenInfo()->protocol() != FBS::Transport::Protocol::UDP) + { + MS_THROW_TYPE_ERROR("unsupported listen protocol"); + } - this->listenIp.ip.assign(jsonIpIt->get()); + this->listenInfo.ip.assign(options->listenInfo()->ip()->str()); // This may throw. - Utils::IP::NormalizeIp(this->listenIp.ip); + Utils::IP::NormalizeIp(this->listenInfo.ip); - auto jsonAnnouncedIpIt = jsonListenIpIt->find("announcedIp"); - - if (jsonAnnouncedIpIt != jsonListenIpIt->end()) + if (flatbuffers::IsFieldPresent( + options->listenInfo(), FBS::Transport::ListenInfo::VT_ANNOUNCEDADDRESS)) { - if (!jsonAnnouncedIpIt->is_string()) - MS_THROW_TYPE_ERROR("wrong listenIp.announcedIp (not an string"); - - this->listenIp.announcedIp.assign(jsonAnnouncedIpIt->get()); + this->listenInfo.announcedAddress.assign(options->listenInfo()->announcedAddress()->str()); } - uint16_t port{ 0 }; - auto jsonPortIt = data.find("port"); - - if (jsonPortIt != data.end()) - { - if (!(jsonPortIt->is_number() && Utils::Json::IsPositiveInteger(*jsonPortIt))) - MS_THROW_TYPE_ERROR("wrong port (not a positive number)"); - - port = jsonPortIt->get(); - } + this->listenInfo.port = options->listenInfo()->port(); + this->listenInfo.portRange.min = options->listenInfo()->portRange()->min(); + this->listenInfo.portRange.max = options->listenInfo()->portRange()->max(); + this->listenInfo.sendBufferSize = options->listenInfo()->sendBufferSize(); + this->listenInfo.recvBufferSize = options->listenInfo()->recvBufferSize(); + this->listenInfo.flags.ipv6Only = options->listenInfo()->flags()->ipv6Only(); + this->listenInfo.flags.udpReusePort = options->listenInfo()->flags()->udpReusePort(); - auto jsonRtcpMuxIt = data.find("rtcpMux"); + this->rtcpMux = options->rtcpMux(); + this->comedia = options->comedia(); - if (jsonRtcpMuxIt != data.end()) + if (!this->rtcpMux) { - if (!jsonRtcpMuxIt->is_boolean()) - MS_THROW_TYPE_ERROR("wrong rtcpMux (not a boolean)"); + if (flatbuffers::IsFieldPresent( + options, FBS::PlainTransport::PlainTransportOptions::VT_RTCPLISTENINFO)) + { + if (options->rtcpListenInfo()->protocol() != FBS::Transport::Protocol::UDP) + { + MS_THROW_TYPE_ERROR("unsupported RTCP listen protocol"); + } - this->rtcpMux = jsonRtcpMuxIt->get(); - } + this->rtcpListenInfo.ip.assign(options->rtcpListenInfo()->ip()->str()); - auto jsonComediaIt = data.find("comedia"); + // This may throw. + Utils::IP::NormalizeIp(this->rtcpListenInfo.ip); - if (jsonComediaIt != data.end()) - { - if (!jsonComediaIt->is_boolean()) - MS_THROW_TYPE_ERROR("wrong comedia (not a boolean)"); + if (flatbuffers::IsFieldPresent( + options->rtcpListenInfo(), FBS::Transport::ListenInfo::VT_ANNOUNCEDADDRESS)) + { + this->rtcpListenInfo.announcedAddress.assign( + options->rtcpListenInfo()->announcedAddress()->str()); + } - this->comedia = jsonComediaIt->get(); + this->rtcpListenInfo.port = options->rtcpListenInfo()->port(); + this->rtcpListenInfo.portRange.min = options->rtcpListenInfo()->portRange()->min(); + this->rtcpListenInfo.portRange.max = options->rtcpListenInfo()->portRange()->max(); + this->rtcpListenInfo.sendBufferSize = options->rtcpListenInfo()->sendBufferSize(); + this->rtcpListenInfo.recvBufferSize = options->rtcpListenInfo()->recvBufferSize(); + this->rtcpListenInfo.flags.ipv6Only = options->rtcpListenInfo()->flags()->ipv6Only(); + this->rtcpListenInfo.flags.udpReusePort = options->rtcpListenInfo()->flags()->udpReusePort(); + } + // If rtcpListenInfo is not given, just clone listenInfo. + else + { + this->rtcpListenInfo = this->listenInfo; + } } - auto jsonEnableSrtpIt = data.find("enableSrtp"); - - // clang-format off - if ( - jsonEnableSrtpIt != data.end() && - jsonEnableSrtpIt->is_boolean() && - jsonEnableSrtpIt->get() - ) - // clang-format on + if (options->enableSrtp()) { - auto jsonSrtpCryptoSuiteIt = data.find("srtpCryptoSuite"); - - if (jsonSrtpCryptoSuiteIt == data.end() || !jsonSrtpCryptoSuiteIt->is_string()) - MS_THROW_TYPE_ERROR("missing srtpCryptoSuite)"); - - // Ensure it's a crypto suite supported by us. - auto it = - PlainTransport::string2SrtpCryptoSuite.find(jsonSrtpCryptoSuiteIt->get()); - - if (it == PlainTransport::string2SrtpCryptoSuite.end()) - MS_THROW_TYPE_ERROR("invalid/unsupported srtpCryptoSuite"); + if (!options->srtpCryptoSuite().has_value()) + { + MS_THROW_TYPE_ERROR("missing srtpCryptoSuite"); + } // NOTE: The SRTP crypto suite may change later on connect(). - this->srtpCryptoSuite = it->second; + this->srtpCryptoSuite = SrtpSession::CryptoSuiteFromFbs(options->srtpCryptoSuite().value()); switch (this->srtpCryptoSuite) { @@ -171,24 +148,104 @@ namespace RTC try { - // This may throw. - if (port != 0) - this->udpSocket = new RTC::UdpSocket(this, this->listenIp.ip, port); + if (this->listenInfo.portRange.min != 0 && this->listenInfo.portRange.max != 0) + { + uint64_t portRangeHash{ 0u }; + + this->udpSocket = new RTC::UdpSocket( + this, + this->listenInfo.ip, + this->listenInfo.portRange.min, + this->listenInfo.portRange.max, + this->listenInfo.flags, + portRangeHash); + } + else if (this->listenInfo.port != 0) + { + this->udpSocket = new RTC::UdpSocket( + this, this->listenInfo.ip, this->listenInfo.port, this->listenInfo.flags); + } + // NOTE: This is temporal to allow deprecated usage of worker port range. + // In the future this should throw since |port| or |portRange| will be + // required. else - this->udpSocket = new RTC::UdpSocket(this, this->listenIp.ip); + { + uint64_t portRangeHash{ 0u }; + + this->udpSocket = new RTC::UdpSocket( + this, + this->listenInfo.ip, + Settings::configuration.rtcMinPort, + Settings::configuration.rtcMaxPort, + this->listenInfo.flags, + portRangeHash); + } + + if (this->listenInfo.sendBufferSize != 0) + { + // NOTE: This may throw. + this->udpSocket->SetSendBufferSize(this->listenInfo.sendBufferSize); + } + + if (this->listenInfo.recvBufferSize != 0) + { + // NOTE: This may throw. + this->udpSocket->SetRecvBufferSize(this->listenInfo.recvBufferSize); + } if (!this->rtcpMux) { - // This may throw. - this->rtcpUdpSocket = new RTC::UdpSocket(this, this->listenIp.ip); + if (this->rtcpListenInfo.portRange.min != 0 && this->rtcpListenInfo.portRange.max != 0) + { + uint64_t portRangeHash{ 0u }; + + this->rtcpUdpSocket = new RTC::UdpSocket( + this, + this->rtcpListenInfo.ip, + this->rtcpListenInfo.portRange.min, + this->rtcpListenInfo.portRange.max, + this->rtcpListenInfo.flags, + portRangeHash); + } + else if (this->rtcpListenInfo.port != 0) + { + this->rtcpUdpSocket = new RTC::UdpSocket( + this, this->rtcpListenInfo.ip, this->rtcpListenInfo.port, this->rtcpListenInfo.flags); + } + // NOTE: This is temporal to allow deprecated usage of worker port range. + // In the future this should throw since |port| or |portRange| will be + // required. + else + { + uint64_t portRangeHash{ 0u }; + + this->rtcpUdpSocket = new RTC::UdpSocket( + this, + this->rtcpListenInfo.ip, + Settings::configuration.rtcMinPort, + Settings::configuration.rtcMaxPort, + this->rtcpListenInfo.flags, + portRangeHash); + } + + if (this->rtcpListenInfo.sendBufferSize != 0) + { + // NOTE: This may throw. + this->rtcpUdpSocket->SetSendBufferSize(this->rtcpListenInfo.sendBufferSize); + } + + if (this->rtcpListenInfo.recvBufferSize != 0) + { + // NOTE: This may throw. + this->rtcpUdpSocket->SetRecvBufferSize(this->rtcpListenInfo.recvBufferSize); + } } // NOTE: This may throw. this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ this, - /*payloadChannelNotificationHandler*/ this); + /*channelNotificationHandler*/ this); } catch (const MediaSoupError& error) { @@ -206,6 +263,10 @@ namespace RTC { MS_TRACE(); + // Tell the Transport parent class that we are about to destroy + // the class instance. + Destroying(); + this->shared->channelMessageRegistrator->UnregisterHandler(this->id); delete this->udpSocket; @@ -227,126 +288,173 @@ namespace RTC this->srtpRecvSession = nullptr; } - void PlainTransport::FillJson(json& jsonObject) const + flatbuffers::Offset PlainTransport::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Call the parent method. - RTC::Transport::FillJson(jsonObject); - - // Add rtcpMux. - jsonObject["rtcpMux"] = this->rtcpMux; - - // Add comedia. - jsonObject["comedia"] = this->comedia; - // Add tuple. + flatbuffers::Offset tuple; + if (this->tuple) { - this->tuple->FillJson(jsonObject["tuple"]); + tuple = this->tuple->FillBuffer(builder); } else { - jsonObject["tuple"] = json::object(); - auto jsonTupleIt = jsonObject.find("tuple"); + std::string localIp; - if (this->listenIp.announcedIp.empty()) - (*jsonTupleIt)["localIp"] = this->udpSocket->GetLocalIp(); + if (this->listenInfo.announcedAddress.empty()) + { + localIp = this->udpSocket->GetLocalIp(); + } else - (*jsonTupleIt)["localIp"] = this->listenIp.announcedIp; + { + localIp = this->listenInfo.announcedAddress; + } - (*jsonTupleIt)["localPort"] = this->udpSocket->GetLocalPort(); - (*jsonTupleIt)["protocol"] = "udp"; + tuple = FBS::Transport::CreateTupleDirect( + builder, + localIp.c_str(), + this->udpSocket->GetLocalPort(), + nullptr, + 0, + FBS::Transport::Protocol::UDP); } // Add rtcpTuple. + flatbuffers::Offset rtcpTuple; + if (!this->rtcpMux) { if (this->rtcpTuple) { - this->rtcpTuple->FillJson(jsonObject["rtcpTuple"]); + rtcpTuple = this->rtcpTuple->FillBuffer(builder); } else { - jsonObject["rtcpTuple"] = json::object(); - auto jsonRtcpTupleIt = jsonObject.find("rtcpTuple"); + std::string localIp; - if (this->listenIp.announcedIp.empty()) - (*jsonRtcpTupleIt)["localIp"] = this->rtcpUdpSocket->GetLocalIp(); + if (this->rtcpListenInfo.announcedAddress.empty()) + { + localIp = this->rtcpUdpSocket->GetLocalIp(); + } else - (*jsonRtcpTupleIt)["localIp"] = this->listenIp.announcedIp; + { + localIp = this->rtcpListenInfo.announcedAddress; + } - (*jsonRtcpTupleIt)["localPort"] = this->rtcpUdpSocket->GetLocalPort(); - (*jsonRtcpTupleIt)["protocol"] = "udp"; + rtcpTuple = FBS::Transport::CreateTupleDirect( + builder, + localIp.c_str(), + this->rtcpUdpSocket->GetLocalPort(), + nullptr, + 0, + FBS::Transport::Protocol::UDP); } } // Add srtpParameters. + flatbuffers::Offset srtpParameters; + if (HasSrtp()) { - jsonObject["srtpParameters"] = json::object(); - auto jsonSrtpParametersIt = jsonObject.find("srtpParameters"); - - (*jsonSrtpParametersIt)["cryptoSuite"] = - PlainTransport::srtpCryptoSuite2String[this->srtpCryptoSuite]; - (*jsonSrtpParametersIt)["keyBase64"] = this->srtpKeyBase64; + srtpParameters = FBS::SrtpParameters::CreateSrtpParametersDirect( + builder, SrtpSession::CryptoSuiteToFbs(this->srtpCryptoSuite), this->srtpKeyBase64.c_str()); } + + // Add base transport dump. + auto base = Transport::FillBuffer(builder); + + return FBS::PlainTransport::CreateDumpResponse( + builder, base, this->rtcpMux, this->comedia, tuple, rtcpTuple, srtpParameters); } - void PlainTransport::FillJsonStats(json& jsonArray) + flatbuffers::Offset PlainTransport::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) { MS_TRACE(); - // Call the parent method. - RTC::Transport::FillJsonStats(jsonArray); - - auto& jsonObject = jsonArray[0]; - - // Add type. - jsonObject["type"] = "plain-rtp-transport"; - - // Add rtcpMux. - jsonObject["rtcpMux"] = this->rtcpMux; - - // Add comedia. - jsonObject["comedia"] = this->comedia; + // Add tuple. + flatbuffers::Offset tuple; if (this->tuple) { - // Add tuple. - this->tuple->FillJson(jsonObject["tuple"]); + tuple = this->tuple->FillBuffer(builder); } else { - // Add tuple. - jsonObject["tuple"] = json::object(); - auto jsonTupleIt = jsonObject.find("tuple"); + std::string localIp; - if (this->listenIp.announcedIp.empty()) - (*jsonTupleIt)["localIp"] = this->udpSocket->GetLocalIp(); + if (this->listenInfo.announcedAddress.empty()) + { + localIp = this->udpSocket->GetLocalIp(); + } else - (*jsonTupleIt)["localIp"] = this->listenIp.announcedIp; + { + localIp = this->listenInfo.announcedAddress; + } - (*jsonTupleIt)["localPort"] = this->udpSocket->GetLocalPort(); - (*jsonTupleIt)["protocol"] = "udp"; + tuple = FBS::Transport::CreateTupleDirect( + builder, + // localIp. + localIp.c_str(), + // localPort, + this->udpSocket->GetLocalPort(), + // remoteIp. + nullptr, + // remotePort. + 0, + // protocol. + FBS::Transport::Protocol::UDP); } // Add rtcpTuple. + flatbuffers::Offset rtcpTuple; + if (!this->rtcpMux && this->rtcpTuple) - this->rtcpTuple->FillJson(jsonObject["rtcpTuple"]); + { + rtcpTuple = this->rtcpTuple->FillBuffer(builder); + } + + // Base Transport stats. + auto base = Transport::FillBufferStats(builder); + + return FBS::PlainTransport::CreateGetStatsResponse( + builder, base, this->rtcpMux, this->comedia, tuple, rtcpTuple); } void PlainTransport::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::TRANSPORT_CONNECT: + case Channel::ChannelRequest::Method::TRANSPORT_DUMP: + { + auto dumpOffset = FillBuffer(request->GetBufferBuilder()); + + request->Accept(FBS::Response::Body::PlainTransport_DumpResponse, dumpOffset); + + break; + } + + case Channel::ChannelRequest::Method::TRANSPORT_GET_STATS: + { + auto responseOffset = FillBufferStats(request->GetBufferBuilder()); + + request->Accept(FBS::Response::Body::PlainTransport_GetStatsResponse, responseOffset); + + break; + } + + case Channel::ChannelRequest::Method::PLAINTRANSPORT_CONNECT: { // Ensure this method is not called twice. if (this->connectCalled) + { MS_THROW_ERROR("connect() already called"); + } try { @@ -355,46 +463,27 @@ namespace RTC uint16_t rtcpPort{ 0u }; std::string srtpKeyBase64; - auto jsonSrtpParametersIt = request->data.find("srtpParameters"); + const auto* body = request->data->body_as(); + + auto srtpParametersPresent = flatbuffers::IsFieldPresent( + body, FBS::PlainTransport::ConnectRequest::VT_SRTPPARAMETERS); - if (!HasSrtp() && jsonSrtpParametersIt != request->data.end()) + if (!HasSrtp() && srtpParametersPresent) { MS_THROW_TYPE_ERROR("invalid srtpParameters (SRTP not enabled)"); } else if (HasSrtp()) { - // clang-format off - if ( - jsonSrtpParametersIt == request->data.end() || - !jsonSrtpParametersIt->is_object() - ) - // clang-format on + if (!srtpParametersPresent) { MS_THROW_TYPE_ERROR("missing srtpParameters (SRTP enabled)"); } - auto jsonCryptoSuiteIt = jsonSrtpParametersIt->find("cryptoSuite"); - - // clang-format off - if ( - jsonCryptoSuiteIt == jsonSrtpParametersIt->end() || - !jsonCryptoSuiteIt->is_string() - ) - // clang-format on - { - MS_THROW_TYPE_ERROR("missing srtpParameters.cryptoSuite)"); - } - - // Ensure it's a crypto suite supported by us. - auto it = - PlainTransport::string2SrtpCryptoSuite.find(jsonCryptoSuiteIt->get()); - - if (it == PlainTransport::string2SrtpCryptoSuite.end()) - MS_THROW_TYPE_ERROR("invalid/unsupported srtpParameters.cryptoSuite"); + const auto* const srtpParameters = body->srtpParameters(); // Update out SRTP crypto suite with the one used by the remote. auto previousSrtpCryptoSuite = this->srtpCryptoSuite; - this->srtpCryptoSuite = it->second; + this->srtpCryptoSuite = SrtpSession::CryptoSuiteFromFbs(srtpParameters->cryptoSuite()); switch (this->srtpCryptoSuite) { @@ -433,26 +522,16 @@ namespace RTC this->srtpKeyBase64 = Utils::String::Base64Encode(this->srtpKey); } - auto jsonKeyBase64It = jsonSrtpParametersIt->find("keyBase64"); - - // clang-format off - if ( - jsonKeyBase64It == jsonSrtpParametersIt->end() || - !jsonKeyBase64It->is_string() - ) - // clang-format on - { - MS_THROW_TYPE_ERROR("missing srtpParameters.keyBase64)"); - } - - srtpKeyBase64 = jsonKeyBase64It->get(); + srtpKeyBase64 = srtpParameters->keyBase64()->str(); size_t outLen; // This may throw. auto* srtpKey = Utils::String::Base64Decode(srtpKeyBase64, outLen); if (outLen != this->srtpMasterLength) + { MS_THROW_TYPE_ERROR("invalid decoded SRTP key length"); + } auto* srtpLocalKey = new uint8_t[this->srtpMasterLength]; auto* srtpRemoteKey = new uint8_t[this->srtpMasterLength]; @@ -498,48 +577,38 @@ namespace RTC if (!this->comedia) { - auto jsonIpIt = request->data.find("ip"); - - if (jsonIpIt == request->data.end() || !jsonIpIt->is_string()) + if (!flatbuffers::IsFieldPresent(body, FBS::PlainTransport::ConnectRequest::VT_IP)) + { MS_THROW_TYPE_ERROR("missing ip"); + } - ip = jsonIpIt->get(); + ip = body->ip()->str(); // This may throw. Utils::IP::NormalizeIp(ip); - auto jsonPortIt = request->data.find("port"); - - // clang-format off - if ( - jsonPortIt == request->data.end() || - !Utils::Json::IsPositiveInteger(*jsonPortIt) - ) - // clang-format on + if (!body->port().has_value()) { MS_THROW_TYPE_ERROR("missing port"); } - port = jsonPortIt->get(); + port = body->port().value(); - auto jsonRtcpPortIt = request->data.find("rtcpPort"); - - // clang-format off - if ( - jsonRtcpPortIt != request->data.end() && - Utils::Json::IsPositiveInteger(*jsonRtcpPortIt) - ) - // clang-format on + if (body->rtcpPort().has_value()) { if (this->rtcpMux) + { MS_THROW_TYPE_ERROR("cannot set rtcpPort with rtcpMux enabled"); + } - rtcpPort = jsonRtcpPortIt->get(); + rtcpPort = body->rtcpPort().value(); } else { if (!this->rtcpMux) + { MS_THROW_TYPE_ERROR("missing rtcpPort (required with rtcpMux disabled)"); + } } int err; @@ -554,7 +623,9 @@ namespace RTC reinterpret_cast(&this->remoteAddrStorage)); if (err != 0) + { MS_THROW_ERROR("uv_ip4_addr() failed: %s", uv_strerror(err)); + } break; } @@ -567,7 +638,9 @@ namespace RTC reinterpret_cast(&this->remoteAddrStorage)); if (err != 0) + { MS_THROW_ERROR("uv_ip6_addr() failed: %s", uv_strerror(err)); + } break; } @@ -582,8 +655,10 @@ namespace RTC this->tuple = new RTC::TransportTuple( this->udpSocket, reinterpret_cast(&this->remoteAddrStorage)); - if (!this->listenIp.announcedIp.empty()) - this->tuple->SetLocalAnnouncedIp(this->listenIp.announcedIp); + if (!this->listenInfo.announcedAddress.empty()) + { + this->tuple->SetLocalAnnouncedAddress(this->listenInfo.announcedAddress); + } if (!this->rtcpMux) { @@ -597,7 +672,9 @@ namespace RTC reinterpret_cast(&this->rtcpRemoteAddrStorage)); if (err != 0) + { MS_THROW_ERROR("uv_ip4_addr() failed: %s", uv_strerror(err)); + } break; } @@ -610,7 +687,9 @@ namespace RTC reinterpret_cast(&this->rtcpRemoteAddrStorage)); if (err != 0) + { MS_THROW_ERROR("uv_ip6_addr() failed: %s", uv_strerror(err)); + } break; } @@ -626,8 +705,10 @@ namespace RTC this->rtcpUdpSocket, reinterpret_cast(&this->rtcpRemoteAddrStorage)); - if (!this->listenIp.announcedIp.empty()) - this->rtcpTuple->SetLocalAnnouncedIp(this->listenIp.announcedIp); + if (!this->rtcpListenInfo.announcedAddress.empty()) + { + this->rtcpTuple->SetLocalAnnouncedAddress(this->rtcpListenInfo.announcedAddress); + } } } } @@ -651,25 +732,32 @@ namespace RTC this->connectCalled = true; // Tell the caller about the selected local DTLS role. - json data = json::object(); + flatbuffers::Offset tupleOffset; + flatbuffers::Offset rtcpTupleOffset; + flatbuffers::Offset srtpParametersOffset; if (this->tuple) - this->tuple->FillJson(data["tuple"]); + { + tupleOffset = this->tuple->FillBuffer(request->GetBufferBuilder()); + } if (!this->rtcpMux && this->rtcpTuple) - this->rtcpTuple->FillJson(data["rtcpTuple"]); + { + rtcpTupleOffset = this->rtcpTuple->FillBuffer(request->GetBufferBuilder()); + } if (HasSrtp()) { - data["srtpParameters"] = json::object(); - auto jsonSrtpParametersIt = data.find("srtpParameters"); - - (*jsonSrtpParametersIt)["cryptoSuite"] = - PlainTransport::srtpCryptoSuite2String[this->srtpCryptoSuite]; - (*jsonSrtpParametersIt)["keyBase64"] = this->srtpKeyBase64; + srtpParametersOffset = FBS::SrtpParameters::CreateSrtpParametersDirect( + request->GetBufferBuilder(), + SrtpSession::CryptoSuiteToFbs(this->srtpCryptoSuite), + this->srtpKeyBase64.c_str()); } - request->Accept(data); + auto responseOffset = FBS::PlainTransport::CreateConnectResponse( + request->GetBufferBuilder(), tupleOffset, rtcpTupleOffset, srtpParametersOffset); + + request->Accept(FBS::Response::Body::PlainTransport_ConnectResponse, responseOffset); // Assume we are connected (there is no much more we can do to know it) // and tell the parent class. @@ -686,7 +774,7 @@ namespace RTC } } - void PlainTransport::HandleNotification(PayloadChannel::PayloadChannelNotification* notification) + void PlainTransport::HandleNotification(Channel::ChannelNotification* notification) { MS_TRACE(); @@ -726,9 +814,9 @@ namespace RTC } const uint8_t* data = packet->GetData(); - auto intLen = static_cast(packet->GetSize()); + auto len = packet->GetSize(); - if (HasSrtp() && !this->srtpSendSession->EncryptRtp(&data, &intLen)) + if (HasSrtp() && !this->srtpSendSession->EncryptRtp(&data, &len)) { if (cb) { @@ -739,8 +827,6 @@ namespace RTC return; } - auto len = static_cast(intLen); - this->tuple->Send(data, len, cb); // Increase send transmission. @@ -752,20 +838,26 @@ namespace RTC MS_TRACE(); if (!IsConnected()) + { return; + } const uint8_t* data = packet->GetData(); - auto intLen = static_cast(packet->GetSize()); + auto len = packet->GetSize(); - if (HasSrtp() && !this->srtpSendSession->EncryptRtcp(&data, &intLen)) + if (HasSrtp() && !this->srtpSendSession->EncryptRtcp(&data, &len)) + { return; - - auto len = static_cast(intLen); + } if (this->rtcpMux) + { this->tuple->Send(data, len); + } else if (this->rtcpTuple) + { this->rtcpTuple->Send(data, len); + } // Increase send transmission. RTC::Transport::DataSent(len); @@ -776,33 +868,39 @@ namespace RTC MS_TRACE(); if (!IsConnected()) + { return; + } packet->Serialize(RTC::RTCP::Buffer); const uint8_t* data = packet->GetData(); - auto intLen = static_cast(packet->GetSize()); + auto len = packet->GetSize(); - if (HasSrtp() && !this->srtpSendSession->EncryptRtcp(&data, &intLen)) + if (HasSrtp() && !this->srtpSendSession->EncryptRtcp(&data, &len)) + { return; - - auto len = static_cast(intLen); + } if (this->rtcpMux) + { this->tuple->Send(data, len); + } else if (this->rtcpTuple) + { this->rtcpTuple->Send(data, len); + } // Increase send transmission. RTC::Transport::DataSent(len); } void PlainTransport::SendMessage( - RTC::DataConsumer* dataConsumer, uint32_t ppid, const uint8_t* msg, size_t len, onQueuedCallback* cb) + RTC::DataConsumer* dataConsumer, const uint8_t* msg, size_t len, uint32_t ppid, onQueuedCallback* cb) { MS_TRACE(); - this->sctpAssociation->SendSctpMessage(dataConsumer, ppid, msg, len, cb); + this->sctpAssociation->SendSctpMessage(dataConsumer, msg, len, ppid, cb); } void PlainTransport::SendSctpData(const uint8_t* data, size_t len) @@ -810,7 +908,9 @@ namespace RTC MS_TRACE(); if (!IsConnected()) + { return; + } this->tuple->Send(data, len); @@ -872,14 +972,14 @@ namespace RTC MS_TRACE(); if (HasSrtp() && !IsSrtpReady()) + { return; + } // Decrypt the SRTP packet. - auto intLen = static_cast(len); - - if (HasSrtp() && !this->srtpRecvSession->DecryptSrtp(const_cast(data), &intLen)) + if (HasSrtp() && !this->srtpRecvSession->DecryptSrtp(const_cast(data), &len)) { - RTC::RtpPacket* packet = RTC::RtpPacket::Parse(data, static_cast(intLen)); + RTC::RtpPacket* packet = RTC::RtpPacket::Parse(data, len); if (!packet) { @@ -900,7 +1000,7 @@ namespace RTC return; } - RTC::RtpPacket* packet = RTC::RtpPacket::Parse(data, static_cast(intLen)); + RTC::RtpPacket* packet = RTC::RtpPacket::Parse(data, len); if (!packet) { @@ -930,18 +1030,16 @@ namespace RTC this->tuple = new RTC::TransportTuple(tuple); - if (!this->listenIp.announcedIp.empty()) - this->tuple->SetLocalAnnouncedIp(this->listenIp.announcedIp); + if (!this->listenInfo.announcedAddress.empty()) + { + this->tuple->SetLocalAnnouncedAddress(this->listenInfo.announcedAddress); + } // If not yet connected do it now. if (!wasConnected) { // Notify the Node PlainTransport. - json data = json::object(); - - this->tuple->FillJson(data["tuple"]); - - this->shared->channelNotifier->Emit(this->id, "tuple", data); + EmitTuple(); RTC::Transport::Connected(); } @@ -970,12 +1068,12 @@ namespace RTC MS_TRACE(); if (HasSrtp() && !IsSrtpReady()) + { return; + } // Decrypt the SRTCP packet. - auto intLen = static_cast(len); - - if (HasSrtp() && !this->srtpRecvSession->DecryptSrtcp(const_cast(data), &intLen)) + if (HasSrtp() && !this->srtpRecvSession->DecryptSrtcp(const_cast(data), &len)) { return; } @@ -997,18 +1095,16 @@ namespace RTC this->tuple = new RTC::TransportTuple(tuple); - if (!this->listenIp.announcedIp.empty()) - this->tuple->SetLocalAnnouncedIp(this->listenIp.announcedIp); + if (!this->listenInfo.announcedAddress.empty()) + { + this->tuple->SetLocalAnnouncedAddress(this->listenInfo.announcedAddress); + } // If not yet connected do it now. if (!wasConnected) { // Notify the Node PlainTransport. - json data = json::object(); - - this->tuple->FillJson(data["tuple"]); - - this->shared->channelNotifier->Emit(this->id, "tuple", data); + EmitTuple(); RTC::Transport::Connected(); } @@ -1028,15 +1124,13 @@ namespace RTC this->rtcpTuple = new RTC::TransportTuple(tuple); - if (!this->listenIp.announcedIp.empty()) - this->rtcpTuple->SetLocalAnnouncedIp(this->listenIp.announcedIp); + if (!this->rtcpListenInfo.announcedAddress.empty()) + { + this->rtcpTuple->SetLocalAnnouncedAddress(this->rtcpListenInfo.announcedAddress); + } // Notify the Node PlainTransport. - json data = json::object(); - - this->rtcpTuple->FillJson(data["rtcpTuple"]); - - this->shared->channelNotifier->Emit(this->id, "rtcptuple", data); + EmitRtcpTuple(); } // If RTCP-mux verify that the packet's tuple matches our RTP tuple. else if (this->rtcpMux && !this->tuple->Compare(tuple)) @@ -1053,7 +1147,7 @@ namespace RTC return; } - RTC::RTCP::Packet* packet = RTC::RTCP::Packet::Parse(data, static_cast(intLen)); + RTC::RTCP::Packet* packet = RTC::RTCP::Packet::Parse(data, len); if (!packet) { @@ -1087,18 +1181,16 @@ namespace RTC this->tuple = new RTC::TransportTuple(tuple); - if (!this->listenIp.announcedIp.empty()) - this->tuple->SetLocalAnnouncedIp(this->listenIp.announcedIp); + if (!this->listenInfo.announcedAddress.empty()) + { + this->tuple->SetLocalAnnouncedAddress(this->listenInfo.announcedAddress); + } // If not yet connected do it now. if (!wasConnected) { // Notify the Node PlainTransport. - json data = json::object(); - - this->tuple->FillJson(data["tuple"]); - - this->shared->channelNotifier->Emit(this->id, "tuple", data); + EmitTuple(); RTC::Transport::Connected(); } @@ -1116,6 +1208,32 @@ namespace RTC RTC::Transport::ReceiveSctpData(data, len); } + inline void PlainTransport::EmitTuple() const + { + auto tuple = this->tuple->FillBuffer(this->shared->channelNotifier->GetBufferBuilder()); + auto notification = FBS::PlainTransport::CreateTupleNotification( + this->shared->channelNotifier->GetBufferBuilder(), tuple); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::PLAINTRANSPORT_TUPLE, + FBS::Notification::Body::PlainTransport_TupleNotification, + notification); + } + + inline void PlainTransport::EmitRtcpTuple() const + { + auto rtcpTuple = this->rtcpTuple->FillBuffer(this->shared->channelNotifier->GetBufferBuilder()); + auto notification = FBS::PlainTransport::CreateRtcpTupleNotification( + this->shared->channelNotifier->GetBufferBuilder(), rtcpTuple); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::PLAINTRANSPORT_RTCP_TUPLE, + FBS::Notification::Body::PlainTransport_RtcpTupleNotification, + notification); + } + inline void PlainTransport::OnUdpSocketPacketReceived( RTC::UdpSocket* socket, const uint8_t* data, size_t len, const struct sockaddr* remoteAddr) { diff --git a/worker/src/RTC/PortManager.cpp b/worker/src/RTC/PortManager.cpp index b8afdfce10..6767ad7f24 100644 --- a/worker/src/RTC/PortManager.cpp +++ b/worker/src/RTC/PortManager.cpp @@ -1,4 +1,4 @@ -#define MS_CLASS "RTC::PortManager" +#define MS_CLASS "PortManager" // #define MS_LOG_DEV_LEVEL 3 #include "RTC/PortManager.hpp" @@ -12,9 +12,16 @@ /* Static methods for UV callbacks. */ -static inline void onClose(uv_handle_t* handle) +// NOTE: We have different onCloseXxx() callbacks to avoid an ASAN warning by +// ensuring that we call `delete xxx` with same type as `new xxx` before. +static inline void onCloseUdp(uv_handle_t* handle) { - delete handle; + delete reinterpret_cast(handle); +} + +static inline void onCloseTcp(uv_handle_t* handle) +{ + delete reinterpret_cast(handle); } inline static void onFakeConnection(uv_stream_t* /*handle*/, int /*status*/) @@ -26,12 +33,12 @@ namespace RTC { /* Class variables. */ - thread_local absl::flat_hash_map> PortManager::mapUdpIpPorts; - thread_local absl::flat_hash_map> PortManager::mapTcpIpPorts; + thread_local absl::flat_hash_map PortManager::mapPortRanges; /* Class methods. */ - uv_handle_t* PortManager::Bind(Transport transport, std::string& ip) + uv_handle_t* PortManager::Bind( + Protocol protocol, std::string& ip, uint16_t port, RTC::Transport::SocketFlags& flags) { MS_TRACE(); @@ -39,26 +46,29 @@ namespace RTC Utils::IP::NormalizeIp(ip); int err; - int family = Utils::IP::GetFamily(ip); - struct sockaddr_storage bindAddr; // NOLINT(cppcoreguidelines-pro-type-member-init) - size_t portIdx; - int flags{ 0 }; - std::vector& ports = PortManager::GetPorts(transport, ip); - size_t attempt{ 0u }; - size_t numAttempts = ports.size(); + const int family = Utils::IP::GetFamily(ip); + struct sockaddr_storage bindAddr + { + }; uv_handle_t* uvHandle{ nullptr }; - uint16_t port; - std::string transportStr; + std::string protocolStr; + const uint8_t bitFlags = ConvertSocketFlags(flags, protocol, family); - switch (transport) + switch (protocol) { - case Transport::UDP: - transportStr.assign("udp"); + case Protocol::UDP: + { + protocolStr.assign("udp"); + break; + } + + case Protocol::TCP: + { + protocolStr.assign("tcp"); - case Transport::TCP: - transportStr.assign("tcp"); break; + } } switch (family) @@ -68,7 +78,9 @@ namespace RTC err = uv_ip4_addr(ip.c_str(), 0, reinterpret_cast(&bindAddr)); if (err != 0) + { MS_THROW_ERROR("uv_ip4_addr() failed: %s", uv_strerror(err)); + } break; } @@ -78,10 +90,222 @@ namespace RTC err = uv_ip6_addr(ip.c_str(), 0, reinterpret_cast(&bindAddr)); if (err != 0) + { MS_THROW_ERROR("uv_ip6_addr() failed: %s", uv_strerror(err)); + } + + break; + } + + // This cannot happen. + default: + { + MS_THROW_ERROR("unknown IP family"); + } + } + + // Set the port into the sockaddr struct. + switch (family) + { + case AF_INET: + { + (reinterpret_cast(&bindAddr))->sin_port = htons(port); + + break; + } + + case AF_INET6: + { + (reinterpret_cast(&bindAddr))->sin6_port = htons(port); + + break; + } + } + + // Try to bind on it. + switch (protocol) + { + case Protocol::UDP: + { + uvHandle = reinterpret_cast(new uv_udp_t()); + err = uv_udp_init_ex( + DepLibUV::GetLoop(), reinterpret_cast(uvHandle), UV_UDP_RECVMMSG); + + break; + } + + case Protocol::TCP: + { + uvHandle = reinterpret_cast(new uv_tcp_t()); + err = uv_tcp_init(DepLibUV::GetLoop(), reinterpret_cast(uvHandle)); + + break; + } + } + + if (err != 0) + { + switch (protocol) + { + case Protocol::UDP: + { + delete reinterpret_cast(uvHandle); + + MS_THROW_ERROR("uv_udp_init_ex() failed: %s", uv_strerror(err)); + + break; + } + + case Protocol::TCP: + { + delete reinterpret_cast(uvHandle); + + MS_THROW_ERROR("uv_tcp_init() failed: %s", uv_strerror(err)); + + break; + } + } + } + + switch (protocol) + { + case Protocol::UDP: + { + err = uv_udp_bind( + reinterpret_cast(uvHandle), + reinterpret_cast(&bindAddr), + bitFlags); + + if (err != 0) + { + // If it failed, close the handle and check the reason. + uv_close(reinterpret_cast(uvHandle), static_cast(onCloseUdp)); + + MS_THROW_ERROR( + "uv_udp_bind() failed [protocol:%s, ip:'%s', port:%" PRIu16 "]: %s", + protocolStr.c_str(), + ip.c_str(), + port, + uv_strerror(err)); + } + + break; + } + + case Protocol::TCP: + { + err = uv_tcp_bind( + reinterpret_cast(uvHandle), + reinterpret_cast(&bindAddr), + bitFlags); + + if (err != 0) + { + // If it failed, close the handle and check the reason. + uv_close(reinterpret_cast(uvHandle), static_cast(onCloseTcp)); + + MS_THROW_ERROR( + "uv_tcp_bind() failed [protocol:%s, ip:'%s', port:%" PRIu16 "]: %s", + protocolStr.c_str(), + ip.c_str(), + port, + uv_strerror(err)); + } + + // uv_tcp_bind() may succeed even if later uv_listen() fails, so + // double check it. + err = uv_listen( + reinterpret_cast(uvHandle), + 256, + static_cast(onFakeConnection)); + + if (err != 0) + { + // If it failed, close the handle and check the reason. + uv_close(reinterpret_cast(uvHandle), static_cast(onCloseTcp)); - // Don't also bind into IPv4 when listening in IPv6. - flags |= UV_UDP_IPV6ONLY; + MS_THROW_ERROR( + "uv_listen() failed [protocol:%s, ip:'%s', port:%" PRIu16 "]: %s", + protocolStr.c_str(), + ip.c_str(), + port, + uv_strerror(err)); + } + + break; + } + } + + MS_DEBUG_DEV( + "bind succeeded [protocol:%s, ip:'%s', port:%" PRIu16 "]", protocolStr.c_str(), ip.c_str(), port); + + return static_cast(uvHandle); + } + + uv_handle_t* PortManager::Bind( + Protocol protocol, + std::string& ip, + uint16_t minPort, + uint16_t maxPort, + RTC::Transport::SocketFlags& flags, + uint64_t& hash) + { + MS_TRACE(); + + if (maxPort < minPort) + { + MS_THROW_TYPE_ERROR("maxPort cannot be less than minPort"); + } + + // First normalize the IP. This may throw if invalid IP. + Utils::IP::NormalizeIp(ip); + + int err; + const int family = Utils::IP::GetFamily(ip); + struct sockaddr_storage bindAddr + { + }; + std::string protocolStr; + + switch (protocol) + { + case Protocol::UDP: + { + protocolStr.assign("udp"); + + break; + } + + case Protocol::TCP: + { + protocolStr.assign("tcp"); + + break; + } + } + + switch (family) + { + case AF_INET: + { + err = uv_ip4_addr(ip.c_str(), 0, reinterpret_cast(&bindAddr)); + + if (err != 0) + { + MS_THROW_ERROR("uv_ip4_addr() failed: %s", uv_strerror(err)); + } + + break; + } + + case AF_INET6: + { + err = uv_ip6_addr(ip.c_str(), 0, reinterpret_cast(&bindAddr)); + + if (err != 0) + { + MS_THROW_ERROR("uv_ip6_addr() failed: %s", uv_strerror(err)); + } break; } @@ -93,9 +317,20 @@ namespace RTC } } + hash = GeneratePortRangeHash(protocol, std::addressof(bindAddr), minPort, maxPort); + + auto& portRange = PortManager::GetOrCreatePortRange(hash, minPort, maxPort); + const size_t numPorts = portRange.ports.size(); + const size_t numAttempts = numPorts; + size_t attempt{ 0u }; + size_t portIdx; + uint16_t port; + uv_handle_t* uvHandle{ nullptr }; + const uint8_t bitFlags = ConvertSocketFlags(flags, protocol, family); + // Choose a random port index to start from. - portIdx = static_cast(Utils::Crypto::GetRandomUInt( - static_cast(0), static_cast(ports.size() - 1))); + portIdx = static_cast( + Utils::Crypto::GetRandomUInt(static_cast(0), static_cast(numPorts - 1))); // Iterate all ports until getting one available. Fail if none found and also // if bind() fails N times in theoretically available ports. @@ -108,32 +343,32 @@ namespace RTC if (attempt > numAttempts) { MS_THROW_ERROR( - "no more available ports [transport:%s, ip:'%s', numAttempt:%zu]", - transportStr.c_str(), + "no more available ports [protocol:%s, ip:'%s', numAttempt:%zu]", + protocolStr.c_str(), ip.c_str(), numAttempts); } // Increase current port index. - portIdx = (portIdx + 1) % ports.size(); + portIdx = (portIdx + 1) % numPorts; // So the corresponding port is the vector position plus the RTC minimum port. - port = static_cast(portIdx + Settings::configuration.rtcMinPort); + port = static_cast(portIdx + minPort); MS_DEBUG_DEV( - "testing port [transport:%s, ip:'%s', port:%" PRIu16 ", attempt:%zu/%zu]", - transportStr.c_str(), + "testing port [protocol:%s, ip:'%s', port:%" PRIu16 ", attempt:%zu/%zu]", + protocolStr.c_str(), ip.c_str(), port, attempt, numAttempts); // Check whether this port is not available. - if (ports[portIdx]) + if (portRange.ports[portIdx]) { MS_DEBUG_DEV( - "port in use, trying again [transport:%s, ip:'%s', port:%" PRIu16 ", attempt:%zu/%zu]", - transportStr.c_str(), + "port in use, trying again [protocol:%s, ip:'%s', port:%" PRIu16 ", attempt:%zu/%zu]", + protocolStr.c_str(), ip.c_str(), port, attempt, @@ -149,59 +384,79 @@ namespace RTC switch (family) { case AF_INET: + { (reinterpret_cast(&bindAddr))->sin_port = htons(port); + break; + } case AF_INET6: + { (reinterpret_cast(&bindAddr))->sin6_port = htons(port); + break; + } } // Try to bind on it. - switch (transport) + switch (protocol) { - case Transport::UDP: + case Protocol::UDP: + { uvHandle = reinterpret_cast(new uv_udp_t()); err = uv_udp_init_ex( DepLibUV::GetLoop(), reinterpret_cast(uvHandle), UV_UDP_RECVMMSG); + break; + } - case Transport::TCP: + case Protocol::TCP: + { uvHandle = reinterpret_cast(new uv_tcp_t()); err = uv_tcp_init(DepLibUV::GetLoop(), reinterpret_cast(uvHandle)); + break; + } } if (err != 0) { - delete uvHandle; - - switch (transport) + switch (protocol) { - case Transport::UDP: + case Protocol::UDP: + { + delete reinterpret_cast(uvHandle); + MS_THROW_ERROR("uv_udp_init_ex() failed: %s", uv_strerror(err)); + break; + } + + case Protocol::TCP: + { + delete reinterpret_cast(uvHandle); - case Transport::TCP: MS_THROW_ERROR("uv_tcp_init() failed: %s", uv_strerror(err)); + break; + } } } - switch (transport) + switch (protocol) { - case Transport::UDP: + case Protocol::UDP: { err = uv_udp_bind( reinterpret_cast(uvHandle), reinterpret_cast(&bindAddr), - flags); + bitFlags); if (err != 0) { MS_WARN_DEV( - "uv_udp_bind() failed [transport:%s, ip:'%s', port:%" PRIu16 ", attempt:%zu/%zu]: %s", - transportStr.c_str(), + "uv_udp_bind() failed [protocol:%s, ip:'%s', port:%" PRIu16 ", attempt:%zu/%zu]: %s", + protocolStr.c_str(), ip.c_str(), port, attempt, @@ -212,18 +467,18 @@ namespace RTC break; } - case Transport::TCP: + case Protocol::TCP: { err = uv_tcp_bind( reinterpret_cast(uvHandle), reinterpret_cast(&bindAddr), - flags); + bitFlags); if (err != 0) { MS_WARN_DEV( - "uv_tcp_bind() failed [transport:%s, ip:'%s', port:%" PRIu16 ", attempt:%zu/%zu]: %s", - transportStr.c_str(), + "uv_tcp_bind() failed [protocol:%s, ip:'%s', port:%" PRIu16 ", attempt:%zu/%zu]: %s", + protocolStr.c_str(), ip.c_str(), port, attempt, @@ -241,8 +496,8 @@ namespace RTC static_cast(onFakeConnection)); MS_WARN_DEV( - "uv_listen() failed [transport:%s, ip:'%s', port:%" PRIu16 ", attempt:%zu/%zu]: %s", - transportStr.c_str(), + "uv_listen() failed [protocol:%s, ip:'%s', port:%" PRIu16 ", attempt:%zu/%zu]: %s", + protocolStr.c_str(), ip.c_str(), port, attempt, @@ -256,10 +511,27 @@ namespace RTC // If it succeeded, exit the loop here. if (err == 0) + { break; + } // If it failed, close the handle and check the reason. - uv_close(reinterpret_cast(uvHandle), static_cast(onClose)); + switch (protocol) + { + case Protocol::UDP: + { + uv_close(reinterpret_cast(uvHandle), static_cast(onCloseUdp)); + + break; + }; + + case Protocol::TCP: + { + uv_close(reinterpret_cast(uvHandle), static_cast(onCloseTcp)); + + break; + } + } switch (err) { @@ -267,9 +539,9 @@ namespace RTC case UV_EMFILE: { MS_THROW_ERROR( - "port bind failed due to too many open files [transport:%s, ip:'%s', port:%" PRIu16 + "port bind failed due to too many open files [protocol:%s, ip:'%s', port:%" PRIu16 ", attempt:%zu/%zu]", - transportStr.c_str(), + protocolStr.c_str(), ip.c_str(), port, attempt, @@ -282,9 +554,9 @@ namespace RTC case UV_EADDRNOTAVAIL: { MS_THROW_ERROR( - "port bind failed due to address not available [transport:%s, ip:'%s', port:%" PRIu16 + "port bind failed due to address not available [protocol:%s, ip:'%s', port:%" PRIu16 ", attempt:%zu/%zu]", - transportStr.c_str(), + protocolStr.c_str(), ip.c_str(), port, attempt, @@ -301,11 +573,14 @@ namespace RTC } // If here, we got an available port. Mark it as unavailable. - ports[portIdx] = true; + portRange.ports[portIdx] = true; + + // Increase number of used ports in the range. + portRange.numUsedPorts++; MS_DEBUG_DEV( - "bind succeeded [transport:%s, ip:'%s', port:%" PRIu16 ", attempt:%zu/%zu]", - transportStr.c_str(), + "bind succeeded [protocol:%s, ip:'%s', port:%" PRIu16 ", attempt:%zu/%zu]", + protocolStr.c_str(), ip.c_str(), port, attempt, @@ -314,349 +589,196 @@ namespace RTC return static_cast(uvHandle); } - uv_handle_t* PortManager::Bind(Transport transport, std::string& ip, uint16_t port) + void PortManager::Unbind(uint64_t hash, uint16_t port) { MS_TRACE(); - // First normalize the IP. This may throw if invalid IP. - Utils::IP::NormalizeIp(ip); + auto it = PortManager::mapPortRanges.find(hash); - int err; - int family = Utils::IP::GetFamily(ip); - struct sockaddr_storage bindAddr; // NOLINT(cppcoreguidelines-pro-type-member-init) - int flags{ 0 }; - uv_handle_t* uvHandle{ nullptr }; - std::string transportStr; - - switch (transport) + // This should not happen. + if (it == PortManager::mapPortRanges.end()) { - case Transport::UDP: - transportStr.assign("udp"); - break; + MS_ERROR("hash %" PRIu64 " doesn't exist in the map", hash); - case Transport::TCP: - transportStr.assign("tcp"); - break; + return; } - switch (family) - { - case AF_INET: - { - err = uv_ip4_addr(ip.c_str(), 0, reinterpret_cast(&bindAddr)); + auto& portRange = it->second; + const auto portIdx = static_cast(port - portRange.minPort); - if (err != 0) - MS_THROW_ERROR("uv_ip4_addr() failed: %s", uv_strerror(err)); + // This should not happen. + MS_ASSERT(portRange.ports.at(portIdx) == true, "port %" PRIu16 " is not used", port); + MS_ASSERT(portRange.numUsedPorts > 0u, "number of used ports is 0"); - break; - } + // Mark the port as available. + portRange.ports[portIdx] = false; - case AF_INET6: - { - err = uv_ip6_addr(ip.c_str(), 0, reinterpret_cast(&bindAddr)); + // Decrease number of used ports in the range. + portRange.numUsedPorts--; - if (err != 0) - MS_THROW_ERROR("uv_ip6_addr() failed: %s", uv_strerror(err)); - - // Don't also bind into IPv4 when listening in IPv6. - flags |= UV_UDP_IPV6ONLY; - - break; - } - - // This cannot happen. - default: - { - MS_THROW_ERROR("unknown IP family"); - } - } - - // Set the port into the sockaddr struct. - switch (family) + // Remove vector if there are no used ports. + if (portRange.numUsedPorts == 0u) { - case AF_INET: - (reinterpret_cast(&bindAddr))->sin_port = htons(port); - break; - - case AF_INET6: - (reinterpret_cast(&bindAddr))->sin6_port = htons(port); - break; + PortManager::mapPortRanges.erase(it); } + } - // Try to bind on it. - switch (transport) - { - case Transport::UDP: - uvHandle = reinterpret_cast(new uv_udp_t()); - err = uv_udp_init_ex( - DepLibUV::GetLoop(), reinterpret_cast(uvHandle), UV_UDP_RECVMMSG); - break; + void PortManager::Dump() + { + MS_DUMP(""); - case Transport::TCP: - uvHandle = reinterpret_cast(new uv_tcp_t()); - err = uv_tcp_init(DepLibUV::GetLoop(), reinterpret_cast(uvHandle)); - break; + for (auto& kv : PortManager::mapPortRanges) + { + auto hash = kv.first; + auto portRange = kv.second; + + MS_DUMP(" "); + MS_DUMP(" hash: %" PRIu64, hash); + MS_DUMP(" minPort: %" PRIu16, portRange.minPort); + MS_DUMP(" maxPort: %zu", portRange.minPort + portRange.ports.size() - 1); + MS_DUMP(" numUsedPorts: %" PRIu16, portRange.numUsedPorts); + MS_DUMP(" "); } - if (err != 0) - { - delete uvHandle; + MS_DUMP(""); + } - switch (transport) - { - case Transport::UDP: - MS_THROW_ERROR("uv_udp_init_ex() failed: %s", uv_strerror(err)); - break; + /* + * Hash for IPv4. + * + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | MIN PORT | MAX PORT | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | IP | IP >> 2 |F|P| + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * Hash for IPv6. + * + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | MIN PORT | MAX PORT | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * |IP[0] ^ IP[1] ^ IP[2] ^ IP[3] | same >> 2 |F|P| + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + uint64_t PortManager::GeneratePortRangeHash( + Protocol protocol, sockaddr_storage* bindAddr, uint16_t minPort, uint16_t maxPort) + { + MS_TRACE(); - case Transport::TCP: - MS_THROW_ERROR("uv_tcp_init() failed: %s", uv_strerror(err)); - break; - } - } + uint64_t hash{ 0u }; - switch (transport) + switch (bindAddr->ss_family) { - case Transport::UDP: + case AF_INET: { - err = uv_udp_bind( - reinterpret_cast(uvHandle), - reinterpret_cast(&bindAddr), - flags); + auto* bindAddrIn = reinterpret_cast(bindAddr); - if (err != 0) - { - // If it failed, close the handle and check the reason. - uv_close(reinterpret_cast(uvHandle), static_cast(onClose)); + // We want it in network order. + const uint64_t address = bindAddrIn->sin_addr.s_addr; - MS_THROW_ERROR( - "uv_udp_bind() failed [transport:%s, ip:'%s', port:%" PRIu16 "]: %s", - transportStr.c_str(), - ip.c_str(), - port, - uv_strerror(err)); - } + hash |= static_cast(minPort) << 48; + hash |= static_cast(maxPort) << 32; + hash |= (address >> 2) << 2; + hash |= 0x0000; // AF_INET. break; } - case Transport::TCP: + case AF_INET6: { - err = uv_tcp_bind( - reinterpret_cast(uvHandle), - reinterpret_cast(&bindAddr), - flags); + auto* bindAddrIn6 = reinterpret_cast(bindAddr); + auto* a = reinterpret_cast(std::addressof(bindAddrIn6->sin6_addr)); - if (err != 0) - { - // If it failed, close the handle and check the reason. - uv_close(reinterpret_cast(uvHandle), static_cast(onClose)); + const auto address = a[0] ^ a[1] ^ a[2] ^ a[3]; - MS_THROW_ERROR( - "uv_tcp_bind() failed [transport:%s, ip:'%s', port:%" PRIu16 "]: %s", - transportStr.c_str(), - ip.c_str(), - port, - uv_strerror(err)); - } - - // uv_tcp_bind() may succeed even if later uv_listen() fails, so - // double check it. - err = uv_listen( - reinterpret_cast(uvHandle), - 256, - static_cast(onFakeConnection)); - - if (err != 0) - { - // If it failed, close the handle and check the reason. - uv_close(reinterpret_cast(uvHandle), static_cast(onClose)); - - MS_THROW_ERROR( - "uv_listen() failed [transport:%s, ip:'%s', port:%" PRIu16 "]: %s", - transportStr.c_str(), - ip.c_str(), - port, - uv_strerror(err)); - } + hash |= static_cast(minPort) << 48; + hash |= static_cast(maxPort) << 32; + hash |= static_cast(address) << 16; + hash |= (static_cast(address) >> 2) << 2; + hash |= 0x0002; // AF_INET6. break; } } - MS_DEBUG_DEV( - "bind succeeded [transport:%s, ip:'%s', port:%" PRIu16 "]", - transportStr.c_str(), - ip.c_str(), - port); + // Override least significant bit with protocol information: + // - If UDP, start with 0. + // - If TCP, start with 1. + if (protocol == Protocol::UDP) + { + hash |= 0x0000; + } + else + { + hash |= 0x0001; + } - return static_cast(uvHandle); + return hash; } - void PortManager::Unbind(Transport transport, std::string& ip, uint16_t port) + PortManager::PortRange& PortManager::GetOrCreatePortRange( + uint64_t hash, uint16_t minPort, uint16_t maxPort) { MS_TRACE(); - if ( - (static_cast(port) < Settings::configuration.rtcMinPort) || - (static_cast(port) > Settings::configuration.rtcMaxPort)) - { - MS_ERROR("given port %" PRIu16 " is out of range", port); - - return; - } - - const size_t portIdx = static_cast(port) - Settings::configuration.rtcMinPort; + auto it = PortManager::mapPortRanges.find(hash); - switch (transport) + // If the hash is already handled, return its port range. + if (it != PortManager::mapPortRanges.end()) { - case Transport::UDP: - { - auto it = PortManager::mapUdpIpPorts.find(ip); - - if (it == PortManager::mapUdpIpPorts.end()) - return; + auto& portRange = it->second; - auto& ports = it->second; - - // Mark the port as available. - ports[portIdx] = false; - - break; - } - - case Transport::TCP: - { - auto it = PortManager::mapTcpIpPorts.find(ip); + return portRange; + } - if (it == PortManager::mapTcpIpPorts.end()) - return; + const uint16_t numPorts = maxPort - minPort + 1; - auto& ports = it->second; + // Emplace a new vector filled with numPorts false values, meaning that + // all ports are available. + auto pair = PortManager::mapPortRanges.emplace( + std::piecewise_construct, std::make_tuple(hash), std::make_tuple(numPorts, minPort)); - // Mark the port as available. - ports[portIdx] = false; + // pair.first is an iterator to the inserted value. + auto& portRange = pair.first->second; - break; - } - } + return portRange; } - std::vector& PortManager::GetPorts(Transport transport, const std::string& ip) + uint8_t PortManager::ConvertSocketFlags(RTC::Transport::SocketFlags& flags, Protocol protocol, int family) { MS_TRACE(); - // Make GCC happy so it does not print: - // "control reaches end of non-void function [-Wreturn-type]" - static std::vector emptyPorts; + uint8_t bitFlags{ 0b00000000 }; - switch (transport) + // Ignore ipv6Only in IPv4, otherwise libuv will throw. + if (flags.ipv6Only && family == AF_INET6) { - case Transport::UDP: + switch (protocol) { - auto it = PortManager::mapUdpIpPorts.find(ip); - - // If the IP is already handled, return its ports vector. - if (it != PortManager::mapUdpIpPorts.end()) + case Protocol::UDP: { - auto& ports = it->second; + bitFlags |= UV_UDP_IPV6ONLY; - return ports; + break; } - // Otherwise add an entry in the map and return it. - const uint16_t numPorts = - Settings::configuration.rtcMaxPort - Settings::configuration.rtcMinPort + 1; - - // Emplace a new vector filled with numPorts false values, meaning that - // all ports are available. - auto pair = PortManager::mapUdpIpPorts.emplace( - std::piecewise_construct, std::make_tuple(ip), std::make_tuple(numPorts, false)); - - // pair.first is an iterator to the inserted value. - auto& ports = pair.first->second; - - return ports; - } - - case Transport::TCP: - { - auto it = PortManager::mapTcpIpPorts.find(ip); - - // If the IP is already handled, return its ports vector. - if (it != PortManager::mapTcpIpPorts.end()) + case Protocol::TCP: { - auto& ports = it->second; + bitFlags |= UV_TCP_IPV6ONLY; - return ports; + break; } - - // Otherwise add an entry in the map and return it. - const uint16_t numPorts = - Settings::configuration.rtcMaxPort - Settings::configuration.rtcMinPort + 1; - - // Emplace a new vector filled with numPorts false values, meaning that - // all ports are available. - auto pair = PortManager::mapTcpIpPorts.emplace( - std::piecewise_construct, std::make_tuple(ip), std::make_tuple(numPorts, false)); - - // pair.first is an iterator to the inserted value. - auto& ports = pair.first->second; - - return ports; } } - return emptyPorts; - } - - void PortManager::FillJson(json& jsonObject) - { - MS_TRACE(); - - // Add udp. - jsonObject["udp"] = json::object(); - auto jsonUdpIt = jsonObject.find("udp"); - - for (auto& kv : PortManager::mapUdpIpPorts) + // Ignore udpReusePort in TCP, otherwise libuv will throw. + if (flags.udpReusePort && protocol == Protocol::UDP) { - const auto& ip = kv.first; - auto& ports = kv.second; - - (*jsonUdpIt)[ip] = json::array(); - auto jsonIpIt = jsonUdpIt->find(ip); - - for (size_t i{ 0 }; i < ports.size(); ++i) - { - if (!ports[i]) - continue; - - auto port = static_cast(i + Settings::configuration.rtcMinPort); - - jsonIpIt->push_back(port); - } + bitFlags |= UV_UDP_REUSEADDR; } - // Add tcp. - jsonObject["tcp"] = json::object(); - auto jsonTcpIt = jsonObject.find("tcp"); - - for (auto& kv : PortManager::mapTcpIpPorts) - { - const auto& ip = kv.first; - auto& ports = kv.second; - - (*jsonTcpIt)[ip] = json::array(); - auto jsonIpIt = jsonTcpIt->find(ip); - - for (size_t i{ 0 }; i < ports.size(); ++i) - { - if (!ports[i]) - continue; - - auto port = static_cast(i + Settings::configuration.rtcMinPort); - - jsonIpIt->emplace_back(port); - } - } + return bitFlags; } } // namespace RTC diff --git a/worker/src/RTC/Producer.cpp b/worker/src/RTC/Producer.cpp index 4b0c353513..e7f93b2792 100644 --- a/worker/src/RTC/Producer.cpp +++ b/worker/src/RTC/Producer.cpp @@ -7,13 +7,10 @@ #include "MediaSoupErrors.hpp" #include "Utils.hpp" #include "RTC/Codecs/Tools.hpp" -#include "RTC/RTCP/FeedbackPs.hpp" -#include "RTC/RTCP/FeedbackRtp.hpp" +#include "RTC/RTCP/Feedback.hpp" #include "RTC/RTCP/XrReceiverReferenceTime.hpp" #include -#include // std::memcpy() -#include // std::ostream_iterator -#include // std::ostringstream +#include // std::memcpy() namespace RTC { @@ -28,46 +25,34 @@ namespace RTC /* Instance methods. */ Producer::Producer( - RTC::Shared* shared, const std::string& id, RTC::Producer::Listener* listener, json& data) - : id(id), shared(shared), listener(listener) + RTC::Shared* shared, + const std::string& id, + RTC::Producer::Listener* listener, + const FBS::Transport::ProduceRequest* data) + : id(id), shared(shared), listener(listener), kind(RTC::Media::Kind(data->kind())) { MS_TRACE(); - auto jsonKindIt = data.find("kind"); - - if (jsonKindIt == data.end() || !jsonKindIt->is_string()) - { - MS_THROW_TYPE_ERROR("missing kind"); - } - // This may throw. - this->kind = RTC::Media::GetKind(jsonKindIt->get()); - - if (this->kind == RTC::Media::Kind::ALL) - { - MS_THROW_TYPE_ERROR("invalid empty kind"); - } + this->rtpParameters = RTC::RtpParameters(data->rtpParameters()); - auto jsonRtpParametersIt = data.find("rtpParameters"); + // Evaluate type. + auto type = RTC::RtpParameters::GetType(this->rtpParameters); - if (jsonRtpParametersIt == data.end() || !jsonRtpParametersIt->is_object()) + if (!type.has_value()) { - MS_THROW_TYPE_ERROR("missing rtpParameters"); + MS_THROW_TYPE_ERROR("invalid RTP parameters"); } - // This may throw. - this->rtpParameters = RTC::RtpParameters(*jsonRtpParametersIt); - - // Evaluate type. - this->type = RTC::RtpParameters::GetType(this->rtpParameters); + this->type = type.value(); // Reserve a slot in rtpStreamByEncodingIdx and rtpStreamsScores vectors // for each RTP stream. this->rtpStreamByEncodingIdx.resize(this->rtpParameters.encodings.size(), nullptr); this->rtpStreamScores.resize(this->rtpParameters.encodings.size(), 0u); - auto& encoding = this->rtpParameters.encodings[0]; - auto* mediaCodec = this->rtpParameters.GetCodecForEncoding(encoding); + auto& encoding = this->rtpParameters.encodings[0]; + const auto* mediaCodec = this->rtpParameters.GetCodecForEncoding(encoding); if (!RTC::Codecs::Tools::IsValidTypeForCodec(this->type, mediaCodec->mimeType)) { @@ -77,102 +62,34 @@ namespace RTC RTC::RtpParameters::GetTypeString(this->type).c_str()); } - auto jsonRtpMappingIt = data.find("rtpMapping"); - - if (jsonRtpMappingIt == data.end() || !jsonRtpMappingIt->is_object()) - { - MS_THROW_TYPE_ERROR("missing rtpMapping"); - } - - auto jsonCodecsIt = jsonRtpMappingIt->find("codecs"); - - if (jsonCodecsIt == jsonRtpMappingIt->end() || !jsonCodecsIt->is_array()) + for (const auto& codec : *data->rtpMapping()->codecs()) { - MS_THROW_TYPE_ERROR("missing rtpMapping.codecs"); - } - - for (auto& codec : *jsonCodecsIt) - { - if (!codec.is_object()) - { - MS_THROW_TYPE_ERROR("wrong entry in rtpMapping.codecs (not an object)"); - } - - auto jsonPayloadTypeIt = codec.find("payloadType"); - - // clang-format off - if ( - jsonPayloadTypeIt == codec.end() || - !Utils::Json::IsPositiveInteger(*jsonPayloadTypeIt) - ) - // clang-format on - { - MS_THROW_TYPE_ERROR("wrong entry in rtpMapping.codecs (missing payloadType)"); - } - - auto jsonMappedPayloadTypeIt = codec.find("mappedPayloadType"); - - // clang-format off - if ( - jsonMappedPayloadTypeIt == codec.end() || - !Utils::Json::IsPositiveInteger(*jsonMappedPayloadTypeIt) - ) - // clang-format on - { - MS_THROW_TYPE_ERROR("wrong entry in rtpMapping.codecs (missing mappedPayloadType)"); - } - - this->rtpMapping.codecs[jsonPayloadTypeIt->get()] = - jsonMappedPayloadTypeIt->get(); + this->rtpMapping.codecs[codec->payloadType()] = codec->mappedPayloadType(); } - auto jsonEncodingsIt = jsonRtpMappingIt->find("encodings"); + const auto* encodings = data->rtpMapping()->encodings(); - if (jsonEncodingsIt == jsonRtpMappingIt->end() || !jsonEncodingsIt->is_array()) - { - MS_THROW_TYPE_ERROR("missing rtpMapping.encodings"); - } - - this->rtpMapping.encodings.reserve(jsonEncodingsIt->size()); + this->rtpMapping.encodings.reserve(encodings->size()); - for (auto& encoding : *jsonEncodingsIt) + for (const auto& encoding : *encodings) { - if (!encoding.is_object()) - { - MS_THROW_TYPE_ERROR("wrong entry in rtpMapping.encodings"); - } - this->rtpMapping.encodings.emplace_back(); auto& encodingMapping = this->rtpMapping.encodings.back(); // ssrc is optional. - auto jsonSsrcIt = encoding.find("ssrc"); - - // clang-format off - if ( - jsonSsrcIt != encoding.end() && - Utils::Json::IsPositiveInteger(*jsonSsrcIt) - ) - // clang-format on + if (encoding->ssrc().has_value()) { - encodingMapping.ssrc = jsonSsrcIt->get(); + encodingMapping.ssrc = encoding->ssrc().value(); } // rid is optional. - auto jsonRidIt = encoding.find("rid"); - - if (jsonRidIt != encoding.end() && jsonRidIt->is_string()) - { - encodingMapping.rid = jsonRidIt->get(); - } - // However ssrc or rid must be present (if more than 1 encoding). // clang-format off if ( - jsonEncodingsIt->size() > 1 && - jsonSsrcIt == encoding.end() && - jsonRidIt == encoding.end() + encodings->size() > 1 && + !encoding->ssrc().has_value() && + !flatbuffers::IsFieldPresent(encoding, FBS::RtpParameters::EncodingMapping::VT_RID) ) // clang-format on { @@ -183,9 +100,9 @@ namespace RTC // clang-format off if ( this->rtpParameters.mid.empty() && - jsonEncodingsIt->size() == 1 && - jsonSsrcIt == encoding.end() && - jsonRidIt == encoding.end() + encodings->size() == 1 && + !encoding->ssrc().has_value() && + !flatbuffers::IsFieldPresent(encoding, FBS::RtpParameters::EncodingMapping::VT_RID) ) // clang-format on { @@ -194,27 +111,15 @@ namespace RTC } // mappedSsrc is mandatory. - auto jsonMappedSsrcIt = encoding.find("mappedSsrc"); - - // clang-format off - if ( - jsonMappedSsrcIt == encoding.end() || - !Utils::Json::IsPositiveInteger(*jsonMappedSsrcIt) - ) - // clang-format on + if (!encoding->mappedSsrc()) { MS_THROW_TYPE_ERROR("wrong entry in rtpMapping.encodings (missing mappedSsrc)"); } - encodingMapping.mappedSsrc = jsonMappedSsrcIt->get(); + encodingMapping.mappedSsrc = encoding->mappedSsrc(); } - auto jsonPausedIt = data.find("paused"); - - if (jsonPausedIt != data.end() && jsonPausedIt->is_boolean()) - { - this->paused = jsonPausedIt->get(); - } + this->paused = data->paused(); // The number of encodings in rtpParameters must match the number of encodings // in rtpMapping. @@ -287,29 +192,27 @@ namespace RTC { this->rtpHeaderExtensionIds.absCaptureTime = exten.id; } + + if (this->rtpHeaderExtensionIds.playoutDelay == 0u && exten.type == RTC::RtpHeaderExtensionUri::Type::PLAYOUT_DELAY) + { + this->rtpHeaderExtensionIds.playoutDelay = exten.id; + } } // Set the RTCP report generation interval. if (this->kind == RTC::Media::Kind::AUDIO) + { this->maxRtcpInterval = RTC::RTCP::MaxAudioIntervalMs; + } else + { this->maxRtcpInterval = RTC::RTCP::MaxVideoIntervalMs; + } // Create a KeyFrameRequestManager. if (this->kind == RTC::Media::Kind::VIDEO) { - auto jsonKeyFrameRequestDelayIt = data.find("keyFrameRequestDelay"); - uint32_t keyFrameRequestDelay = 0u; - - // clang-format off - if ( - jsonKeyFrameRequestDelayIt != data.end() && - jsonKeyFrameRequestDelayIt->is_number_integer() - ) - // clang-format on - { - keyFrameRequestDelay = jsonKeyFrameRequestDelayIt->get(); - } + auto keyFrameRequestDelay = data->keyFrameRequestDelay(); this->keyFrameRequestManager = new RTC::KeyFrameRequestManager(this, keyFrameRequestDelay); } @@ -318,8 +221,7 @@ namespace RTC this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ nullptr, - /*payloadChannelNotificationHandler*/ this); + /*channelNotificationHandler*/ this); } Producer::~Producer() @@ -347,171 +249,141 @@ namespace RTC delete this->keyFrameRequestManager; } - void Producer::FillJson(json& jsonObject) const + flatbuffers::Offset Producer::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Add id. - jsonObject["id"] = this->id; - - // Add kind. - jsonObject["kind"] = RTC::Media::GetString(this->kind); - // Add rtpParameters. - this->rtpParameters.FillJson(jsonObject["rtpParameters"]); - - // Add type. - jsonObject["type"] = RTC::RtpParameters::GetTypeString(this->type); - - // Add rtpMapping. - jsonObject["rtpMapping"] = json::object(); - auto jsonRtpMappingIt = jsonObject.find("rtpMapping"); + auto rtpParameters = this->rtpParameters.FillBuffer(builder); // Add rtpMapping.codecs. - { - (*jsonRtpMappingIt)["codecs"] = json::array(); - auto jsonCodecsIt = jsonRtpMappingIt->find("codecs"); - size_t idx{ 0 }; - - for (auto& kv : this->rtpMapping.codecs) - { - jsonCodecsIt->emplace_back(json::value_t::object); - - auto& jsonEntry = (*jsonCodecsIt)[idx]; - auto payloadType = kv.first; - auto mappedPayloadType = kv.second; - - jsonEntry["payloadType"] = payloadType; - jsonEntry["mappedPayloadType"] = mappedPayloadType; + std::vector> codecs; - ++idx; - } + for (const auto& kv : this->rtpMapping.codecs) + { + codecs.emplace_back(FBS::RtpParameters::CreateCodecMapping(builder, kv.first, kv.second)); } // Add rtpMapping.encodings. - { - (*jsonRtpMappingIt)["encodings"] = json::array(); - auto jsonEncodingsIt = jsonRtpMappingIt->find("encodings"); - - for (size_t i{ 0 }; i < this->rtpMapping.encodings.size(); ++i) - { - jsonEncodingsIt->emplace_back(json::value_t::object); - - auto& jsonEntry = (*jsonEncodingsIt)[i]; - const auto& encodingMapping = this->rtpMapping.encodings[i]; - - if (!encodingMapping.rid.empty()) - jsonEntry["rid"] = encodingMapping.rid; - else - jsonEntry["rid"] = nullptr; + std::vector> encodings; + encodings.reserve(this->rtpMapping.encodings.size()); - if (encodingMapping.ssrc != 0u) - jsonEntry["ssrc"] = encodingMapping.ssrc; - else - jsonEntry["ssrc"] = nullptr; - - jsonEntry["mappedSsrc"] = encodingMapping.mappedSsrc; - } + for (const auto& encodingMapping : this->rtpMapping.encodings) + { + encodings.emplace_back(FBS::RtpParameters::CreateEncodingMappingDirect( + builder, + encodingMapping.rid.c_str(), + encodingMapping.ssrc != 0u ? flatbuffers::Optional(encodingMapping.ssrc) + : flatbuffers::nullopt, + nullptr, /* capability mode. NOTE: Present in NODE*/ + encodingMapping.mappedSsrc)); } + // Build rtpMapping. + auto rtpMapping = FBS::RtpParameters::CreateRtpMappingDirect(builder, &codecs, &encodings); + // Add rtpStreams. - jsonObject["rtpStreams"] = json::array(); - auto jsonRtpStreamsIt = jsonObject.find("rtpStreams"); + std::vector> rtpStreams; - for (auto* rtpStream : this->rtpStreamByEncodingIdx) + for (const auto* rtpStream : this->rtpStreamByEncodingIdx) { if (!rtpStream) + { continue; + } - jsonRtpStreamsIt->emplace_back(json::value_t::object); - - auto& jsonEntry = (*jsonRtpStreamsIt)[jsonRtpStreamsIt->size() - 1]; - - rtpStream->FillJson(jsonEntry); + rtpStreams.emplace_back(rtpStream->FillBuffer(builder)); } - // Add paused. - jsonObject["paused"] = this->paused; - // Add traceEventTypes. - std::vector traceEventTypes; - std::ostringstream traceEventTypesStream; + std::vector traceEventTypes; if (this->traceEventTypes.rtp) - traceEventTypes.emplace_back("rtp"); + { + traceEventTypes.emplace_back(FBS::Producer::TraceEventType::RTP); + } if (this->traceEventTypes.keyframe) - traceEventTypes.emplace_back("keyframe"); + { + traceEventTypes.emplace_back(FBS::Producer::TraceEventType::KEYFRAME); + } if (this->traceEventTypes.nack) - traceEventTypes.emplace_back("nack"); + { + traceEventTypes.emplace_back(FBS::Producer::TraceEventType::NACK); + } if (this->traceEventTypes.pli) - traceEventTypes.emplace_back("pli"); + { + traceEventTypes.emplace_back(FBS::Producer::TraceEventType::PLI); + } if (this->traceEventTypes.fir) - traceEventTypes.emplace_back("fir"); - - if (!traceEventTypes.empty()) { - std::copy( - traceEventTypes.begin(), - traceEventTypes.end() - 1, - std::ostream_iterator(traceEventTypesStream, ",")); - traceEventTypesStream << traceEventTypes.back(); + traceEventTypes.emplace_back(FBS::Producer::TraceEventType::FIR); } - jsonObject["traceEventTypes"] = traceEventTypesStream.str(); + return FBS::Producer::CreateDumpResponseDirect( + builder, + this->id.c_str(), + this->kind == RTC::Media::Kind::AUDIO ? FBS::RtpParameters::MediaKind::AUDIO + : FBS::RtpParameters::MediaKind::VIDEO, + RTC::RtpParameters::TypeToFbs(this->type), + rtpParameters, + rtpMapping, + &rtpStreams, + &traceEventTypes, + this->paused); } - void Producer::FillJsonStats(json& jsonArray) const + flatbuffers::Offset Producer::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) { MS_TRACE(); + std::vector> rtpStreams; + for (auto* rtpStream : this->rtpStreamByEncodingIdx) { if (!rtpStream) + { continue; + } - jsonArray.emplace_back(json::value_t::object); - - auto& jsonEntry = jsonArray[jsonArray.size() - 1]; - - rtpStream->FillJsonStats(jsonEntry); + rtpStreams.emplace_back(rtpStream->FillBufferStats(builder)); } + + return FBS::Producer::CreateGetStatsResponseDirect(builder, &rtpStreams); } void Producer::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::PRODUCER_DUMP: + case Channel::ChannelRequest::Method::PRODUCER_DUMP: { - json data = json::object(); + auto dumpOffset = FillBuffer(request->GetBufferBuilder()); - FillJson(data); - - request->Accept(data); + request->Accept(FBS::Response::Body::Producer_DumpResponse, dumpOffset); break; } - case Channel::ChannelRequest::MethodId::PRODUCER_GET_STATS: + case Channel::ChannelRequest::Method::PRODUCER_GET_STATS: { - json data = json::array(); - - FillJsonStats(data); + auto responseOffset = FillBufferStats(request->GetBufferBuilder()); - request->Accept(data); + request->Accept(FBS::Response::Body::Producer_GetStatsResponse, responseOffset); break; } - case Channel::ChannelRequest::MethodId::PRODUCER_PAUSE: + case Channel::ChannelRequest::Method::PRODUCER_PAUSE: { if (this->paused) { request->Accept(); - return; + break; } // Pause all streams. @@ -533,13 +405,13 @@ namespace RTC break; } - case Channel::ChannelRequest::MethodId::PRODUCER_RESUME: + case Channel::ChannelRequest::Method::PRODUCER_RESUME: { if (!this->paused) { request->Accept(); - return; + break; } // Resume all streams. @@ -574,34 +446,54 @@ namespace RTC break; } - case Channel::ChannelRequest::MethodId::PRODUCER_ENABLE_TRACE_EVENT: + case Channel::ChannelRequest::Method::PRODUCER_ENABLE_TRACE_EVENT: { - auto jsonTypesIt = request->data.find("types"); - - // Disable all if no entries. - if (jsonTypesIt == request->data.end() || !jsonTypesIt->is_array()) - MS_THROW_TYPE_ERROR("wrong types (not an array)"); + const auto* body = request->data->body_as(); // Reset traceEventTypes. struct TraceEventTypes newTraceEventTypes; - for (const auto& type : *jsonTypesIt) + for (const auto& type : *body->events()) { - if (!type.is_string()) - MS_THROW_TYPE_ERROR("wrong type (not a string)"); - - std::string typeStr = type.get(); - - if (typeStr == "rtp") - newTraceEventTypes.rtp = true; - else if (typeStr == "keyframe") - newTraceEventTypes.keyframe = true; - else if (typeStr == "nack") - newTraceEventTypes.nack = true; - else if (typeStr == "pli") - newTraceEventTypes.pli = true; - else if (typeStr == "fir") - newTraceEventTypes.fir = true; + switch (type) + { + case FBS::Producer::TraceEventType::KEYFRAME: + { + newTraceEventTypes.keyframe = true; + + break; + } + case FBS::Producer::TraceEventType::FIR: + { + newTraceEventTypes.fir = true; + + break; + } + case FBS::Producer::TraceEventType::NACK: + { + newTraceEventTypes.nack = true; + + break; + } + case FBS::Producer::TraceEventType::PLI: + { + newTraceEventTypes.pli = true; + + break; + } + case FBS::Producer::TraceEventType::RTP: + { + newTraceEventTypes.rtp = true; + + break; + } + case FBS::Producer::TraceEventType::SR: + { + newTraceEventTypes.sr = true; + + break; + } + } } this->traceEventTypes = newTraceEventTypes; @@ -613,38 +505,41 @@ namespace RTC default: { - MS_THROW_ERROR("unknown method '%s'", request->method.c_str()); + MS_THROW_ERROR("unknown method '%s'", request->methodCStr); } } } - void Producer::HandleNotification(PayloadChannel::PayloadChannelNotification* notification) + void Producer::HandleNotification(Channel::ChannelNotification* notification) { MS_TRACE(); - switch (notification->eventId) + switch (notification->event) { - case PayloadChannel::PayloadChannelNotification::EventId::PRODUCER_SEND: + case Channel::ChannelNotification::Event::PRODUCER_SEND: { - const auto* data = notification->payload; - auto len = notification->payloadLen; + const auto* body = notification->data->body_as(); + auto len = body->data()->size(); // Increase receive transmission. this->listener->OnProducerReceiveData(this, len); if (len > RTC::MtuSize + 100) { - MS_WARN_TAG(rtp, "given RTP packet exceeds maximum size [len:%zu]", len); + MS_WARN_TAG(rtp, "given RTP packet exceeds maximum size [len:%i]", len); break; } - // If this is the first time to receive a RTP packet then allocate the receiving buffer now. + // If this is the first time to receive a RTP packet then allocate the + // receiving buffer now. if (!Producer::buffer) + { Producer::buffer = new uint8_t[RTC::MtuSize + 100]; + } // Copy the received packet into this buffer so it can be expanded later. - std::memcpy(Producer::buffer, data, static_cast(len)); + std::memcpy(Producer::buffer, body->data()->data(), static_cast(len)); RTC::RtpPacket* packet = RTC::RtpPacket::Parse(Producer::buffer, len); @@ -663,7 +558,7 @@ namespace RTC default: { - MS_ERROR("unknown event '%s'", notification->event.c_str()); + MS_ERROR("unknown event '%s'", notification->eventCStr); } } } @@ -672,6 +567,10 @@ namespace RTC { MS_TRACE(); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.producerId = this->id; +#endif + // Reset current packet. this->currentRtpPacket = nullptr; @@ -684,6 +583,10 @@ namespace RTC { MS_WARN_TAG(rtp, "no stream found for received packet [ssrc:%" PRIu32 "]", packet->GetSsrc()); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::RECV_RTP_STREAM_NOT_FOUND); +#endif + return ReceiveRtpPacketResult::DISCARDED; } @@ -703,7 +606,13 @@ namespace RTC { // May have to announce a new RTP stream to the listener. if (this->mapSsrcRtpStream.size() > numRtpStreamsBefore) + { NotifyNewRtpStream(rtpStream); + } + +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::RECV_RTP_STREAM_DISCARDED); +#endif return result; } @@ -716,7 +625,13 @@ namespace RTC // Process the packet. if (!rtpStream->ReceiveRtxPacket(packet)) + { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::RECV_RTP_STREAM_NOT_FOUND); +#endif + return result; + } } // Should not happen. else @@ -734,7 +649,9 @@ namespace RTC // Tell the keyFrameRequestManager. if (this->keyFrameRequestManager) + { this->keyFrameRequestManager->KeyFrameReceived(packet->GetSsrc()); + } } // May have to announce a new RTP stream to the listener. @@ -743,7 +660,9 @@ namespace RTC // Request a key frame for this stream since we may have lost the first packets // (do not do it if this is a key frame). if (this->keyFrameRequestManager && !this->paused && !packet->IsKeyFrame()) + { this->keyFrameRequestManager->ForceKeyFrameNeeded(packet->GetSsrc()); + } // Update current packet. this->currentRtpPacket = packet; @@ -756,14 +675,18 @@ namespace RTC // If paused stop here. if (this->paused) + { return result; + } // May emit 'trace' event. EmitTraceEventRtpAndKeyFrameTypes(packet, isRtx); // Mangle the packet before providing the listener with it. if (!MangleRtpPacket(packet, rtpStream)) + { return ReceiveRtpPacketResult::DISCARDED; + } // Post-process the packet. PostProcessRtpPacket(packet); @@ -781,13 +704,15 @@ namespace RTC if (it != this->mapSsrcRtpStream.end()) { - auto* rtpStream = it->second; - bool first = rtpStream->GetSenderReportNtpMs() == 0; + auto* rtpStream = it->second; + const bool first = rtpStream->GetSenderReportNtpMs() == 0; rtpStream->ReceiveRtcpSenderReport(report); this->listener->OnProducerRtcpSenderReport(this, rtpStream, first); + EmitTraceEventSrType(report); + return; } @@ -829,7 +754,9 @@ namespace RTC MS_TRACE(); if (static_cast((nowMs - this->lastRtcpSentTime) * 1.15) < this->maxRtcpInterval) + { return true; + } std::vector receiverReports; RTCP::ReceiverReferenceTime* receiverReferenceTimeReport{ nullptr }; @@ -844,7 +771,9 @@ namespace RTC auto* rtxReport = rtpStream->GetRtxRtcpReceiverReport(); if (rtxReport) + { receiverReports.push_back(rtxReport); + } } // Add a receiver reference time report if no present in the packet. @@ -859,7 +788,9 @@ namespace RTC // RTCP Compound packet buffer cannot hold the data. if (!packet->Add(receiverReports, receiverReferenceTimeReport)) + { return false; + } this->lastRtcpSentTime = nowMs; @@ -871,7 +802,9 @@ namespace RTC MS_TRACE(); if (!this->keyFrameRequestManager || this->paused) + { return; + } auto it = this->mapMappedSsrcSsrc.find(mappedSsrc); @@ -882,7 +815,7 @@ namespace RTC return; } - uint32_t ssrc = it->second; + const uint32_t ssrc = it->second; // If the current RTP packet is a key frame for the given mapped SSRC do // nothing since we are gonna provide Consumers with the requested key frame @@ -909,8 +842,8 @@ namespace RTC { MS_TRACE(); - uint32_t ssrc = packet->GetSsrc(); - uint8_t payloadType = packet->GetPayloadType(); + const uint32_t ssrc = packet->GetSsrc(); + const uint8_t payloadType = packet->GetPayloadType(); // If stream found in media ssrcs map, return it. { @@ -941,11 +874,11 @@ namespace RTC // First, look for an encoding with matching media or RTX ssrc value. for (size_t i{ 0 }; i < this->rtpParameters.encodings.size(); ++i) { - auto& encoding = this->rtpParameters.encodings[i]; - const auto* mediaCodec = this->rtpParameters.GetCodecForEncoding(encoding); - const auto* rtxCodec = this->rtpParameters.GetRtxCodecForEncoding(encoding); - bool isMediaPacket = (mediaCodec->payloadType == payloadType); - bool isRtxPacket = (rtxCodec && rtxCodec->payloadType == payloadType); + auto& encoding = this->rtpParameters.encodings[i]; + const auto* mediaCodec = this->rtpParameters.GetCodecForEncoding(encoding); + const auto* rtxCodec = this->rtpParameters.GetRtxCodecForEncoding(encoding); + const bool isMediaPacket = (mediaCodec->payloadType == payloadType); + const bool isRtxPacket = (rtxCodec && rtxCodec->payloadType == payloadType); if (isMediaPacket && encoding.ssrc == ssrc) { @@ -995,12 +928,14 @@ namespace RTC auto& encoding = this->rtpParameters.encodings[i]; if (encoding.rid != rid) + { continue; + } - const auto* mediaCodec = this->rtpParameters.GetCodecForEncoding(encoding); - const auto* rtxCodec = this->rtpParameters.GetRtxCodecForEncoding(encoding); - bool isMediaPacket = (mediaCodec->payloadType == payloadType); - bool isRtxPacket = (rtxCodec && rtxCodec->payloadType == payloadType); + const auto* mediaCodec = this->rtpParameters.GetCodecForEncoding(encoding); + const auto* rtxCodec = this->rtpParameters.GetRtxCodecForEncoding(encoding); + const bool isMediaPacket = (mediaCodec->payloadType == payloadType); + const bool isRtxPacket = (rtxCodec && rtxCodec->payloadType == payloadType); if (isMediaPacket) { @@ -1071,11 +1006,11 @@ namespace RTC ) // clang-format on { - auto& encoding = this->rtpParameters.encodings[0]; - const auto* mediaCodec = this->rtpParameters.GetCodecForEncoding(encoding); - const auto* rtxCodec = this->rtpParameters.GetRtxCodecForEncoding(encoding); - bool isMediaPacket = (mediaCodec->payloadType == payloadType); - bool isRtxPacket = (rtxCodec && rtxCodec->payloadType == payloadType); + auto& encoding = this->rtpParameters.encodings[0]; + const auto* mediaCodec = this->rtpParameters.GetCodecForEncoding(encoding); + const auto* rtxCodec = this->rtpParameters.GetRtxCodecForEncoding(encoding); + const bool isMediaPacket = (mediaCodec->payloadType == payloadType); + const bool isRtxPacket = (rtxCodec && rtxCodec->payloadType == payloadType); if (isMediaPacket) { @@ -1134,7 +1069,7 @@ namespace RTC { MS_TRACE(); - uint32_t ssrc = packet->GetSsrc(); + const uint32_t ssrc = packet->GetSsrc(); MS_ASSERT( this->mapSsrcRtpStream.find(ssrc) == this->mapSsrcRtpStream.end(), @@ -1213,8 +1148,13 @@ namespace RTC } } + // Only perform RTP inactivity check on simulcast and only if there are + // more than 1 stream. + auto useRtpInactivityCheck = + this->type == RtpParameters::Type::SIMULCAST && this->rtpMapping.encodings.size() > 1; + // Create a RtpStreamRecv for receiving a media stream. - auto* rtpStream = new RTC::RtpStreamRecv(this, params, SendNackDelay); + auto* rtpStream = new RTC::RtpStreamRecv(this, params, SendNackDelay, useRtpInactivityCheck); // Insert into the maps. this->mapSsrcRtpStream[ssrc] = rtpStream; @@ -1227,7 +1167,9 @@ namespace RTC // If the Producer is paused tell it to the new RtpStreamRecv. if (this->paused) + { rtpStream->Pause(); + } // Emit the first score event right now. EmitScore(); @@ -1242,7 +1184,7 @@ namespace RTC auto mappedSsrc = this->mapRtpStreamMappedSsrc.at(rtpStream); // Notify the listener. - this->listener->OnProducerNewRtpStream(this, static_cast(rtpStream), mappedSsrc); + this->listener->OnProducerNewRtpStream(this, rtpStream, mappedSsrc); } inline void Producer::PreProcessRtpPacket(RTC::RtpPacket* packet) @@ -1263,8 +1205,8 @@ namespace RTC // Mangle the payload type. { - uint8_t payloadType = packet->GetPayloadType(); - auto it = this->rtpMapping.codecs.find(payloadType); + const uint8_t payloadType = packet->GetPayloadType(); + auto it = this->rtpMapping.codecs.find(payloadType); if (it == this->rtpMapping.codecs.end()) { @@ -1273,14 +1215,14 @@ namespace RTC return false; } - uint8_t mappedPayloadType = it->second; + const uint8_t mappedPayloadType = it->second; packet->SetPayloadType(mappedPayloadType); } // Mangle the SSRC. { - uint32_t mappedSsrc = this->mapRtpStreamMappedSsrc.at(rtpStream); + const uint32_t mappedSsrc = this->mapRtpStreamMappedSsrc.at(rtpStream); packet->SetSsrc(mappedSsrc); } @@ -1292,7 +1234,9 @@ namespace RTC // This happens just once. if (extensions.capacity() != 24) + { extensions.reserve(24); + } extensions.clear(); @@ -1325,6 +1269,19 @@ namespace RTC bufferPtr += extenLen; } + // Proxy http://www.webrtc.org/experiments/rtp-hdrext/playout-delay + extenValue = packet->GetExtension(this->rtpHeaderExtensionIds.playoutDelay, extenLen); + + if (extenValue) + { + std::memcpy(bufferPtr, extenValue, extenLen); + + extensions.emplace_back( + static_cast(RTC::RtpHeaderExtensionUri::Type::PLAYOUT_DELAY), extenLen, bufferPtr); + + bufferPtr += extenLen; + } + if (this->kind == RTC::Media::Kind::AUDIO) { // Proxy urn:ietf:params:rtp-hdrext:ssrc-audio-level. @@ -1457,6 +1414,8 @@ namespace RTC static_cast(RTC::RtpHeaderExtensionUri::Type::SSRC_AUDIO_LEVEL)); packet->SetVideoOrientationExtensionId( static_cast(RTC::RtpHeaderExtensionUri::Type::VIDEO_ORIENTATION)); + packet->SetPlayoutDelayExtensionId( + static_cast(RTC::RtpHeaderExtensionUri::Type::PLAYOUT_DELAY)); } return true; @@ -1490,13 +1449,17 @@ namespace RTC this->videoOrientation.flip = flip; this->videoOrientation.rotation = rotation; - json data = json::object(); - - data["camera"] = this->videoOrientation.camera; - data["flip"] = this->videoOrientation.flip; - data["rotation"] = this->videoOrientation.rotation; - - this->shared->channelNotifier->Emit(this->id, "videoorientationchange", data); + auto notification = FBS::Producer::CreateVideoOrientationChangeNotification( + this->shared->channelNotifier->GetBufferBuilder(), + this->videoOrientation.camera, + this->videoOrientation.flip, + this->videoOrientation.rotation); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::PRODUCER_VIDEO_ORIENTATION_CHANGE, + FBS::Notification::Body::Producer_VideoOrientationChangeNotification, + notification); } } } @@ -1506,27 +1469,31 @@ namespace RTC { MS_TRACE(); - json data = json::array(); + std::vector> scores; - for (auto* rtpStream : this->rtpStreamByEncodingIdx) + for (const auto* rtpStream : this->rtpStreamByEncodingIdx) { if (!rtpStream) + { continue; + } - data.emplace_back(json::value_t::object); - - auto& jsonEntry = data[data.size() - 1]; - - jsonEntry["encodingIdx"] = rtpStream->GetEncodingIdx(); - jsonEntry["ssrc"] = rtpStream->GetSsrc(); - - if (!rtpStream->GetRid().empty()) - jsonEntry["rid"] = rtpStream->GetRid(); - - jsonEntry["score"] = rtpStream->GetScore(); + scores.emplace_back(FBS::Producer::CreateScoreDirect( + this->shared->channelNotifier->GetBufferBuilder(), + rtpStream->GetEncodingIdx(), + rtpStream->GetSsrc(), + !rtpStream->GetRid().empty() ? rtpStream->GetRid().c_str() : nullptr, + rtpStream->GetScore())); } - this->shared->channelNotifier->Emit(this->id, "score", data); + auto notification = FBS::Producer::CreateScoreNotificationDirect( + this->shared->channelNotifier->GetBufferBuilder(), &scores); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::PRODUCER_SCORE, + FBS::Notification::Body::Producer_ScoreNotification, + notification); } inline void Producer::EmitTraceEventRtpAndKeyFrameTypes(RTC::RtpPacket* packet, bool isRtx) const @@ -1535,33 +1502,35 @@ namespace RTC if (this->traceEventTypes.keyframe && packet->IsKeyFrame()) { - json data = json::object(); - - data["type"] = "keyframe"; - data["timestamp"] = DepLibUV::GetTimeMs(); - data["direction"] = "in"; - - packet->FillJson(data["info"]); - - if (isRtx) - data["info"]["isRtx"] = true; - - this->shared->channelNotifier->Emit(this->id, "trace", data); + auto rtpPacketDump = packet->FillBuffer(this->shared->channelNotifier->GetBufferBuilder()); + auto traceInfo = FBS::Producer::CreateKeyFrameTraceInfo( + this->shared->channelNotifier->GetBufferBuilder(), rtpPacketDump, isRtx); + + auto notification = FBS::Producer::CreateTraceNotification( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::Producer::TraceEventType::KEYFRAME, + DepLibUV::GetTimeMs(), + FBS::Common::TraceDirection::DIRECTION_IN, + FBS::Producer::TraceInfo::KeyFrameTraceInfo, + traceInfo.Union()); + + EmitTraceEvent(notification); } else if (this->traceEventTypes.rtp) { - json data = json::object(); - - data["type"] = "rtp"; - data["timestamp"] = DepLibUV::GetTimeMs(); - data["direction"] = "in"; - - packet->FillJson(data["info"]); - - if (isRtx) - data["info"]["isRtx"] = true; - - this->shared->channelNotifier->Emit(this->id, "trace", data); + auto rtpPacketDump = packet->FillBuffer(this->shared->channelNotifier->GetBufferBuilder()); + auto traceInfo = FBS::Producer::CreateRtpTraceInfo( + this->shared->channelNotifier->GetBufferBuilder(), rtpPacketDump, isRtx); + + auto notification = FBS::Producer::CreateTraceNotification( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::Producer::TraceEventType::RTP, + DepLibUV::GetTimeMs(), + FBS::Common::TraceDirection::DIRECTION_IN, + FBS::Producer::TraceInfo::RtpTraceInfo, + traceInfo.Union()); + + EmitTraceEvent(notification); } } @@ -1570,16 +1539,22 @@ namespace RTC MS_TRACE(); if (!this->traceEventTypes.pli) + { return; + } - json data = json::object(); + auto traceInfo = + FBS::Producer::CreatePliTraceInfo(this->shared->channelNotifier->GetBufferBuilder(), ssrc); - data["type"] = "pli"; - data["timestamp"] = DepLibUV::GetTimeMs(); - data["direction"] = "out"; - data["info"]["ssrc"] = ssrc; + auto notification = FBS::Producer::CreateTraceNotification( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::Producer::TraceEventType::PLI, + DepLibUV::GetTimeMs(), + FBS::Common::TraceDirection::DIRECTION_OUT, + FBS::Producer::TraceInfo::PliTraceInfo, + traceInfo.Union()); - this->shared->channelNotifier->Emit(this->id, "trace", data); + EmitTraceEvent(notification); } inline void Producer::EmitTraceEventFirType(uint32_t ssrc) const @@ -1587,16 +1562,22 @@ namespace RTC MS_TRACE(); if (!this->traceEventTypes.fir) + { return; + } - json data = json::object(); + auto traceInfo = + FBS::Producer::CreateFirTraceInfo(this->shared->channelNotifier->GetBufferBuilder(), ssrc); - data["type"] = "fir"; - data["timestamp"] = DepLibUV::GetTimeMs(); - data["direction"] = "out"; - data["info"]["ssrc"] = ssrc; + auto notification = FBS::Producer::CreateTraceNotification( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::Producer::TraceEventType::FIR, + DepLibUV::GetTimeMs(), + FBS::Common::TraceDirection::DIRECTION_OUT, + FBS::Producer::TraceInfo::FirTraceInfo, + traceInfo.Union()); - this->shared->channelNotifier->Emit(this->id, "trace", data); + EmitTraceEvent(notification); } inline void Producer::EmitTraceEventNackType() const @@ -1604,16 +1585,58 @@ namespace RTC MS_TRACE(); if (!this->traceEventTypes.nack) + { + return; + } + + auto notification = FBS::Producer::CreateTraceNotification( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::Producer::TraceEventType::NACK, + DepLibUV::GetTimeMs(), + FBS::Common::TraceDirection::DIRECTION_OUT); + + EmitTraceEvent(notification); + } + + inline void Producer::EmitTraceEventSrType(RTC::RTCP::SenderReport* report) const + { + MS_TRACE(); + + if (!this->traceEventTypes.sr) + { return; + } - json data = json::object(); + auto traceInfo = FBS::Producer::CreateSrTraceInfo( + this->shared->channelNotifier->GetBufferBuilder(), + report->GetSsrc(), + report->GetNtpSec(), + report->GetNtpFrac(), + report->GetRtpTs(), + report->GetPacketCount(), + report->GetOctetCount()); + + auto notification = FBS::Producer::CreateTraceNotification( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::Producer::TraceEventType::SR, + DepLibUV::GetTimeMs(), + FBS::Common::TraceDirection::DIRECTION_IN, + FBS::Producer::TraceInfo::SrTraceInfo, + traceInfo.Union()); + + EmitTraceEvent(notification); + } - data["type"] = "nack"; - data["timestamp"] = DepLibUV::GetTimeMs(); - data["direction"] = "out"; - data["info"] = json::object(); + inline void Producer::EmitTraceEvent( + flatbuffers::Offset& notification) const + { + MS_TRACE(); - this->shared->channelNotifier->Emit(this->id, "trace", data); + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::PRODUCER_TRACE, + FBS::Notification::Body::Producer_TraceNotification, + notification); } inline void Producer::OnRtpStreamScore(RTC::RtpStream* rtpStream, uint8_t score, uint8_t previousScore) @@ -1624,7 +1647,8 @@ namespace RTC this->rtpStreamScores[rtpStream->GetEncodingIdx()] = score; // Notify the listener. - this->listener->OnProducerRtpStreamScore(this, rtpStream, score, previousScore); + this->listener->OnProducerRtpStreamScore( + this, static_cast(rtpStream), score, previousScore); // Emit the score event. EmitScore(); diff --git a/worker/src/RTC/RTCP/Bye.cpp b/worker/src/RTC/RTCP/Bye.cpp index 70981cf319..beea0f4273 100644 --- a/worker/src/RTC/RTCP/Bye.cpp +++ b/worker/src/RTC/RTCP/Bye.cpp @@ -42,7 +42,9 @@ namespace RTC offset += 1u; if (length <= len - offset) + { packet->SetReason(std::string(reinterpret_cast(data) + offset, length)); + } } return packet.release(); @@ -75,7 +77,7 @@ namespace RTC } // 32 bits padding. - size_t padding = (-offset) & 3; + const size_t padding = (-offset) & 3; for (size_t i{ 0 }; i < padding; ++i) { @@ -92,10 +94,12 @@ namespace RTC MS_DUMP(""); for (auto ssrc : this->ssrcs) { - MS_DUMP(" ssrc : %" PRIu32, ssrc); + MS_DUMP(" ssrc: %" PRIu32, ssrc); } if (!this->reason.empty()) - MS_DUMP(" reason : %s", this->reason.c_str()); + { + MS_DUMP(" reason: %s", this->reason.c_str()); + } MS_DUMP(""); } } // namespace RTCP diff --git a/worker/src/RTC/RTCP/CompoundPacket.cpp b/worker/src/RTC/RTCP/CompoundPacket.cpp index ff1b875373..4429f4d2f6 100644 --- a/worker/src/RTC/RTCP/CompoundPacket.cpp +++ b/worker/src/RTC/RTCP/CompoundPacket.cpp @@ -18,16 +18,21 @@ namespace RTC { size += this->senderReportPacket.GetSize(); } + if (this->receiverReportPacket.GetCount() > 0u) { size += this->receiverReportPacket.GetSize(); } if (this->sdesPacket.GetCount() > 0u) + { size += this->sdesPacket.GetSize(); + } if (this->xrPacket.Begin() != this->xrPacket.End()) + { size += this->xrPacket.GetSize(); + } return size; } @@ -67,22 +72,39 @@ namespace RTC } bool CompoundPacket::Add( - SenderReport* senderReport, SdesChunk* sdesChunk, DelaySinceLastRr* delaySinceLastRrReport) + SenderReport* senderReport, + SdesChunk* sdesChunk, + DelaySinceLastRr::SsrcInfo* delaySinceLastRrSsrcInfo) { // Add the items into the packet. if (senderReport) + { this->senderReportPacket.AddReport(senderReport); + } if (sdesChunk) + { this->sdesPacket.AddChunk(sdesChunk); + } - if (delaySinceLastRrReport) - this->xrPacket.AddReport(delaySinceLastRrReport); + if (delaySinceLastRrSsrcInfo) + { + // Add a DLRR block into the XR packet if no present. + if (!this->delaySinceLastRr) + { + this->delaySinceLastRr = new RTC::RTCP::DelaySinceLastRr(); + this->xrPacket.AddReport(this->delaySinceLastRr); + } + + this->delaySinceLastRr->AddSsrcInfo(delaySinceLastRrSsrcInfo); + } // New items can hold in the packet, report it. if (GetSize() <= MaxSize) + { return true; + } // New items can not hold in the packet, remove them, // delete and report it. @@ -99,10 +121,10 @@ namespace RTC delete sdesChunk; } - if (delaySinceLastRrReport) + if (delaySinceLastRrSsrcInfo) { - this->xrPacket.RemoveReport(delaySinceLastRrReport); - delete delaySinceLastRrReport; + // NOTE: This method deletes the removed instances in place. + this->delaySinceLastRr->RemoveLastSsrcInfos(1); } return false; @@ -111,22 +133,37 @@ namespace RTC bool CompoundPacket::Add( std::vector& senderReports, std::vector& sdesChunks, - std::vector& delaySinceLastRrReports) + std::vector& delaySinceLastRrSsrcInfos) { // Add the items into the packet. for (auto* report : senderReports) + { this->senderReportPacket.AddReport(report); + } for (auto* chunk : sdesChunks) + { this->sdesPacket.AddChunk(chunk); + } - for (auto* report : delaySinceLastRrReports) - this->xrPacket.AddReport(report); + // Add a DLRR block into the XR packet if no present. + if (!delaySinceLastRrSsrcInfos.empty() && !this->delaySinceLastRr) + { + this->delaySinceLastRr = new RTC::RTCP::DelaySinceLastRr(); + this->xrPacket.AddReport(this->delaySinceLastRr); + } + + for (auto* ssrcInfo : delaySinceLastRrSsrcInfos) + { + this->delaySinceLastRr->AddSsrcInfo(ssrcInfo); + } // New items can hold in the packet, report it. if (GetSize() <= MaxSize) + { return true; + } // New items can not hold in the packet, remove them, // delete and report it. @@ -143,10 +180,10 @@ namespace RTC delete chunk; } - for (auto* report : delaySinceLastRrReports) + if (!delaySinceLastRrSsrcInfos.empty()) { - this->xrPacket.RemoveReport(report); - delete report; + // NOTE: This method deletes the instances in place. + this->delaySinceLastRr->RemoveLastSsrcInfos(delaySinceLastRrSsrcInfos.size()); } return false; @@ -159,14 +196,20 @@ namespace RTC // Add the items into the packet. for (auto* report : receiverReports) + { this->receiverReportPacket.AddReport(report); + } if (receiverReferenceTimeReport) + { this->xrPacket.AddReport(receiverReferenceTimeReport); + } // New items can hold in the packet, report it. if (GetSize() <= MaxSize) + { return true; + } // New items can not hold in the packet, remove them, // delete and report it. @@ -197,7 +240,9 @@ namespace RTC this->senderReportPacket.Dump(); if (this->receiverReportPacket.GetCount() != 0u) + { this->receiverReportPacket.Dump(); + } } else { @@ -205,10 +250,14 @@ namespace RTC } if (this->sdesPacket.GetCount() != 0u) + { this->sdesPacket.Dump(); + } if (this->xrPacket.Begin() != this->xrPacket.End()) + { this->xrPacket.Dump(); + } MS_DUMP(""); } @@ -233,19 +282,5 @@ namespace RTC this->sdesPacket.AddChunk(chunk); } - - void CompoundPacket::AddReceiverReferenceTime(ReceiverReferenceTime* report) - { - MS_TRACE(); - - this->xrPacket.AddReport(report); - } - - void CompoundPacket::AddDelaySinceLastRr(DelaySinceLastRr* report) - { - MS_TRACE(); - - this->xrPacket.AddReport(report); - } } // namespace RTCP } // namespace RTC diff --git a/worker/src/RTC/RTCP/Feedback.cpp b/worker/src/RTC/RTCP/Feedback.cpp index 8d290e10e9..907322dee9 100644 --- a/worker/src/RTC/RTCP/Feedback.cpp +++ b/worker/src/RTC/RTCP/Feedback.cpp @@ -3,7 +3,6 @@ #include "RTC/RTCP/Feedback.hpp" #include "Logger.hpp" -#include "Utils.hpp" #include "RTC/RTCP/FeedbackPsAfb.hpp" #include "RTC/RTCP/FeedbackPsFir.hpp" #include "RTC/RTCP/FeedbackPsLei.hpp" @@ -34,7 +33,9 @@ namespace RTC auto it = FeedbackPacket::type2String.find(type); if (it == FeedbackPacket::type2String.end()) + { return Unknown; + } return it->second; } @@ -61,7 +62,7 @@ namespace RTC } template - FeedbackPacket::~FeedbackPacket() + FeedbackPacket::~FeedbackPacket() { delete[] this->raw; } @@ -86,9 +87,9 @@ namespace RTC { MS_TRACE(); - MS_DUMP(" sender ssrc : %" PRIu32, GetSenderSsrc()); - MS_DUMP(" media ssrc : %" PRIu32, GetMediaSsrc()); - MS_DUMP(" size : %zu", this->GetSize()); + MS_DUMP(" sender ssrc: %" PRIu32, GetSenderSsrc()); + MS_DUMP(" media ssrc: %" PRIu32, GetMediaSsrc()); + MS_DUMP(" size: %zu", this->GetSize()); } /* Specialization for Ps class. */ diff --git a/worker/src/RTC/RTCP/FeedbackPsFir.cpp b/worker/src/RTC/RTCP/FeedbackPsFir.cpp index 1cc6656c9d..d39ff4aed9 100644 --- a/worker/src/RTC/RTCP/FeedbackPsFir.cpp +++ b/worker/src/RTC/RTCP/FeedbackPsFir.cpp @@ -3,7 +3,6 @@ #include "RTC/RTCP/FeedbackPsFir.hpp" #include "Logger.hpp" -#include #include // std::memset() namespace RTC @@ -42,8 +41,8 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" ssrc : %" PRIu32, this->GetSsrc()); - MS_DUMP(" sequence number : %" PRIu8, this->GetSequenceNumber()); + MS_DUMP(" ssrc: %" PRIu32, this->GetSsrc()); + MS_DUMP(" sequence number: %" PRIu8, this->GetSequenceNumber()); MS_DUMP(""); } } // namespace RTCP diff --git a/worker/src/RTC/RTCP/FeedbackPsLei.cpp b/worker/src/RTC/RTCP/FeedbackPsLei.cpp index 8ecc265461..147c469993 100644 --- a/worker/src/RTC/RTCP/FeedbackPsLei.cpp +++ b/worker/src/RTC/RTCP/FeedbackPsLei.cpp @@ -34,7 +34,7 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" ssrc : %" PRIu32, this->GetSsrc()); + MS_DUMP(" ssrc: %" PRIu32, this->GetSsrc()); MS_DUMP(""); } } // namespace RTCP diff --git a/worker/src/RTC/RTCP/FeedbackPsRemb.cpp b/worker/src/RTC/RTCP/FeedbackPsRemb.cpp index 7bb9833d69..cef3adcc45 100644 --- a/worker/src/RTC/RTCP/FeedbackPsRemb.cpp +++ b/worker/src/RTC/RTCP/FeedbackPsRemb.cpp @@ -32,7 +32,9 @@ namespace RTC std::unique_ptr packet(new FeedbackPsRembPacket(commonHeader, len)); if (!packet->IsCorrect()) + { return nullptr; + } return packet.release(); } @@ -145,10 +147,10 @@ namespace RTC MS_DUMP(""); FeedbackPsPacket::Dump(); - MS_DUMP(" bitrate (bps) : %" PRIu64, this->bitrate); + MS_DUMP(" bitrate (bps): %" PRIu64, this->bitrate); for (auto ssrc : this->ssrcs) { - MS_DUMP(" ssrc : %" PRIu32, ssrc); + MS_DUMP(" ssrc: %" PRIu32, ssrc); } MS_DUMP(""); } diff --git a/worker/src/RTC/RTCP/FeedbackPsRpsi.cpp b/worker/src/RTC/RTCP/FeedbackPsRpsi.cpp index 7f87aeb668..746e7b4e7a 100644 --- a/worker/src/RTC/RTCP/FeedbackPsRpsi.cpp +++ b/worker/src/RTC/RTCP/FeedbackPsRpsi.cpp @@ -25,7 +25,7 @@ namespace RTC isCorrect = false; } - size_t paddingBytes = this->header->paddingBits / 8; + const size_t paddingBytes = this->header->paddingBits / 8; if (paddingBytes > FeedbackPsRpsiItem::maxBitStringSize) { @@ -50,7 +50,7 @@ namespace RTC this->header = reinterpret_cast(this->raw); // 32 bits padding. - size_t padding = (-length) & 3; + const size_t padding = (-length) & 3; this->header->paddingBits = padding * 8; this->header->zero = 0; @@ -77,9 +77,9 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" padding bits : %" PRIu8, this->header->paddingBits); - MS_DUMP(" payload type : %" PRIu8, this->GetPayloadType()); - MS_DUMP(" length : %zu", this->GetLength()); + MS_DUMP(" padding bits %" PRIu8, this->header->paddingBits); + MS_DUMP(" payload type: %" PRIu8, this->GetPayloadType()); + MS_DUMP(" length: %zu", this->GetLength()); MS_DUMP(""); } } // namespace RTCP diff --git a/worker/src/RTC/RTCP/FeedbackPsSli.cpp b/worker/src/RTC/RTCP/FeedbackPsSli.cpp index 596ba29ded..feeb2854e8 100644 --- a/worker/src/RTC/RTCP/FeedbackPsSli.cpp +++ b/worker/src/RTC/RTCP/FeedbackPsSli.cpp @@ -3,7 +3,6 @@ #include "RTC/RTCP/FeedbackPsSli.hpp" #include "Logger.hpp" -#include namespace RTC { @@ -26,8 +25,8 @@ namespace RTC size_t FeedbackPsSliItem::Serialize(uint8_t* buffer) { - uint32_t compact = (this->first << 19) | (this->number << 6) | this->pictureId; - auto* header = reinterpret_cast(buffer); + const uint32_t compact = (this->first << 19) | (this->number << 6) | this->pictureId; + auto* header = reinterpret_cast(buffer); header->compact = uint32_t{ htonl(compact) }; std::memcpy(buffer, header, HeaderSize); @@ -40,9 +39,9 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" first : %" PRIu16, this->first); - MS_DUMP(" number : %" PRIu16, this->number); - MS_DUMP(" picture id : %" PRIu8, this->pictureId); + MS_DUMP(" first: %" PRIu16, this->first); + MS_DUMP(" number: %" PRIu16, this->number); + MS_DUMP(" picture id: %" PRIu8, this->pictureId); MS_DUMP(""); } } // namespace RTCP diff --git a/worker/src/RTC/RTCP/FeedbackPsTst.cpp b/worker/src/RTC/RTCP/FeedbackPsTst.cpp index 4f27b3156a..25cf5f69bd 100644 --- a/worker/src/RTC/RTCP/FeedbackPsTst.cpp +++ b/worker/src/RTC/RTCP/FeedbackPsTst.cpp @@ -3,7 +3,7 @@ #include "RTC/RTCP/FeedbackPsTst.hpp" #include "Logger.hpp" -#include +#include // std::memcpy namespace RTC { @@ -41,9 +41,9 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" ssrc : %" PRIu32, this->GetSsrc()); - MS_DUMP(" sequence number : %" PRIu32, this->GetSequenceNumber()); - MS_DUMP(" index : %" PRIu32, this->GetIndex()); + MS_DUMP(" ssrc: %" PRIu32, this->GetSsrc()); + MS_DUMP(" sequence number: %" PRIu32, this->GetSequenceNumber()); + MS_DUMP(" index: %" PRIu32, this->GetIndex()); MS_DUMP(""); } diff --git a/worker/src/RTC/RTCP/FeedbackPsVbcm.cpp b/worker/src/RTC/RTCP/FeedbackPsVbcm.cpp index 49314ce0f3..590e810f76 100644 --- a/worker/src/RTC/RTCP/FeedbackPsVbcm.cpp +++ b/worker/src/RTC/RTCP/FeedbackPsVbcm.cpp @@ -3,7 +3,7 @@ #include "RTC/RTCP/FeedbackPsVbcm.hpp" #include "Logger.hpp" -#include +#include // std::memcpy namespace RTC { @@ -34,9 +34,9 @@ namespace RTC // Copy the content. std::memcpy(buffer + 8, this->header->value, GetLength()); - size_t offset = 8 + GetLength(); + const size_t offset = 8 + GetLength(); // 32 bits padding. - size_t padding = (-offset) & 3; + const size_t padding = (-offset) & 3; for (size_t i{ 0 }; i < padding; ++i) { @@ -51,10 +51,10 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" ssrc : %" PRIu32, this->GetSsrc()); - MS_DUMP(" sequence number : %" PRIu8, this->GetSequenceNumber()); - MS_DUMP(" payload type : %" PRIu8, this->GetPayloadType()); - MS_DUMP(" length : %" PRIu16, this->GetLength()); + MS_DUMP(" ssrc: %" PRIu32, this->GetSsrc()); + MS_DUMP(" sequence number: %" PRIu8, this->GetSequenceNumber()); + MS_DUMP(" payload type: %" PRIu8, this->GetPayloadType()); + MS_DUMP(" length: %" PRIu16, this->GetLength()); MS_DUMP(""); } } // namespace RTCP diff --git a/worker/src/RTC/RTCP/FeedbackRtpEcn.cpp b/worker/src/RTC/RTCP/FeedbackRtpEcn.cpp index 096d0c9084..26f6105480 100644 --- a/worker/src/RTC/RTCP/FeedbackRtpEcn.cpp +++ b/worker/src/RTC/RTCP/FeedbackRtpEcn.cpp @@ -3,7 +3,7 @@ #include "RTC/RTCP/FeedbackRtpEcn.hpp" #include "Logger.hpp" -#include +#include // std::memcpy namespace RTC { @@ -24,13 +24,13 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" sequence number : %" PRIu32, this->GetSequenceNumber()); - MS_DUMP(" ect0 counter : %" PRIu32, this->GetEct0Counter()); - MS_DUMP(" ect1 counter : %" PRIu32, this->GetEct1Counter()); - MS_DUMP(" ecn ce counter : %" PRIu16, this->GetEcnCeCounter()); - MS_DUMP(" not ect counter : %" PRIu16, this->GetNotEctCounter()); - MS_DUMP(" lost packets : %" PRIu16, this->GetLostPackets()); - MS_DUMP(" duplicated packets : %" PRIu16, this->GetDuplicatedPackets()); + MS_DUMP(" sequence number: %" PRIu32, this->GetSequenceNumber()); + MS_DUMP(" ect0 counter: %" PRIu32, this->GetEct0Counter()); + MS_DUMP(" ect1 counter: %" PRIu32, this->GetEct1Counter()); + MS_DUMP(" ecn ce counter: %" PRIu16, this->GetEcnCeCounter()); + MS_DUMP(" not ect counter: %" PRIu16, this->GetNotEctCounter()); + MS_DUMP(" lost packets: %" PRIu16, this->GetLostPackets()); + MS_DUMP(" duplicated packets: %" PRIu16, this->GetDuplicatedPackets()); MS_DUMP(""); } } // namespace RTCP diff --git a/worker/src/RTC/RTCP/FeedbackRtpNack.cpp b/worker/src/RTC/RTCP/FeedbackRtpNack.cpp index f0e153cd15..c3876051e4 100644 --- a/worker/src/RTC/RTCP/FeedbackRtpNack.cpp +++ b/worker/src/RTC/RTCP/FeedbackRtpNack.cpp @@ -3,8 +3,8 @@ #include "RTC/RTCP/FeedbackRtpNack.hpp" #include "Logger.hpp" -#include // std::bitset() -#include +#include // std::bitset() +#include // std::memcpy namespace RTC { @@ -37,8 +37,8 @@ namespace RTC std::bitset<16> nackBitset(this->GetLostPacketBitmask()); MS_DUMP(""); - MS_DUMP(" pid : %" PRIu16, this->GetPacketId()); - MS_DUMP(" bpl : %s", nackBitset.to_string().c_str()); + MS_DUMP(" pid: %" PRIu16, this->GetPacketId()); + MS_DUMP(" bpl: %s", nackBitset.to_string().c_str()); MS_DUMP(""); } } // namespace RTCP diff --git a/worker/src/RTC/RTCP/FeedbackRtpTllei.cpp b/worker/src/RTC/RTCP/FeedbackRtpTllei.cpp index d02b9eed61..4f7087dace 100644 --- a/worker/src/RTC/RTCP/FeedbackRtpTllei.cpp +++ b/worker/src/RTC/RTCP/FeedbackRtpTllei.cpp @@ -3,7 +3,7 @@ #include "RTC/RTCP/FeedbackRtpTllei.hpp" #include "Logger.hpp" -#include +#include // std::memcpy namespace RTC { diff --git a/worker/src/RTC/RTCP/FeedbackRtpTmmb.cpp b/worker/src/RTC/RTCP/FeedbackRtpTmmb.cpp index fd8234e993..f443e64c42 100644 --- a/worker/src/RTC/RTCP/FeedbackRtpTmmb.cpp +++ b/worker/src/RTC/RTCP/FeedbackRtpTmmb.cpp @@ -4,7 +4,6 @@ #include "RTC/RTCP/FeedbackRtpTmmb.hpp" #include "Logger.hpp" #include "Utils.hpp" -#include namespace RTC { @@ -23,10 +22,10 @@ namespace RTC this->ssrc = Utils::Byte::Get4Bytes(data, 0); // Read the 4 bytes block. - uint32_t compact = Utils::Byte::Get4Bytes(data, 4); + const uint32_t compact = Utils::Byte::Get4Bytes(data, 4); // Read each component. - uint8_t exponent = compact >> 26; // 6 bits. - uint64_t mantissa = (compact >> 9) & 0x1ffff; // 17 bits. + const uint8_t exponent = compact >> 26; // 6 bits. + const uint64_t mantissa = (compact >> 9) & 0x1ffff; // 17 bits. this->overhead = compact & 0x1ff; // 9 bits. // Get the bitrate out of exponent and mantissa. @@ -69,9 +68,9 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" ssrc : %" PRIu32, this->GetSsrc()); - MS_DUMP(" bitrate : %" PRIu64, this->GetBitrate()); - MS_DUMP(" overhead : %" PRIu16, this->GetOverhead()); + MS_DUMP(" ssrc: %" PRIu32, this->GetSsrc()); + MS_DUMP(" bitrate: %" PRIu64, this->GetBitrate()); + MS_DUMP(" overhead: %" PRIu16, this->GetOverhead()); MS_DUMP(""); } diff --git a/worker/src/RTC/RTCP/FeedbackRtpTransport.cpp b/worker/src/RTC/RTCP/FeedbackRtpTransport.cpp index b1c99f034c..cf85d1153b 100644 --- a/worker/src/RTC/RTCP/FeedbackRtpTransport.cpp +++ b/worker/src/RTC/RTCP/FeedbackRtpTransport.cpp @@ -5,8 +5,7 @@ #include "Logger.hpp" #include "Utils.hpp" #include "RTC/SeqManager.hpp" -#include // std::numeric_limits() -#include +#include // std::ostringstream namespace RTC { @@ -48,7 +47,9 @@ namespace RTC new FeedbackRtpTransportPacket(commonHeader, len)); if (!packet->IsCorrect()) + { return nullptr; + } return packet.release(); } @@ -60,7 +61,7 @@ namespace RTC { MS_TRACE(); - size_t len = static_cast(ntohs(commonHeader->length) + 1) * 4; + const size_t len = static_cast(ntohs(commonHeader->length) + 1) * 4; if (len > availableLen) { @@ -77,15 +78,15 @@ namespace RTC this->baseSequenceNumber = Utils::Byte::Get2Bytes(data, 0); this->packetStatusCount = Utils::Byte::Get2Bytes(data, 2); - this->referenceTime = Utils::Byte::Get3Bytes(data, 4); + this->referenceTime = Utils::Byte::Get3BytesSigned(data, 4); this->feedbackPacketCount = Utils::Byte::Get1Byte(data, 7); this->size = len; // Make contentData point to the beginning of the chunks. uint8_t* contentData = data + FeedbackRtpTransportPacket::fixedHeaderSize; // Make contentLen be the available length for chunks. - size_t contentLen = len - Packet::CommonHeaderSize - FeedbackPacket::HeaderSize - - FeedbackRtpTransportPacket::fixedHeaderSize; + const size_t contentLen = len - Packet::CommonHeaderSize - FeedbackPacket::HeaderSize - + FeedbackRtpTransportPacket::fixedHeaderSize; size_t offset{ 0u }; uint16_t count{ 0u }; uint16_t receivedPacketStatusCount{ 0u }; @@ -178,11 +179,11 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" base sequence : %" PRIu16, this->baseSequenceNumber); - MS_DUMP(" packet status count : %" PRIu16, this->packetStatusCount); - MS_DUMP(" reference time : %" PRIi32, this->referenceTime); - MS_DUMP(" feedback packet count : %" PRIu8, this->feedbackPacketCount); - MS_DUMP(" size : %zu", GetSize()); + MS_DUMP(" base sequence: %" PRIu16, this->baseSequenceNumber); + MS_DUMP(" packet status count: %" PRIu16, this->packetStatusCount); + MS_DUMP(" reference time: %" PRIi32, this->referenceTime); + MS_DUMP(" feedback packet count: %" PRIu8, this->feedbackPacketCount); + MS_DUMP(" size: %zu", GetSize()); for (auto* chunk : this->chunks) { @@ -236,7 +237,7 @@ namespace RTC offset += 2; // Reference time. - Utils::Byte::Set3Bytes(buffer, offset, static_cast(this->referenceTime)); + Utils::Byte::Set3BytesSigned(buffer, offset, this->referenceTime); offset += 3; // Feedback packet count. @@ -265,7 +266,7 @@ namespace RTC } // 32 bits padding. - size_t padding = (-offset) & 3; + const size_t padding = (-offset) & 3; for (size_t i{ 0u }; i < padding; ++i) { @@ -277,26 +278,29 @@ namespace RTC return offset; } + void FeedbackRtpTransportPacket::SetBase(uint16_t sequenceNumber, uint64_t timestamp) + { + MS_TRACE(); + + MS_ASSERT(!this->baseSet, "base already set"); + + this->baseSet = true; + this->baseSequenceNumber = sequenceNumber; + this->referenceTime = static_cast((timestamp & 0x1FFFFFC0) / 64); + this->latestSequenceNumber = sequenceNumber - 1; + this->latestTimestamp = (timestamp >> 6) * 64; // IMPORTANT: Loose precision. + } + FeedbackRtpTransportPacket::AddPacketResult FeedbackRtpTransportPacket::AddPacket( uint16_t sequenceNumber, uint64_t timestamp, size_t maxRtcpPacketLen) { MS_TRACE(); + MS_ASSERT(this->baseSet, "base not set"); MS_ASSERT(!IsFull(), "packet is full"); - // Let's see if we must set our base. - if (this->latestTimestamp == 0u) - { - this->baseSequenceNumber = sequenceNumber + 1; - this->referenceTime = static_cast((timestamp & 0x1FFFFFC0) / 64); - this->latestSequenceNumber = sequenceNumber; - this->latestTimestamp = (timestamp >> 6) * 64; // IMPORTANT: Loose precision. - - return AddPacketResult::SUCCESS; - } - - // If the wide sequence number of the new packet is lower than the latest seen, - // ignore it. + // If the wide sequence number of the new packet is lower than the latest + // seen, ignore it. // NOTE: Not very spec compliant but libwebrtc does it. // Also ignore if the sequence number matches the latest seen. if (!RTC::SeqManager::IsSeqHigherThan(sequenceNumber, this->latestSequenceNumber)) @@ -306,7 +310,9 @@ namespace RTC // Check if there are too many missing packets. { - auto missingPackets = sequenceNumber - (this->latestSequenceNumber + 1); + // NOTE: We CANNOT use auto here, we must use uint16_t. Otherwise this is a bug. + // https://github.com/versatica/mediasoup/issues/1385#issuecomment-2084982087 + const uint16_t missingPackets = sequenceNumber - (this->latestSequenceNumber + 1); if (missingPackets > FeedbackRtpTransportPacket::maxMissingPackets) { @@ -318,7 +324,7 @@ namespace RTC // Deltas are represented as multiples of 250 us. // NOTE: Read it as int 64 to detect long elapsed times. - int64_t delta64 = (timestamp - this->latestTimestamp) * 4; + const int64_t delta64 = (timestamp - this->latestTimestamp) * 4; // clang-format off if ( @@ -338,7 +344,8 @@ namespace RTC // Delta in 16 bits signed. auto delta = static_cast(delta64); - // Check whether another chunks and corresponding delta infos could be added. + // Check whether another chunks and corresponding delta infos could be + // added. { // Fixed packet size. size_t size = FeedbackRtpPacket::GetSize(); @@ -399,7 +406,9 @@ namespace RTC auto& packetResult = packetResults[idx]; if (!packetResult.received) + { continue; + } currentReceivedAtMs += this->deltas.at(deltaIdx) / 4; packetResult.delta = this->deltas.at(deltaIdx); @@ -414,11 +423,13 @@ namespace RTC { MS_TRACE(); - uint16_t expected = this->packetStatusCount; + const uint16_t expected = this->packetStatusCount; uint16_t lost{ 0u }; if (expected == 0u) + { return 0u; + } for (auto* chunk : this->chunks) { @@ -428,7 +439,9 @@ namespace RTC // NOTE: If lost equals expected, the math below would produce 256, which // becomes 0 in uint8_t. if (lost == expected) + { return 255u; + } return (lost << 8) / expected; } @@ -487,9 +500,13 @@ namespace RTC Status status; if (delta >= 0 && delta <= 255) + { status = Status::SmallDelta; + } else + { status = Status::LargeDelta; + } // Create a long run chunk before processing this packet, if needed. // clang-format off @@ -576,7 +593,9 @@ namespace RTC { // No pending status packets. if (this->context.statuses.empty()) + { return; + } if (this->context.allSameStatus) { @@ -604,8 +623,8 @@ namespace RTC return nullptr; } - auto bytes = Utils::Byte::Get2Bytes(data, 0); - uint8_t chunkType = (bytes >> 15) & 0x01; + auto bytes = Utils::Byte::Get2Bytes(data, 0); + const uint8_t chunkType = (bytes >> 15) & 0x01; // Run length chunk. if (chunkType == 0) @@ -635,12 +654,16 @@ namespace RTC // Vector chunk. else { - uint8_t symbolSize = data[0] & 0x40; + const uint8_t symbolSize = data[0] & 0x40; if (symbolSize == 0) + { return new OneBitVectorChunk(bytes, count); + } else + { return new TwoBitVectorChunk(bytes, count); + } } return nullptr; @@ -707,8 +730,8 @@ namespace RTC MS_TRACE(); MS_DUMP(" "); - MS_DUMP(" status : %s", FeedbackRtpTransportPacket::status2String[this->status].c_str()); - MS_DUMP(" count : %" PRIu16, this->count); + MS_DUMP(" status: %s", FeedbackRtpTransportPacket::status2String[this->status].c_str()); + MS_DUMP(" count: %" PRIu16, this->count); MS_DUMP(" "); } @@ -717,9 +740,13 @@ namespace RTC MS_TRACE(); if (this->status == Status::SmallDelta || this->status == Status::LargeDelta) + { return this->count; + } else + { return 0u; + } } void FeedbackRtpTransportPacket::RunLengthChunk::FillResults( @@ -728,7 +755,8 @@ namespace RTC { MS_TRACE(); - bool received = (this->status == Status::SmallDelta || this->status == Status::LargeDelta); + const bool received = + (this->status == Status::SmallDelta || this->status == Status::LargeDelta); for (uint16_t count{ 1u }; count <= this->count; ++count) { @@ -837,7 +865,9 @@ namespace RTC for (auto status : this->statuses) { if (status == Status::SmallDelta || status == Status::LargeDelta) + { count++; + } } return count; @@ -851,7 +881,7 @@ namespace RTC for (auto status : this->statuses) { - bool received = (status == Status::SmallDelta || status == Status::LargeDelta); + const bool received = (status == Status::SmallDelta || status == Status::LargeDelta); packetResults.emplace_back(++currentSequenceNumber, received); } @@ -975,7 +1005,9 @@ namespace RTC for (auto status : this->statuses) { if (status == Status::SmallDelta || status == Status::LargeDelta) + { count++; + } } return count; @@ -989,7 +1021,7 @@ namespace RTC for (auto status : this->statuses) { - bool received = (status == Status::SmallDelta || status == Status::LargeDelta); + const bool received = (status == Status::SmallDelta || status == Status::LargeDelta); packetResults.emplace_back(++currentSequenceNumber, received); } diff --git a/worker/src/RTC/RTCP/Packet.cpp b/worker/src/RTC/RTCP/Packet.cpp index d2f38e8c0d..0cae29f65e 100644 --- a/worker/src/RTC/RTCP/Packet.cpp +++ b/worker/src/RTC/RTCP/Packet.cpp @@ -54,8 +54,8 @@ namespace RTC return first; } - auto* header = const_cast(reinterpret_cast(data)); - size_t packetLen = static_cast(ntohs(header->length) + 1) * 4; + auto* header = const_cast(reinterpret_cast(data)); + const size_t packetLen = static_cast(ntohs(header->length) + 1) * 4; if (len < packetLen) { @@ -76,14 +76,18 @@ namespace RTC current = SenderReportPacket::Parse(data, packetLen); if (!current) + { break; + } if (header->count > 0) { Packet* rr = ReceiverReportPacket::Parse(data, packetLen, current->GetSize()); if (!rr) + { break; + } current->SetNext(rr); } @@ -172,9 +176,13 @@ namespace RTC len -= packetLen; if (!first) + { first = current; + } else + { last->SetNext(current); + } last = current->GetNext() != nullptr ? current->GetNext() : current; } @@ -191,7 +199,9 @@ namespace RTC auto it = Packet::type2String.find(type); if (it == Packet::type2String.end()) + { return Unknown; + } return it->second; } @@ -204,7 +214,7 @@ namespace RTC this->header = reinterpret_cast(buffer); - size_t length = (GetSize() / 4) - 1; + const size_t length = (GetSize() / 4) - 1; // Fill the common header. this->header->version = 2; diff --git a/worker/src/RTC/RTCP/ReceiverReport.cpp b/worker/src/RTC/RTCP/ReceiverReport.cpp index 761f6dcd49..e90938a6f6 100644 --- a/worker/src/RTC/RTCP/ReceiverReport.cpp +++ b/worker/src/RTC/RTCP/ReceiverReport.cpp @@ -4,7 +4,7 @@ #include "RTC/RTCP/ReceiverReport.hpp" #include "Logger.hpp" #include "Utils.hpp" -#include +#include // std::memcpy namespace RTC { @@ -37,13 +37,13 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" ssrc : %" PRIu32, GetSsrc()); - MS_DUMP(" fraction lost : %" PRIu8, GetFractionLost()); - MS_DUMP(" total lost : %" PRIu32, GetTotalLost()); - MS_DUMP(" last seq : %" PRIu32, GetLastSeq()); - MS_DUMP(" jitter : %" PRIu32, GetJitter()); - MS_DUMP(" lsr : %" PRIu32, GetLastSenderReport()); - MS_DUMP(" dlsr : %" PRIu32, GetDelaySinceLastSenderReport()); + MS_DUMP(" ssrc: %" PRIu32, GetSsrc()); + MS_DUMP(" fraction lost: %" PRIu8, GetFractionLost()); + MS_DUMP(" total lost: %" PRIu32, GetTotalLost()); + MS_DUMP(" last seq: %" PRIu32, GetLastSeq()); + MS_DUMP(" jitter: %" PRIu32, GetJitter()); + MS_DUMP(" lsr: %" PRIu32, GetLastSenderReport()); + MS_DUMP(" dlsr: %" PRIu32, GetDelaySinceLastSenderReport()); MS_DUMP(""); } @@ -86,13 +86,15 @@ namespace RTC std::unique_ptr packet(new ReceiverReportPacket(header)); - uint32_t ssrc = + const uint32_t ssrc = Utils::Byte::Get4Bytes(reinterpret_cast(header), Packet::CommonHeaderSize); packet->SetSsrc(ssrc); if (offset == 0) + { offset = Packet::CommonHeaderSize + 4u /* ssrc */; + } uint8_t count = header->count; diff --git a/worker/src/RTC/RTCP/Sdes.cpp b/worker/src/RTC/RTCP/Sdes.cpp index 574df0b4dc..7c7d9c1ef8 100644 --- a/worker/src/RTC/RTCP/Sdes.cpp +++ b/worker/src/RTC/RTCP/Sdes.cpp @@ -4,7 +4,7 @@ #include "RTC/RTCP/Sdes.hpp" #include "Logger.hpp" #include "Utils.hpp" -#include +#include // std::memcpy namespace RTC { @@ -36,17 +36,21 @@ namespace RTC // Get the header. auto* header = const_cast(reinterpret_cast(data)); + // If item type is 0, there is no need for length field (unless padding + // is needed). + if (len > 0 && header->type == SdesItem::Type::END) + { + return nullptr; + } + // data size must be >= header + length value. - if (len < HeaderSize || len < (1u * 2) + header->length) + if (len < HeaderSize || len < HeaderSize + header->length) { MS_WARN_TAG(rtcp, "not enough space for SDES item, discarded"); return nullptr; } - if (header->type == SdesItem::Type::END) - return nullptr; - return new SdesItem(header); } @@ -57,7 +61,9 @@ namespace RTC auto it = SdesItem::type2String.find(type); if (it == SdesItem::type2String.end()) + { return Unknown; + } return it->second; } @@ -86,9 +92,9 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" type : %s", SdesItem::Type2String(this->GetType()).c_str()); - MS_DUMP(" length : %" PRIu8, this->header->length); - MS_DUMP(" value : %.*s", this->header->length, this->header->value); + MS_DUMP(" type: %s", SdesItem::Type2String(this->GetType()).c_str()); + MS_DUMP(" length: %" PRIu8, this->header->length); + MS_DUMP(" value: %.*s", this->header->length, this->header->value); MS_DUMP(""); } @@ -119,23 +125,81 @@ namespace RTC return nullptr; } - uint32_t ssrc = Utils::Byte::Get4Bytes(data, 0); + const uint32_t ssrc = Utils::Byte::Get4Bytes(data, 0); std::unique_ptr chunk(new SdesChunk(ssrc)); - size_t offset = 4u /* ssrc */; + size_t offset{ 4u }; /* ssrc */ + size_t chunkLength{ 4u }; while (len > offset) { - SdesItem* item = SdesItem::Parse(data + offset, len - offset); + auto* item = SdesItem::Parse(data + offset, len - offset); if (!item) + { break; + } chunk->AddItem(item); + chunkLength += item->GetSize(); offset += item->GetSize(); } + // Once all items have been parsed, there must be 1, 2, 3 or 4 null octets + // (this is mandatory). The first one (AKA item type 0) means "end of + // items", and the others maybe needed for padding the chunk to 4 bytes. + + // First ensure that there is at least one null octet. + if (offset == len || (offset < len && Utils::Byte::Get1Byte(data, offset) != 0u)) + { + MS_WARN_TAG(rtcp, "invalid SDES chunk (missing mandatory null octet), discarded"); + + return nullptr; + } + + // So we have the mandatory null octet. + ++chunkLength; + ++offset; + + // Then check that there are 0, 1, 2 or 3 (no more) null octets that pad + // the chunk to 4 bytes. + uint16_t neededAdditionalNullOctets = + Utils::Byte::PadTo4Bytes(static_cast(chunkLength)) - + static_cast(chunkLength); + uint16_t foundAdditionalNullOctets{ 0u }; + + for (uint16_t i{ 0u }; len > offset && i < neededAdditionalNullOctets; ++i) + { + if (Utils::Byte::Get1Byte(data, offset) != 0u) + { + MS_WARN_TAG( + rtcp, + "invalid SDES chunk (missing additional null octets [needed:%" PRIu16 ", found:%" PRIu16 + "]), discarded", + neededAdditionalNullOctets, + foundAdditionalNullOctets); + + return nullptr; + } + + ++foundAdditionalNullOctets; + ++chunkLength; + ++offset; + } + + if (foundAdditionalNullOctets != neededAdditionalNullOctets) + { + MS_WARN_TAG( + rtcp, + "invalid SDES chunk (missing additional null octets [needed:%" PRIu16 ", found:%" PRIu16 + "]), discarded", + neededAdditionalNullOctets, + foundAdditionalNullOctets); + + return nullptr; + } + return chunk.release(); } @@ -155,10 +219,15 @@ namespace RTC offset += item->Serialize(buffer + offset); } + // Add the mandatory null octet. + buffer[offset] = 0; + + ++offset; + // 32 bits padding. - size_t padding = (-offset) & 3; + const size_t padding = (-offset) & 3; - for (size_t i{ 0 }; i < padding; ++i) + for (size_t i{ 0u }; i < padding; ++i) { buffer[offset + i] = 0; } @@ -171,7 +240,7 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" ssrc : %" PRIu32, this->ssrc); + MS_DUMP(" ssrc: %" PRIu32, this->ssrc); for (auto* item : this->items) { item->Dump(); @@ -195,9 +264,9 @@ namespace RTC size_t offset = Packet::CommonHeaderSize; uint8_t count = header->count; - while ((count-- != 0u) && (len > offset)) + while (count-- && (len > offset)) { - SdesChunk* chunk = SdesChunk::Parse(data + offset, len - offset); + auto* chunk = SdesChunk::Parse(data + offset, len - offset); if (chunk != nullptr) { @@ -206,10 +275,17 @@ namespace RTC } else { - return packet.release(); + break; } } + if (packet->GetCount() != header->count) + { + MS_WARN_TAG(rtcp, "RTCP count value doesn't match found number of chunks, discarded"); + + return nullptr; + } + return packet.release(); } @@ -223,7 +299,7 @@ namespace RTC size_t length = 0; uint8_t* header = { nullptr }; - for (size_t i = 0; i < this->GetCount(); i++) + for (size_t i{ 0u }; i < this->GetCount(); ++i) { // Create a new SDES packet header for each 31 chunks. if (i % MaxChunksPerPacket == 0) diff --git a/worker/src/RTC/RTCP/SenderReport.cpp b/worker/src/RTC/RTCP/SenderReport.cpp index a3d2171588..2b0eb9d265 100644 --- a/worker/src/RTC/RTCP/SenderReport.cpp +++ b/worker/src/RTC/RTCP/SenderReport.cpp @@ -3,7 +3,7 @@ #include "RTC/RTCP/SenderReport.hpp" #include "Logger.hpp" -#include +#include // std::memcpy namespace RTC { @@ -36,12 +36,12 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" ssrc : %" PRIu32, GetSsrc()); - MS_DUMP(" ntp sec : %" PRIu32, GetNtpSec()); - MS_DUMP(" ntp frac : %" PRIu32, GetNtpFrac()); - MS_DUMP(" rtp ts : %" PRIu32, GetRtpTs()); - MS_DUMP(" packet count : %" PRIu32, GetPacketCount()); - MS_DUMP(" octet count : %" PRIu32, GetOctetCount()); + MS_DUMP(" ssrc: %" PRIu32, GetSsrc()); + MS_DUMP(" ntp sec: %" PRIu32, GetNtpSec()); + MS_DUMP(" ntp frac: %" PRIu32, GetNtpFrac()); + MS_DUMP(" rtp ts: %" PRIu32, GetRtpTs()); + MS_DUMP(" packet count: %" PRIu32, GetPacketCount()); + MS_DUMP(" octet count: %" PRIu32, GetOctetCount()); MS_DUMP(""); } @@ -65,12 +65,14 @@ namespace RTC auto* header = const_cast(reinterpret_cast(data)); std::unique_ptr packet(new SenderReportPacket(header)); - size_t offset = Packet::CommonHeaderSize; + const size_t offset = Packet::CommonHeaderSize; SenderReport* report = SenderReport::Parse(data + offset, len - offset); if (report) + { packet->AddReport(report); + } return packet.release(); } diff --git a/worker/src/RTC/RTCP/XR.cpp b/worker/src/RTC/RTCP/XR.cpp index 26cdb8240a..973ff942ab 100644 --- a/worker/src/RTC/RTCP/XR.cpp +++ b/worker/src/RTC/RTCP/XR.cpp @@ -71,7 +71,7 @@ namespace RTC std::unique_ptr packet(new ExtendedReportPacket(header)); - uint32_t ssrc = + const uint32_t ssrc = Utils::Byte::Get4Bytes(reinterpret_cast(header), Packet::CommonHeaderSize); packet->SetSsrc(ssrc); diff --git a/worker/src/RTC/RTCP/XrDelaySinceLastRr.cpp b/worker/src/RTC/RTCP/XrDelaySinceLastRr.cpp index eab47c8073..beb6cc083f 100644 --- a/worker/src/RTC/RTCP/XrDelaySinceLastRr.cpp +++ b/worker/src/RTC/RTCP/XrDelaySinceLastRr.cpp @@ -3,7 +3,7 @@ #include "RTC/RTCP/XrDelaySinceLastRr.hpp" #include "Logger.hpp" -#include +#include // std::memcpy namespace RTC { @@ -38,9 +38,9 @@ namespace RTC MS_TRACE(); MS_DUMP(" "); - MS_DUMP(" ssrc : %" PRIu32, GetSsrc()); - MS_DUMP(" lrr : %" PRIu32, GetLastReceiverReport()); - MS_DUMP(" dlrr : %" PRIu32, GetDelaySinceLastReceiverReport()); + MS_DUMP(" ssrc: %" PRIu32, GetSsrc()); + MS_DUMP(" lrr: %" PRIu32, GetLastReceiverReport()); + MS_DUMP(" dlrr: %" PRIu32, GetDelaySinceLastReceiverReport()); MS_DUMP(" "); } @@ -75,14 +75,12 @@ namespace RTC { report->AddSsrcInfo(ssrcInfo); offset += ssrcInfo->GetSize(); + reportLength -= ssrcInfo->GetSize(); } else { return report.release(); } - - offset += ssrcInfo->GetSize(); - reportLength -= DelaySinceLastRr::SsrcInfo::BodySize; } return report.release(); @@ -94,7 +92,7 @@ namespace RTC { MS_TRACE(); - size_t length = static_cast((SsrcInfo::BodySize * this->ssrcInfos.size() / 4)); + const size_t length = static_cast((SsrcInfo::BodySize * this->ssrcInfos.size() / 4)); // Fill the common header. this->header->blockType = static_cast(this->type); @@ -119,10 +117,10 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" block type : %" PRIu8, (uint8_t)this->type); - MS_DUMP(" reserved : 0"); + MS_DUMP(" block type: %" PRIu8, (uint8_t)this->type); + MS_DUMP(" reserved: 0"); MS_DUMP( - " length : %" PRIu16, + " length: %" PRIu16, static_cast((SsrcInfo::BodySize * this->ssrcInfos.size() / 4))); for (auto* ssrcInfo : this->ssrcInfos) { diff --git a/worker/src/RTC/RTCP/XrReceiverReferenceTime.cpp b/worker/src/RTC/RTCP/XrReceiverReferenceTime.cpp index 674595296f..948c5c8884 100644 --- a/worker/src/RTC/RTCP/XrReceiverReferenceTime.cpp +++ b/worker/src/RTC/RTCP/XrReceiverReferenceTime.cpp @@ -3,7 +3,7 @@ #include "RTC/RTCP/XrReceiverReferenceTime.hpp" #include "Logger.hpp" -#include +#include // std::memcpy namespace RTC { @@ -35,11 +35,11 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" block type : %" PRIu8, static_cast(this->type)); - MS_DUMP(" reserved : 0"); - MS_DUMP(" length : 2"); - MS_DUMP(" ntp sec : %" PRIu32, GetNtpSec()); - MS_DUMP(" ntp frac : %" PRIu32, GetNtpFrac()); + MS_DUMP(" block type: %" PRIu8, static_cast(this->type)); + MS_DUMP(" reserved: 0"); + MS_DUMP(" length: 2"); + MS_DUMP(" ntp sec: %" PRIu32, GetNtpSec()); + MS_DUMP(" ntp frac: %" PRIu32, GetNtpFrac()); MS_DUMP(""); } diff --git a/worker/src/RTC/RateCalculator.cpp b/worker/src/RTC/RateCalculator.cpp index 929692fc8f..eb78248a74 100644 --- a/worker/src/RTC/RateCalculator.cpp +++ b/worker/src/RTC/RateCalculator.cpp @@ -13,7 +13,9 @@ namespace RTC // Ignore too old data. Should never happen. if (nowMs < this->oldestItemStartTime) + { return; + } // Increase bytes. this->bytes += size; @@ -26,12 +28,18 @@ namespace RTC { this->newestItemIndex++; this->newestItemStartTime = nowMs; + if (this->newestItemIndex >= this->windowItems) + { this->newestItemIndex = 0; + } MS_ASSERT( this->newestItemIndex != this->oldestItemIndex || this->oldestItemIndex == -1, - "newest index overlaps with the oldest one"); + "newest index overlaps with the oldest one [newestItemIndex:%" PRId32 + ", oldestItemIndex:%" PRId32 "]", + this->newestItemIndex, + this->oldestItemIndex); // Set the newest item. BufferItem& item = this->buffer[this->newestItemIndex]; @@ -65,7 +73,9 @@ namespace RTC MS_TRACE(); if (nowMs == this->lastTime) + { return this->lastRate; + } RemoveOldData(nowMs); @@ -83,13 +93,17 @@ namespace RTC // No item set. if (this->newestItemIndex < 0 || this->oldestItemIndex < 0) + { return; + } const uint64_t newOldestTime = nowMs - this->windowSizeMs; // Oldest item already removed. if (newOldestTime < this->oldestItemStartTime) + { return; + } // A whole window size time has elapsed since last entry. Reset the buffer. if (newOldestTime >= this->newestItemStartTime) @@ -107,7 +121,9 @@ namespace RTC oldestItem.time = 0u; if (++this->oldestItemIndex >= this->windowItems) + { this->oldestItemIndex = 0; + } const BufferItem& newOldestItem = this->buffer[this->oldestItemIndex]; this->oldestItemStartTime = newOldestItem.time; @@ -116,7 +132,7 @@ namespace RTC void RtpDataCounter::Update(RTC::RtpPacket* packet) { - uint64_t nowMs = DepLibUV::GetTimeMs(); + const uint64_t nowMs = DepLibUV::GetTimeMs(); this->packets++; this->rate.Update(packet->GetSize(), nowMs); diff --git a/worker/src/RTC/Router.cpp b/worker/src/RTC/Router.cpp index 0dd3b34a43..b6e8ff513c 100644 --- a/worker/src/RTC/Router.cpp +++ b/worker/src/RTC/Router.cpp @@ -2,9 +2,11 @@ // #define MS_LOG_DEV_LEVEL 3 #include "RTC/Router.hpp" +#ifdef MS_LIBURING_SUPPORTED +#include "DepLibUring.hpp" +#endif #include "Logger.hpp" #include "MediaSoupErrors.hpp" -#include "Utils.hpp" #include "RTC/ActiveSpeakerObserver.hpp" #include "RTC/AudioLevelObserver.hpp" #include "RTC/DirectTransport.hpp" @@ -25,8 +27,7 @@ namespace RTC this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ nullptr, - /*payloadChannelNotificationHandler*/ nullptr); + /*ChannelNotificationHandler*/ nullptr); } Router::~Router() @@ -63,227 +64,200 @@ namespace RTC this->mapDataProducers.clear(); } - void Router::FillJson(json& jsonObject) const + flatbuffers::Offset Router::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Add id. - jsonObject["id"] = this->id; - // Add transportIds. - jsonObject["transportIds"] = json::array(); - auto jsonTransportIdsIt = jsonObject.find("transportIds"); + std::vector> transportIds; + transportIds.reserve(this->mapTransports.size()); for (const auto& kv : this->mapTransports) { const auto& transportId = kv.first; - jsonTransportIdsIt->emplace_back(transportId); + transportIds.push_back(builder.CreateString(transportId)); } // Add rtpObserverIds. - jsonObject["rtpObserverIds"] = json::array(); - auto jsonRtpObserverIdsIt = jsonObject.find("rtpObserverIds"); + std::vector> rtpObserverIds; + rtpObserverIds.reserve(this->mapRtpObservers.size()); for (const auto& kv : this->mapRtpObservers) { const auto& rtpObserverId = kv.first; - jsonRtpObserverIdsIt->emplace_back(rtpObserverId); + rtpObserverIds.push_back(builder.CreateString(rtpObserverId)); } // Add mapProducerIdConsumerIds. - jsonObject["mapProducerIdConsumerIds"] = json::object(); - auto jsonMapProducerConsumersIt = jsonObject.find("mapProducerIdConsumerIds"); + std::vector> mapProducerIdConsumerIds; + mapProducerIdConsumerIds.reserve(this->mapProducerConsumers.size()); for (const auto& kv : this->mapProducerConsumers) { auto* producer = kv.first; const auto& consumers = kv.second; - (*jsonMapProducerConsumersIt)[producer->id] = json::array(); - auto jsonProducerIdIt = jsonMapProducerConsumersIt->find(producer->id); + std::vector> consumerIds; + consumerIds.reserve(consumers.size()); for (auto* consumer : consumers) { - jsonProducerIdIt->emplace_back(consumer->id); + consumerIds.emplace_back(builder.CreateString(consumer->id)); } + + mapProducerIdConsumerIds.emplace_back( + FBS::Common::CreateStringStringArrayDirect(builder, producer->id.c_str(), &consumerIds)); } // Add mapConsumerIdProducerId. - jsonObject["mapConsumerIdProducerId"] = json::object(); - auto jsonMapConsumerProducerIt = jsonObject.find("mapConsumerIdProducerId"); + std::vector> mapConsumerIdProducerId; + mapConsumerIdProducerId.reserve(this->mapConsumerProducer.size()); for (const auto& kv : this->mapConsumerProducer) { auto* consumer = kv.first; auto* producer = kv.second; - (*jsonMapConsumerProducerIt)[consumer->id] = producer->id; + mapConsumerIdProducerId.emplace_back( + FBS::Common::CreateStringStringDirect(builder, consumer->id.c_str(), producer->id.c_str())); } // Add mapProducerIdObserverIds. - jsonObject["mapProducerIdObserverIds"] = json::object(); - auto jsonMapProducerRtpObserversIt = jsonObject.find("mapProducerIdObserverIds"); + std::vector> mapProducerIdObserverIds; + mapProducerIdObserverIds.reserve(this->mapProducerRtpObservers.size()); for (const auto& kv : this->mapProducerRtpObservers) { auto* producer = kv.first; const auto& rtpObservers = kv.second; - (*jsonMapProducerRtpObserversIt)[producer->id] = json::array(); - auto jsonProducerIdIt = jsonMapProducerRtpObserversIt->find(producer->id); + std::vector> observerIds; + observerIds.reserve(rtpObservers.size()); for (auto* rtpObserver : rtpObservers) { - jsonProducerIdIt->emplace_back(rtpObserver->id); + observerIds.emplace_back(builder.CreateString(rtpObserver->id)); } + + mapProducerIdObserverIds.emplace_back( + FBS::Common::CreateStringStringArrayDirect(builder, producer->id.c_str(), &observerIds)); } // Add mapDataProducerIdDataConsumerIds. - jsonObject["mapDataProducerIdDataConsumerIds"] = json::object(); - auto jsonMapDataProducerDataConsumersIt = jsonObject.find("mapDataProducerIdDataConsumerIds"); + std::vector> mapDataProducerIdDataConsumerIds; + mapDataProducerIdDataConsumerIds.reserve(this->mapDataProducerDataConsumers.size()); for (const auto& kv : this->mapDataProducerDataConsumers) { auto* dataProducer = kv.first; const auto& dataConsumers = kv.second; - (*jsonMapDataProducerDataConsumersIt)[dataProducer->id] = json::array(); - auto jsonDataProducerIdIt = jsonMapDataProducerDataConsumersIt->find(dataProducer->id); + std::vector> dataConsumerIds; + dataConsumerIds.reserve(dataConsumers.size()); for (auto* dataConsumer : dataConsumers) { - jsonDataProducerIdIt->emplace_back(dataConsumer->id); + dataConsumerIds.emplace_back(builder.CreateString(dataConsumer->id)); } + + mapDataProducerIdDataConsumerIds.emplace_back(FBS::Common::CreateStringStringArrayDirect( + builder, dataProducer->id.c_str(), &dataConsumerIds)); } // Add mapDataConsumerIdDataProducerId. - jsonObject["mapDataConsumerIdDataProducerId"] = json::object(); - auto jsonMapDataConsumerDataProducerIt = jsonObject.find("mapDataConsumerIdDataProducerId"); + std::vector> mapDataConsumerIdDataProducerId; + mapDataConsumerIdDataProducerId.reserve(this->mapDataConsumerDataProducer.size()); for (const auto& kv : this->mapDataConsumerDataProducer) { auto* dataConsumer = kv.first; auto* dataProducer = kv.second; - (*jsonMapDataConsumerDataProducerIt)[dataConsumer->id] = dataProducer->id; + mapDataConsumerIdDataProducerId.emplace_back(FBS::Common::CreateStringStringDirect( + builder, dataConsumer->id.c_str(), dataProducer->id.c_str())); } + + return FBS::Router::CreateDumpResponseDirect( + builder, + this->id.c_str(), + &transportIds, + &rtpObserverIds, + &mapProducerIdConsumerIds, + &mapConsumerIdProducerId, + &mapProducerIdObserverIds, + &mapDataProducerIdDataConsumerIds, + &mapDataConsumerIdDataProducerId); } void Router::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::ROUTER_DUMP: + case Channel::ChannelRequest::Method::ROUTER_DUMP: { - json data = json::object(); - - FillJson(data); + auto dumpOffset = FillBuffer(request->GetBufferBuilder()); - request->Accept(data); + request->Accept(FBS::Response::Body::Router_DumpResponse, dumpOffset); break; } - case Channel::ChannelRequest::MethodId::ROUTER_CREATE_WEBRTC_TRANSPORT: + case Channel::ChannelRequest::Method::ROUTER_CREATE_WEBRTCTRANSPORT: { - std::string transportId; + const auto* body = request->data->body_as(); + + auto transportId = body->transportId()->str(); // This may throw. - SetNewTransportIdFromData(request->data, transportId); + CheckNoTransport(transportId); // This may throw. auto* webRtcTransport = - new RTC::WebRtcTransport(this->shared, transportId, this, request->data); + new RTC::WebRtcTransport(this->shared, transportId, this, body->options()); // Insert into the map. this->mapTransports[transportId] = webRtcTransport; MS_DEBUG_DEV("WebRtcTransport created [transportId:%s]", transportId.c_str()); - json data = json::object(); - - webRtcTransport->FillJson(data); + auto dumpOffset = webRtcTransport->FillBuffer(request->GetBufferBuilder()); - request->Accept(data); + request->Accept(FBS::Response::Body::WebRtcTransport_DumpResponse, dumpOffset); break; } - case Channel::ChannelRequest::MethodId::ROUTER_CREATE_WEBRTC_TRANSPORT_WITH_SERVER: + case Channel::ChannelRequest::Method::ROUTER_CREATE_WEBRTCTRANSPORT_WITH_SERVER: { - std::string transportId; + const auto* body = request->data->body_as(); + auto transportId = body->transportId()->str(); // This may throw. - SetNewTransportIdFromData(request->data, transportId); + CheckNoTransport(transportId); - auto jsonWebRtcServerIdIt = request->data.find("webRtcServerId"); + const auto* options = body->options(); + const auto* listenInfo = options->listen_as(); - if (jsonWebRtcServerIdIt == request->data.end() || !jsonWebRtcServerIdIt->is_string()) - { - MS_THROW_TYPE_ERROR("missing webRtcServerId"); - } - - std::string webRtcServerId = jsonWebRtcServerIdIt->get(); + auto webRtcServerId = listenInfo->webRtcServerId()->str(); auto* webRtcServer = this->listener->OnRouterNeedWebRtcServer(this, webRtcServerId); if (!webRtcServer) - MS_THROW_ERROR("wrong webRtcServerId (no associated WebRtcServer found)"); - - bool enableUdp{ true }; - auto jsonEnableUdpIt = request->data.find("enableUdp"); - - if (jsonEnableUdpIt != request->data.end()) { - if (!jsonEnableUdpIt->is_boolean()) - MS_THROW_TYPE_ERROR("wrong enableUdp (not a boolean)"); - - enableUdp = jsonEnableUdpIt->get(); - } - - bool enableTcp{ false }; - auto jsonEnableTcpIt = request->data.find("enableTcp"); - - if (jsonEnableTcpIt != request->data.end()) - { - if (!jsonEnableTcpIt->is_boolean()) - MS_THROW_TYPE_ERROR("wrong enableTcp (not a boolean)"); - - enableTcp = jsonEnableTcpIt->get(); - } - - bool preferUdp{ false }; - auto jsonPreferUdpIt = request->data.find("preferUdp"); - - if (jsonPreferUdpIt != request->data.end()) - { - if (!jsonPreferUdpIt->is_boolean()) - MS_THROW_TYPE_ERROR("wrong preferUdp (not a boolean)"); - - preferUdp = jsonPreferUdpIt->get(); - } - - bool preferTcp{ false }; - auto jsonPreferTcpIt = request->data.find("preferTcp"); - - if (jsonPreferTcpIt != request->data.end()) - { - if (!jsonPreferTcpIt->is_boolean()) - MS_THROW_TYPE_ERROR("wrong preferTcp (not a boolean)"); - - preferTcp = jsonPreferTcpIt->get(); + MS_THROW_ERROR("wrong webRtcServerId (no associated WebRtcServer found)"); } - auto iceCandidates = - webRtcServer->GetIceCandidates(enableUdp, enableTcp, preferUdp, preferTcp); + auto iceCandidates = webRtcServer->GetIceCandidates( + options->enableUdp(), options->enableTcp(), options->preferUdp(), options->preferTcp()); // This may throw. auto* webRtcTransport = new RTC::WebRtcTransport( - this->shared, transportId, this, webRtcServer, iceCandidates, request->data); + this->shared, transportId, this, webRtcServer, iceCandidates, options); // Insert into the map. this->mapTransports[transportId] = webRtcTransport; @@ -291,95 +265,92 @@ namespace RTC MS_DEBUG_DEV( "WebRtcTransport with WebRtcServer created [transportId:%s]", transportId.c_str()); - json data = json::object(); + auto dumpOffset = webRtcTransport->FillBuffer(request->GetBufferBuilder()); - webRtcTransport->FillJson(data); - - request->Accept(data); + request->Accept(FBS::Response::Body::WebRtcTransport_DumpResponse, dumpOffset); break; } - case Channel::ChannelRequest::MethodId::ROUTER_CREATE_PLAIN_TRANSPORT: + case Channel::ChannelRequest::Method::ROUTER_CREATE_PLAINTRANSPORT: { - std::string transportId; + const auto* body = request->data->body_as(); + auto transportId = body->transportId()->str(); - // This may throw - SetNewTransportIdFromData(request->data, transportId); + // This may throw. + CheckNoTransport(transportId); auto* plainTransport = - new RTC::PlainTransport(this->shared, transportId, this, request->data); + new RTC::PlainTransport(this->shared, transportId, this, body->options()); // Insert into the map. this->mapTransports[transportId] = plainTransport; MS_DEBUG_DEV("PlainTransport created [transportId:%s]", transportId.c_str()); - json data = json::object(); + auto dumpOffset = plainTransport->FillBuffer(request->GetBufferBuilder()); - plainTransport->FillJson(data); - - request->Accept(data); + request->Accept(FBS::Response::Body::PlainTransport_DumpResponse, dumpOffset); break; } - case Channel::ChannelRequest::MethodId::ROUTER_CREATE_PIPE_TRANSPORT: + case Channel::ChannelRequest::Method::ROUTER_CREATE_PIPETRANSPORT: { - std::string transportId; + const auto* body = request->data->body_as(); + auto transportId = body->transportId()->str(); - // This may throw - SetNewTransportIdFromData(request->data, transportId); + // This may throw. + CheckNoTransport(transportId); - auto* pipeTransport = new RTC::PipeTransport(this->shared, transportId, this, request->data); + auto* pipeTransport = + new RTC::PipeTransport(this->shared, transportId, this, body->options()); // Insert into the map. this->mapTransports[transportId] = pipeTransport; MS_DEBUG_DEV("PipeTransport created [transportId:%s]", transportId.c_str()); - json data = json::object(); + auto dumpOffset = pipeTransport->FillBuffer(request->GetBufferBuilder()); - pipeTransport->FillJson(data); - - request->Accept(data); + request->Accept(FBS::Response::Body::PipeTransport_DumpResponse, dumpOffset); break; } - case Channel::ChannelRequest::MethodId::ROUTER_CREATE_DIRECT_TRANSPORT: + case Channel::ChannelRequest::Method::ROUTER_CREATE_DIRECTTRANSPORT: { - std::string transportId; + const auto* body = request->data->body_as(); + auto transportId = body->transportId()->str(); - // This may throw - SetNewTransportIdFromData(request->data, transportId); + // This may throw. + CheckNoTransport(transportId); auto* directTransport = - new RTC::DirectTransport(this->shared, transportId, this, request->data); + new RTC::DirectTransport(this->shared, transportId, this, body->options()); // Insert into the map. this->mapTransports[transportId] = directTransport; MS_DEBUG_DEV("DirectTransport created [transportId:%s]", transportId.c_str()); - json data = json::object(); + auto dumpOffset = directTransport->FillBuffer(request->GetBufferBuilder()); - directTransport->FillJson(data); - - request->Accept(data); + request->Accept(FBS::Response::Body::DirectTransport_DumpResponse, dumpOffset); break; } - case Channel::ChannelRequest::MethodId::ROUTER_CREATE_ACTIVE_SPEAKER_OBSERVER: + case Channel::ChannelRequest::Method::ROUTER_CREATE_ACTIVESPEAKEROBSERVER: { - std::string rtpObserverId; + const auto* body = request->data->body_as(); + auto rtpObserverId = body->rtpObserverId()->str(); // This may throw. - SetNewRtpObserverIdFromData(request->data, rtpObserverId); + CheckNoRtpObserver(rtpObserverId); auto* activeSpeakerObserver = - new RTC::ActiveSpeakerObserver(this->shared, rtpObserverId, this, request->data); + new RTC::ActiveSpeakerObserver(this->shared, rtpObserverId, this, body->options()); // Insert into the map. this->mapRtpObservers[rtpObserverId] = activeSpeakerObserver; @@ -391,15 +362,16 @@ namespace RTC break; } - case Channel::ChannelRequest::MethodId::ROUTER_CREATE_AUDIO_LEVEL_OBSERVER: + case Channel::ChannelRequest::Method::ROUTER_CREATE_AUDIOLEVELOBSERVER: { - std::string rtpObserverId; + const auto* body = request->data->body_as(); + auto rtpObserverId = body->rtpObserverId()->str(); - // This may throw - SetNewRtpObserverIdFromData(request->data, rtpObserverId); + // This may throw. + CheckNoRtpObserver(rtpObserverId); auto* audioLevelObserver = - new RTC::AudioLevelObserver(this->shared, rtpObserverId, this, request->data); + new RTC::AudioLevelObserver(this->shared, rtpObserverId, this, body->options()); // Insert into the map. this->mapRtpObservers[rtpObserverId] = audioLevelObserver; @@ -411,10 +383,13 @@ namespace RTC break; } - case Channel::ChannelRequest::MethodId::ROUTER_CLOSE_TRANSPORT: + case Channel::ChannelRequest::Method::ROUTER_CLOSE_TRANSPORT: { + const auto* body = request->data->body_as(); + auto transportId = body->transportId()->str(); + // This may throw. - RTC::Transport* transport = GetTransportFromData(request->data); + RTC::Transport* transport = GetTransportById(transportId); // Tell the Transport to close all its Producers and Consumers so it will // notify us about their closures. @@ -433,10 +408,13 @@ namespace RTC break; } - case Channel::ChannelRequest::MethodId::ROUTER_CLOSE_RTP_OBSERVER: + case Channel::ChannelRequest::Method::ROUTER_CLOSE_RTPOBSERVER: { + const auto* body = request->data->body_as(); + auto rtpObserverId = body->rtpObserverId()->str(); + // This may throw. - RTC::RtpObserver* rtpObserver = GetRtpObserverFromData(request->data); + RTC::RtpObserver* rtpObserver = GetRtpObserverById(rtpObserverId); // Remove it from the map. this->mapRtpObservers.erase(rtpObserver->id); @@ -461,89 +439,53 @@ namespace RTC default: { - MS_THROW_ERROR("unknown method '%s'", request->method.c_str()); + MS_THROW_ERROR("unknown method '%s'", Channel::ChannelRequest::method2String[request->method]); } } } - void Router::SetNewTransportIdFromData(json& data, std::string& transportId) const + void Router::CheckNoTransport(const std::string& transportId) const { - MS_TRACE(); - - auto jsonTransportIdIt = data.find("transportId"); - - if (jsonTransportIdIt == data.end() || !jsonTransportIdIt->is_string()) - { - MS_THROW_TYPE_ERROR("missing transportId"); - } - - transportId.assign(jsonTransportIdIt->get()); - if (this->mapTransports.find(transportId) != this->mapTransports.end()) { - MS_THROW_ERROR("a Transport with same transportId already exists"); + MS_THROW_ERROR("a Transport with same id already exists"); } } - RTC::Transport* Router::GetTransportFromData(json& data) const + void Router::CheckNoRtpObserver(const std::string& rtpObserverId) const { - MS_TRACE(); - - auto jsonTransportIdIt = data.find("transportId"); - - if (jsonTransportIdIt == data.end() || !jsonTransportIdIt->is_string()) + if (this->mapRtpObservers.find(rtpObserverId) != this->mapRtpObservers.end()) { - MS_THROW_TYPE_ERROR("missing transportId"); + MS_THROW_ERROR("an RtpObserver with same id already exists"); } - - auto it = this->mapTransports.find(jsonTransportIdIt->get()); - - if (it == this->mapTransports.end()) - MS_THROW_ERROR("Transport not found"); - - RTC::Transport* transport = it->second; - - return transport; } - void Router::SetNewRtpObserverIdFromData(json& data, std::string& rtpObserverId) const + RTC::Transport* Router::GetTransportById(const std::string& transportId) const { MS_TRACE(); - auto jsonRtpObserverIdIt = data.find("rtpObserverId"); + auto it = this->mapTransports.find(transportId); - if (jsonRtpObserverIdIt == data.end() || !jsonRtpObserverIdIt->is_string()) + if (this->mapTransports.find(transportId) == this->mapTransports.end()) { - MS_THROW_TYPE_ERROR("missing rtpObserverId"); + MS_THROW_ERROR("Transport not found"); } - rtpObserverId.assign(jsonRtpObserverIdIt->get()); - - if (this->mapRtpObservers.find(rtpObserverId) != this->mapRtpObservers.end()) - { - MS_THROW_ERROR("an RtpObserver with same rtpObserverId already exists"); - } + return it->second; } - RTC::RtpObserver* Router::GetRtpObserverFromData(json& data) const + RTC::RtpObserver* Router::GetRtpObserverById(const std::string& rtpObserverId) const { MS_TRACE(); - auto jsonRtpObserverIdIt = data.find("rtpObserverId"); + auto it = this->mapRtpObservers.find(rtpObserverId); - if (jsonRtpObserverIdIt == data.end() || !jsonRtpObserverIdIt->is_string()) + if (this->mapRtpObservers.find(rtpObserverId) == this->mapRtpObservers.end()) { - MS_THROW_TYPE_ERROR("missing rtpObserverId"); - } - - auto it = this->mapRtpObservers.find(jsonRtpObserverIdIt->get()); - - if (it == this->mapRtpObservers.end()) MS_THROW_ERROR("RtpObserver not found"); + } - RTC::RtpObserver* rtpObserver = it->second; - - return rtpObserver; + return it->second; } inline void Router::OnTransportNewProducer(RTC::Transport* /*transport*/, RTC::Producer* producer) @@ -659,7 +601,10 @@ namespace RTC } inline void Router::OnTransportProducerNewRtpStream( - RTC::Transport* /*transport*/, RTC::Producer* producer, RTC::RtpStream* rtpStream, uint32_t mappedSsrc) + RTC::Transport* /*transport*/, + RTC::Producer* producer, + RTC::RtpStreamRecv* rtpStream, + uint32_t mappedSsrc) { MS_TRACE(); @@ -674,7 +619,7 @@ namespace RTC inline void Router::OnTransportProducerRtpStreamScore( RTC::Transport* /*transport*/, RTC::Producer* producer, - RTC::RtpStream* rtpStream, + RTC::RtpStreamRecv* rtpStream, uint8_t score, uint8_t previousScore) { @@ -689,7 +634,7 @@ namespace RTC } inline void Router::OnTransportProducerRtcpSenderReport( - RTC::Transport* /*transport*/, RTC::Producer* producer, RTC::RtpStream* rtpStream, bool first) + RTC::Transport* /*transport*/, RTC::Producer* producer, RTC::RtpStreamRecv* rtpStream, bool first) { MS_TRACE(); @@ -706,6 +651,10 @@ namespace RTC { MS_TRACE(); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.routerId = this->id; +#endif + auto& consumers = this->mapProducerConsumers.at(producer); if (!consumers.empty()) @@ -715,16 +664,34 @@ namespace RTC // Clone only happens if needed. std::shared_ptr sharedPacket; +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + // Activate liburing usage. + DepLibUring::SetActive(); + } +#endif + for (auto* consumer : consumers) { // Update MID RTP extension value. const auto& mid = consumer->GetRtpParameters().mid; if (!mid.empty()) + { packet->UpdateMid(mid); + } consumer->SendRtpPacket(packet, sharedPacket); } + +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + // Submit all prepared submission entries. + DepLibUring::Submit(); + } +#endif } auto it = this->mapProducerRtpObservers.find(producer); @@ -757,14 +724,16 @@ namespace RTC } inline void Router::OnTransportNewConsumer( - RTC::Transport* /*transport*/, RTC::Consumer* consumer, std::string& producerId) + RTC::Transport* /*transport*/, RTC::Consumer* consumer, const std::string& producerId) { MS_TRACE(); auto mapProducersIt = this->mapProducers.find(producerId); if (mapProducersIt == this->mapProducers.end()) + { MS_THROW_ERROR("Producer not found [producerId:%s]", producerId.c_str()); + } auto* producer = mapProducersIt->second; auto mapProducerConsumersIt = this->mapProducerConsumers.find(producer); @@ -778,7 +747,9 @@ namespace RTC // Update the Consumer status based on the Producer status. if (producer->IsPaused()) + { consumer->ProducerPaused(); + } // Insert the Consumer in the maps. auto& consumers = mapProducerConsumersIt->second; @@ -789,8 +760,8 @@ namespace RTC // Get all streams in the Producer and provide the Consumer with them. for (const auto& kv : producer->GetRtpStreams()) { - auto* rtpStream = kv.first; - uint32_t mappedSsrc = kv.second; + auto* rtpStream = kv.first; + const uint32_t mappedSsrc = kv.second; consumer->ProducerRtpStream(rtpStream, mappedSsrc); } @@ -918,20 +889,69 @@ namespace RTC this->mapDataProducerDataConsumers.erase(mapDataProducerDataConsumersIt); } + inline void Router::OnTransportDataProducerPaused( + RTC::Transport* /*transport*/, RTC::DataProducer* dataProducer) + { + MS_TRACE(); + + auto& dataConsumers = this->mapDataProducerDataConsumers.at(dataProducer); + + for (auto* dataConsumer : dataConsumers) + { + dataConsumer->DataProducerPaused(); + } + } + + inline void Router::OnTransportDataProducerResumed( + RTC::Transport* /*transport*/, RTC::DataProducer* dataProducer) + { + MS_TRACE(); + + auto& dataConsumers = this->mapDataProducerDataConsumers.at(dataProducer); + + for (auto* dataConsumer : dataConsumers) + { + dataConsumer->DataProducerResumed(); + } + } + inline void Router::OnTransportDataProducerMessageReceived( RTC::Transport* /*transport*/, RTC::DataProducer* dataProducer, - uint32_t ppid, const uint8_t* msg, - size_t len) + size_t len, + uint32_t ppid, + std::vector& subchannels, + std::optional requiredSubchannel) { MS_TRACE(); auto& dataConsumers = this->mapDataProducerDataConsumers.at(dataProducer); - for (auto* consumer : dataConsumers) + if (!dataConsumers.empty()) { - consumer->SendMessage(ppid, msg, len); +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + // Activate liburing usage. + // The effective sending could be synchronous, thus we would send those + // messages within a single system call. + DepLibUring::SetActive(); + } +#endif + + for (auto* dataConsumer : dataConsumers) + { + dataConsumer->SendMessage(msg, len, ppid, subchannels, requiredSubchannel); + } + +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + // Submit all prepared submission entries. + DepLibUring::Submit(); + } +#endif } } @@ -943,7 +963,9 @@ namespace RTC auto mapDataProducersIt = this->mapDataProducers.find(dataProducerId); if (mapDataProducersIt == this->mapDataProducers.end()) + { MS_THROW_ERROR("DataProducer not found [dataProducerId:%s]", dataProducerId.c_str()); + } auto* dataProducer = mapDataProducersIt->second; auto mapDataProducerDataConsumersIt = this->mapDataProducerDataConsumers.find(dataProducer); @@ -955,6 +977,12 @@ namespace RTC this->mapDataConsumerDataProducer.find(dataConsumer) == this->mapDataConsumerDataProducer.end(), "DataConsumer already present in mapDataConsumerDataProducer"); + // Update the DataConsumer status based on the DataProducer status. + if (dataProducer->IsPaused()) + { + dataConsumer->DataProducerPaused(); + } + // Insert the DataConsumer in the maps. auto& dataConsumers = mapDataProducerDataConsumersIt->second; @@ -1052,7 +1080,9 @@ namespace RTC auto it = this->mapProducers.find(id); if (it == this->mapProducers.end()) + { MS_THROW_ERROR("Producer not found"); + } RTC::Producer* producer = it->second; diff --git a/worker/src/RTC/RtcLogger.cpp b/worker/src/RTC/RtcLogger.cpp new file mode 100644 index 0000000000..4237dcd90f --- /dev/null +++ b/worker/src/RTC/RtcLogger.cpp @@ -0,0 +1,100 @@ +#define MS_CLASS "RTC::RtcLogger" +// #define MS_LOG_DEV_LEVEL 3 + +#include "RTC/RtcLogger.hpp" +#include "Logger.hpp" + +namespace RTC +{ + namespace RtcLogger + { + // clang-format off + absl::flat_hash_map RtpPacket::dropReason2String = { + { RtpPacket::DropReason::NONE, "None" }, + { RtpPacket::DropReason::PRODUCER_NOT_FOUND, "ProducerNotFound" }, + { RtpPacket::DropReason::RECV_RTP_STREAM_NOT_FOUND, "RecvRtpStreamNotFound" }, + { RtpPacket::DropReason::RECV_RTP_STREAM_DISCARDED, "RecvRtpStreamDiscarded" }, + { RtpPacket::DropReason::CONSUMER_INACTIVE, "ConsumerInactive" }, + { RtpPacket::DropReason::INVALID_TARGET_LAYER, "InvalidTargetLayer" }, + { RtpPacket::DropReason::UNSUPPORTED_PAYLOAD_TYPE, "UnsupportedPayloadType" }, + { RtpPacket::DropReason::NOT_A_KEYFRAME, "NotAKeyframe" }, + { RtpPacket::DropReason::EMPTY_PAYLOAD, "EmptyPayload" }, + { RtpPacket::DropReason::SPATIAL_LAYER_MISMATCH, "SpatialLayerMismatch" }, + { RtpPacket::DropReason::TOO_HIGH_TIMESTAMP_EXTRA_NEEDED, "TooHighTimestampExtraNeeded" }, + { RtpPacket::DropReason::PACKET_PREVIOUS_TO_SPATIAL_LAYER_SWITCH, "PacketPreviousToSpatialLayerSwitch" }, + { RtpPacket::DropReason::DROPPED_BY_CODEC, "DroppedByCodec" }, + { RtpPacket::DropReason::SEND_RTP_STREAM_DISCARDED, "SendRtpStreamDiscarded" }, + }; + // clang-format on + + void RtpPacket::Sent() + { + MS_TRACE(); + + this->dropped = false; + + Log(); + Clear(); + } + + void RtpPacket::Dropped(DropReason dropReason) + { + MS_TRACE(); + + this->dropped = true; + this->dropReason = dropReason; + + Log(); + Clear(); + } + + void RtpPacket::Log() const + { + MS_TRACE(); + + std::cout << "{"; + std::cout << "\"timestamp\": " << this->timestamp; + + if (!this->recvTransportId.empty()) + { + std::cout << R"(, "recvTransportId": ")" << this->recvTransportId << "\""; + } + if (!this->sendTransportId.empty()) + { + std::cout << R"(, "sendTransportId": ")" << this->sendTransportId << "\""; + } + if (!this->routerId.empty()) + { + std::cout << R"(, "routerId": ")" << this->routerId << "\""; + } + if (!this->producerId.empty()) + { + std::cout << R"(, "producerId": ")" << this->producerId << "\""; + } + if (!this->consumerId.empty()) + { + std::cout << R"(, "consumerId": ")" << this->consumerId << "\""; + } + + std::cout << ", \"recvRtpTimestamp\": " << this->recvRtpTimestamp; + std::cout << ", \"sendRtpTimestamp\": " << this->sendRtpTimestamp; + std::cout << ", \"recvSeqNumber\": " << this->recvSeqNumber; + std::cout << ", \"sendSeqNumber\": " << this->sendSeqNumber; + std::cout << ", \"dropped\": " << (this->dropped ? "true" : "false"); + std::cout << ", \"dropReason\": '" << dropReason2String[this->dropReason] << "'"; + std::cout << "}" << std::endl; + } + + void RtpPacket::Clear() + { + MS_TRACE(); + + this->sendTransportId = {}; + this->routerId = {}; + this->producerId = {}; + this->sendSeqNumber = { 0 }; + this->dropped = { false }; + this->dropReason = { DropReason::NONE }; + } + } // namespace RtcLogger +} // namespace RTC diff --git a/worker/src/RTC/RtpDictionaries/Media.cpp b/worker/src/RTC/RtpDictionaries/Media.cpp deleted file mode 100644 index 35a780546d..0000000000 --- a/worker/src/RTC/RtpDictionaries/Media.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#define MS_CLASS "RTC::Media" -// #define MS_LOG_DEV_LEVEL 3 - -#include "Logger.hpp" -#include "MediaSoupErrors.hpp" -#include "Utils.hpp" -#include "RTC/RtpDictionaries.hpp" - -namespace RTC -{ - /* Class variables. */ - - // clang-format off - absl::flat_hash_map Media::string2Kind = - { - { "", Media::Kind::ALL }, - { "audio", Media::Kind::AUDIO }, - { "video", Media::Kind::VIDEO } - }; - absl::flat_hash_map Media::kind2String = - { - { Media::Kind::ALL, "" }, - { Media::Kind::AUDIO, "audio" }, - { Media::Kind::VIDEO, "video" } - }; - // clang-format on - - /* Class methods. */ - - Media::Kind Media::GetKind(std::string& str) - { - MS_TRACE(); - - // Force lowcase kind. - Utils::String::ToLowerCase(str); - - auto it = Media::string2Kind.find(str); - - if (it == Media::string2Kind.end()) - MS_THROW_TYPE_ERROR("invalid media kind [kind:%s]", str.c_str()); - - return it->second; - } - - Media::Kind Media::GetKind(std::string&& str) - { - MS_TRACE(); - - // Force lowcase kind. - Utils::String::ToLowerCase(str); - - auto it = Media::string2Kind.find(str); - - if (it == Media::string2Kind.end()) - MS_THROW_TYPE_ERROR("invalid media kind [kind:%s]", str.c_str()); - - return it->second; - } - - const std::string& Media::GetString(Media::Kind kind) - { - MS_TRACE(); - - return Media::kind2String.at(kind); - } -} // namespace RTC diff --git a/worker/src/RTC/RtpDictionaries/Parameters.cpp b/worker/src/RTC/RtpDictionaries/Parameters.cpp index c51b8c9b9f..2376c73838 100644 --- a/worker/src/RTC/RtpDictionaries/Parameters.cpp +++ b/worker/src/RTC/RtpDictionaries/Parameters.cpp @@ -8,104 +8,121 @@ namespace RTC { /* Instance methods. */ - void Parameters::FillJson(json& jsonObject) const + std::vector> Parameters::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Force it to be an object even if no key/values are added below. - jsonObject = json::object(); + std::vector> parameters; - for (auto& kv : this->mapKeyValues) + for (const auto& kv : this->mapKeyValues) { - auto& key = kv.first; - auto& value = kv.second; + const auto& key = kv.first; + const auto& value = kv.second; + + flatbuffers::Offset parameter; switch (value.type) { case Value::Type::BOOLEAN: - jsonObject[key] = value.booleanValue; + { + auto valueOffset = FBS::RtpParameters::CreateBoolean(builder, value.booleanValue); + + parameter = FBS::RtpParameters::CreateParameterDirect( + builder, key.c_str(), FBS::RtpParameters::Value::Boolean, valueOffset.Union()); + break; + } case Value::Type::INTEGER: - jsonObject[key] = value.integerValue; + { + auto valueOffset = FBS::RtpParameters::CreateInteger32(builder, value.integerValue); + + parameters.emplace_back(FBS::RtpParameters::CreateParameterDirect( + builder, key.c_str(), FBS::RtpParameters::Value::Integer32, valueOffset.Union())); + break; + } case Value::Type::DOUBLE: - jsonObject[key] = value.doubleValue; + { + auto valueOffset = FBS::RtpParameters::CreateDouble(builder, value.doubleValue); + + parameters.emplace_back(FBS::RtpParameters::CreateParameterDirect( + builder, key.c_str(), FBS::RtpParameters::Value::Double, valueOffset.Union())); + break; + } case Value::Type::STRING: - jsonObject[key] = value.stringValue; + { + auto valueOffset = + FBS::RtpParameters::CreateStringDirect(builder, value.stringValue.c_str()); + + parameters.emplace_back(FBS::RtpParameters::CreateParameterDirect( + builder, key.c_str(), FBS::RtpParameters::Value::String, valueOffset.Union())); + break; + } case Value::Type::ARRAY_OF_INTEGERS: - jsonObject[key] = value.arrayOfIntegers; + { + auto valueOffset = + FBS::RtpParameters::CreateInteger32ArrayDirect(builder, &value.arrayOfIntegers); + + parameters.emplace_back(FBS::RtpParameters::CreateParameterDirect( + builder, key.c_str(), FBS::RtpParameters::Value::Integer32Array, valueOffset.Union())); + break; + } } } + + return parameters; } - void Parameters::Set(json& data) + void Parameters::Set(const flatbuffers::Vector>* data) { MS_TRACE(); - MS_ASSERT(data.is_object(), "data is not an object"); - - for (json::iterator it = data.begin(); it != data.end(); ++it) + for (const auto* parameter : *data) { - const std::string& key = it.key(); - auto& value = it.value(); + const auto key = parameter->name()->str(); - switch (value.type()) + switch (parameter->value_type()) { - case json::value_t::boolean: + case FBS::RtpParameters::Value::Boolean: { - this->mapKeyValues.emplace(key, Value(value.get())); + this->mapKeyValues.emplace( + key, Value((parameter->value_as_Boolean()->value() == 0) ? false : true)); break; } - case json::value_t::number_integer: - case json::value_t::number_unsigned: + case FBS::RtpParameters::Value::Integer32: { - this->mapKeyValues.emplace(key, Value(value.get())); + this->mapKeyValues.emplace(key, Value(parameter->value_as_Integer32()->value())); break; } - case json::value_t::number_float: + case FBS::RtpParameters::Value::Double: { - this->mapKeyValues.emplace(key, Value(value.get())); + this->mapKeyValues.emplace(key, Value(parameter->value_as_Double()->value())); break; } - case json::value_t::string: + case FBS::RtpParameters::Value::String: { - this->mapKeyValues.emplace(key, Value(value.get())); + this->mapKeyValues.emplace(key, Value(parameter->value_as_String()->value()->str())); break; } - case json::value_t::array: + case FBS::RtpParameters::Value::Integer32Array: { - std::vector arrayOfIntegers; - bool isValid = true; - - for (auto& entry : value) - { - if (!entry.is_number_integer()) - { - isValid = false; - - break; - } - - arrayOfIntegers.emplace_back(entry.get()); - } - - if (!arrayOfIntegers.empty() && isValid) - this->mapKeyValues.emplace(key, Value(arrayOfIntegers)); + this->mapKeyValues.emplace(key, Value(parameter->value_as_Integer32Array()->value())); break; } @@ -122,9 +139,11 @@ namespace RTC auto it = this->mapKeyValues.find(key); if (it == this->mapKeyValues.end()) + { return false; + } - auto& value = it->second; + const auto& value = it->second; return value.type == Value::Type::BOOLEAN; } @@ -136,9 +155,11 @@ namespace RTC auto it = this->mapKeyValues.find(key); if (it == this->mapKeyValues.end()) + { return false; + } - auto& value = it->second; + const auto& value = it->second; return value.type == Value::Type::INTEGER; } @@ -150,9 +171,11 @@ namespace RTC auto it = this->mapKeyValues.find(key); if (it == this->mapKeyValues.end()) + { return false; + } - auto& value = it->second; + const auto& value = it->second; return value.type == Value::Type::INTEGER && value.integerValue >= 0; } @@ -164,9 +187,11 @@ namespace RTC auto it = this->mapKeyValues.find(key); if (it == this->mapKeyValues.end()) + { return false; + } - auto& value = it->second; + const auto& value = it->second; return value.type == Value::Type::DOUBLE; } @@ -178,9 +203,11 @@ namespace RTC auto it = this->mapKeyValues.find(key); if (it == this->mapKeyValues.end()) + { return false; + } - auto& value = it->second; + const auto& value = it->second; return value.type == Value::Type::STRING; } @@ -192,9 +219,11 @@ namespace RTC auto it = this->mapKeyValues.find(key); if (it == this->mapKeyValues.end()) + { return false; + } - auto& value = it->second; + const auto& value = it->second; return value.type == Value::Type::ARRAY_OF_INTEGERS; } @@ -206,10 +235,12 @@ namespace RTC auto it = this->mapKeyValues.find(key); if (it == this->mapKeyValues.end()) + { return false; + } - auto& value = it->second; - auto& array = value.arrayOfIntegers; + const auto& value = it->second; + const auto& array = value.arrayOfIntegers; return std::find(array.begin(), array.end(), integer) != array.end(); } @@ -222,7 +253,7 @@ namespace RTC MS_ASSERT(it != this->mapKeyValues.end(), "key does not exist [key:%s]", key.c_str()); - auto& value = it->second; + const auto& value = it->second; return value.booleanValue; } @@ -235,7 +266,7 @@ namespace RTC MS_ASSERT(it != this->mapKeyValues.end(), "key does not exist [key:%s]", key.c_str()); - auto& value = it->second; + const auto& value = it->second; return value.integerValue; } @@ -248,7 +279,7 @@ namespace RTC MS_ASSERT(it != this->mapKeyValues.end(), "key does not exist [key:%s]", key.c_str()); - auto& value = it->second; + const auto& value = it->second; return value.doubleValue; } @@ -261,7 +292,7 @@ namespace RTC MS_ASSERT(it != this->mapKeyValues.end(), "key does not exist [key:%s]", key.c_str()); - auto& value = it->second; + const auto& value = it->second; return value.stringValue; } @@ -274,7 +305,7 @@ namespace RTC MS_ASSERT(it != this->mapKeyValues.end(), "key does not exist [key:%s]", key.c_str()); - auto& value = it->second; + const auto& value = it->second; return value.arrayOfIntegers; } diff --git a/worker/src/RTC/RtpDictionaries/RtcpFeedback.cpp b/worker/src/RTC/RtpDictionaries/RtcpFeedback.cpp index 4c0f90f054..b95bb46e36 100644 --- a/worker/src/RTC/RtpDictionaries/RtcpFeedback.cpp +++ b/worker/src/RTC/RtpDictionaries/RtcpFeedback.cpp @@ -2,43 +2,29 @@ // #define MS_LOG_DEV_LEVEL 3 #include "Logger.hpp" -#include "MediaSoupErrors.hpp" #include "RTC/RtpDictionaries.hpp" namespace RTC { /* Instance methods. */ - RtcpFeedback::RtcpFeedback(json& data) + RtcpFeedback::RtcpFeedback(const FBS::RtpParameters::RtcpFeedback* data) { MS_TRACE(); - if (!data.is_object()) - MS_THROW_TYPE_ERROR("data is not an object"); - - auto jsonTypeIt = data.find("type"); - auto jsonParameterIt = data.find("parameter"); - - // type is mandatory. - if (jsonTypeIt == data.end() || !jsonTypeIt->is_string()) - MS_THROW_TYPE_ERROR("missing type"); - - this->type = jsonTypeIt->get(); - - // parameter is optional. - if (jsonParameterIt != data.end() && jsonParameterIt->is_string()) - this->parameter = jsonParameterIt->get(); + this->type = data->type()->str(); + if (flatbuffers::IsFieldPresent(data, FBS::RtpParameters::RtcpFeedback::VT_PARAMETER)) + { + this->parameter = data->parameter()->str(); + } } - void RtcpFeedback::FillJson(json& jsonObject) const + flatbuffers::Offset RtcpFeedback::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Add type. - jsonObject["type"] = this->type; - - // Add parameter (optional). - if (this->parameter.length() > 0) - jsonObject["parameter"] = this->parameter; + return FBS::RtpParameters::CreateRtcpFeedbackDirect( + builder, this->type.c_str(), this->parameter.empty() ? nullptr : this->parameter.c_str()); } } // namespace RTC diff --git a/worker/src/RTC/RtpDictionaries/RtcpParameters.cpp b/worker/src/RTC/RtpDictionaries/RtcpParameters.cpp index fe77eeebd1..d9283771d1 100644 --- a/worker/src/RTC/RtpDictionaries/RtcpParameters.cpp +++ b/worker/src/RTC/RtpDictionaries/RtcpParameters.cpp @@ -2,58 +2,32 @@ // #define MS_LOG_DEV_LEVEL 3 #include "Logger.hpp" -#include "MediaSoupErrors.hpp" -#include "Utils.hpp" #include "RTC/RtpDictionaries.hpp" namespace RTC { /* Instance methods. */ - RtcpParameters::RtcpParameters(json& data) + RtcpParameters::RtcpParameters(const FBS::RtpParameters::RtcpParameters* data) { MS_TRACE(); - if (!data.is_object()) - MS_THROW_TYPE_ERROR("data is not an object"); - - auto jsonCnameIt = data.find("cname"); - auto jsonSsrcIt = data.find("ssrc"); - auto jsonRedicedSizeIt = data.find("reducedSize"); - // cname is optional. - if (jsonCnameIt != data.end() && jsonCnameIt->is_string()) - this->cname = jsonCnameIt->get(); - - // ssrc is optional. - // clang-format off - if ( - jsonSsrcIt != data.end() && - Utils::Json::IsPositiveInteger(*jsonSsrcIt) - ) - // clang-format on + if (flatbuffers::IsFieldPresent(data, FBS::RtpParameters::RtcpParameters::VT_CNAME)) { - this->ssrc = jsonSsrcIt->get(); + this->cname = data->cname()->str(); } - // reducedSize is optional. - if (jsonRedicedSizeIt != data.end() && jsonRedicedSizeIt->is_boolean()) - this->reducedSize = jsonRedicedSizeIt->get(); + // reducedSize is optional, default value is true. + this->reducedSize = data->reducedSize(); } - void RtcpParameters::FillJson(json& jsonObject) const + flatbuffers::Offset RtcpParameters::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Add cname. - if (!this->cname.empty()) - jsonObject["cname"] = this->cname; - - // Add ssrc. - if (this->ssrc != 0u) - jsonObject["ssrc"] = this->ssrc; - - // Add reducedSize. - jsonObject["reducedSize"] = this->reducedSize; + return FBS::RtpParameters::CreateRtcpParametersDirect( + builder, this->cname.c_str(), this->reducedSize); } } // namespace RTC diff --git a/worker/src/RTC/RtpDictionaries/RtpCodecMimeType.cpp b/worker/src/RTC/RtpDictionaries/RtpCodecMimeType.cpp index 3b5874ef78..ae87b1eea7 100644 --- a/worker/src/RTC/RtpDictionaries/RtpCodecMimeType.cpp +++ b/worker/src/RTC/RtpDictionaries/RtpCodecMimeType.cpp @@ -104,7 +104,9 @@ namespace RTC auto it = RtpCodecMimeType::string2Type.find(type); if (it == RtpCodecMimeType::string2Type.end()) + { MS_THROW_TYPE_ERROR("unknown codec MIME type '%s'", type.c_str()); + } this->type = it->second; } @@ -114,7 +116,9 @@ namespace RTC auto it = RtpCodecMimeType::string2Subtype.find(subtype); if (it == RtpCodecMimeType::string2Subtype.end()) + { MS_THROW_TYPE_ERROR("unknown codec MIME subtype '%s'", subtype.c_str()); + } this->subtype = it->second; } @@ -128,9 +132,6 @@ namespace RTC { MS_TRACE(); - MS_ASSERT(this->type != Type::UNSET, "type unset"); - MS_ASSERT(this->subtype != Subtype::UNSET, "subtype unset"); - // Set mimeType. this->mimeType = RtpCodecMimeType::type2String[this->type] + "/" + RtpCodecMimeType::subtype2String[this->subtype]; diff --git a/worker/src/RTC/RtpDictionaries/RtpCodecParameters.cpp b/worker/src/RTC/RtpDictionaries/RtpCodecParameters.cpp index caab4b63ce..b0fc5001cc 100644 --- a/worker/src/RTC/RtpDictionaries/RtpCodecParameters.cpp +++ b/worker/src/RTC/RtpDictionaries/RtpCodecParameters.cpp @@ -3,84 +3,45 @@ #include "Logger.hpp" #include "MediaSoupErrors.hpp" -#include "Utils.hpp" #include "RTC/RtpDictionaries.hpp" namespace RTC { /* Instance methods. */ - RtpCodecParameters::RtpCodecParameters(json& data) + RtpCodecParameters::RtpCodecParameters(const FBS::RtpParameters::RtpCodecParameters* data) { MS_TRACE(); - if (!data.is_object()) - MS_THROW_TYPE_ERROR("data is not an object"); - - auto jsonMimeTypeIt = data.find("mimeType"); - auto jsonPayloadTypeIt = data.find("payloadType"); - auto jsonClockRateIt = data.find("clockRate"); - auto jsonChannelsIt = data.find("channels"); - auto jsonParametersIt = data.find("parameters"); - auto jsonRtcpFeedbackIt = data.find("rtcpFeedback"); - - // mimeType is mandatory. - if (jsonMimeTypeIt == data.end() || !jsonMimeTypeIt->is_string()) - MS_THROW_TYPE_ERROR("missing mimeType"); - // Set MIME field. // This may throw. - this->mimeType.SetMimeType(jsonMimeTypeIt->get()); - - // payloadType is mandatory. - // clang-format off - if ( - jsonPayloadTypeIt == data.end() || - !Utils::Json::IsPositiveInteger(*jsonPayloadTypeIt) - ) - // clang-format on - { - MS_THROW_TYPE_ERROR("missing payloadType"); - } + this->mimeType.SetMimeType(data->mimeType()->str()); - this->payloadType = jsonPayloadTypeIt->get(); + // payloadType. + this->payloadType = data->payloadType(); - // clockRate is mandatory. - // clang-format off - if ( - jsonClockRateIt == data.end() || - !Utils::Json::IsPositiveInteger(*jsonClockRateIt) - ) - // clang-format on - { - MS_THROW_TYPE_ERROR("missing clockRate"); - } - - this->clockRate = jsonClockRateIt->get(); + // clockRate. + this->clockRate = data->clockRate(); // channels is optional. - // clang-format off - if ( - jsonChannelsIt != data.end() && - Utils::Json::IsPositiveInteger(*jsonChannelsIt) - ) - // clang-format on + if (data->channels().has_value()) { - this->channels = jsonChannelsIt->get(); + this->channels = data->channels().value(); } // parameters is optional. - if (jsonParametersIt != data.end() && jsonParametersIt->is_object()) - this->parameters.Set(*jsonParametersIt); + if (flatbuffers::IsFieldPresent(data, FBS::RtpParameters::RtpCodecParameters::VT_PARAMETERS)) + { + this->parameters.Set(data->parameters()); + } // rtcpFeedback is optional. - if (jsonRtcpFeedbackIt != data.end() && jsonRtcpFeedbackIt->is_array()) + if (flatbuffers::IsFieldPresent(data, FBS::RtpParameters::RtpCodecParameters::VT_RTCPFEEDBACK)) { - this->rtcpFeedback.reserve(jsonRtcpFeedbackIt->size()); + this->rtcpFeedback.reserve(data->rtcpFeedback()->size()); - for (auto& entry : *jsonRtcpFeedbackIt) + for (const auto* entry : *data->rtcpFeedback()) { - // This may throw due the constructor of RTC::RtcpFeedback. this->rtcpFeedback.emplace_back(entry); } } @@ -89,39 +50,29 @@ namespace RTC CheckCodec(); } - void RtpCodecParameters::FillJson(json& jsonObject) const + flatbuffers::Offset RtpCodecParameters::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Add mimeType. - jsonObject["mimeType"] = this->mimeType.ToString(); - - // Add payloadType. - jsonObject["payloadType"] = this->payloadType; - - // Add clockRate. - jsonObject["clockRate"] = this->clockRate; - - // Add channels. - if (this->channels > 1) - jsonObject["channels"] = this->channels; + auto parameters = this->parameters.FillBuffer(builder); - // Add parameters. - this->parameters.FillJson(jsonObject["parameters"]); + std::vector> rtcpFeedback; + rtcpFeedback.reserve(this->rtcpFeedback.size()); - // Add rtcpFeedback. - jsonObject["rtcpFeedback"] = json::array(); - auto jsonRtcpFeedbackIt = jsonObject.find("rtcpFeedback"); - - for (size_t i{ 0 }; i < this->rtcpFeedback.size(); ++i) + for (const auto& fb : this->rtcpFeedback) { - jsonRtcpFeedbackIt->emplace_back(json::value_t::object); - - auto& jsonEntry = (*jsonRtcpFeedbackIt)[i]; - auto& fb = this->rtcpFeedback[i]; - - fb.FillJson(jsonEntry); + rtcpFeedback.emplace_back(fb.FillBuffer(builder)); } + + return FBS::RtpParameters::CreateRtpCodecParametersDirect( + builder, + this->mimeType.ToString().c_str(), + this->payloadType, + this->clockRate, + this->channels > 1 ? flatbuffers::Optional(this->channels) : flatbuffers::nullopt, + ¶meters, + &rtcpFeedback); } inline void RtpCodecParameters::CheckCodec() @@ -137,7 +88,9 @@ namespace RTC { // A RTX codec must have 'apt' parameter. if (!this->parameters.HasPositiveInteger(aptString)) + { MS_THROW_TYPE_ERROR("missing apt parameter in RTX codec"); + } break; } diff --git a/worker/src/RTC/RtpDictionaries/RtpEncodingParameters.cpp b/worker/src/RTC/RtpDictionaries/RtpEncodingParameters.cpp index 6c745802bd..ccab2e2f37 100644 --- a/worker/src/RTC/RtpDictionaries/RtpEncodingParameters.cpp +++ b/worker/src/RTC/RtpDictionaries/RtpEncodingParameters.cpp @@ -3,7 +3,6 @@ #include "Logger.hpp" #include "MediaSoupErrors.hpp" -#include "Utils.hpp" #include "RTC/RtpDictionaries.hpp" #include #include @@ -12,80 +11,50 @@ namespace RTC { /* Instance methods. */ - RtpEncodingParameters::RtpEncodingParameters(json& data) + RtpEncodingParameters::RtpEncodingParameters(const FBS::RtpParameters::RtpEncodingParameters* data) { MS_TRACE(); - if (!data.is_object()) - MS_THROW_TYPE_ERROR("data is not an object"); - - auto jsonSsrcIt = data.find("ssrc"); - auto jsonRidIt = data.find("rid"); - auto jsonCodecPayloadTypeIt = data.find("codecPayloadType"); - auto jsonRtxIt = data.find("rtx"); - auto jsonMaxBitrateIt = data.find("maxBitrate"); - auto jsonMaxFramerateIt = data.find("maxFramerate"); - auto jsonDtxIt = data.find("dtx"); - auto jsonScalabilityModeIt = data.find("scalabilityMode"); - // ssrc is optional. - // clang-format off - if ( - jsonSsrcIt != data.end() && - Utils::Json::IsPositiveInteger(*jsonSsrcIt) - ) - // clang-format on + if (data->ssrc().has_value()) { - this->ssrc = jsonSsrcIt->get(); + this->ssrc = data->ssrc().value(); } // rid is optional. - if (jsonRidIt != data.end() && jsonRidIt->is_string()) - this->rid = jsonRidIt->get(); + if (flatbuffers::IsFieldPresent(data, FBS::RtpParameters::RtpEncodingParameters::VT_RID)) + { + this->rid = data->rid()->str(); + } // codecPayloadType is optional. - // clang-format off - if ( - jsonCodecPayloadTypeIt != data.end() && - Utils::Json::IsPositiveInteger(*jsonCodecPayloadTypeIt) - ) - // clang-format on + if (data->codecPayloadType().has_value()) { - this->codecPayloadType = jsonCodecPayloadTypeIt->get(); + this->codecPayloadType = data->codecPayloadType().value(); this->hasCodecPayloadType = true; } // rtx is optional. - // This may throw. - if (jsonRtxIt != data.end() && jsonRtxIt->is_object()) + if (flatbuffers::IsFieldPresent(data, FBS::RtpParameters::RtpEncodingParameters::VT_RTX)) { - this->rtx = RtpRtxParameters(*jsonRtxIt); + this->rtx = RtpRtxParameters(data->rtx()); this->hasRtx = true; } // maxBitrate is optional. - // clang-format off - if ( - jsonMaxBitrateIt != data.end() && - Utils::Json::IsPositiveInteger(*jsonMaxBitrateIt) - ) - // clang-format on + if (data->maxBitrate().has_value()) { - this->maxBitrate = jsonMaxBitrateIt->get(); + this->maxBitrate = data->maxBitrate().value(); } - // maxFramerate is optional. - if (jsonMaxFramerateIt != data.end() && jsonMaxFramerateIt->is_number()) - this->maxFramerate = jsonMaxFramerateIt->get(); - - // dtx is optional. - if (jsonDtxIt != data.end() && jsonDtxIt->is_boolean()) - this->dtx = jsonDtxIt->get(); + // dtx is optional, default is false. + this->dtx = data->dtx(); // scalabilityMode is optional. - if (jsonScalabilityModeIt != data.end() && jsonScalabilityModeIt->is_string()) + if (flatbuffers::IsFieldPresent( + data, FBS::RtpParameters::RtpEncodingParameters::VT_SCALABILITYMODE)) { - std::string scalabilityMode = jsonScalabilityModeIt->get(); + const std::string scalabilityMode = data->scalabilityMode()->str(); static const std::regex ScalabilityModeRegex( "^[LS]([1-9]\\d{0,1})T([1-9]\\d{0,1})(_KEY)?.*", std::regex_constants::ECMAScript); @@ -112,48 +81,19 @@ namespace RTC } } - void RtpEncodingParameters::FillJson(json& jsonObject) const + flatbuffers::Offset RtpEncodingParameters::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Force it to be an object even if no key/values are added below. - jsonObject = json::object(); - - // Add ssrc. - if (this->ssrc != 0u) - jsonObject["ssrc"] = this->ssrc; - - // Add rid. - if (!this->rid.empty()) - jsonObject["rid"] = this->rid; - - // Add codecPayloadType. - if (this->hasCodecPayloadType) - jsonObject["codecPayloadType"] = this->codecPayloadType; - - // Add rtx. - if (this->hasRtx) - this->rtx.FillJson(jsonObject["rtx"]); - - // Add maxBitrate. - if (this->maxBitrate != 0u) - jsonObject["maxBitrate"] = this->maxBitrate; - - // Add maxFramerate. - if (this->maxFramerate > 0) - jsonObject["maxFramerate"] = this->maxFramerate; - - // Add dtx. - if (this->dtx) - jsonObject["dtx"] = this->dtx; - - // Add scalabilityMode. - if (!this->scalabilityMode.empty()) - { - jsonObject["scalabilityMode"] = this->scalabilityMode; - jsonObject["spatialLayers"] = this->spatialLayers; - jsonObject["temporalLayers"] = this->temporalLayers; - jsonObject["ksvc"] = this->ksvc; - } + return FBS::RtpParameters::CreateRtpEncodingParametersDirect( + builder, + this->ssrc != 0u ? flatbuffers::Optional(this->ssrc) : flatbuffers::nullopt, + this->rid.size() > 0 ? this->rid.c_str() : nullptr, + this->hasCodecPayloadType ? flatbuffers::Optional(this->codecPayloadType) + : flatbuffers::nullopt, + this->hasRtx ? this->rtx.FillBuffer(builder) : 0u, + this->dtx, + this->scalabilityMode.c_str()); } } // namespace RTC diff --git a/worker/src/RTC/RtpDictionaries/RtpHeaderExtensionParameters.cpp b/worker/src/RTC/RtpDictionaries/RtpHeaderExtensionParameters.cpp index bb762881bc..636e175b69 100644 --- a/worker/src/RTC/RtpDictionaries/RtpHeaderExtensionParameters.cpp +++ b/worker/src/RTC/RtpDictionaries/RtpHeaderExtensionParameters.cpp @@ -3,70 +3,47 @@ #include "Logger.hpp" #include "MediaSoupErrors.hpp" -#include "Utils.hpp" #include "RTC/RtpDictionaries.hpp" namespace RTC { /* Instance methods. */ - RtpHeaderExtensionParameters::RtpHeaderExtensionParameters(json& data) + RtpHeaderExtensionParameters::RtpHeaderExtensionParameters( + const FBS::RtpParameters::RtpHeaderExtensionParameters* const data) { MS_TRACE(); - if (!data.is_object()) - MS_THROW_TYPE_ERROR("data is not an object"); - - auto jsonUriIt = data.find("uri"); - auto jsonIdIt = data.find("id"); - auto jsonEncryptIt = data.find("encrypt"); - auto jsonParametersIt = data.find("parameters"); - - // uri is mandatory. - if (jsonUriIt == data.end() || !jsonUriIt->is_string()) - MS_THROW_TYPE_ERROR("missing uri"); - - this->uri = jsonUriIt->get(); - - if (this->uri.empty()) - MS_THROW_TYPE_ERROR("empty uri"); - // Get the type. - this->type = RTC::RtpHeaderExtensionUri::GetType(this->uri); - - // id is mandatory. - if (jsonIdIt == data.end() || !Utils::Json::IsPositiveInteger(*jsonIdIt)) - MS_THROW_TYPE_ERROR("missing id"); + this->type = RTC::RtpHeaderExtensionUri::TypeFromFbs(data->uri()); - this->id = jsonIdIt->get(); + this->id = data->id(); // Don't allow id 0. if (this->id == 0u) + { MS_THROW_TYPE_ERROR("invalid id 0"); + } - // encrypt is optional. - if (jsonEncryptIt != data.end() && jsonEncryptIt->is_boolean()) - this->encrypt = jsonEncryptIt->get(); + // encrypt is false by default. + this->encrypt = data->encrypt(); // parameters is optional. - if (jsonParametersIt != data.end() && jsonParametersIt->is_object()) - this->parameters.Set(*jsonParametersIt); + if (flatbuffers::IsFieldPresent( + data, FBS::RtpParameters::RtpHeaderExtensionParameters::VT_PARAMETERS)) + { + this->parameters.Set(data->parameters()); + } } - void RtpHeaderExtensionParameters::FillJson(json& jsonObject) const + flatbuffers::Offset RtpHeaderExtensionParameters::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Add uri. - jsonObject["uri"] = this->uri; - - // Add id. - jsonObject["id"] = this->id; - - // Add encrypt. - jsonObject["encrypt"] = this->encrypt; + auto parameters = this->parameters.FillBuffer(builder); - // Add parameters. - this->parameters.FillJson(jsonObject["parameters"]); + return FBS::RtpParameters::CreateRtpHeaderExtensionParametersDirect( + builder, RTC::RtpHeaderExtensionUri::TypeToFbs(this->type), this->id, this->encrypt, ¶meters); } } // namespace RTC diff --git a/worker/src/RTC/RtpDictionaries/RtpHeaderExtensionUri.cpp b/worker/src/RTC/RtpDictionaries/RtpHeaderExtensionUri.cpp index e4e11e67fe..f451e7c7ba 100644 --- a/worker/src/RTC/RtpDictionaries/RtpHeaderExtensionUri.cpp +++ b/worker/src/RTC/RtpDictionaries/RtpHeaderExtensionUri.cpp @@ -2,45 +2,144 @@ // #define MS_LOG_DEV_LEVEL 3 #include "Logger.hpp" -#include "Utils.hpp" #include "RTC/RtpDictionaries.hpp" namespace RTC { - /* Class variables. */ + /* Class methods. */ - // clang-format off - absl::flat_hash_map RtpHeaderExtensionUri::string2Type = + RtpHeaderExtensionUri::Type RtpHeaderExtensionUri::TypeFromFbs( + FBS::RtpParameters::RtpHeaderExtensionUri uri) { - { "urn:ietf:params:rtp-hdrext:sdes:mid", RtpHeaderExtensionUri::Type::MID }, - { "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", RtpHeaderExtensionUri::Type::RTP_STREAM_ID }, - { "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", RtpHeaderExtensionUri::Type::REPAIRED_RTP_STREAM_ID }, - { "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", RtpHeaderExtensionUri::Type::ABS_SEND_TIME }, - { "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", RtpHeaderExtensionUri::Type::TRANSPORT_WIDE_CC_01 }, - // NOTE: Remove this once framemarking draft becomes RFC. - { "http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07", RtpHeaderExtensionUri::Type::FRAME_MARKING_07 }, - { "urn:ietf:params:rtp-hdrext:framemarking", RtpHeaderExtensionUri::Type::FRAME_MARKING }, - { "urn:ietf:params:rtp-hdrext:ssrc-audio-level", RtpHeaderExtensionUri::Type::SSRC_AUDIO_LEVEL }, - { "urn:3gpp:video-orientation", RtpHeaderExtensionUri::Type::VIDEO_ORIENTATION }, - { "urn:ietf:params:rtp-hdrext:toffset", RtpHeaderExtensionUri::Type::TOFFSET }, - { "http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time", RtpHeaderExtensionUri::Type::ABS_CAPTURE_TIME }, - }; - // clang-format on + switch (uri) + { + case FBS::RtpParameters::RtpHeaderExtensionUri::Mid: + { + return RtpHeaderExtensionUri::Type::MID; + } - /* Class methods. */ + case FBS::RtpParameters::RtpHeaderExtensionUri::RtpStreamId: + { + return RtpHeaderExtensionUri::Type::RTP_STREAM_ID; + } + + case FBS::RtpParameters::RtpHeaderExtensionUri::RepairRtpStreamId: + { + return RtpHeaderExtensionUri::Type::REPAIRED_RTP_STREAM_ID; + } + + case FBS::RtpParameters::RtpHeaderExtensionUri::FrameMarkingDraft07: + { + return RtpHeaderExtensionUri::Type::FRAME_MARKING_07; + } + + case FBS::RtpParameters::RtpHeaderExtensionUri::FrameMarking: + { + return RtpHeaderExtensionUri::Type::FRAME_MARKING; + } + + case FBS::RtpParameters::RtpHeaderExtensionUri::AudioLevel: + { + return RtpHeaderExtensionUri::Type::SSRC_AUDIO_LEVEL; + } + + case FBS::RtpParameters::RtpHeaderExtensionUri::VideoOrientation: + { + return RtpHeaderExtensionUri::Type::VIDEO_ORIENTATION; + } + + case FBS::RtpParameters::RtpHeaderExtensionUri::PlayoutDelay: + { + return RtpHeaderExtensionUri::Type::PLAYOUT_DELAY; + } + + case FBS::RtpParameters::RtpHeaderExtensionUri::TimeOffset: + { + return RtpHeaderExtensionUri::Type::TOFFSET; + } + + case FBS::RtpParameters::RtpHeaderExtensionUri::TransportWideCcDraft01: + { + return RtpHeaderExtensionUri::Type::TRANSPORT_WIDE_CC_01; + } + + case FBS::RtpParameters::RtpHeaderExtensionUri::AbsSendTime: + { + return RtpHeaderExtensionUri::Type::ABS_SEND_TIME; + } - RtpHeaderExtensionUri::Type RtpHeaderExtensionUri::GetType(std::string& uri) + case FBS::RtpParameters::RtpHeaderExtensionUri::AbsCaptureTime: + { + return RtpHeaderExtensionUri::Type::ABS_CAPTURE_TIME; + } + } + } + + FBS::RtpParameters::RtpHeaderExtensionUri RtpHeaderExtensionUri::TypeToFbs( + RtpHeaderExtensionUri::Type uri) { - MS_TRACE(); + switch (uri) + { + case RtpHeaderExtensionUri::Type::MID: + { + return FBS::RtpParameters::RtpHeaderExtensionUri::Mid; + } + + case RtpHeaderExtensionUri::Type::RTP_STREAM_ID: + { + return FBS::RtpParameters::RtpHeaderExtensionUri::RtpStreamId; + } + + case RtpHeaderExtensionUri::Type::REPAIRED_RTP_STREAM_ID: + { + return FBS::RtpParameters::RtpHeaderExtensionUri::RepairRtpStreamId; + } + + case RtpHeaderExtensionUri::Type::ABS_SEND_TIME: + { + return FBS::RtpParameters::RtpHeaderExtensionUri::AbsSendTime; + } - // Force lowcase names. - Utils::String::ToLowerCase(uri); + case RtpHeaderExtensionUri::Type::TRANSPORT_WIDE_CC_01: + { + return FBS::RtpParameters::RtpHeaderExtensionUri::TransportWideCcDraft01; + } - auto it = RtpHeaderExtensionUri::string2Type.find(uri); + case RtpHeaderExtensionUri::Type::FRAME_MARKING_07: + { + return FBS::RtpParameters::RtpHeaderExtensionUri::FrameMarkingDraft07; + } - if (it != RtpHeaderExtensionUri::string2Type.end()) - return it->second; + case RtpHeaderExtensionUri::Type::FRAME_MARKING: + { + return FBS::RtpParameters::RtpHeaderExtensionUri::FrameMarking; + } - return RtpHeaderExtensionUri::Type::UNKNOWN; + case RtpHeaderExtensionUri::Type::SSRC_AUDIO_LEVEL: + { + return FBS::RtpParameters::RtpHeaderExtensionUri::AudioLevel; + } + + case RtpHeaderExtensionUri::Type::VIDEO_ORIENTATION: + { + return FBS::RtpParameters::RtpHeaderExtensionUri::VideoOrientation; + } + + case RtpHeaderExtensionUri::Type::PLAYOUT_DELAY: + { + return FBS::RtpParameters::RtpHeaderExtensionUri::PlayoutDelay; + } + + case RtpHeaderExtensionUri::Type::TOFFSET: + { + return FBS::RtpParameters::RtpHeaderExtensionUri::TimeOffset; + } + + case RtpHeaderExtensionUri::Type::ABS_CAPTURE_TIME: + { + return FBS::RtpParameters::RtpHeaderExtensionUri::AbsCaptureTime; + } + } } + } // namespace RTC diff --git a/worker/src/RTC/RtpDictionaries/RtpParameters.cpp b/worker/src/RTC/RtpDictionaries/RtpParameters.cpp index 5ea8bc1bc4..490d1f8dbe 100644 --- a/worker/src/RTC/RtpDictionaries/RtpParameters.cpp +++ b/worker/src/RTC/RtpDictionaries/RtpParameters.cpp @@ -3,6 +3,7 @@ #include "Logger.hpp" #include "MediaSoupErrors.hpp" +#include "RTC/Codecs/Tools.hpp" #include "RTC/RtpDictionaries.hpp" #include @@ -10,213 +11,186 @@ namespace RTC { /* Class variables. */ - // clang-format off - absl::flat_hash_map RtpParameters::string2Type = - { - { "none", RtpParameters::Type::NONE }, - { "simple", RtpParameters::Type::SIMPLE }, - { "simulcast", RtpParameters::Type::SIMULCAST }, - { "svc", RtpParameters::Type::SVC }, - { "pipe", RtpParameters::Type::PIPE } - }; - absl::flat_hash_map RtpParameters::type2String = - { - { RtpParameters::Type::NONE, "none" }, - { RtpParameters::Type::SIMPLE, "simple" }, + absl::flat_hash_map RtpParameters::type2String = { + { RtpParameters::Type::SIMPLE, "simple" }, { RtpParameters::Type::SIMULCAST, "simulcast" }, - { RtpParameters::Type::SVC, "svc" }, - { RtpParameters::Type::PIPE, "pipe" } + { RtpParameters::Type::SVC, "svc" }, + { RtpParameters::Type::PIPE, "pipe" } }; // clang-format on /* Class methods. */ - RtpParameters::Type RtpParameters::GetType(const RtpParameters& rtpParameters) + std::optional RtpParameters::GetType(const RtpParameters& rtpParameters) { MS_TRACE(); + std::optional type; + if (rtpParameters.encodings.size() == 1) { - auto& encoding = rtpParameters.encodings[0]; + const auto& encoding = rtpParameters.encodings[0]; + const auto* mediaCodec = + rtpParameters.GetCodecForEncoding(const_cast(encoding)); if (encoding.spatialLayers > 1 || encoding.temporalLayers > 1) - return RtpParameters::Type::SVC; + { + if (RTC::Codecs::Tools::IsValidTypeForCodec(RtpParameters::Type::SVC, mediaCodec->mimeType)) + { + type.emplace(RtpParameters::Type::SVC); + } + else if (RTC::Codecs::Tools::IsValidTypeForCodec( + RtpParameters::Type::SIMULCAST, mediaCodec->mimeType)) + { + type.emplace(RtpParameters::Type::SIMULCAST); + } + } else - return RtpParameters::Type::SIMPLE; + { + type.emplace(RtpParameters::Type::SIMPLE); + } } else if (rtpParameters.encodings.size() > 1) { - return RtpParameters::Type::SIMULCAST; + type.emplace(RtpParameters::Type::SIMULCAST); } - return RtpParameters::Type::NONE; + return type; } - RtpParameters::Type RtpParameters::GetType(std::string& str) + std::string& RtpParameters::GetTypeString(RtpParameters::Type type) { MS_TRACE(); - auto it = RtpParameters::string2Type.find(str); - - if (it == RtpParameters::string2Type.end()) - MS_THROW_TYPE_ERROR("invalid RtpParameters type [type:%s]", str.c_str()); - - return it->second; + return RtpParameters::type2String.at(type); } - RtpParameters::Type RtpParameters::GetType(std::string&& str) + FBS::RtpParameters::Type RtpParameters::TypeToFbs(RtpParameters::Type type) { MS_TRACE(); - auto it = RtpParameters::string2Type.find(str); - - if (it == RtpParameters::string2Type.end()) - MS_THROW_TYPE_ERROR("invalid RtpParameters type [type:%s]", str.c_str()); + switch (type) + { + case Type::SIMPLE: + { + return FBS::RtpParameters::Type::SIMPLE; + } - return it->second; - } + case Type::SIMULCAST: + { + return FBS::RtpParameters::Type::SIMULCAST; + } - std::string& RtpParameters::GetTypeString(RtpParameters::Type type) - { - MS_TRACE(); + case Type::SVC: + { + return FBS::RtpParameters::Type::SVC; + } - return RtpParameters::type2String.at(type); + case Type::PIPE: + { + return FBS::RtpParameters::Type::PIPE; + } + } } /* Instance methods. */ - RtpParameters::RtpParameters(json& data) + RtpParameters::RtpParameters(const FBS::RtpParameters::RtpParameters* data) { MS_TRACE(); - if (!data.is_object()) - MS_THROW_TYPE_ERROR("data is not an object"); - - auto jsonMidIt = data.find("mid"); - auto jsonCodecsIt = data.find("codecs"); - auto jsonEncodingsIt = data.find("encodings"); - auto jsonHeaderExtensionsIt = data.find("headerExtensions"); - auto jsonRtcpIt = data.find("rtcp"); - // mid is optional. - if (jsonMidIt != data.end() && jsonMidIt->is_string()) + if (data->mid()) { - this->mid = jsonMidIt->get(); + this->mid = data->mid()->str(); if (this->mid.empty()) + { MS_THROW_TYPE_ERROR("empty mid"); + } } - // codecs is mandatory. - if (jsonCodecsIt == data.end() || !jsonCodecsIt->is_array()) - MS_THROW_TYPE_ERROR("missing codecs"); + this->codecs.reserve(data->codecs()->size()); - this->codecs.reserve(jsonCodecsIt->size()); - - for (auto& entry : *jsonCodecsIt) + for (const auto* entry : *data->codecs()) { // This may throw due the constructor of RTC::RtpCodecParameters. this->codecs.emplace_back(entry); } if (this->codecs.empty()) + { MS_THROW_TYPE_ERROR("empty codecs"); + } - // encodings is mandatory. - if (jsonEncodingsIt == data.end() || !jsonEncodingsIt->is_array()) - MS_THROW_TYPE_ERROR("missing encodings"); - - this->encodings.reserve(jsonEncodingsIt->size()); + this->encodings.reserve(data->encodings()->size()); - for (auto& entry : *jsonEncodingsIt) + for (const auto* entry : *data->encodings()) { // This may throw due the constructor of RTC::RtpEncodingParameters. this->encodings.emplace_back(entry); } if (this->encodings.empty()) - MS_THROW_TYPE_ERROR("empty encodings"); - - // headerExtensions is optional. - if (jsonHeaderExtensionsIt != data.end() && jsonHeaderExtensionsIt->is_array()) { - this->headerExtensions.reserve(jsonHeaderExtensionsIt->size()); - - for (auto& entry : *jsonHeaderExtensionsIt) - { - // This may throw due the constructor of RTC::RtpHeaderExtensionParameters. - this->headerExtensions.emplace_back(entry); - } + MS_THROW_TYPE_ERROR("empty encodings"); } - // rtcp is optional. - if (jsonRtcpIt != data.end() && jsonRtcpIt->is_object()) + this->headerExtensions.reserve(data->headerExtensions()->size()); + + for (const auto* entry : *data->headerExtensions()) { - // This may throw. - this->rtcp = RTC::RtcpParameters(*jsonRtcpIt); - this->hasRtcp = true; + // This may throw due the constructor of RTC::RtpHeaderExtensionParameters. + this->headerExtensions.emplace_back(entry); } + // This may throw. + this->rtcp = RTC::RtcpParameters(data->rtcp()); + // Validate RTP parameters. ValidateCodecs(); ValidateEncodings(); } - void RtpParameters::FillJson(json& jsonObject) const + flatbuffers::Offset RtpParameters::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Add mid. - if (!this->mid.empty()) - jsonObject["mid"] = this->mid; - // Add codecs. - jsonObject["codecs"] = json::array(); - auto jsonCodecsIt = jsonObject.find("codecs"); + std::vector> codecs; + codecs.reserve(this->codecs.size()); - for (size_t i{ 0 }; i < this->codecs.size(); ++i) + for (const auto& codec : this->codecs) { - jsonCodecsIt->emplace_back(json::value_t::object); - - auto& jsonEntry = (*jsonCodecsIt)[i]; - auto& codec = this->codecs[i]; - - codec.FillJson(jsonEntry); + codecs.emplace_back(codec.FillBuffer(builder)); } // Add encodings. - jsonObject["encodings"] = json::array(); - auto jsonEncodingsIt = jsonObject.find("encodings"); + std::vector> encodings; + encodings.reserve(this->encodings.size()); - for (size_t i{ 0 }; i < this->encodings.size(); ++i) + for (const auto& encoding : this->encodings) { - jsonEncodingsIt->emplace_back(json::value_t::object); - - auto& jsonEntry = (*jsonEncodingsIt)[i]; - auto& encoding = this->encodings[i]; - - encoding.FillJson(jsonEntry); + encodings.emplace_back(encoding.FillBuffer(builder)); } // Add headerExtensions. - jsonObject["headerExtensions"] = json::array(); - auto jsonHeaderExtensionsIt = jsonObject.find("headerExtensions"); + std::vector> headerExtensions; + headerExtensions.reserve(this->headerExtensions.size()); - for (size_t i{ 0 }; i < this->headerExtensions.size(); ++i) + for (const auto& headerExtension : this->headerExtensions) { - jsonHeaderExtensionsIt->emplace_back(json::value_t::object); - - auto& jsonEntry = (*jsonHeaderExtensionsIt)[i]; - auto& headerExtension = this->headerExtensions[i]; - - headerExtension.FillJson(jsonEntry); + headerExtensions.emplace_back(headerExtension.FillBuffer(builder)); } // Add rtcp. - if (this->hasRtcp) - this->rtcp.FillJson(jsonObject["rtcp"]); - else - jsonObject["rtcp"] = json::object(); + flatbuffers::Offset rtcp; + + rtcp = this->rtcp.FillBuffer(builder); + + return FBS::RtpParameters::CreateRtpParametersDirect( + builder, mid.c_str(), &codecs, &headerExtensions, &encodings, rtcp); } const RTC::RtpCodecParameters* RtpParameters::GetCodecForEncoding(RtpEncodingParameters& encoding) const @@ -228,15 +202,19 @@ namespace RTC for (; it != this->codecs.end(); ++it) { - auto& codec = *it; + const auto& codec = *it; if (codec.payloadType == payloadType) + { return std::addressof(codec); + } } // This should never happen. if (it == this->codecs.end()) + { MS_ABORT("no valid codec payload type for the given encoding"); + } return nullptr; } @@ -271,7 +249,9 @@ namespace RTC for (auto& codec : this->codecs) { if (payloadTypes.find(codec.payloadType) != payloadTypes.end()) + { MS_THROW_TYPE_ERROR("duplicated payloadType"); + } payloadTypes.insert(codec.payloadType); @@ -291,18 +271,28 @@ namespace RTC if (static_cast(codec.payloadType) == apt) { if (codec.mimeType.subtype == RTC::RtpCodecMimeType::Subtype::RTX) + { MS_THROW_TYPE_ERROR("apt in RTX codec points to a RTX codec"); + } else if (codec.mimeType.subtype == RTC::RtpCodecMimeType::Subtype::ULPFEC) + { MS_THROW_TYPE_ERROR("apt in RTX codec points to a ULPFEC codec"); + } else if (codec.mimeType.subtype == RTC::RtpCodecMimeType::Subtype::FLEXFEC) + { MS_THROW_TYPE_ERROR("apt in RTX codec points to a FLEXFEC codec"); + } else + { break; + } } } if (it == this->codecs.end()) + { MS_THROW_TYPE_ERROR("apt in RTX codec points to a non existing codec"); + } break; } @@ -333,7 +323,9 @@ namespace RTC } if (it == this->codecs.end()) + { MS_THROW_TYPE_ERROR("no media codecs found"); + } } // Iterate all the encodings, set the first payloadType in all of them with @@ -366,14 +358,18 @@ namespace RTC { // Must be a media codec. if (codec.mimeType.IsMediaCodec()) + { break; + } MS_THROW_TYPE_ERROR("invalid codecPayloadType"); } } if (it == this->codecs.end()) + { MS_THROW_TYPE_ERROR("unknown codecPayloadType"); + } } } } diff --git a/worker/src/RTC/RtpDictionaries/RtpRtxParameters.cpp b/worker/src/RTC/RtpDictionaries/RtpRtxParameters.cpp index eaf8726395..3e760bb581 100644 --- a/worker/src/RTC/RtpDictionaries/RtpRtxParameters.cpp +++ b/worker/src/RTC/RtpDictionaries/RtpRtxParameters.cpp @@ -2,44 +2,24 @@ // #define MS_LOG_DEV_LEVEL 3 #include "Logger.hpp" -#include "MediaSoupErrors.hpp" -#include "Utils.hpp" #include "RTC/RtpDictionaries.hpp" namespace RTC { /* Instance methods. */ - RtpRtxParameters::RtpRtxParameters(json& data) + RtpRtxParameters::RtpRtxParameters(const FBS::RtpParameters::Rtx* data) { MS_TRACE(); - if (!data.is_object()) - MS_THROW_TYPE_ERROR("data is not an object"); - - auto jsonSsrcIt = data.find("ssrc"); - - // ssrc is optional. - // clang-format off - if ( - jsonSsrcIt != data.end() && - Utils::Json::IsPositiveInteger(*jsonSsrcIt) - ) - // clang-format on - { - this->ssrc = jsonSsrcIt->get(); - } + this->ssrc = data->ssrc(); } - void RtpRtxParameters::FillJson(json& jsonObject) const + flatbuffers::Offset RtpRtxParameters::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Force it to be an object even if no key/values are added below. - jsonObject = json::object(); - - // Add ssrc (optional). - if (this->ssrc != 0u) - jsonObject["ssrc"] = this->ssrc; + return FBS::RtpParameters::CreateRtx(builder, this->ssrc); } } // namespace RTC diff --git a/worker/src/RTC/RtpListener.cpp b/worker/src/RTC/RtpListener.cpp index 2afef5e1b4..c11b43e44a 100644 --- a/worker/src/RTC/RtpListener.cpp +++ b/worker/src/RTC/RtpListener.cpp @@ -10,44 +10,48 @@ namespace RTC { /* Instance methods. */ - void RtpListener::FillJson(json& jsonObject) const + flatbuffers::Offset RtpListener::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - jsonObject["ssrcTable"] = json::object(); - jsonObject["midTable"] = json::object(); - jsonObject["ridTable"] = json::object(); - - auto jsonSsrcTableIt = jsonObject.find("ssrcTable"); - auto jsonMidTableIt = jsonObject.find("midTable"); - auto jsonRidTableIt = jsonObject.find("ridTable"); - // Add ssrcTable. - for (auto& kv : this->ssrcTable) + std::vector> ssrcTable; + + for (const auto& kv : this->ssrcTable) { auto ssrc = kv.first; auto* producer = kv.second; - (*jsonSsrcTableIt)[std::to_string(ssrc)] = producer->id; + ssrcTable.emplace_back( + FBS::Common::CreateUint32StringDirect(builder, ssrc, producer->id.c_str())); } // Add midTable. - for (auto& kv : this->midTable) + std::vector> midTable; + + for (const auto& kv : this->midTable) { - auto& mid = kv.first; - auto* producer = kv.second; + const auto& mid = kv.first; + auto* producer = kv.second; - (*jsonMidTableIt)[mid] = producer->id; + midTable.emplace_back( + FBS::Common::CreateStringStringDirect(builder, mid.c_str(), producer->id.c_str())); } // Add ridTable. - for (auto& kv : this->ridTable) + std::vector> ridTable; + + for (const auto& kv : this->ridTable) { - auto& rid = kv.first; - auto* producer = kv.second; + const auto& rid = kv.first; + auto* producer = kv.second; - (*jsonRidTableIt)[rid] = producer->id; + ridTable.emplace_back( + FBS::Common::CreateStringStringDirect(builder, rid.c_str(), producer->id.c_str())); } + + return FBS::Transport::CreateRtpListenerDirect(builder, &ssrcTable, &midTable, &ridTable); } void RtpListener::AddProducer(RTC::Producer* producer) @@ -57,7 +61,7 @@ namespace RTC const auto& rtpParameters = producer->GetRtpParameters(); // Add entries into the ssrcTable. - for (auto& encoding : rtpParameters.encodings) + for (const auto& encoding : rtpParameters.encodings) { uint32_t ssrc; @@ -99,7 +103,7 @@ namespace RTC // Add entries into midTable. if (!rtpParameters.mid.empty()) { - auto& mid = rtpParameters.mid; + const auto& mid = rtpParameters.mid; if (this->midTable.find(mid) == this->midTable.end()) { @@ -114,12 +118,14 @@ namespace RTC } // Add entries into ridTable. - for (auto& encoding : rtpParameters.encodings) + for (const auto& encoding : rtpParameters.encodings) { - auto& rid = encoding.rid; + const auto& rid = encoding.rid; if (rid.empty()) + { continue; + } if (this->ridTable.find(rid) == this->ridTable.end()) { @@ -144,25 +150,37 @@ namespace RTC for (auto it = this->ssrcTable.begin(); it != this->ssrcTable.end();) { if (it->second == producer) + { it = this->ssrcTable.erase(it); + } else + { ++it; + } } for (auto it = this->midTable.begin(); it != this->midTable.end();) { if (it->second == producer) + { it = this->midTable.erase(it); + } else + { ++it; + } } for (auto it = this->ridTable.begin(); it != this->ridTable.end();) { if (it->second == producer) + { it = this->ridTable.erase(it); + } else + { ++it; + } } } diff --git a/worker/src/RTC/RtpObserver.cpp b/worker/src/RTC/RtpObserver.cpp index e57dfe542b..b8c66bf72b 100644 --- a/worker/src/RTC/RtpObserver.cpp +++ b/worker/src/RTC/RtpObserver.cpp @@ -24,9 +24,9 @@ namespace RTC { MS_TRACE(); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::RTP_OBSERVER_PAUSE: + case Channel::ChannelRequest::Method::RTPOBSERVER_PAUSE: { this->Pause(); @@ -35,7 +35,7 @@ namespace RTC break; } - case Channel::ChannelRequest::MethodId::RTP_OBSERVER_RESUME: + case Channel::ChannelRequest::Method::RTPOBSERVER_RESUME: { this->Resume(); @@ -44,10 +44,11 @@ namespace RTC break; } - case Channel::ChannelRequest::MethodId::RTP_OBSERVER_ADD_PRODUCER: + case Channel::ChannelRequest::Method::RTPOBSERVER_ADD_PRODUCER: { - // This may throw. - auto producerId = GetProducerIdFromData(request->data); + const auto* body = request->data->body_as(); + auto producerId = body->producerId()->str(); + RTC::Producer* producer = this->listener->RtpObserverGetProducer(this, producerId); this->AddProducer(producer); @@ -59,10 +60,11 @@ namespace RTC break; } - case Channel::ChannelRequest::MethodId::RTP_OBSERVER_REMOVE_PRODUCER: + case Channel::ChannelRequest::Method::RTPOBSERVER_REMOVE_PRODUCER: { - // This may throw. - auto producerId = GetProducerIdFromData(request->data); + const auto* body = request->data->body_as(); + auto producerId = body->producerId()->str(); + RTC::Producer* producer = this->listener->RtpObserverGetProducer(this, producerId); this->RemoveProducer(producer); @@ -77,7 +79,7 @@ namespace RTC default: { - MS_THROW_ERROR("unknown method '%s'", request->method.c_str()); + MS_THROW_ERROR("unknown method '%s'", request->methodCStr); } } } @@ -87,7 +89,9 @@ namespace RTC MS_TRACE(); if (this->paused) + { return; + } this->paused = true; @@ -99,22 +103,12 @@ namespace RTC MS_TRACE(); if (!this->paused) + { return; + } this->paused = false; Resumed(); } - - std::string RtpObserver::GetProducerIdFromData(json& data) const - { - MS_TRACE(); - - auto jsonRouterIdIt = data.find("producerId"); - - if (jsonRouterIdIt == data.end() || !jsonRouterIdIt->is_string()) - MS_THROW_ERROR("missing data.producerId"); - - return jsonRouterIdIt->get(); - } } // namespace RTC diff --git a/worker/src/RTC/RtpPacket.cpp b/worker/src/RTC/RtpPacket.cpp index 51d4757eab..0196234f89 100644 --- a/worker/src/RTC/RtpPacket.cpp +++ b/worker/src/RTC/RtpPacket.cpp @@ -2,6 +2,7 @@ // #define MS_LOG_DEV_LEVEL 3 #include "RTC/RtpPacket.hpp" +#include "DepLibUV.hpp" #include "Logger.hpp" #include // std::memcpy(), std::memmove(), std::memset() #include // std::ostream_iterator @@ -16,7 +17,9 @@ namespace RTC MS_TRACE(); if (!RtpPacket::IsRtp(data, len)) + { return nullptr; + } auto* ptr = const_cast(data); @@ -93,6 +96,7 @@ namespace RTC } payloadPadding = data[len - 1]; + if (payloadPadding == 0) { MS_WARN_TAG(rtp, "padding byte cannot be 0, packet discarded"); @@ -109,6 +113,7 @@ namespace RTC return nullptr; } + payloadLength -= size_t{ payloadPadding }; } @@ -135,20 +140,27 @@ namespace RTC MS_TRACE(); if (this->header->csrcCount != 0u) + { this->csrcList = reinterpret_cast(header) + HeaderSize; + } // Parse RFC 5285 header extension. ParseExtensions(); + +// Avoid retrieving the time if RTC logger is disabled. +#ifdef MS_RTC_LOGGER_RTP + // Initialize logger. + this->logger.timestamp = DepLibUV::GetTimeMs(); + this->logger.recvRtpTimestamp = this->GetTimestamp(); + this->logger.recvSeqNumber = this->GetSequenceNumber(); +#endif } RtpPacket::~RtpPacket() { MS_TRACE(); - if (this->buffer) - { - delete[] this->buffer; - } + delete[] this->buffer; } void RtpPacket::Dump() const @@ -156,21 +168,21 @@ namespace RTC MS_TRACE(); MS_DUMP(""); - MS_DUMP(" padding : %s", this->header->padding ? "true" : "false"); + MS_DUMP(" padding: %s", this->header->padding ? "true" : "false"); if (HasHeaderExtension()) { MS_DUMP( - " header extension : id:%" PRIu16 ", length:%zu", + " header extension: id:%" PRIu16 ", length:%zu", GetHeaderExtensionId(), GetHeaderExtensionLength()); } if (HasOneByteExtensions()) { - MS_DUMP(" RFC5285 ext style : One-Byte Header"); + MS_DUMP(" RFC5285 ext style: One-Byte Header"); } if (HasTwoBytesExtensions()) { - MS_DUMP(" RFC5285 ext style : Two-Bytes Header"); + MS_DUMP(" RFC5285 ext style: Two-Bytes Header"); } if (HasOneByteExtensions() || HasTwoBytesExtensions()) { @@ -203,7 +215,7 @@ namespace RTC extIds.begin(), extIds.end() - 1, std::ostream_iterator(extIdsStream, ",")); extIdsStream << extIds.back(); - MS_DUMP(" RFC5285 ext ids : %s", extIdsStream.str().c_str()); + MS_DUMP(" RFC5285 ext ids: %s", extIdsStream.str().c_str()); } } if (this->midExtensionId != 0u) @@ -212,8 +224,7 @@ namespace RTC if (ReadMid(mid)) { - MS_DUMP( - " mid : extId:%" PRIu8 ", value:'%s'", this->midExtensionId, mid.c_str()); + MS_DUMP(" mid: extId:%" PRIu8 ", value:'%s'", this->midExtensionId, mid.c_str()); } } if (this->ridExtensionId != 0u) @@ -222,8 +233,7 @@ namespace RTC if (ReadRid(rid)) { - MS_DUMP( - " rid : extId:%" PRIu8 ", value:'%s'", this->ridExtensionId, rid.c_str()); + MS_DUMP(" rid: extId:%" PRIu8 ", value:'%s'", this->ridExtensionId, rid.c_str()); } } if (this->rridExtensionId != 0u) @@ -232,13 +242,12 @@ namespace RTC if (ReadRid(rid)) { - MS_DUMP( - " rrid : extId:%" PRIu8 ", value:'%s'", this->rridExtensionId, rid.c_str()); + MS_DUMP(" rrid: extId:%" PRIu8 ", value:'%s'", this->rridExtensionId, rid.c_str()); } } if (this->absSendTimeExtensionId != 0u) { - MS_DUMP(" absSendTime : extId:%" PRIu8, this->absSendTimeExtensionId); + MS_DUMP(" absSendTime: extId:%" PRIu8, this->absSendTimeExtensionId); } if (this->transportWideCc01ExtensionId != 0u) { @@ -247,7 +256,7 @@ namespace RTC if (ReadTransportWideCc01(wideSeqNumber)) { MS_DUMP( - " transportWideCc01 : extId:%" PRIu8 ", value:%" PRIu16, + " transportWideCc01: extId:%" PRIu8 ", value:%" PRIu16, this->transportWideCc01ExtensionId, wideSeqNumber); } @@ -255,11 +264,11 @@ namespace RTC // Remove once it becomes RFC. if (this->frameMarking07ExtensionId != 0u) { - MS_DUMP(" frameMarking07 : extId:%" PRIu8, this->frameMarking07ExtensionId); + MS_DUMP(" frameMarking07: extId:%" PRIu8, this->frameMarking07ExtensionId); } if (this->frameMarkingExtensionId != 0u) { - MS_DUMP(" frameMarking : extId:%" PRIu8, this->frameMarkingExtensionId); + MS_DUMP(" frameMarking: extId:%" PRIu8, this->frameMarkingExtensionId); } if (this->ssrcAudioLevelExtensionId != 0u) { @@ -269,7 +278,7 @@ namespace RTC if (ReadSsrcAudioLevel(volume, voice)) { MS_DUMP( - " ssrcAudioLevel : extId:%" PRIu8 ", volume:%" PRIu8 ", voice:%s", + " ssrcAudioLevel: extId:%" PRIu8 ", volume:%" PRIu8 ", voice:%s", this->ssrcAudioLevelExtensionId, volume, voice ? "true" : "false"); @@ -284,87 +293,98 @@ namespace RTC if (ReadVideoOrientation(camera, flip, rotation)) { MS_DUMP( - " videoOrientation : extId:%" PRIu8 ", camera:%s, flip:%s, rotation:%" PRIu16, + " videoOrientation: extId:%" PRIu8 ", camera:%s, flip:%s, rotation:%" PRIu16, this->videoOrientationExtensionId, camera ? "true" : "false", flip ? "true" : "false", rotation); } } - MS_DUMP(" csrc count : %" PRIu8, this->header->csrcCount); - MS_DUMP(" marker : %s", HasMarker() ? "true" : "false"); - MS_DUMP(" payload type : %" PRIu8, GetPayloadType()); - MS_DUMP(" sequence number : %" PRIu16, GetSequenceNumber()); - MS_DUMP(" timestamp : %" PRIu32, GetTimestamp()); - MS_DUMP(" ssrc : %" PRIu32, GetSsrc()); - MS_DUMP(" payload size : %zu bytes", GetPayloadLength()); + if (this->playoutDelayExtensionId != 0u) + { + uint16_t minDelay; + uint16_t maxDelay; + + if (ReadPlayoutDelay(minDelay, maxDelay)) + { + MS_DUMP( + " playoutDelay: extId:%" PRIu8 ", minDelay:%" PRIu16 ", maxDelay:%" PRIu16, + this->videoOrientationExtensionId, + minDelay, + maxDelay); + } + } + MS_DUMP(" csrc count: %" PRIu8, this->header->csrcCount); + MS_DUMP(" marker: %s", HasMarker() ? "true" : "false"); + MS_DUMP(" payload type: %" PRIu8, GetPayloadType()); + MS_DUMP(" sequence number: %" PRIu16, GetSequenceNumber()); + MS_DUMP(" timestamp: %" PRIu32, GetTimestamp()); + MS_DUMP(" ssrc: %" PRIu32, GetSsrc()); + MS_DUMP(" payload size: %zu bytes", GetPayloadLength()); if (this->header->padding != 0u) { - MS_DUMP(" padding size : %" PRIu8 " bytes", this->payloadPadding); + MS_DUMP(" padding size: %" PRIu8 " bytes", this->payloadPadding); } - MS_DUMP(" packet size : %zu bytes", GetSize()); - MS_DUMP(" spatial layer : %" PRIu8, GetSpatialLayer()); - MS_DUMP(" temporal layer : %" PRIu8, GetTemporalLayer()); + MS_DUMP(" packet size: %zu bytes", GetSize()); + MS_DUMP(" spatial layer: %" PRIu8, GetSpatialLayer()); + MS_DUMP(" temporal layer: %" PRIu8, GetTemporalLayer()); MS_DUMP(""); } - void RtpPacket::FillJson(json& jsonObject) const + flatbuffers::Offset RtpPacket::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { - MS_TRACE(); - - // Add payloadType. - jsonObject["payloadType"] = GetPayloadType(); - - // Add sequenceNumber. - jsonObject["sequenceNumber"] = GetSequenceNumber(); - - // Add timestamp. - jsonObject["timestamp"] = GetTimestamp(); - - // Add marker. - jsonObject["marker"] = HasMarker(); - - // Add ssrc. - jsonObject["ssrc"] = GetSsrc(); - - // Add isKeyFrame. - jsonObject["isKeyFrame"] = IsKeyFrame(); - - // Add size. - jsonObject["size"] = GetSize(); - - // Add payloadSize. - jsonObject["payloadSize"] = GetPayloadLength(); - - // Add spatialLayer. - jsonObject["spatialLayer"] = GetSpatialLayer(); - - // Add temporalLayer. - jsonObject["temporalLayer"] = GetTemporalLayer(); - // Add mid. std::string mid; - if (this->midExtensionId != 0u && ReadMid(mid)) - jsonObject["mid"] = mid; + if (this->midExtensionId != 0u) + { + ReadMid(mid); + } // Add rid. std::string rid; - if (this->ridExtensionId != 0u && ReadRid(rid)) - jsonObject["rid"] = rid; + if (this->ridExtensionId != 0u) + { + ReadRid(rid); + } // Add rrid. std::string rrid; - if (this->rridExtensionId != 0u && ReadRid(rrid)) - jsonObject["rrid"] = rrid; + if (this->rridExtensionId != 0u) + { + ReadRid(rrid); + } // Add wideSequenceNumber. uint16_t wideSequenceNumber; + bool wideSequenceNumberSet = false; - if (this->transportWideCc01ExtensionId != 0u && ReadTransportWideCc01(wideSequenceNumber)) - jsonObject["wideSequenceNumber"] = wideSequenceNumber; + if (this->transportWideCc01ExtensionId != 0u) + { + wideSequenceNumberSet = true; + ReadTransportWideCc01(wideSequenceNumber); + } + + return FBS::RtpPacket::CreateDumpDirect( + builder, + this->GetPayloadType(), + this->GetSequenceNumber(), + this->GetTimestamp(), + this->HasMarker(), + this->GetSsrc(), + this->IsKeyFrame(), + this->GetSize(), + this->GetPayloadLength(), + this->GetSpatialLayer(), + this->GetTemporalLayer(), + mid.empty() ? nullptr : mid.c_str(), + rid.empty() ? nullptr : rid.c_str(), + rrid.empty() ? nullptr : rrid.c_str(), + wideSequenceNumberSet ? flatbuffers::Optional(wideSequenceNumber) + : flatbuffers::nullopt); } void RtpPacket::SetExtensions(uint8_t type, const std::vector& extensions) @@ -381,6 +401,7 @@ namespace RTC this->frameMarkingExtensionId = 0u; this->ssrcAudioLevelExtensionId = 0u; this->videoOrientationExtensionId = 0u; + this->playoutDelayExtensionId = 0u; // Clear the One-Byte and Two-Bytes extension elements maps. std::fill(std::begin(this->oneByteExtensions), std::end(this->oneByteExtensions), nullptr); @@ -402,9 +423,13 @@ namespace RTC else if (this->headerExtension) { if (type == 1u) + { this->headerExtension->id = uint16_t{ htons(0xBEDE) }; + } else if (type == 2u) + { this->headerExtension->id = uint16_t{ htons(0b0001000000000000) }; + } } // Calculate total size required for all extensions (with padding if needed). @@ -480,9 +505,13 @@ namespace RTC // Set the header extension id. if (type == 1u) + { this->headerExtension->id = uint16_t{ htons(0xBEDE) }; + } else if (type == 2u) + { this->headerExtension->id = uint16_t{ htons(0b0001000000000000) }; + } // Set the header extension length. this->headerExtension->length = htons(extensionsTotalSize / 4); @@ -496,7 +525,9 @@ namespace RTC if (type == 1u) { if (extension.id == 0 || extension.id > 14 || extension.len == 0 || extension.len > 16) + { continue; + } // Store the One-Byte extension element in an array. // `-1` because we have 14 elements total 0..13 and `id` is in the range 1..14. @@ -510,7 +541,9 @@ namespace RTC else if (type == 2u) { if (extension.id == 0) + { continue; + } // Store the Two-Bytes extension element in the map. this->mapTwoBytesExtensions[extension.id] = reinterpret_cast(ptr); @@ -541,9 +574,11 @@ namespace RTC uint8_t* extenValue = GetExtension(this->midExtensionId, extenLen); if (!extenValue) + { return; + } - size_t midLen = mid.length(); + const size_t midLen = mid.length(); // Here we assume that there is MidMaxLength available bytes, even if now // they are padding bytes. @@ -587,13 +622,17 @@ namespace RTC auto* extension = this->oneByteExtensions[id - 1]; if (!extension) + { return false; + } auto currentLen = extension->len + 1; // Fill with 0's if new length is minor. if (len < currentLen) + { std::memset(extension->value + len, 0, currentLen - len); + } // In One-Byte extensions value length 0 means 1. extension->len = len - 1; @@ -605,14 +644,18 @@ namespace RTC auto it = this->mapTwoBytesExtensions.find(id); if (it == this->mapTwoBytesExtensions.end()) + { return false; + } auto* extension = it->second; auto currentLen = extension->len; // Fill with 0's if new length is minor. if (len < currentLen) + { std::memset(extension->value + len, 0, currentLen - len); + } extension->len = len; @@ -624,20 +667,25 @@ namespace RTC } } + /** + * NOTE: This method automatically removes payload padding if present. + */ void RtpPacket::SetPayloadLength(size_t length) { MS_TRACE(); - // Pad desired length to 4 bytes. - length = static_cast(Utils::Byte::PadTo4Bytes(static_cast(length))); - this->size -= this->payloadLength; - this->size -= size_t{ this->payloadPadding }; - this->payloadLength = length; - this->payloadPadding = 0u; - this->size += length; + this->payloadLength = length; + this->size += this->payloadLength; - SetPayloadPaddingFlag(false); + // Remove padding if present. + if (this->payloadPadding != 0u) + { + SetPayloadPaddingFlag(false); + + this->size -= size_t{ this->payloadPadding }; + this->payloadPadding = 0u; + } } RtpPacket* RtpPacket::Clone() const @@ -715,6 +763,7 @@ namespace RTC packet->frameMarkingExtensionId = this->frameMarkingExtensionId; packet->ssrcAudioLevelExtensionId = this->ssrcAudioLevelExtensionId; packet->videoOrientationExtensionId = this->videoOrientationExtensionId; + packet->playoutDelayExtensionId = this->playoutDelayExtensionId; // Assign the payload descriptor handler. packet->payloadDescriptorHandler = this->payloadDescriptorHandler; // Store allocated buffer. @@ -723,8 +772,12 @@ namespace RTC return packet; } - // NOTE: The caller must ensure that the buffer/memmory of the packet has - // space enough for adding 2 extra bytes. + /** + * NOTE: The caller must ensure that the buffer/memmory of the packet has + * space enough for adding 2 extra bytes. + * + * NOTE: This method automatically removes payload padding if present. + */ void RtpPacket::RtxEncode(uint8_t payloadType, uint32_t ssrc, uint16_t seq) { MS_TRACE(); @@ -758,6 +811,9 @@ namespace RTC } } + /** + * NOTE: This method automatically removes payload padding if present. + */ bool RtpPacket::RtxDecode(uint8_t payloadType, uint32_t ssrc) { MS_TRACE(); @@ -765,7 +821,9 @@ namespace RTC // Chrome sends some RTX packets with no payload when the stream is started. // Just ignore them. if (this->payloadLength < 2u) + { return false; + } // Rewrite the payload type. SetPayloadType(payloadType); @@ -802,16 +860,11 @@ namespace RTC MS_TRACE(); if (!this->payloadDescriptorHandler) - return true; - - if (this->payloadDescriptorHandler->Process(context, this->payload, marker)) { return true; } - else - { - return false; - } + + return this->payloadDescriptorHandler->Process(context, this->payload, marker); } void RtpPacket::RestorePayload() @@ -819,29 +872,40 @@ namespace RTC MS_TRACE(); if (!this->payloadDescriptorHandler) + { return; + } this->payloadDescriptorHandler->Restore(this->payload); } + /** + * Shifts the payload given offset (to right or to left). + * + * NOTE: This method automatically removes payload padding if present. + */ void RtpPacket::ShiftPayload(size_t payloadOffset, size_t shift, bool expand) { MS_TRACE(); if (shift == 0u) + { return; + } MS_ASSERT(payloadOffset < this->payloadLength, "payload offset bigger than payload size"); if (!expand) + { MS_ASSERT(shift <= (this->payloadLength - payloadOffset), "shift too big"); + } uint8_t* payloadOffsetPtr = this->payload + payloadOffset; size_t shiftedLen{ 0 }; if (expand) { - shiftedLen = this->payloadLength + size_t{ this->payloadPadding } - payloadOffset; + shiftedLen = this->payloadLength - payloadOffset; std::memmove(payloadOffsetPtr + shift, payloadOffsetPtr, shiftedLen); @@ -850,13 +914,22 @@ namespace RTC } else { - shiftedLen = this->payloadLength + size_t{ this->payloadPadding } - payloadOffset - shift; + shiftedLen = this->payloadLength - payloadOffset - shift; std::memmove(payloadOffsetPtr, payloadOffsetPtr + shift, shiftedLen); this->payloadLength -= shift; this->size -= shift; } + + // Remove padding if present. + if (this->payloadPadding != 0u) + { + SetPayloadPaddingFlag(false); + + this->size -= size_t{ this->payloadPadding }; + this->payloadPadding = 0u; + } } void RtpPacket::ParseExtensions() @@ -881,7 +954,9 @@ namespace RTC // id=15 in One-Byte extensions means "stop parsing here". if (id == 15u) + { break; + } // Valid extension id. if (id != 0u) diff --git a/worker/src/RTC/RtpProbationGenerator.cpp b/worker/src/RTC/RtpProbationGenerator.cpp index 66984ec1d7..e6f9a6dd92 100644 --- a/worker/src/RTC/RtpProbationGenerator.cpp +++ b/worker/src/RTC/RtpProbationGenerator.cpp @@ -2,7 +2,6 @@ // #define MS_LOG_DEV_LEVEL 3 #include "RTC/RtpProbationGenerator.hpp" -#include "DepLibUV.hpp" #include "Logger.hpp" #include "Utils.hpp" #include "RTC/RtpDictionaries.hpp" @@ -17,7 +16,7 @@ namespace RTC // Probation RTP header. // Caution: This must have an exact size for the RTP extensions to be added // and must align extensions to 4 bytes. - static uint8_t ProbationPacketHeader[] = + static const uint8_t ProbationPacketHeader[] = { 0b10010000, 0b01111111, 0, 0, // PayloadType: 127, Sequence Number: 0 0, 0, 0, 0, // Timestamp: 0 @@ -139,9 +138,13 @@ namespace RTC // Make the packet length fit into our available limits. if (size > MaxProbationPacketSize) + { size = MaxProbationPacketSize; + } else if (size < ProbationPacketHeaderSize) + { size = ProbationPacketHeaderSize; + } // Just send up to StepNumPackets per step. // Increase RTP seq number and timestamp. diff --git a/worker/src/RTC/RtpRetransmissionBuffer.cpp b/worker/src/RTC/RtpRetransmissionBuffer.cpp new file mode 100644 index 0000000000..7b846bd672 --- /dev/null +++ b/worker/src/RTC/RtpRetransmissionBuffer.cpp @@ -0,0 +1,598 @@ +#define MS_CLASS "RTC::RtpRetransmissionBuffer" +// #define MS_LOG_DEV_LEVEL 3 + +#include "RTC/RtpRetransmissionBuffer.hpp" +#include "Logger.hpp" +#include "RTC/SeqManager.hpp" + +namespace RTC +{ + /* Class methods. */ + + RtpRetransmissionBuffer::Item* RtpRetransmissionBuffer::FillItem( + RtpRetransmissionBuffer::Item* item, + RTC::RtpPacket* packet, + std::shared_ptr& sharedPacket) + { + MS_TRACE(); + + // Store original packet into the item. Only clone once and only if + // necessary. + // + // NOTE: This must be done BEFORE assigning item->packet = sharedPacket, + // otherwise the value being copied in item->packet will remain nullptr. + // This is because we are copying an **empty** shared_ptr into another + // shared_ptr (item->packet), so future value assigned via reset() in the + // former doesn't update the value in the copy. + if (!sharedPacket) + { + sharedPacket.reset(packet->Clone()); + } + + // Store original packet and some extra info into the item. + item->packet = sharedPacket; + item->ssrc = packet->GetSsrc(); + item->sequenceNumber = packet->GetSequenceNumber(); + item->timestamp = packet->GetTimestamp(); + + return item; + } + + /* Instance methods. */ + + RtpRetransmissionBuffer::RtpRetransmissionBuffer( + uint16_t maxItems, uint32_t maxRetransmissionDelayMs, uint32_t clockRate) + : maxItems(maxItems), maxRetransmissionDelayMs(maxRetransmissionDelayMs), clockRate(clockRate) + { + MS_TRACE(); + + MS_ASSERT(maxItems > 0u, "maxItems must be greater than 0"); + } + + RtpRetransmissionBuffer::~RtpRetransmissionBuffer() + { + MS_TRACE(); + + Clear(); + } + + RtpRetransmissionBuffer::Item* RtpRetransmissionBuffer::Get(uint16_t seq) const + { + MS_TRACE(); + + const auto* oldestItem = GetOldest(); + + if (!oldestItem) + { + return nullptr; + } + + if (RTC::SeqManager::IsSeqLowerThan(seq, oldestItem->sequenceNumber)) + { + return nullptr; + } + + const uint16_t idx = seq - oldestItem->sequenceNumber; + + if (static_cast(idx) > this->buffer.size() - 1) + { + return nullptr; + } + + return this->buffer.at(idx); + } + + /** + * This method tries to insert given packet into the buffer. Here we assume + * that packet seq number is legitimate according to the content of the buffer. + * We discard the packet if too old and also discard it if its timestamp does + * not properly fit (by ensuring that elements in the buffer are not only + * ordered by increasing seq but also that their timestamp are incremental). + */ + void RtpRetransmissionBuffer::Insert( + RTC::RtpPacket* packet, std::shared_ptr& sharedPacket) + { + MS_TRACE(); + + const auto ssrc = packet->GetSsrc(); + const auto seq = packet->GetSequenceNumber(); + const auto timestamp = packet->GetTimestamp(); + + MS_DEBUG_DEV("packet [seq:%" PRIu16 ", timestamp:%" PRIu32 "]", seq, timestamp); + + // Buffer is empty, so just insert new item. + if (this->buffer.empty()) + { + MS_DEBUG_DEV("buffer empty [seq:%" PRIu16 ", timestamp:%" PRIu32 "]", seq, timestamp); + + auto* item = new Item(); + + this->buffer.push_back(RtpRetransmissionBuffer::FillItem(item, packet, sharedPacket)); + + return; + } + + auto* oldestItem = GetOldest(); + auto* newestItem = GetNewest(); + + // Special case: Received packet has lower seq than newest packet in the + // buffer, however its timestamp is higher. If so, clear the whole buffer. + if ( + RTC::SeqManager::IsSeqLowerThan(seq, newestItem->sequenceNumber) && + RTC::SeqManager::IsSeqHigherThan(timestamp, newestItem->timestamp)) + { + MS_WARN_TAG( + rtp, + "packet has lower seq but higher timestamp than newest packet in the buffer, emptying the buffer [ssrc:%" PRIu32 + ", seq:%" PRIu16 ", timestamp:%" PRIu32 "]", + ssrc, + seq, + timestamp); + + Clear(); + + auto* item = new Item(); + + this->buffer.push_back(RtpRetransmissionBuffer::FillItem(item, packet, sharedPacket)); + + return; + } + + // Clear too old packets in the buffer. + // NOTE: Here we must consider the case in which, due for example to huge + // packet loss, received packet has higher timestamp but "older" seq number + // than the newest packet in the buffer and, if so, use it to clear too old + // packets rather than the newest packet in the buffer. + auto newestTimestamp = + RTC::SeqManager::IsSeqHigherThan(timestamp, newestItem->timestamp) + ? timestamp + : newestItem->timestamp; + + // ClearTooOldByTimestamp() returns true if at least one packet has been + // removed from the front. + if (ClearTooOldByTimestamp(newestTimestamp)) + { + // Buffer content has been modified so we must check it again. + if (this->buffer.empty()) + { + MS_WARN_TAG( + rtp, + "buffer empty after clearing too old packets [seq:%" PRIu16 ", timestamp:%" PRIu32 "]", + seq, + timestamp); + + auto* item = new Item(); + + this->buffer.push_back(RtpRetransmissionBuffer::FillItem(item, packet, sharedPacket)); + + return; + } + + oldestItem = GetOldest(); + newestItem = GetNewest(); + } + + MS_ASSERT(oldestItem != nullptr, "oldest item doesn't exist"); + MS_ASSERT(newestItem != nullptr, "newest item doesn't exist"); + + // Packet arrived in order (its seq is higher than seq of the newest stored + // packet) so will become the newest one in the buffer. + if (RTC::SeqManager::IsSeqHigherThan(seq, newestItem->sequenceNumber)) + { + MS_DEBUG_DEV("packet in order [seq:%" PRIu16 ", timestamp:%" PRIu32 "]", seq, timestamp); + + // Ensure that the timestamp of the packet is equal or higher than the + // timestamp of the newest stored packet. + if (RTC::SeqManager::IsSeqLowerThan(timestamp, newestItem->timestamp)) + { + MS_WARN_TAG( + rtp, + "packet has higher seq but lower timestamp than newest packet in the buffer, discarding it [ssrc:%" PRIu32 + ", seq:%" PRIu16 ", timestamp:%" PRIu32 "]", + ssrc, + seq, + timestamp); + + return; + } + + // Calculate how many blank slots it would be necessary to add when + // pushing new item to the back of the buffer. + uint16_t numBlankSlots = seq - newestItem->sequenceNumber - 1; + + // We may have to remove oldest items not to exceed the maximum size of + // the buffer. + if (this->buffer.size() + numBlankSlots + 1 > this->maxItems) + { + const uint16_t numItemsToRemove = this->buffer.size() + numBlankSlots + 1 - this->maxItems; + + // If num of items to be removed exceed buffer size minus one (needed to + // allocate current packet) then we must clear the entire buffer. + if (numItemsToRemove > this->buffer.size() - 1) + { + MS_WARN_TAG( + rtp, + "packet has too high seq and forces buffer emptying [ssrc:%" PRIu32 ", seq:%" PRIu16 + ", timestamp:%" PRIu32 "]", + ssrc, + seq, + timestamp); + + numBlankSlots = 0u; + + Clear(); + } + else + { + MS_DEBUG_DEV( + "calling RemoveOldest(%" PRIu16 ") [bufferSize:%zu, numBlankSlots:%" PRIu16 + ", maxItems:%" PRIu16 "]", + numItemsToRemove, + this->buffer.size(), + numBlankSlots, + this->maxItems); + + RemoveOldest(numItemsToRemove); + } + } + + // Push blank slots to the back. + for (uint16_t i{ 0u }; i < numBlankSlots; ++i) + { + this->buffer.push_back(nullptr); + } + + // Push the packet, which becomes the newest one in the buffer. + auto* item = new Item(); + + this->buffer.push_back(RtpRetransmissionBuffer::FillItem(item, packet, sharedPacket)); + } + // Packet arrived out order and its seq is less than seq of the oldest + // stored packet, so will become the oldest one in the buffer. + else if (RTC::SeqManager::IsSeqLowerThan(seq, oldestItem->sequenceNumber)) + { + MS_DEBUG_DEV( + "packet out of order and older than oldest packet in the buffer [seq:%" PRIu16 + ", timestamp:%" PRIu32 "]", + seq, + timestamp); + + // Ensure that packet is not too old to be stored. + if (IsTooOldTimestamp(timestamp, newestItem->timestamp)) + { + MS_WARN_DEV( + "packet's timestamp too old, discarding it [seq:%" PRIu16 ", timestamp:%" PRIu32 "]", + seq, + timestamp); + + return; + } + + // Ensure that the timestamp of the packet is equal or less than the + // timestamp of the oldest stored packet. + if (RTC::SeqManager::IsSeqHigherThan(timestamp, oldestItem->timestamp)) + { + MS_WARN_TAG( + rtp, + "packet has lower seq but higher timestamp than oldest packet in the buffer, discarding it [ssrc:%" PRIu32 + ", seq:%" PRIu16 ", timestamp:%" PRIu32 "]", + ssrc, + seq, + timestamp); + + return; + } + + // Calculate how many blank slots it would be necessary to add when + // pushing new item to the fton of the buffer. + const uint16_t numBlankSlots = oldestItem->sequenceNumber - seq - 1; + + // If adding this packet (and needed blank slots) to the front makes the + // buffer exceed its max size, discard this packet. + if (this->buffer.size() + numBlankSlots + 1 > this->maxItems) + { + MS_WARN_TAG( + rtp, + "discarding received old packet to not exceed max buffer size [ssrc:%" PRIu32 + ", seq:%" PRIu16 ", timestamp:%" PRIu32 "]", + ssrc, + seq, + timestamp); + + return; + } + + // Push blank slots to the front. + for (uint16_t i{ 0u }; i < numBlankSlots; ++i) + { + this->buffer.push_front(nullptr); + } + + // Insert the packet, which becomes the oldest one in the buffer. + auto* item = new Item(); + + this->buffer.push_front(RtpRetransmissionBuffer::FillItem(item, packet, sharedPacket)); + } + // Otherwise packet must be inserted between oldest and newest stored items + // so there is already an allocated slot for it. + else + { + MS_DEBUG_DEV( + "packet out of order and in between oldest and newest packets in the buffer [seq:%" PRIu16 + ", timestamp:%" PRIu32 "]", + seq, + timestamp); + + // Let's check if an item already exist in same position. If so, assume + // it's duplicated. + auto* item = Get(seq); + + if (item) + { + MS_DEBUG_DEV( + "packet already in the buffer, discarding [seq:%" PRIu16 ", timestamp:%" PRIu32 "]", + seq, + timestamp); + + return; + } + + // idx is the intended position of the received packet in the buffer. + const uint16_t idx = seq - oldestItem->sequenceNumber; + + // Validate that packet timestamp is equal or higher than the timestamp of + // the immediate older packet (if any). + for (int32_t idx2 = idx - 1; idx2 >= 0; --idx2) + { + const auto* olderItem = this->buffer.at(idx2); + + // Blank slot, continue. + if (!olderItem) + { + continue; + } + + // We are done. + if (timestamp >= olderItem->timestamp) + { + break; + } + else + { + MS_WARN_TAG( + rtp, + "packet timestamp is lower than timestamp of immediate older packet in the buffer, discarding it [ssrc:%" PRIu32 + ", seq:%" PRIu16 ", timestamp:%" PRIu32 "]", + ssrc, + seq, + timestamp); + + return; + } + } + + // Validate that packet timestamp is equal or less than the timestamp of + // the immediate newer packet (if any). + for (size_t idx2 = idx + 1; idx2 < this->buffer.size(); ++idx2) + { + const auto* newerItem = this->buffer.at(idx2); + + // Blank slot, continue. + if (!newerItem) + { + continue; + } + + // We are done. + if (timestamp <= newerItem->timestamp) + { + break; + } + else + { + MS_WARN_TAG( + rtp, + "packet timestamp is higher than timestamp of immediate newer packet in the buffer, discarding it [ssrc:%" PRIu32 + ", seq:%" PRIu16 ", timestamp:%" PRIu32 "]", + ssrc, + seq, + timestamp); + + return; + } + } + + // Store the packet. + item = new Item(); + + this->buffer[idx] = RtpRetransmissionBuffer::FillItem(item, packet, sharedPacket); + } + + MS_ASSERT( + this->buffer.size() <= this->maxItems, + "buffer contains %zu items (more than %" PRIu16 " max items)", + this->buffer.size(), + this->maxItems); + } + + void RtpRetransmissionBuffer::Clear() + { + MS_TRACE(); + + for (auto* item : this->buffer) + { + if (!item) + { + continue; + } + + // Reset the stored item (decrease RTP packet shared pointer counter). + item->Reset(); + + delete item; + } + + this->buffer.clear(); + } + + void RtpRetransmissionBuffer::Dump() const + { + MS_TRACE(); + + MS_DUMP(""); + MS_DUMP(" buffer [size:%zu, maxSize:%" PRIu16 "]", this->buffer.size(), this->maxItems); + if (!this->buffer.empty()) + { + const auto* oldestItem = GetOldest(); + const auto* newestItem = GetNewest(); + + MS_DUMP( + " oldest item [seq:%" PRIu16 ", timestamp:%" PRIu32 "]", + oldestItem->sequenceNumber, + oldestItem->timestamp); + MS_DUMP( + " newest item [seq:%" PRIu16 ", timestamp:%" PRIu32 "]", + newestItem->sequenceNumber, + newestItem->timestamp); + MS_DUMP( + " buffer window: %" PRIu32 "ms", + static_cast(newestItem->timestamp * 1000 / this->clockRate) - + static_cast(oldestItem->timestamp * 1000 / this->clockRate)); + } + MS_DUMP(""); + } + + RtpRetransmissionBuffer::Item* RtpRetransmissionBuffer::GetOldest() const + { + MS_TRACE(); + + if (this->buffer.empty()) + { + return nullptr; + } + + return this->buffer.front(); + } + + RtpRetransmissionBuffer::Item* RtpRetransmissionBuffer::GetNewest() const + { + MS_TRACE(); + + if (this->buffer.empty()) + { + return nullptr; + } + + return this->buffer.back(); + } + + void RtpRetransmissionBuffer::RemoveOldest() + { + MS_TRACE(); + + if (this->buffer.empty()) + { + return; + } + + auto* item = this->buffer.front(); + + // Reset the stored item (decrease RTP packet shared pointer counter). + item->Reset(); + + delete item; + + this->buffer.pop_front(); + + MS_DEBUG_DEV("removed 1 item from the front"); + + // Remove all nullptr elements from the beginning of the buffer. + // NOTE: Calling front on an empty container is undefined. + size_t numItemsRemoved{ 0u }; + + while (!this->buffer.empty() && this->buffer.front() == nullptr) + { + this->buffer.pop_front(); + + ++numItemsRemoved; + } + + if (numItemsRemoved) + { + MS_DEBUG_DEV("removed %zu blank slots from the front", numItemsRemoved); + } + } + + void RtpRetransmissionBuffer::RemoveOldest(uint16_t numItems) + { + MS_TRACE(); + + MS_ASSERT( + numItems <= this->buffer.size(), + "attempting to remove more items than current buffer size [numItems:%" PRIu16 + ", bufferSize:%zu]", + numItems, + this->buffer.size()); + + const size_t intendedBufferSize = this->buffer.size() - numItems; + + while (this->buffer.size() > intendedBufferSize) + { + RemoveOldest(); + } + } + + bool RtpRetransmissionBuffer::ClearTooOldByTimestamp(uint32_t newestTimestamp) + { + MS_TRACE(); + + RtpRetransmissionBuffer::Item* oldestItem{ nullptr }; + bool itemsRemoved{ false }; + + // Go through all buffer items starting with the first and free all items + // that contain too old packets. + while ((oldestItem = GetOldest())) + { + if (IsTooOldTimestamp(oldestItem->timestamp, newestTimestamp)) + { + RemoveOldest(); + + itemsRemoved = true; + } + // If current oldest stored packet is not too old, exit the loop since we + // know that packets stored after it are guaranteed to be newer. + else + { + break; + } + } + + return itemsRemoved; + } + + bool RtpRetransmissionBuffer::IsTooOldTimestamp(uint32_t timestamp, uint32_t newestTimestamp) const + { + MS_TRACE(); + + if (RTC::SeqManager::IsSeqHigherThan(timestamp, newestTimestamp)) + { + return false; + } + + const int64_t diffTs = newestTimestamp - timestamp; + + return static_cast(diffTs * 1000 / this->clockRate) > this->maxRetransmissionDelayMs; + } + + void RtpRetransmissionBuffer::Item::Reset() + { + MS_TRACE(); + + this->packet.reset(); + this->ssrc = 0u; + this->sequenceNumber = 0u; + this->timestamp = 0u; + this->resentAtMs = 0u; + this->sentTimes = 0u; + } +} // namespace RTC diff --git a/worker/src/RTC/RtpStream.cpp b/worker/src/RTC/RtpStream.cpp index b8e123c6af..9e3b9754d2 100644 --- a/worker/src/RTC/RtpStream.cpp +++ b/worker/src/RTC/RtpStream.cpp @@ -28,55 +28,62 @@ namespace RTC MS_TRACE(); delete this->rtxStream; + this->rtxStream = nullptr; } - void RtpStream::FillJson(json& jsonObject) const + flatbuffers::Offset RtpStream::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); // Add params. - this->params.FillJson(jsonObject["params"]); - - // Add score. - jsonObject["score"] = this->score; + auto params = this->params.FillBuffer(builder); // Add rtxStream. + flatbuffers::Offset rtxStream; + if (HasRtx()) - this->rtxStream->FillJson(jsonObject["rtxStream"]); + { + rtxStream = this->rtxStream->FillBuffer(builder); + } + + return FBS::RtpStream::CreateDump(builder, params, this->score, rtxStream); } - void RtpStream::FillJsonStats(json& jsonObject) + flatbuffers::Offset RtpStream::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) { MS_TRACE(); const uint64_t nowMs = DepLibUV::GetTimeMs(); - - jsonObject["timestamp"] = nowMs; - jsonObject["ssrc"] = this->params.ssrc; - jsonObject["kind"] = RtpCodecMimeType::type2String[this->params.mimeType.type]; - jsonObject["mimeType"] = this->params.mimeType.ToString(); - jsonObject["packetsLost"] = this->packetsLost; - jsonObject["fractionLost"] = this->fractionLost; - jsonObject["packetsDiscarded"] = this->packetsDiscarded; - jsonObject["packetsRetransmitted"] = this->packetsRetransmitted; - jsonObject["packetsRepaired"] = this->packetsRepaired; - jsonObject["nackCount"] = this->nackCount; - jsonObject["nackPacketCount"] = this->nackPacketCount; - jsonObject["pliCount"] = this->pliCount; - jsonObject["firCount"] = this->firCount; - jsonObject["score"] = this->score; - - if (!this->params.rid.empty()) - jsonObject["rid"] = this->params.rid; - - if (this->params.rtxSsrc) - jsonObject["rtxSsrc"] = this->params.rtxSsrc; - - if (this->rtxStream) - jsonObject["rtxPacketsDiscarded"] = this->rtxStream->GetPacketsDiscarded(); - - if (this->hasRtt) - jsonObject["roundTripTime"] = this->rtt; + const auto mediaKind = this->params.mimeType.type == RTC::RtpCodecMimeType::Type::AUDIO + ? FBS::RtpParameters::MediaKind::AUDIO + : FBS::RtpParameters::MediaKind::VIDEO; + + auto baseStats = FBS::RtpStream::CreateBaseStatsDirect( + builder, + nowMs, + this->params.ssrc, + mediaKind, + this->params.mimeType.ToString().c_str(), + this->packetsLost, + this->fractionLost, + this->packetsDiscarded, + this->packetsRetransmitted, + this->packetsRepaired, + this->nackCount, + this->nackPacketCount, + this->pliCount, + this->firCount, + this->score, + !this->params.rid.empty() ? this->params.rid.c_str() : nullptr, + this->params.rtxSsrc ? flatbuffers::Optional(this->params.rtxSsrc) + : flatbuffers::nullopt, + this->rtxStream ? this->rtxStream->GetPacketsDiscarded() : 0, + this->rtt > 0.0f ? this->rtt : 0); + + return FBS::RtpStream::CreateStats( + builder, FBS::RtpStream::StatsData::BaseStats, baseStats.Union()); } void RtpStream::SetRtx(uint8_t payloadType, uint32_t ssrc) @@ -162,11 +169,15 @@ namespace RTC // If previous score was 0 (and new one is not 0) then update activeSinceMs. if (previousScore == 0u) + { this->activeSinceMs = DepLibUV::GetTimeMs(); + } // Notify the listener. if (notify) + { this->listener->OnRtpStreamScore(this, score, previousScore); + } } } @@ -193,7 +204,7 @@ namespace RTC this->maxSeq = seq; } // Too old packet received (older than the allowed misorder). - // Or to new packet (more than acceptable dropout). + // Or too new packet (more than acceptable dropout). else if (udelta <= RtpSeqMod - MaxMisorder) { // The sequence number made a very large jump. If two sequential packets @@ -212,6 +223,9 @@ namespace RTC this->maxPacketTs = packet->GetTimestamp(); this->maxPacketMs = DepLibUV::GetTimeMs(); + + // Notify the subclass about it. + UserOnSequenceNumberReset(); } else { @@ -244,7 +258,9 @@ namespace RTC // Add the score into the histogram. if (this->scores.size() == ScoreHistogramLength) + { this->scores.erase(this->scores.begin()); + } auto previousScore = this->score; @@ -292,7 +308,9 @@ namespace RTC // If previous score was 0 (and new one is not 0) then update activeSinceMs. if (previousScore == 0u) + { this->activeSinceMs = DepLibUV::GetTimeMs(); + } this->listener->OnRtpStreamScore(this, this->score, previousScore); } @@ -334,33 +352,28 @@ namespace RTC this->badSeq = RtpSeqMod + 1; // So seq == badSeq is false. } - void RtpStream::Params::FillJson(json& jsonObject) const + flatbuffers::Offset RtpStream::Params::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - jsonObject["encodingIdx"] = this->encodingIdx; - jsonObject["ssrc"] = this->ssrc; - jsonObject["payloadType"] = this->payloadType; - jsonObject["mimeType"] = this->mimeType.ToString(); - jsonObject["clockRate"] = this->clockRate; - - if (!this->rid.empty()) - jsonObject["rid"] = this->rid; - - jsonObject["cname"] = this->cname; - - if (this->rtxSsrc != 0) - { - jsonObject["rtxSsrc"] = this->rtxSsrc; - jsonObject["rtxPayloadType"] = this->rtxPayloadType; - } - - jsonObject["useNack"] = this->useNack; - jsonObject["usePli"] = this->usePli; - jsonObject["useFir"] = this->useFir; - jsonObject["useInBandFec"] = this->useInBandFec; - jsonObject["useDtx"] = this->useDtx; - jsonObject["spatialLayers"] = this->spatialLayers; - jsonObject["temporalLayers"] = this->temporalLayers; + return FBS::RtpStream::CreateParamsDirect( + builder, + this->encodingIdx, + this->ssrc, + this->payloadType, + this->mimeType.ToString().c_str(), + this->clockRate, + this->rid.c_str(), + this->cname.c_str(), + this->rtxSsrc != 0 ? flatbuffers::Optional(this->rtxSsrc) : flatbuffers::nullopt, + this->rtxSsrc != 0 ? flatbuffers::Optional(this->rtxPayloadType) : flatbuffers::nullopt, + this->useNack, + this->usePli, + this->useFir, + this->useInBandFec, + this->useDtx, + this->spatialLayers, + this->temporalLayers); } } // namespace RTC diff --git a/worker/src/RTC/RtpStreamRecv.cpp b/worker/src/RTC/RtpStreamRecv.cpp index 4988e4163f..439f186908 100644 --- a/worker/src/RTC/RtpStreamRecv.cpp +++ b/worker/src/RTC/RtpStreamRecv.cpp @@ -27,7 +27,7 @@ namespace RTC { for (uint8_t tIdx{ 0u }; tIdx < temporalLayers; ++tIdx) { - spatialLayerCounter.emplace_back(RTC::RtpDataCounter(windowSize)); + spatialLayerCounter.emplace_back(windowSize); } } } @@ -41,11 +41,15 @@ namespace RTC // Sanity check. Do not allow spatial layers higher than defined. if (spatialLayer > this->spatialLayerCounters.size() - 1) + { spatialLayer = this->spatialLayerCounters.size() - 1; + } // Sanity check. Do not allow temporal layers higher than defined. if (temporalLayer > this->spatialLayerCounters[0].size() - 1) + { temporalLayer = this->spatialLayerCounters[0].size() - 1; + } auto& counter = this->spatialLayerCounters[spatialLayer][temporalLayer]; @@ -82,7 +86,9 @@ namespace RTC auto& counter = this->spatialLayerCounters[spatialLayer][temporalLayer]; if (counter.GetBitrate(nowMs) == 0) + { return 0u; + } uint32_t rate{ 0u }; @@ -152,9 +158,9 @@ namespace RTC size_t packetCount{ 0u }; - for (auto& spatialLayerCounter : this->spatialLayerCounters) + for (const auto& spatialLayerCounter : this->spatialLayerCounters) { - for (auto& temporalLayerCounter : spatialLayerCounter) + for (const auto& temporalLayerCounter : spatialLayerCounter) { packetCount += temporalLayerCounter.GetPacketCount(); } @@ -183,25 +189,33 @@ namespace RTC /* Instance methods. */ RtpStreamRecv::RtpStreamRecv( - RTC::RtpStreamRecv::Listener* listener, RTC::RtpStream::Params& params, unsigned int sendNackDelayMs) + RTC::RtpStreamRecv::Listener* listener, + RTC::RtpStream::Params& params, + unsigned int sendNackDelayMs, + bool useRtpInactivityCheck) : RTC::RtpStream::RtpStream(listener, params, 10), sendNackDelayMs(sendNackDelayMs), + useRtpInactivityCheck(useRtpInactivityCheck), transmissionCounter( params.spatialLayers, params.temporalLayers, this->params.useDtx ? 6000 : 2500) { MS_TRACE(); if (this->params.useNack) + { this->nackGenerator.reset(new RTC::NackGenerator(this, this->sendNackDelayMs)); + } - // Run the RTP inactivity periodic timer (use a different timeout if DTX is - // enabled). - this->inactivityCheckPeriodicTimer = new Timer(this); - this->inactive = false; + this->inactive = false; - if (!this->params.useDtx) - this->inactivityCheckPeriodicTimer->Start(InactivityCheckInterval); - else - this->inactivityCheckPeriodicTimer->Start(InactivityCheckIntervalWithDtx); + if (this->useRtpInactivityCheck) + { + // Run the RTP inactivity periodic timer (use a different timeout if DTX is + // enabled). + this->inactivityCheckPeriodicTimer = new TimerHandle(this); + + this->inactivityCheckPeriodicTimer->Start( + this->params.useDtx ? InactivityCheckIntervalWithDtx : InactivityCheckInterval); + } } RtpStreamRecv::~RtpStreamRecv() @@ -210,36 +224,44 @@ namespace RTC // Close the RTP inactivity check periodic timer. delete this->inactivityCheckPeriodicTimer; + this->inactivityCheckPeriodicTimer = nullptr; } - void RtpStreamRecv::FillJsonStats(json& jsonObject) + flatbuffers::Offset RtpStreamRecv::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) { MS_TRACE(); const uint64_t nowMs = DepLibUV::GetTimeMs(); - RTC::RtpStream::FillJsonStats(jsonObject); + auto baseStats = RTC::RtpStream::FillBufferStats(builder); - jsonObject["type"] = "inbound-rtp"; - jsonObject["jitter"] = this->jitter; - jsonObject["packetCount"] = this->transmissionCounter.GetPacketCount(); - jsonObject["byteCount"] = this->transmissionCounter.GetBytes(); - jsonObject["bitrate"] = this->transmissionCounter.GetBitrate(nowMs); + std::vector> bitrateByLayer; if (GetSpatialLayers() > 1 || GetTemporalLayers() > 1) { - jsonObject["bitrateByLayer"] = json::object(); - auto jsonBitrateByLayerIt = jsonObject.find("bitrateByLayer"); - for (uint8_t sIdx = 0; sIdx < GetSpatialLayers(); ++sIdx) { for (uint8_t tIdx = 0; tIdx < GetTemporalLayers(); ++tIdx) { - (*jsonBitrateByLayerIt)[std::to_string(sIdx) + "." + std::to_string(tIdx)] = - GetBitrate(nowMs, sIdx, tIdx); + auto layer = std::to_string(sIdx) + "." + std::to_string(tIdx); + + bitrateByLayer.emplace_back(FBS::RtpStream::CreateBitrateByLayerDirect( + builder, layer.c_str(), GetBitrate(nowMs, sIdx, tIdx))); } } } + + auto stats = FBS::RtpStream::CreateRecvStatsDirect( + builder, + baseStats, + static_cast(this->jitter), + this->transmissionCounter.GetPacketCount(), + this->transmissionCounter.GetBytes(), + this->transmissionCounter.GetBitrate(nowMs), + &bitrateByLayer); + + return FBS::RtpStream::CreateStats(builder, FBS::RtpStream::StatsData::RecvStats, stats.Union()); } bool RtpStreamRecv::ReceivePacket(RTC::RtpPacket* packet) @@ -256,7 +278,9 @@ namespace RTC // Process the packet at codec level. if (packet->GetPayloadType() == GetPayloadType()) + { RTC::Codecs::Tools::ProcessRtpPacket(packet, GetMimeType()); + } // Pass the packet to the NackGenerator. if (this->params.useNack) @@ -279,6 +303,13 @@ namespace RTC // Calculate Jitter. CalculateJitter(packet->GetTimestamp()); + // Padding only packet, do not consider it for counter increase nor + // stream activation. + if (packet->GetPayloadLength() == 0) + { + return true; + } + // Increase transmission counter. this->transmissionCounter.Update(packet); @@ -295,7 +326,9 @@ namespace RTC // Restart the inactivityCheckPeriodicTimer. if (this->inactivityCheckPeriodicTimer) + { this->inactivityCheckPeriodicTimer->Restart(); + } return true; } @@ -376,7 +409,9 @@ namespace RTC // Process the packet at codec level. if (packet->GetPayloadType() == GetPayloadType()) + { RTC::Codecs::Tools::ProcessRtpPacket(packet, GetMimeType()); + } // Mark the packet as retransmitted. RTC::RtpStream::PacketRetransmitted(packet); @@ -385,6 +420,13 @@ namespace RTC // NACKed packet. if (this->nackGenerator->ReceivePacket(packet, /*isRecovered*/ true)) { + // Padding only packet, do not consider it for counter increase nor + // stream activation. + if (packet->GetPayloadLength() == 0) + { + return true; + } + // Mark the packet as repaired. RTC::RtpStream::PacketRepaired(packet); @@ -401,7 +443,9 @@ namespace RTC // Restart the inactivityCheckPeriodicTimer. if (this->inactivityCheckPeriodicTimer) + { this->inactivityCheckPeriodicTimer->Restart(); + } return true; } @@ -422,7 +466,9 @@ namespace RTC ->OnRtpStreamNeedWorstRemoteFractionLost(this, worstRemoteFractionLost); if (worstRemoteFractionLost > 0) + { MS_DEBUG_TAG(rtcp, "using worst remote fraction lost:%" PRIu8, worstRemoteFractionLost); + } } auto* report = new RTC::RTCP::ReceiverReport(); @@ -435,25 +481,34 @@ namespace RTC auto expected = GetExpectedPackets(); if (expected > this->mediaTransmissionCounter.GetPacketCount()) + { this->packetsLost = expected - this->mediaTransmissionCounter.GetPacketCount(); + } else + { this->packetsLost = 0u; + } // Calculate Fraction Lost. - uint32_t expectedInterval = expected - this->expectedPrior; + const uint32_t expectedInterval = expected - this->expectedPrior; this->expectedPrior = expected; - uint32_t receivedInterval = this->mediaTransmissionCounter.GetPacketCount() - this->receivedPrior; + const uint32_t receivedInterval = + this->mediaTransmissionCounter.GetPacketCount() - this->receivedPrior; this->receivedPrior = this->mediaTransmissionCounter.GetPacketCount(); const int32_t lostInterval = expectedInterval - receivedInterval; if (expectedInterval == 0 || lostInterval <= 0) + { this->fractionLost = 0; + } else + { this->fractionLost = std::round((static_cast(lostInterval << 8) / expectedInterval)); + } // Worst remote fraction lost is not worse than local one. if (worstRemoteFractionLost <= this->fractionLost) @@ -504,7 +559,9 @@ namespace RTC MS_TRACE(); if (HasRtx()) + { return this->rtxStream->GetRtcpReceiverReport(); + } return nullptr; } @@ -518,7 +575,7 @@ namespace RTC this->lastSrTimestamp += report->GetNtpFrac() >> 16; // Update info about last Sender Report. - Utils::Time::Ntp ntp; // NOLINT(cppcoreguidelines-pro-type-member-init) + Utils::Time::Ntp ntp{}; // NOLINT(cppcoreguidelines-pro-type-member-init) ntp.seconds = report->GetNtpSec(); ntp.fractions = report->GetNtpFrac(); @@ -535,7 +592,9 @@ namespace RTC MS_TRACE(); if (HasRtx()) + { this->rtxStream->ReceiveRtcpSenderReport(report); + } } void RtpStreamRecv::ReceiveRtcpXrDelaySinceLastRr(RTC::RTCP::DelaySinceLastRr::SsrcInfo* ssrcInfo) @@ -562,18 +621,25 @@ namespace RTC // If no Receiver Extended Report was received by the remote endpoint yet, // ignore lastRr and dlrr values in the Sender Extended Report. if (lastRr && dlrr && (compactNtp > dlrr + lastRr)) + { rtt = compactNtp - dlrr - lastRr; + } // RTT in milliseconds. this->rtt = static_cast(rtt >> 16) * 1000; this->rtt += (static_cast(rtt & 0x0000FFFF) / 65536) * 1000; - if (this->rtt > 0.0f) - this->hasRtt = true; + // Avoid negative RTT value since it doesn't make sense. + if (this->rtt <= 0.0f) + { + this->rtt = 0.0f; + } // Tell it to the NackGenerator. if (this->params.useNack) + { this->nackGenerator->UpdateRtt(static_cast(this->rtt)); + } } void RtpStreamRecv::RequestKeyFrame() @@ -619,10 +685,14 @@ namespace RTC MS_TRACE(); if (this->inactivityCheckPeriodicTimer) + { this->inactivityCheckPeriodicTimer->Stop(); + } if (this->params.useNack) + { this->nackGenerator->Reset(); + } // Reset jitter. this->transit = 0; @@ -634,19 +704,23 @@ namespace RTC MS_TRACE(); if (this->inactivityCheckPeriodicTimer && !this->inactive) + { this->inactivityCheckPeriodicTimer->Restart(); + } } void RtpStreamRecv::CalculateJitter(uint32_t rtpTimestamp) { MS_TRACE(); - if (this->params.clockRate == 0u) + if (GetClockRate() == 0u) + { return; + } - auto transit = - static_cast(DepLibUV::GetTimeMs() - (rtpTimestamp * 1000 / this->params.clockRate)); - int d = transit - this->transit; + // NOTE: Based on https://github.com/versatica/mediasoup/issues/1018. + auto transit = static_cast((DepLibUV::GetTimeMs() * GetClockRate() / 1000) - rtpTimestamp); + int d = transit - this->transit; // First transit calculation, save and return. if (this->transit == 0) @@ -659,9 +733,11 @@ namespace RTC this->transit = transit; if (d < 0) + { d = -d; + } - this->jitter += (1. / 16.) * (static_cast(d) - this->jitter); + this->jitter += (1. / 16.) * (static_cast(d) - this->jitter); } void RtpStreamRecv::UpdateScore() @@ -669,14 +745,14 @@ namespace RTC MS_TRACE(); // Calculate number of packets expected in this interval. - auto totalExpected = GetExpectedPackets(); - uint32_t expected = totalExpected - this->expectedPriorScore; + const auto totalExpected = GetExpectedPackets(); + const uint32_t expected = totalExpected - this->expectedPriorScore; this->expectedPriorScore = totalExpected; // Calculate number of packets received in this interval. - auto totalReceived = this->mediaTransmissionCounter.GetPacketCount(); - uint32_t received = totalReceived - this->receivedPriorScore; + const auto totalReceived = this->mediaTransmissionCounter.GetPacketCount(); + const uint32_t received = totalReceived - this->receivedPriorScore; this->receivedPriorScore = totalReceived; @@ -684,24 +760,30 @@ namespace RTC uint32_t lost; if (expected < received) + { lost = 0; + } else + { lost = expected - received; + } // Calculate number of packets repaired in this interval. - auto totalRepaired = this->packetsRepaired; - uint32_t repaired = totalRepaired - this->repairedPriorScore; + const auto totalRepaired = this->packetsRepaired; + uint32_t repaired = totalRepaired - this->repairedPriorScore; this->repairedPriorScore = totalRepaired; // Calculate number of packets retransmitted in this interval. - auto totatRetransmitted = this->packetsRetransmitted; - uint32_t retransmitted = totatRetransmitted - this->retransmittedPriorScore; + const auto totatRetransmitted = this->packetsRetransmitted; + uint32_t retransmitted = totatRetransmitted - this->retransmittedPriorScore; this->retransmittedPriorScore = totatRetransmitted; if (this->inactive) + { return; + } // We didn't expect more packets to come. if (expected == 0) @@ -712,7 +794,9 @@ namespace RTC } if (lost > received) + { lost = received; + } if (repaired > lost) { @@ -752,7 +836,9 @@ namespace RTC MS_ASSERT(retransmitted >= repaired, "repaired packets cannot be more than retransmitted ones"); if (retransmitted > 0) + { repairedWeight *= static_cast(repaired) / retransmitted; + } lost -= repaired * repairedWeight; @@ -771,10 +857,18 @@ namespace RTC score); #endif - RtpStream::UpdateScore(score); + // Call the parent method for update score. + RTC::RtpStream::UpdateScore(score); + } + + void RtpStreamRecv::UserOnSequenceNumberReset() + { + MS_TRACE(); + + // Nothing to do. } - inline void RtpStreamRecv::OnTimer(Timer* timer) + inline void RtpStreamRecv::OnTimer(TimerHandle* timer) { MS_TRACE(); @@ -821,10 +915,12 @@ namespace RTC while (it != end) { - uint16_t shift = *it - seq - 1; + const uint16_t shift = *it - seq - 1; if (shift > 15) + { break; + } bitmask |= (1 << shift); ++it; diff --git a/worker/src/RTC/RtpStreamSend.cpp b/worker/src/RTC/RtpStreamSend.cpp index 85eb69dae1..d508bce83e 100644 --- a/worker/src/RTC/RtpStreamSend.cpp +++ b/worker/src/RTC/RtpStreamSend.cpp @@ -2,192 +2,89 @@ // #define MS_LOG_DEV_LEVEL 3 #include "RTC/RtpStreamSend.hpp" +#ifdef MS_LIBURING_SUPPORTED +#include "DepLibUring.hpp" +#endif #include "Logger.hpp" #include "Utils.hpp" -#include "RTC/SeqManager.hpp" +#include "RTC/RtpDictionaries.hpp" namespace RTC { /* Static. */ + // Limit max number of items in the retransmission buffer. + static constexpr size_t RetransmissionBufferMaxItems{ 2500u }; // 17: 16 bit mask + the initial sequence number. static constexpr size_t MaxRequestedPackets{ 17u }; - thread_local static std::vector RetransmissionContainer( + thread_local static std::vector RetransmissionContainer( MaxRequestedPackets + 1); static constexpr uint32_t DefaultRtt{ 100u }; - static constexpr uint16_t MaxSeq = std::numeric_limits::max(); /* Class Static. */ - // Minimum retransmission buffer size (ms). - const uint32_t RtpStreamSend::MinRetransmissionDelay{ 200u }; - // Maximum retransmission buffer size (ms). - const uint32_t RtpStreamSend::MaxRetransmissionDelay{ 2000u }; - - void RtpStreamSend::StorageItem::Reset() - { - MS_TRACE(); - - this->packet.reset(); - this->ssrc = 0; - this->sequenceNumber = 0; - this->timestamp = 0; - this->resentAtMs = 0; - this->sentTimes = 0; - } - - RtpStreamSend::StorageItem* RtpStreamSend::StorageItemBuffer::GetFirst() const - { - auto* storageItem = this->Get(this->startSeq); - - MS_ASSERT(storageItem, "first storage item is missing"); - MS_ASSERT(storageItem->packet, "storage item does not contain original packet"); - - return storageItem; - } - - RtpStreamSend::StorageItem* RtpStreamSend::StorageItemBuffer::Get(uint16_t seq) const - { - if (RTC::SeqManager::IsSeqLowerThan(seq, this->startSeq)) - return nullptr; - - auto idx{ static_cast(seq - this->startSeq) }; - - if (this->buffer.empty() || idx > static_cast(this->buffer.size() - 1)) - return nullptr; + const uint32_t RtpStreamSend::MaxRetransmissionDelayForVideoMs{ 2000u }; + const uint32_t RtpStreamSend::MaxRetransmissionDelayForAudioMs{ 1000u }; - return this->buffer.at(idx); - } + /* Instance methods. */ - size_t RtpStreamSend::StorageItemBuffer::GetBufferSize() const + RtpStreamSend::RtpStreamSend( + RTC::RtpStreamSend::Listener* listener, RTC::RtpStream::Params& params, std::string& mid) + : RTC::RtpStream::RtpStream(listener, params, 10), mid(mid) { - return this->buffer.size(); - } + MS_TRACE(); - void RtpStreamSend::StorageItemBuffer::Insert(uint16_t seq, StorageItem* storageItem) - { - if (this->buffer.empty()) - { - this->startSeq = seq; - this->buffer.push_back(storageItem); - } - // Packet sequence number is higher than startSeq. - else if (RTC::SeqManager::IsSeqHigherThan(seq, this->startSeq)) + if (this->params.useNack) { - auto idx{ static_cast(seq - this->startSeq) }; + uint32_t maxRetransmissionDelayMs; - // Packet arrived out of order, so we already have a slot allocated for it. - if (idx <= static_cast(this->buffer.size() - 1)) + switch (params.mimeType.type) { - MS_ASSERT(this->buffer[idx] == nullptr, "Must insert into empty slot"); + case RTC::RtpCodecMimeType::Type::VIDEO: + { + maxRetransmissionDelayMs = RtpStreamSend::MaxRetransmissionDelayForVideoMs; - this->buffer[idx] = storageItem; - } - else - { - // Calculate how many elements would it be necessary to add when pushing new item - // to the back of the deque. - auto addToBack = static_cast(seq - (this->startSeq + this->buffer.size() - 1)); + break; + } - // Packets can arrive out of order, add blank slots. - for (uint16_t i{ 1 }; i < addToBack; ++i) - this->buffer.push_back(nullptr); + case RTC::RtpCodecMimeType::Type::AUDIO: + { + maxRetransmissionDelayMs = RtpStreamSend::MaxRetransmissionDelayForAudioMs; - this->buffer.push_back(storageItem); + break; + } } - } - // Packet sequence number is the same or lower than startSeq. - else - { - // Calculate how many elements would it be necessary to add when pushing new item - // to the front of the deque. - auto addToFront = static_cast(this->startSeq - seq); - - // Packets can arrive out of order, add blank slots. - for (uint16_t i{ 1 }; i < addToFront; ++i) - this->buffer.push_front(nullptr); - - this->buffer.push_front(storageItem); - this->startSeq = seq; - } - - MS_ASSERT( - this->buffer.size() <= MaxSeq, - "StorageItemBuffer contains more than %" PRIu16 " entries", - MaxSeq); - } - - void RtpStreamSend::StorageItemBuffer::RemoveFirst() - { - MS_ASSERT(!this->buffer.empty(), "buffer is empty"); - - auto* storageItem = this->buffer[0]; - - delete storageItem; - - this->buffer[0] = nullptr; - - // Remove all `nullptr` elements from the beginning of the buffer. - // NOTE: Calling front on an empty container is undefined. - while (!this->buffer.empty() && this->buffer.front() == nullptr) - { - this->buffer.pop_front(); - this->startSeq++; - } - } - - void RtpStreamSend::StorageItemBuffer::Clear() - { - for (auto* storageItem : this->buffer) - { - if (!storageItem) - continue; - - // Reset the storage item (decrease RTP packet shared pointer counter). - storageItem->Reset(); - delete storageItem; + this->retransmissionBuffer = new RTC::RtpRetransmissionBuffer( + RetransmissionBufferMaxItems, maxRetransmissionDelayMs, params.clockRate); } - - this->buffer.clear(); - this->startSeq = 0; - } - - RtpStreamSend::StorageItemBuffer::~StorageItemBuffer() - { - Clear(); - } - - /* Instance methods. */ - - RtpStreamSend::RtpStreamSend( - RTC::RtpStreamSend::Listener* listener, RTC::RtpStream::Params& params, std::string& mid) - : RTC::RtpStream::RtpStream(listener, params, 10), mid(mid), - retransmissionBufferSize(RtpStreamSend::MaxRetransmissionDelay) - { - MS_TRACE(); } RtpStreamSend::~RtpStreamSend() { MS_TRACE(); - // Clear the RTP buffer. - ClearBuffer(); + // Delete retransmission buffer. + delete this->retransmissionBuffer; + this->retransmissionBuffer = nullptr; } - void RtpStreamSend::FillJsonStats(json& jsonObject) + flatbuffers::Offset RtpStreamSend::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) { MS_TRACE(); - uint64_t nowMs = DepLibUV::GetTimeMs(); + const uint64_t nowMs = DepLibUV::GetTimeMs(); - RTC::RtpStream::FillJsonStats(jsonObject); + auto baseStats = RTC::RtpStream::FillBufferStats(builder); + auto stats = FBS::RtpStream::CreateSendStats( + builder, + baseStats, + this->transmissionCounter.GetPacketCount(), + this->transmissionCounter.GetBytes(), + this->transmissionCounter.GetBitrate(nowMs)); - jsonObject["type"] = "outbound-rtp"; - jsonObject["packetCount"] = this->transmissionCounter.GetPacketCount(); - jsonObject["byteCount"] = this->transmissionCounter.GetBytes(); - jsonObject["bitrate"] = this->transmissionCounter.GetBitrate(nowMs); + return FBS::RtpStream::CreateStats(builder, FBS::RtpStream::StatsData::SendStats, stats.Union()); } void RtpStreamSend::SetRtx(uint8_t payloadType, uint32_t ssrc) @@ -203,13 +100,20 @@ namespace RTC { MS_TRACE(); + MS_ASSERT( + packet->GetSsrc() == this->params.ssrc, "RTP packet SSRC does not match the encodings SSRC"); + // Call the parent method. if (!RtpStream::ReceiveStreamPacket(packet)) + { return false; + } // If NACK is enabled, store the packet into the buffer. - if (this->params.useNack) + if (this->retransmissionBuffer) + { StorePacket(packet, sharedPacket); + } // Increase transmission counter. this->transmissionCounter.Update(packet); @@ -223,6 +127,14 @@ namespace RTC this->nackCount++; +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + // Activate liburing usage. + DepLibUring::SetActive(); + } +#endif + for (auto it = nackPacket->Begin(); it != nackPacket->End(); ++it) { RTC::RTCP::FeedbackRtpNackItem* item = *it; @@ -231,14 +143,16 @@ namespace RTC FillRetransmissionContainer(item->GetPacketId(), item->GetLostPacketBitmask()); - for (auto* storageItem : RetransmissionContainer) + for (auto* item : RetransmissionContainer) { - if (!storageItem) + if (!item) + { break; + } // Note that this is an already RTX encoded packet if RTX is used // (FillRetransmissionContainer() did it). - auto packet = storageItem->packet; + auto packet = item->packet; // Retransmit the packet. static_cast(this->listener) @@ -248,16 +162,26 @@ namespace RTC RTC::RtpStream::PacketRetransmitted(packet.get()); // Mark the packet as repaired (only if this is the first retransmission). - if (storageItem->sentTimes == 1) + if (item->sentTimes == 1) + { RTC::RtpStream::PacketRepaired(packet.get()); + } if (HasRtx()) { // Restore the packet. - packet->RtxDecode(RtpStream::GetPayloadType(), storageItem->ssrc); + packet->RtxDecode(RtpStream::GetPayloadType(), item->ssrc); } } } + +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + // Submit all prepared submission entries. + DepLibUring::Submit(); + } +#endif } void RtpStreamSend::ReceiveKeyFrameRequest(RTC::RTCP::FeedbackPs::MessageType messageType) @@ -267,12 +191,18 @@ namespace RTC switch (messageType) { case RTC::RTCP::FeedbackPs::MessageType::PLI: + { this->pliCount++; + break; + } case RTC::RTCP::FeedbackPs::MessageType::FIR: + { this->firCount++; + break; + } default:; } @@ -302,26 +232,20 @@ namespace RTC // If no Sender Report was received by the remote endpoint yet, ignore lastSr // and dlsr values in the Receiver Report. if (lastSr && dlsr && (compactNtp > dlsr + lastSr)) + { rtt = compactNtp - dlsr - lastSr; + } // RTT in milliseconds. this->rtt = static_cast(rtt >> 16) * 1000; this->rtt += (static_cast(rtt & 0x0000FFFF) / 65536) * 1000; - if (this->rtt > 0.0f) + // Avoid negative RTT value since it doesn't make sense. + if (this->rtt <= 0.0f) { - this->hasRtt = true; + this->rtt = 0.0f; } - // Smoothly change retransmission buffer size towards RTT + 100ms, but not more than - // `MaxRetransmissionDelay`. - auto newRetransmissionBufferSize = static_cast(this->rtt + 100.0); - auto avgRetransmissionBufferSize = - (this->retransmissionBufferSize * 7 + newRetransmissionBufferSize) / 8; - this->retransmissionBufferSize = std::max( - std::min(avgRetransmissionBufferSize, RtpStreamSend::MaxRetransmissionDelay), - RtpStreamSend::MinRetransmissionDelay); - this->packetsLost = report->GetTotalLost(); this->fractionLost = report->GetFractionLost(); @@ -343,7 +267,9 @@ namespace RTC MS_TRACE(); if (this->transmissionCounter.GetPacketCount() == 0u) + { return nullptr; + } auto ntp = Utils::Time::TimeMs2Ntp(nowMs); auto* report = new RTC::RTCP::SenderReport(); @@ -366,12 +292,14 @@ namespace RTC return report; } - RTC::RTCP::DelaySinceLastRr::SsrcInfo* RtpStreamSend::GetRtcpXrDelaySinceLastRr(uint64_t nowMs) + RTC::RTCP::DelaySinceLastRr::SsrcInfo* RtpStreamSend::GetRtcpXrDelaySinceLastRrSsrcInfo(uint64_t nowMs) { MS_TRACE(); if (this->lastRrReceivedMs == 0u) + { return nullptr; + } // Get delay in milliseconds. auto delayMs = static_cast(nowMs - this->lastRrReceivedMs); @@ -407,7 +335,11 @@ namespace RTC { MS_TRACE(); - ClearBuffer(); + // Clear retransmission buffer. + if (this->retransmissionBuffer) + { + this->retransmissionBuffer->Clear(); + } } void RtpStreamSend::Resume() @@ -442,9 +374,6 @@ namespace RTC { MS_TRACE(); - MS_ASSERT( - packet->GetSsrc() == this->params.ssrc, "RTP packet SSRC does not match the encodings SSRC"); - if (packet->GetSize() > RTC::MtuSize) { MS_WARN_TAG( @@ -457,93 +386,7 @@ namespace RTC return; } - // Check if RTP packet is too old to be stored. - if (this->storageItemBuffer.GetBufferSize() > 0) - { - auto* storageItem = this->storageItemBuffer.GetFirst(); - - // Processing RTP packet is older than first one. - if (RTC::SeqManager::IsSeqLowerThan(packet->GetTimestamp(), storageItem->timestamp)) - { - uint32_t diffTs{ storageItem->timestamp - packet->GetTimestamp() }; - - // RTP packet is older than the retransmission buffer size. - if (static_cast(diffTs * 1000 / this->params.clockRate) >= this->retransmissionBufferSize) - return; - } - } - - this->ClearOldPackets(packet); - - auto seq = packet->GetSequenceNumber(); - auto* storageItem = this->storageItemBuffer.Get(seq); - - // The buffer item is already used. Check whether we should replace its - // storage with the new packet or just ignore it (if duplicated packet). - if (storageItem) - { - if (packet->GetTimestamp() == storageItem->timestamp) - return; - - // Reset the storage item. - storageItem->Reset(); - } - // Allocate new buffer item. - else - { - // Allocate a new storage item. - storageItem = new StorageItem(); - - this->storageItemBuffer.Insert(seq, storageItem); - } - - // Only clone once and only if necessary. - if (!sharedPacket.get()) - { - sharedPacket.reset(packet->Clone()); - } - - // Store original packet and some extra info into the storage item. - storageItem->packet = sharedPacket; - storageItem->ssrc = packet->GetSsrc(); - storageItem->sequenceNumber = packet->GetSequenceNumber(); - storageItem->timestamp = packet->GetTimestamp(); - } - - void RtpStreamSend::ClearOldPackets(const RtpPacket* packet) - { - MS_TRACE(); - - auto clockRate{ this->params.clockRate }; - - const auto bufferSize = this->storageItemBuffer.GetBufferSize(); - - // Go through all buffer items starting with the first and free all storage - // items that contain packets older than `MaxRetransmissionDelay`. - for (size_t i{ 0 }; i < bufferSize && this->storageItemBuffer.GetBufferSize() != 0; ++i) - { - auto* storageItem = this->storageItemBuffer.GetFirst(); - uint32_t diffTs{ packet->GetTimestamp() - storageItem->timestamp }; - - // Processing RTP packet is older than first one. - if (RTC::SeqManager::IsSeqLowerThan(packet->GetTimestamp(), storageItem->timestamp)) - break; - - // First RTP packet is recent enough. - if (static_cast(diffTs * 1000 / clockRate) < this->retransmissionBufferSize) - break; - - // Unfill the buffer start item. - this->storageItemBuffer.RemoveFirst(); - } - } - - void RtpStreamSend::ClearBuffer() - { - MS_TRACE(); - - // Reset buffer. - this->storageItemBuffer.Clear(); + this->retransmissionBuffer->Insert(packet, sharedPacket); } // This method looks for the requested RTP packets and inserts them into the @@ -551,6 +394,12 @@ namespace RTC // // If RTX is used the stored packet will be RTX encoded now (if not already // encoded in a previous resend). + // + // NOTE: This method doesn't verify whether requested stored packet is too + // old, why? Because, if we verified it, we would do it by comparing its + // timestamp with the newest one in the retransmission buffer. However we + // already clean old packets upon receipt of any new packet (see Insert() + // method in RetransmissionBuffer class). void RtpStreamSend::FillRetransmissionContainer(uint16_t seq, uint16_t bitmask) { MS_TRACE(); @@ -559,7 +408,7 @@ namespace RTC RetransmissionContainer[0] = nullptr; // If NACK is not supported, exit. - if (!this->params.useNack) + if (!this->retransmissionBuffer) { MS_WARN_TAG(rtx, "NACK not supported"); @@ -568,7 +417,7 @@ namespace RTC // Look for each requested packet. const uint64_t nowMs = DepLibUV::GetTimeMs(); - const uint16_t rtt = (this->rtt != 0u ? this->rtt : DefaultRtt); + const uint16_t rtt = (this->rtt > 0.0f ? this->rtt : DefaultRtt); uint16_t currentSeq = seq; bool requested{ true }; size_t containerIdx{ 0 }; @@ -579,7 +428,6 @@ namespace RTC bool isFirstPacket{ true }; bool firstPacketSent{ false }; uint8_t bitmaskCounter{ 0 }; - bool tooOldPacketFound{ false }; while (requested || bitmask != 0) { @@ -587,55 +435,36 @@ namespace RTC if (requested) { - auto* storageItem = this->storageItemBuffer.Get(currentSeq); + auto* item = this->retransmissionBuffer->Get(currentSeq); std::shared_ptr packet{ nullptr }; - uint32_t diffMs; // Calculate the elapsed time between the max timestamp seen and the // requested packet's timestamp (in ms). - if (storageItem) + if (item) { - packet = storageItem->packet; + packet = item->packet; // Put correct info into the packet. - packet->SetSsrc(storageItem->ssrc); - packet->SetSequenceNumber(storageItem->sequenceNumber); - packet->SetTimestamp(storageItem->timestamp); + packet->SetSsrc(item->ssrc); + packet->SetSequenceNumber(item->sequenceNumber); + packet->SetTimestamp(item->timestamp); // Update MID RTP extension value. if (!this->mid.empty()) + { packet->UpdateMid(mid); - - uint32_t diffTs = this->maxPacketTs - packet->GetTimestamp(); - - diffMs = diffTs * 1000 / this->params.clockRate; + } } // Packet not found. - if (!storageItem) + if (!item) { // Do nothing. } - // Don't resend the packet if older than MaxRetransmissionDelay ms. - else if (diffMs > MaxRetransmissionDelay) - { - if (!tooOldPacketFound) - { - MS_WARN_TAG( - rtx, - "ignoring retransmission for too old packet " - "[seq:%" PRIu16 ", max age:%" PRIu32 "ms, packet age:%" PRIu32 "ms]", - packet->GetSequenceNumber(), - MaxRetransmissionDelay, - diffMs); - - tooOldPacketFound = true; - } - } // Don't resent the packet if it was resent in the last RTT ms. // clang-format off else if ( - storageItem->resentAtMs != 0u && - nowMs - storageItem->resentAtMs <= static_cast(rtt) + item->resentAtMs != 0u && + nowMs - item->resentAtMs <= static_cast(rtt) ) // clang-format on { @@ -659,18 +488,20 @@ namespace RTC } // Save when this packet was resent. - storageItem->resentAtMs = nowMs; + item->resentAtMs = nowMs; // Increase the number of times this packet was sent. - storageItem->sentTimes++; + item->sentTimes++; - // Store the storage item in the container and then increment its index. - RetransmissionContainer[containerIdx++] = storageItem; + // Store the item in the container and then increment its index. + RetransmissionContainer[containerIdx++] = item; sent = true; if (isFirstPacket) + { firstPacketSent = true; + } } } @@ -728,9 +559,13 @@ namespace RTC uint32_t lost; if (totalLost < this->lostPriorScore) + { lost = 0; + } else + { lost = totalLost - this->lostPriorScore; + } this->lostPriorScore = totalLost; @@ -755,10 +590,14 @@ namespace RTC } if (lost > sent) + { lost = sent; + } if (repaired > lost) + { repaired = lost; + } #if MS_LOG_DEV_LEVEL == 3 MS_DEBUG_TAG( @@ -783,7 +622,9 @@ namespace RTC MS_ASSERT(retransmitted >= repaired, "repaired packets cannot be more than retransmitted ones"); if (retransmitted > 0) + { repairedWeight *= static_cast(repaired) / retransmitted; + } lost -= repaired * repairedWeight; @@ -802,6 +643,18 @@ namespace RTC score); #endif - RtpStream::UpdateScore(score); + // Call the parent method for update score. + RTC::RtpStream::UpdateScore(score); + } + + void RtpStreamSend::UserOnSequenceNumberReset() + { + MS_TRACE(); + + // Clear retransmission buffer. + if (this->retransmissionBuffer) + { + this->retransmissionBuffer->Clear(); + } } } // namespace RTC diff --git a/worker/src/RTC/RtxStream.cpp b/worker/src/RTC/RtxStream.cpp index 44b5c3bc12..59f1e3a5b3 100644 --- a/worker/src/RTC/RtxStream.cpp +++ b/worker/src/RTC/RtxStream.cpp @@ -28,12 +28,15 @@ namespace RTC MS_TRACE(); } - void RtxStream::FillJson(json& jsonObject) const + flatbuffers::Offset RtxStream::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); // Add params. - this->params.FillJson(jsonObject["params"]); + auto params = this->params.FillBuffer(builder); + + return FBS::RtxStream::CreateRtxDump(builder, params); } bool RtxStream::ReceivePacket(RTC::RtpPacket* packet) @@ -92,9 +95,13 @@ namespace RTC auto expected = GetExpectedPackets(); if (expected > this->packetsCount) + { this->packetsLost = expected - this->packetsCount; + } else + { this->packetsLost = 0u; + } // Calculate Fraction Lost. const uint32_t expectedInterval = expected - this->expectedPrior; @@ -108,9 +115,13 @@ namespace RTC const int32_t lostInterval = expectedInterval - receivedInterval; if (expectedInterval == 0 || lostInterval <= 0) + { this->fractionLost = 0; + } else + { this->fractionLost = std::round((static_cast(lostInterval << 8) / expectedInterval)); + } this->reportedPacketLost += (this->packetsLost - prevPacketsLost); @@ -126,7 +137,7 @@ namespace RTC if (this->lastSrReceived != 0) { // Get delay in milliseconds. - auto delayMs = static_cast(DepLibUV::GetTimeMs() - this->lastSrReceived); + const uint32_t delayMs = DepLibUV::GetTimeMs() - this->lastSrReceived; // Express delay in units of 1/65536 seconds. uint32_t dlsr = (delayMs / 1000) << 16; @@ -231,18 +242,18 @@ namespace RTC this->badSeq = RtpSeqMod + 1; // So seq == badSeq is false. } - void RtxStream::Params::FillJson(json& jsonObject) const + flatbuffers::Offset RtxStream::Params::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - jsonObject["ssrc"] = this->ssrc; - jsonObject["payloadType"] = this->payloadType; - jsonObject["mimeType"] = this->mimeType.ToString(); - jsonObject["clockRate"] = this->clockRate; - - if (!this->rrid.empty()) - jsonObject["rrid"] = this->rrid; - - jsonObject["cname"] = this->cname; + return FBS::RtxStream::CreateParamsDirect( + builder, + this->ssrc, + this->payloadType, + this->mimeType.ToString().c_str(), + this->clockRate, + this->rrid.c_str(), + this->cname.c_str()); } } // namespace RTC diff --git a/worker/src/RTC/SctpAssociation.cpp b/worker/src/RTC/SctpAssociation.cpp index 2c0ee8cf6e..e78745b4d5 100644 --- a/worker/src/RTC/SctpAssociation.cpp +++ b/worker/src/RTC/SctpAssociation.cpp @@ -11,12 +11,12 @@ #include // Free send buffer threshold (in bytes) upon which send_cb will be executed. -static uint32_t SendBufferThreshold{ 256u }; +static const uint32_t SendBufferThreshold{ 256u }; /* SCTP events to which we are subscribing. */ /* clang-format off */ -uint16_t EventTypes[] = +const uint16_t EventTypes[] = { SCTP_ADAPTATION_INDICATION, SCTP_ASSOC_CHANGE, @@ -59,7 +59,7 @@ inline static int onRecvSctpData( else { const uint16_t streamId = rcv.rcv_sid; - uint32_t ppid = ntohl(rcv.rcv_ppid); + const uint32_t ppid = ntohl(rcv.rcv_ppid); const uint16_t ssn = rcv.rcv_ssn; MS_DEBUG_TAG( @@ -115,20 +115,19 @@ namespace RTC size_t maxSctpMessageSize, size_t sctpSendBufferSize, bool isDataChannel) - : listener(listener), os(os), mis(mis), maxSctpMessageSize(maxSctpMessageSize), - sctpSendBufferSize(sctpSendBufferSize), isDataChannel(isDataChannel) + : id(DepUsrSCTP::GetNextSctpAssociationId()), listener(listener), os(os), mis(mis), + maxSctpMessageSize(maxSctpMessageSize), sctpSendBufferSize(sctpSendBufferSize), + isDataChannel(isDataChannel) { MS_TRACE(); - // Get a id for this SctpAssociation. - this->id = DepUsrSCTP::GetNextSctpAssociationId(); - // Register ourselves in usrsctp. // NOTE: This must be done before calling usrsctp_bind(). usrsctp_register_address(reinterpret_cast(this->id)); int ret; + // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer) this->socket = usrsctp_socket( AF_CONN, SOCK_STREAM, @@ -161,7 +160,9 @@ namespace RTC // This ensures that the usrsctp close call deletes the association. This // prevents usrsctp from calling the global send callback with references to // this class as the address. - struct linger lingerOpt; // NOLINT(cppcoreguidelines-pro-type-member-init) + struct linger lingerOpt + { + }; // NOLINT(cppcoreguidelines-pro-type-member-init) lingerOpt.l_onoff = 1; lingerOpt.l_linger = 0; @@ -176,7 +177,9 @@ namespace RTC } // Set SCTP_ENABLE_STREAM_RESET. - struct sctp_assoc_value av; // NOLINT(cppcoreguidelines-pro-type-member-init) + struct sctp_assoc_value av + { + }; // NOLINT(cppcoreguidelines-pro-type-member-init) av.assoc_value = SCTP_ENABLE_RESET_STREAM_REQ | SCTP_ENABLE_RESET_ASSOC_REQ | SCTP_ENABLE_CHANGE_ASSOC_REQ; @@ -203,7 +206,9 @@ namespace RTC } // Enable events. - struct sctp_event event; // NOLINT(cppcoreguidelines-pro-type-member-init) + struct sctp_event event + { + }; // NOLINT(cppcoreguidelines-pro-type-member-init) std::memset(&event, 0, sizeof(event)); event.se_on = 1; @@ -223,7 +228,9 @@ namespace RTC } // Init message. - struct sctp_initmsg initmsg; // NOLINT(cppcoreguidelines-pro-type-member-init) + struct sctp_initmsg initmsg + { + }; // NOLINT(cppcoreguidelines-pro-type-member-init) std::memset(&initmsg, 0, sizeof(initmsg)); initmsg.sinit_num_ostreams = this->os; @@ -239,7 +246,9 @@ namespace RTC } // Server side. - struct sockaddr_conn sconn; // NOLINT(cppcoreguidelines-pro-type-member-init) + struct sockaddr_conn sconn + { + }; // NOLINT(cppcoreguidelines-pro-type-member-init) std::memset(&sconn, 0, sizeof(sconn)); sconn.sconn_family = AF_CONN; @@ -293,12 +302,16 @@ namespace RTC // Just run the SCTP stack if our state is 'new'. if (this->state != SctpState::NEW) + { return; + } try { int ret; - struct sockaddr_conn rconn; // NOLINT(cppcoreguidelines-pro-type-member-init) + struct sockaddr_conn rconn + { + }; // NOLINT(cppcoreguidelines-pro-type-member-init) std::memset(&rconn, 0, sizeof(rconn)); rconn.sconn_family = AF_CONN; @@ -311,10 +324,12 @@ namespace RTC ret = usrsctp_connect(this->socket, reinterpret_cast(&rconn), sizeof(rconn)); if (ret < 0 && errno != EINPROGRESS) + { MS_THROW_ERROR("usrsctp_connect() failed: %s", std::strerror(errno)); + } // Disable MTU discovery. - sctp_paddrparams peerAddrParams; // NOLINT(cppcoreguidelines-pro-type-member-init) + sctp_paddrparams peerAddrParams{}; // NOLINT(cppcoreguidelines-pro-type-member-init) std::memset(&peerAddrParams, 0, sizeof(peerAddrParams)); std::memcpy(&peerAddrParams.spp_address, &rconn, sizeof(rconn)); @@ -328,7 +343,9 @@ namespace RTC this->socket, IPPROTO_SCTP, SCTP_PEER_ADDR_PARAMS, &peerAddrParams, sizeof(peerAddrParams)); if (ret < 0) + { MS_THROW_ERROR("usrsctp_setsockopt(SCTP_PEER_ADDR_PARAMS) failed: %s", std::strerror(errno)); + } // Announce connecting state. this->state = SctpState::CONNECTING; @@ -341,33 +358,30 @@ namespace RTC } } - void SctpAssociation::FillJson(json& jsonObject) const + flatbuffers::Offset SctpAssociation::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Add port (always 5000). - jsonObject["port"] = 5000; - - // Add OS. - jsonObject["OS"] = this->os; - - // Add MIS. - jsonObject["MIS"] = this->mis; - - // Add maxMessageSize. - jsonObject["maxMessageSize"] = this->maxSctpMessageSize; - - // Add sendBufferSize. - jsonObject["sendBufferSize"] = this->sctpSendBufferSize; - - // Add sctpBufferedAmountLowThreshold. - jsonObject["sctpBufferedAmount"] = this->sctpBufferedAmount; - - // Add isDataChannel. - jsonObject["isDataChannel"] = this->isDataChannel; + return FBS::SctpParameters::CreateSctpParameters( + builder, + // Add port (always 5000). + 5000, + // Add OS. + this->os, + // Add MIS. + this->mis, + // Add maxMessageSize. + this->maxSctpMessageSize, + // Add sendBufferSize. + this->sctpSendBufferSize, + // Add sctpBufferedAmountLowThreshold. + this->sctpBufferedAmount, + // Add isDataChannel. + this->isDataChannel); } - void SctpAssociation::ProcessSctpData(const uint8_t* data, size_t len) + void SctpAssociation::ProcessSctpData(const uint8_t* data, size_t len) const { MS_TRACE(); @@ -379,7 +393,7 @@ namespace RTC } void SctpAssociation::SendSctpMessage( - RTC::DataConsumer* dataConsumer, uint32_t ppid, const uint8_t* msg, size_t len, onQueuedCallback* cb) + RTC::DataConsumer* dataConsumer, const uint8_t* msg, size_t len, uint32_t ppid, onQueuedCallback* cb) { MS_TRACE(); @@ -393,7 +407,9 @@ namespace RTC const auto& parameters = dataConsumer->GetSctpStreamParameters(); // Fill sctp_sendv_spa. - struct sctp_sendv_spa spa; // NOLINT(cppcoreguidelines-pro-type-member-init) + struct sctp_sendv_spa spa + { + }; // NOLINT(cppcoreguidelines-pro-type-member-init) std::memset(&spa, 0, sizeof(spa)); spa.sendv_flags = SCTP_SEND_SNDINFO_VALID; @@ -433,18 +449,17 @@ namespace RTC // via onSendSctpData. this->listener->OnSctpAssociationBufferedAmount(this, this->sctpBufferedAmount); - int ret = usrsctp_sendv( + const int ret = usrsctp_sendv( this->socket, msg, len, nullptr, 0, &spa, static_cast(sizeof(spa)), SCTP_SENDV_SPA, 0); if (ret < 0) { - bool sctpSendBufferFull = errno == EWOULDBLOCK || errno == EAGAIN; + const bool sctpSendBufferFull = errno == EWOULDBLOCK || errno == EAGAIN; // SCTP send buffer being full is legit, not an error. if (sctpSendBufferFull) { MS_DEBUG_DEV( - sctp, "error sending SCTP message [sid:%" PRIu16 ", ppid:%" PRIu32 ", message size:%zu]: %s", parameters.streamId, ppid, @@ -488,7 +503,9 @@ namespace RTC // We need more OS. if (streamId > this->os - 1) + { AddOutgoingStreams(/*force*/ false); + } } void SctpAssociation::DataProducerClosed(RTC::DataProducer* dataProducer) @@ -498,11 +515,15 @@ namespace RTC auto streamId = dataProducer->GetSctpStreamParameters().streamId; // Send SCTP_RESET_STREAMS to the remote. - // https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13#section-6.7 + // https://tools.ietf.org/html/rfc8831#section-6.7 if (this->isDataChannel) + { ResetSctpStream(streamId, StreamDirection::OUTGOING); + } else + { ResetSctpStream(streamId, StreamDirection::INCOMING); + } } void SctpAssociation::DataConsumerClosed(RTC::DataConsumer* dataConsumer) @@ -521,10 +542,14 @@ namespace RTC // Do nothing if an outgoing stream that could not be allocated by us. if (direction == StreamDirection::OUTGOING && streamId > this->os - 1) + { return; + } int ret; - struct sctp_assoc_value av; // NOLINT(cppcoreguidelines-pro-type-member-init) + struct sctp_assoc_value av + { + }; // NOLINT(cppcoreguidelines-pro-type-member-init) socklen_t len = sizeof(av); ret = usrsctp_getsockopt(this->socket, IPPROTO_SCTP, SCTP_RECONFIG_SUPPORTED, &av, &len); @@ -588,9 +613,13 @@ namespace RTC uint16_t additionalOs{ 0 }; if (MaxSctpStreams - this->os >= 32) + { additionalOs = 32; + } else + { additionalOs = MaxSctpStreams - this->os; + } if (additionalOs == 0) { @@ -603,7 +632,9 @@ namespace RTC // Already in progress, ignore (unless forced). if (!force && nextDesiredOs == this->desiredOs) + { return; + } // Update desired value. this->desiredOs = nextDesiredOs; @@ -616,7 +647,9 @@ namespace RTC return; } - struct sctp_add_streams sas; // NOLINT(cppcoreguidelines-pro-type-member-init) + struct sctp_add_streams sas + { + }; // NOLINT(cppcoreguidelines-pro-type-member-init) std::memset(&sas, 0, sizeof(sas)); sas.sas_instrms = 0; @@ -624,11 +657,13 @@ namespace RTC MS_DEBUG_TAG(sctp, "adding %" PRIu16 " outgoing streams", additionalOs); - int ret = usrsctp_setsockopt( + const int ret = usrsctp_setsockopt( this->socket, IPPROTO_SCTP, SCTP_ADD_STREAMS, &sas, static_cast(sizeof(sas))); if (ret < 0) + { MS_WARN_TAG(sctp, "usrsctp_setsockopt(SCTP_ADD_STREAMS) failed: %s", std::strerror(errno)); + } } void SctpAssociation::OnUsrSctpSendSctpData(void* buffer, size_t len) @@ -691,7 +726,7 @@ namespace RTC { MS_DEBUG_DEV("directly notifying listener [eor:1, buffer len:0]"); - this->listener->OnSctpAssociationMessageReceived(this, streamId, ppid, data, len); + this->listener->OnSctpAssociationMessageReceived(this, streamId, data, len, ppid); } // If end of message and there is buffered data, append data and notify buffer. else if (eor && this->messageBufferLen != 0) @@ -702,7 +737,7 @@ namespace RTC MS_DEBUG_DEV("notifying listener [eor:1, buffer len:%zu]", this->messageBufferLen); this->listener->OnSctpAssociationMessageReceived( - this, streamId, ppid, this->messageBuffer, this->messageBufferLen); + this, streamId, this->messageBuffer, this->messageBufferLen, ppid); this->messageBufferLen = 0; } @@ -711,7 +746,9 @@ namespace RTC { // Allocate the buffer if not already done. if (!this->messageBuffer) + { this->messageBuffer = new uint8_t[this->maxSctpMessageSize]; + } std::memcpy(this->messageBuffer + this->messageBufferLen, data, len); this->messageBufferLen += len; @@ -723,7 +760,9 @@ namespace RTC void SctpAssociation::OnUsrSctpReceiveSctpNotification(union sctp_notification* notification, size_t len) { if (notification->sn_header.sn_length != (uint32_t)len) + { return; + } switch (notification->sn_header.sn_type) { @@ -754,7 +793,9 @@ namespace RTC // Increase if requested before connected. if (this->desiredOs > this->os) + { AddOutgoingStreams(/*force*/ true); + } if (this->state != SctpState::CONNECTED) { @@ -810,7 +851,9 @@ namespace RTC // Increase if requested before connected. if (this->desiredOs > this->os) + { AddOutgoingStreams(/*force*/ true); + } if (this->state != SctpState::CONNECTED) { @@ -950,10 +993,14 @@ namespace RTC sizeof(uint16_t); if (notification->sn_strreset_event.strreset_flags & SCTP_STREAM_RESET_INCOMING_SSN) + { incoming = true; + } if (notification->sn_strreset_event.strreset_flags & SCTP_STREAM_RESET_OUTGOING_SSN) + { outgoing = true; + } if (MS_HAS_DEBUG_TAG(sctp)) { @@ -972,7 +1019,9 @@ namespace RTC } if (i > 0) + { streamIds.append(","); + } streamIds.append(std::to_string(streamId)); } diff --git a/worker/src/RTC/SctpDictionaries/SctpStreamParameters.cpp b/worker/src/RTC/SctpDictionaries/SctpStreamParameters.cpp index 5a6e4156c5..6a2d6fa8d6 100644 --- a/worker/src/RTC/SctpDictionaries/SctpStreamParameters.cpp +++ b/worker/src/RTC/SctpDictionaries/SctpStreamParameters.cpp @@ -3,67 +3,48 @@ #include "Logger.hpp" #include "MediaSoupErrors.hpp" -#include "Utils.hpp" #include "RTC/SctpDictionaries.hpp" namespace RTC { /* Instance methods. */ - SctpStreamParameters::SctpStreamParameters(json& data) + SctpStreamParameters::SctpStreamParameters(const FBS::SctpParameters::SctpStreamParameters* data) { MS_TRACE(); - if (!data.is_object()) - MS_THROW_TYPE_ERROR("data is not an object"); - - auto jsonStreamIdIt = data.find("streamId"); - auto jsonOrderedIdIt = data.find("ordered"); - auto jsonMaxPacketLifeTimeIt = data.find("maxPacketLifeTime"); - auto jsonMaxRetransmitsIt = data.find("maxRetransmits"); - - // streamId is mandatory. - if (jsonStreamIdIt == data.end() || !Utils::Json::IsPositiveInteger(*jsonStreamIdIt)) - MS_THROW_TYPE_ERROR("missing streamId"); - - this->streamId = jsonStreamIdIt->get(); + this->streamId = data->streamId(); if (this->streamId > 65534) + { MS_THROW_TYPE_ERROR("streamId must not be greater than 65534"); + } // ordered is optional. bool orderedGiven = false; - if (jsonOrderedIdIt != data.end() && jsonOrderedIdIt->is_boolean()) + if (data->ordered().has_value()) { orderedGiven = true; - this->ordered = jsonOrderedIdIt->get(); + this->ordered = data->ordered().value(); } // maxPacketLifeTime is optional. - // clang-format off - if ( - jsonMaxPacketLifeTimeIt != data.end() && - Utils::Json::IsPositiveInteger(*jsonMaxPacketLifeTimeIt) - ) - // clang-format on + if (data->maxPacketLifeTime().has_value()) { - this->maxPacketLifeTime = jsonMaxPacketLifeTimeIt->get(); + this->maxPacketLifeTime = data->maxPacketLifeTime().value(); } // maxRetransmits is optional. - // clang-format off - if ( - jsonMaxRetransmitsIt != data.end() && - Utils::Json::IsPositiveInteger(*jsonMaxRetransmitsIt) - ) - // clang-format on + if (data->maxRetransmits().has_value()) { - this->maxRetransmits = jsonMaxRetransmitsIt->get(); + this->maxRetransmits = data->maxRetransmits().value(); } if (this->maxPacketLifeTime && this->maxRetransmits) + { MS_THROW_TYPE_ERROR("cannot provide both maxPacketLifeTime and maxRetransmits"); + } // clang-format off if ( @@ -81,22 +62,18 @@ namespace RTC } } - void SctpStreamParameters::FillJson(json& jsonObject) const + flatbuffers::Offset SctpStreamParameters::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Add streamId. - jsonObject["streamId"] = this->streamId; - - // Add ordered. - jsonObject["ordered"] = this->ordered; - - // Add maxPacketLifeTime. - if (this->maxPacketLifeTime) - jsonObject["maxPacketLifeTime"] = this->maxPacketLifeTime; - - // Add maxRetransmits. - if (this->maxRetransmits) - jsonObject["maxRetransmits"] = this->maxRetransmits; + return FBS::SctpParameters::CreateSctpStreamParameters( + builder, + this->streamId, + this->ordered, + this->maxPacketLifeTime ? flatbuffers::Optional(this->maxPacketLifeTime) + : flatbuffers::nullopt, + this->maxRetransmits ? flatbuffers::Optional(this->maxRetransmits) + : flatbuffers::nullopt); } } // namespace RTC diff --git a/worker/src/RTC/SctpListener.cpp b/worker/src/RTC/SctpListener.cpp index 64300c6c0a..bce61d30f0 100644 --- a/worker/src/RTC/SctpListener.cpp +++ b/worker/src/RTC/SctpListener.cpp @@ -10,22 +10,24 @@ namespace RTC { /* Instance methods. */ - void SctpListener::FillJson(json& jsonObject) const + flatbuffers::Offset SctpListener::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - jsonObject["streamIdTable"] = json::object(); - - auto jsonStreamIdTableIt = jsonObject.find("streamIdTable"); - // Add streamIdTable. + std::vector> streamIdTable; + for (const auto& kv : this->streamIdTable) { auto streamId = kv.first; auto* dataProducer = kv.second; - (*jsonStreamIdTableIt)[std::to_string(streamId)] = dataProducer->id; + streamIdTable.emplace_back( + FBS::Common::CreateUint16StringDirect(builder, streamId, dataProducer->id.c_str())); } + + return FBS::Transport::CreateSctpListenerDirect(builder, &streamIdTable); } void SctpListener::AddDataProducer(RTC::DataProducer* dataProducer) @@ -55,9 +57,13 @@ namespace RTC for (auto it = this->streamIdTable.begin(); it != this->streamIdTable.end();) { if (it->second == dataProducer) + { it = this->streamIdTable.erase(it); + } else + { ++it; + } } } diff --git a/worker/src/RTC/SenderBandwidthEstimator.cpp b/worker/src/RTC/SenderBandwidthEstimator.cpp index ca98eda585..6b02411428 100644 --- a/worker/src/RTC/SenderBandwidthEstimator.cpp +++ b/worker/src/RTC/SenderBandwidthEstimator.cpp @@ -4,7 +4,6 @@ #include "RTC/SenderBandwidthEstimator.hpp" #include "DepLibUV.hpp" #include "Logger.hpp" -#include namespace RTC { @@ -47,7 +46,7 @@ namespace RTC this->cummulativeResult.Reset(); } - void SenderBandwidthEstimator::RtpPacketSent(SentInfo& sentInfo) + void SenderBandwidthEstimator::RtpPacketSent(const SentInfo& sentInfo) { MS_TRACE(); @@ -88,15 +87,19 @@ namespace RTC // Drop ongoing cummulative result if too old. if (elapsedMs > 1000u) + { this->cummulativeResult.Reset(); + } for (auto& result : feedback->GetPacketResults()) { if (!result.received) + { continue; + } - uint16_t wideSeq = result.sequenceNumber; - auto it = this->sentInfos.find(wideSeq); + const uint16_t wideSeq = result.sequenceNumber; + auto it = this->sentInfos.find(wideSeq); if (it == this->sentInfos.end()) { @@ -238,16 +241,24 @@ namespace RTC else { if (sentAtMs < this->firstPacketSentAtMs) + { this->firstPacketSentAtMs = sentAtMs; + } if (receivedAtMs < this->firstPacketReceivedAtMs) + { this->firstPacketReceivedAtMs = receivedAtMs; + } if (sentAtMs > this->lastPacketSentAtMs) + { this->lastPacketSentAtMs = sentAtMs; + } if (receivedAtMs > this->lastPacketReceivedAtMs) + { this->lastPacketReceivedAtMs = receivedAtMs; + } } this->numPackets++; diff --git a/worker/src/RTC/SeqManager.cpp b/worker/src/RTC/SeqManager.cpp index 47f1731f69..119f46148a 100644 --- a/worker/src/RTC/SeqManager.cpp +++ b/worker/src/RTC/SeqManager.cpp @@ -8,14 +8,14 @@ namespace RTC { template - bool SeqManager::SeqLowerThan::operator()(const T lhs, const T rhs) const + bool SeqManager::SeqLowerThan::operator()(T lhs, T rhs) const { return ((rhs > lhs) && (rhs - lhs <= MaxValue / 2)) || ((lhs > rhs) && (lhs - rhs > MaxValue / 2)); } template - bool SeqManager::SeqHigherThan::operator()(const T lhs, const T rhs) const + bool SeqManager::SeqHigherThan::operator()(T lhs, T rhs) const { return ((lhs > rhs) && (lhs - rhs <= MaxValue / 2)) || ((rhs > lhs) && (rhs - lhs > MaxValue / 2)); @@ -28,28 +28,28 @@ namespace RTC const typename SeqManager::SeqHigherThan SeqManager::isSeqHigherThan{}; template - bool SeqManager::IsSeqLowerThan(const T lhs, const T rhs) + bool SeqManager::IsSeqLowerThan(T lhs, T rhs) { return isSeqLowerThan(lhs, rhs); } template - bool SeqManager::IsSeqHigherThan(const T lhs, const T rhs) + bool SeqManager::IsSeqHigherThan(T lhs, T rhs) { return isSeqHigherThan(lhs, rhs); } template - T SeqManager::Delta(const T lhs, const T rhs) + SeqManager::SeqManager(T initialOutput) : initialOutput(initialOutput) { - T value = (lhs > rhs) ? (lhs - rhs) : (MaxValue - rhs + lhs); - - return value & MaxValue; + MS_TRACE(); } template void SeqManager::Sync(T input) { + MS_TRACE(); + // Update base. this->base = (this->maxOutput - input) & MaxValue; @@ -63,68 +63,96 @@ namespace RTC template void SeqManager::Drop(T input) { + MS_TRACE(); + // Mark as dropped if 'input' is higher than anyone already processed. if (SeqManager::IsSeqHigherThan(input, this->maxInput)) { - this->dropped.insert(input); + this->maxInput = input; + // Insert input in the last position. + // Explicitly indicate insert() to add the input at the end, which is + // more performant. + this->dropped.insert(this->dropped.end(), input); + + ClearDropped(); } } template - void SeqManager::Offset(T offset) + bool SeqManager::Input(T input, T& output) { - this->base = (this->base + offset) & MaxValue; - } + MS_TRACE(); - template - bool SeqManager::Input(const T input, T& output) - { auto base = this->base; - // There are dropped inputs. Synchronize. - if (!this->dropped.empty()) + // No dropped inputs to consider. + if (this->dropped.empty()) + { + goto done; + } + // Dropped inputs present, cleanup and update base. + else { - // Delete dropped inputs older than input - MaxValue/2. - size_t droppedCount = this->dropped.size(); - size_t threshold = (input - MaxValue / 2) & MaxValue; - auto it = this->dropped.lower_bound(threshold); - this->dropped.erase(this->dropped.begin(), it); - this->base = (this->base - (droppedCount - this->dropped.size())) & MaxValue; - - // Count dropped entries before 'input' in order to adapt the base. - droppedCount = this->dropped.size(); - it = this->dropped.lower_bound(input); - - if (it != this->dropped.end()) + // Set 'maxInput' here if needed before calling ClearDropped(). + if (this->started && IsSeqHigherThan(input, this->maxInput)) { - // Check whether this input was dropped. - if (*it == input) - { - MS_DEBUG_DEV("trying to send a dropped input"); + this->maxInput = input; + } - return false; - } + ClearDropped(); - droppedCount -= std::distance(it, this->dropped.end()); - } + base = this->base; + } + // No dropped inputs to consider after cleanup. + if (this->dropped.empty()) + { + goto done; + } + // This input was dropped. + else if (this->dropped.find(input) != this->dropped.end()) + { + MS_DEBUG_DEV("trying to send a dropped input"); + + return false; + } + // There are dropped inputs, calculate 'base' for this input. + else + { + auto droppedCount = this->dropped.size(); + + // Get the first dropped input which is higher than or equal 'input'. + auto it = this->dropped.lower_bound(input); + + droppedCount -= std::distance(it, this->dropped.end()); base = (this->base - droppedCount) & MaxValue; } + done: output = (input + base) & MaxValue; - T idelta = SeqManager::Delta(input, this->maxInput); - T odelta = SeqManager::Delta(output, this->maxOutput); + if (!this->started) + { + this->started = true; + this->maxInput = input; + this->maxOutput = output; + } + else + { + // New input is higher than the maximum seen. + if (IsSeqHigherThan(input, this->maxInput)) + { + this->maxInput = input; + } - // New input is higher than the maximum seen. But less than acceptable units higher. - // Keep it as the maximum seen. See Drop(). - if (idelta < MaxValue / 2) - this->maxInput = input; + // New output is higher than the maximum seen. + if (IsSeqHigherThan(output, this->maxOutput)) + { + this->maxOutput = output; + } + } - // New output is higher than the maximum seen. But less than acceptable units higher. - // Keep it as the maximum seen. See Sync(). - if (odelta < MaxValue / 2) - this->maxOutput = output; + output = (output + this->initialOutput) & MaxValue; return true; } @@ -141,8 +169,44 @@ namespace RTC return this->maxOutput; } + /* + * Delete droped inputs greater than maxInput, which belong to a previous + * cycle. + */ + template + void SeqManager::ClearDropped() + { + MS_TRACE(); + + // Cleanup dropped values. + if (this->dropped.empty()) + { + return; + } + + const size_t previousDroppedSize = this->dropped.size(); + + for (auto it = this->dropped.begin(); it != this->dropped.end();) + { + auto value = *it; + + if (isSeqHigherThan(value, this->maxInput)) + { + it = this->dropped.erase(it); + } + else + { + break; + } + } + + // Adapt base. + this->base = (this->base - (previousDroppedSize - this->dropped.size())) & MaxValue; + } + // Explicit instantiation to have all SeqManager definitions in this file. template class SeqManager; + template class SeqManager; // For testing. template class SeqManager; template class SeqManager; // For PictureID (15 bits). template class SeqManager; diff --git a/worker/src/RTC/Shared.cpp b/worker/src/RTC/Shared.cpp index e984862df0..037eca59c8 100644 --- a/worker/src/RTC/Shared.cpp +++ b/worker/src/RTC/Shared.cpp @@ -7,11 +7,8 @@ namespace RTC { Shared::Shared( - ChannelMessageRegistrator* channelMessageRegistrator, - Channel::ChannelNotifier* channelNotifier, - PayloadChannel::PayloadChannelNotifier* payloadChannelNotifier) - : channelMessageRegistrator(channelMessageRegistrator), channelNotifier(channelNotifier), - payloadChannelNotifier(payloadChannelNotifier) + ChannelMessageRegistrator* channelMessageRegistrator, Channel::ChannelNotifier* channelNotifier) + : channelMessageRegistrator(channelMessageRegistrator), channelNotifier(channelNotifier) { MS_TRACE(); } @@ -22,6 +19,5 @@ namespace RTC delete this->channelMessageRegistrator; delete this->channelNotifier; - delete this->payloadChannelNotifier; } } // namespace RTC diff --git a/worker/src/RTC/SimpleConsumer.cpp b/worker/src/RTC/SimpleConsumer.cpp index 7ed5f44d2f..65deccbe2d 100644 --- a/worker/src/RTC/SimpleConsumer.cpp +++ b/worker/src/RTC/SimpleConsumer.cpp @@ -1,11 +1,13 @@ +#include "FBS/consumer.h" #define MS_CLASS "RTC::SimpleConsumer" // #define MS_LOG_DEV_LEVEL 3 -#include "RTC/SimpleConsumer.hpp" #include "DepLibUV.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" +#include "Utils.hpp" #include "RTC/Codecs/Tools.hpp" +#include "RTC/SimpleConsumer.hpp" namespace RTC { @@ -16,14 +18,16 @@ namespace RTC const std::string& id, const std::string& producerId, RTC::Consumer::Listener* listener, - json& data) + const FBS::Transport::ConsumeRequest* data) : RTC::Consumer::Consumer(shared, id, producerId, listener, data, RTC::RtpParameters::Type::SIMPLE) { MS_TRACE(); // Ensure there is a single encoding. if (this->consumableRtpEncodings.size() != 1u) + { MS_THROW_TYPE_ERROR("invalid consumableRtpEncodings with size != 1"); + } auto& encoding = this->rtpParameters.encodings[0]; const auto* mediaCodec = this->rtpParameters.GetCodecForEncoding(encoding); @@ -33,6 +37,14 @@ namespace RTC // Create RtpStreamSend instance for sending a single stream to the remote. CreateRtpStream(); + // Let's chosee an initial output seq number between 1000 and 32768 to avoid + // libsrtp bug: + // https://github.com/versatica/mediasoup/issues/1437 + const uint16_t initialOutputSeq = + Utils::Crypto::GetRandomUInt(1000u, std::numeric_limits::max() / 2); + + this->rtpSeqManager.reset(new RTC::SeqManager(initialOutputSeq)); + // Create the encoding context for Opus. if ( mediaCodec->mimeType.type == RTC::RtpCodecMimeType::Type::AUDIO && @@ -44,22 +56,15 @@ namespace RTC this->encodingContext.reset( RTC::Codecs::Tools::GetEncodingContext(mediaCodec->mimeType, params)); - auto jsonIgnoreDtx = data.find("ignoreDtx"); - - if (jsonIgnoreDtx != data.end() && jsonIgnoreDtx->is_boolean()) - { - auto ignoreDtx = jsonIgnoreDtx->get(); - - this->encodingContext->SetIgnoreDtx(ignoreDtx); - } + // ignoreDtx is set to false by default. + this->encodingContext->SetIgnoreDtx(data->ignoreDtx()); } // NOTE: This may throw. this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ nullptr, - /*payloadChannelNotificationHandler*/ nullptr); + /*channelNotificationHandler*/ nullptr); } SimpleConsumer::~SimpleConsumer() @@ -71,70 +76,94 @@ namespace RTC delete this->rtpStream; } - void SimpleConsumer::FillJson(json& jsonObject) const + flatbuffers::Offset SimpleConsumer::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); // Call the parent method. - RTC::Consumer::FillJson(jsonObject); - + auto base = RTC::Consumer::FillBuffer(builder); // Add rtpStream. - this->rtpStream->FillJson(jsonObject["rtpStream"]); + std::vector> rtpStreams; + rtpStreams.emplace_back(this->rtpStream->FillBuffer(builder)); + + auto dump = FBS::Consumer::CreateConsumerDumpDirect(builder, base, &rtpStreams); + + return FBS::Consumer::CreateDumpResponse(builder, dump); } - void SimpleConsumer::FillJsonStats(json& jsonArray) const + flatbuffers::Offset SimpleConsumer::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) { MS_TRACE(); + std::vector> rtpStreams; + // Add stats of our send stream. - jsonArray.emplace_back(json::value_t::object); - this->rtpStream->FillJsonStats(jsonArray[0]); + rtpStreams.emplace_back(this->rtpStream->FillBufferStats(builder)); // Add stats of our recv stream. if (this->producerRtpStream) { - jsonArray.emplace_back(json::value_t::object); - this->producerRtpStream->FillJsonStats(jsonArray[1]); + rtpStreams.emplace_back(this->producerRtpStream->FillBufferStats(builder)); } + + return FBS::Consumer::CreateGetStatsResponseDirect(builder, &rtpStreams); } - void SimpleConsumer::FillJsonScore(json& jsonObject) const + flatbuffers::Offset SimpleConsumer::FillBufferScore( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); MS_ASSERT(this->producerRtpStreamScores, "producerRtpStreamScores not set"); - jsonObject["score"] = this->rtpStream->GetScore(); + uint8_t producerScore{ 0 }; if (this->producerRtpStream) - jsonObject["producerScore"] = this->producerRtpStream->GetScore(); - else - jsonObject["producerScore"] = 0; + { + producerScore = this->producerRtpStream->GetScore(); + } - jsonObject["producerScores"] = *this->producerRtpStreamScores; + return FBS::Consumer::CreateConsumerScoreDirect( + builder, this->rtpStream->GetScore(), producerScore, this->producerRtpStreamScores); } void SimpleConsumer::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::CONSUMER_REQUEST_KEY_FRAME: + case Channel::ChannelRequest::Method::CONSUMER_DUMP: + { + auto dumpOffset = FillBuffer(request->GetBufferBuilder()); + + request->Accept(FBS::Response::Body::Consumer_DumpResponse, dumpOffset); + + break; + } + + case Channel::ChannelRequest::Method::CONSUMER_REQUEST_KEY_FRAME: { if (IsActive()) + { RequestKeyFrame(); + } request->Accept(); break; } - case Channel::ChannelRequest::MethodId::CONSUMER_SET_PREFERRED_LAYERS: + case Channel::ChannelRequest::Method::CONSUMER_SET_PREFERRED_LAYERS: { - // Do nothing. + // Accept with empty preferred layers object. - request->Accept(); + auto responseOffset = + FBS::Consumer::CreateSetPreferredLayersResponse(request->GetBufferBuilder()); + + request->Accept(FBS::Response::Body::Consumer_SetPreferredLayersResponse, responseOffset); break; } @@ -147,14 +176,14 @@ namespace RTC } } - void SimpleConsumer::ProducerRtpStream(RTC::RtpStream* rtpStream, uint32_t /*mappedSsrc*/) + void SimpleConsumer::ProducerRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t /*mappedSsrc*/) { MS_TRACE(); this->producerRtpStream = rtpStream; } - void SimpleConsumer::ProducerNewRtpStream(RTC::RtpStream* rtpStream, uint32_t /*mappedSsrc*/) + void SimpleConsumer::ProducerNewRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t /*mappedSsrc*/) { MS_TRACE(); @@ -165,7 +194,7 @@ namespace RTC } void SimpleConsumer::ProducerRtpStreamScore( - RTC::RtpStream* /*rtpStream*/, uint8_t /*score*/, uint8_t /*previousScore*/) + RTC::RtpStreamRecv* /*rtpStream*/, uint8_t /*score*/, uint8_t /*previousScore*/) { MS_TRACE(); @@ -173,7 +202,7 @@ namespace RTC EmitScore(); } - void SimpleConsumer::ProducerRtcpSenderReport(RTC::RtpStream* /*rtpStream*/, bool /*first*/) + void SimpleConsumer::ProducerRtcpSenderReport(RTC::RtpStreamRecv* /*rtpStream*/, bool /*first*/) { MS_TRACE(); @@ -188,10 +217,14 @@ namespace RTC // Audio SimpleConsumer does not play the BWE game. if (this->kind != RTC::Media::Kind::VIDEO) + { return 0u; + } if (!IsActive()) + { return 0u; + } return this->priority; } @@ -207,7 +240,9 @@ namespace RTC // If this is not the first time this method is called within the same iteration, // return 0 since a video SimpleConsumer does not keep state about this. if (this->managingBitrate) + { return 0u; + } this->managingBitrate = true; @@ -217,9 +252,13 @@ namespace RTC auto desiredBitrate = this->producerRtpStream->GetBitrate(nowMs); if (desiredBitrate < bitrate) + { return desiredBitrate; + } else + { return bitrate; + } } void SimpleConsumer::ApplyLayers() @@ -243,10 +282,14 @@ namespace RTC // Audio SimpleConsumer does not play the BWE game. if (this->kind != RTC::Media::Kind::VIDEO) + { return 0u; + } if (!IsActive()) + { return 0u; + } auto nowMs = DepLibUV::GetTimeMs(); auto desiredBitrate = this->producerRtpStream->GetBitrate(nowMs); @@ -256,7 +299,9 @@ namespace RTC auto maxBitrate = this->rtpParameters.encodings[0].maxBitrate; if (maxBitrate > desiredBitrate) + { desiredBitrate = maxBitrate; + } return desiredBitrate; } @@ -265,8 +310,18 @@ namespace RTC { MS_TRACE(); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.consumerId = this->id; +#endif + if (!IsActive()) + { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::CONSUMER_INACTIVE); +#endif + return; + } auto payloadType = packet->GetPayloadType(); @@ -276,6 +331,10 @@ namespace RTC { MS_DEBUG_DEV("payload type not supported [payloadType:%" PRIu8 "]", payloadType); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::UNSUPPORTED_PAYLOAD_TYPE); +#endif + return; } @@ -290,7 +349,11 @@ namespace RTC packet->GetSequenceNumber(), packet->GetTimestamp()); - this->rtpSeqManager.Drop(packet->GetSequenceNumber()); + this->rtpSeqManager->Drop(packet->GetSequenceNumber()); + +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::DROPPED_BY_CODEC); +#endif return; } @@ -298,7 +361,25 @@ namespace RTC // If we need to sync, support key frames and this is not a key frame, ignore // the packet. if (this->syncRequired && this->keyFrameSupported && !packet->IsKeyFrame()) + { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::NOT_A_KEYFRAME); +#endif + + return; + } + + // Packets with only padding are not forwarded. + if (packet->GetPayloadLength() == 0) + { + this->rtpSeqManager->Drop(packet->GetSequenceNumber()); + +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::EMPTY_PAYLOAD); +#endif + return; + } // Whether this is the first packet after re-sync. const bool isSyncPacket = this->syncRequired; @@ -307,9 +388,11 @@ namespace RTC if (isSyncPacket) { if (packet->IsKeyFrame()) + { MS_DEBUG_TAG(rtp, "sync key frame received"); + } - this->rtpSeqManager.Sync(packet->GetSequenceNumber() - 1); + this->rtpSeqManager->Sync(packet->GetSequenceNumber() - 1); this->syncRequired = false; } @@ -317,7 +400,7 @@ namespace RTC // Update RTP seq number and timestamp. uint16_t seq; - this->rtpSeqManager.Input(packet->GetSequenceNumber(), seq); + this->rtpSeqManager->Input(packet->GetSequenceNumber(), seq); // Save original packet fields. auto origSsrc = packet->GetSsrc(); @@ -327,6 +410,11 @@ namespace RTC packet->SetSsrc(this->rtpParameters.encodings[0].ssrc); packet->SetSequenceNumber(seq); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.sendRtpTimestamp = packet->GetTimestamp(); + packet->logger.sendSeqNumber = seq; +#endif + if (isSyncPacket) { MS_DEBUG_TAG( @@ -370,29 +458,27 @@ namespace RTC MS_TRACE(); if (static_cast((nowMs - this->lastRtcpSentTime) * 1.15) < this->maxRtcpInterval) + { return true; + } auto* senderReport = this->rtpStream->GetRtcpSenderReport(nowMs); if (!senderReport) + { return true; + } // Build SDES chunk for this sender. auto* sdesChunk = this->rtpStream->GetRtcpSdesChunk(); - RTC::RTCP::DelaySinceLastRr* delaySinceLastRrReport{ nullptr }; - - auto* dlrr = this->rtpStream->GetRtcpXrDelaySinceLastRr(nowMs); - - if (dlrr) - { - delaySinceLastRrReport = new RTC::RTCP::DelaySinceLastRr(); - delaySinceLastRrReport->AddSsrcInfo(dlrr); - } + auto* delaySinceLastRrSsrcInfo = this->rtpStream->GetRtcpXrDelaySinceLastRrSsrcInfo(nowMs); // RTCP Compound packet buffer cannot hold the data. - if (!packet->Add(senderReport, sdesChunk, delaySinceLastRrReport)) + if (!packet->Add(senderReport, sdesChunk, delaySinceLastRrSsrcInfo)) + { return false; + } this->lastRtcpSentTime = nowMs; @@ -405,13 +491,17 @@ namespace RTC MS_TRACE(); if (!IsActive()) + { return; + } auto fractionLost = this->rtpStream->GetFractionLost(); // If our fraction lost is worse than the given one, update it. if (fractionLost > worstRemoteFractionLost) + { worstRemoteFractionLost = fractionLost; + } } void SimpleConsumer::ReceiveNack(RTC::RTCP::FeedbackRtpNackPacket* nackPacket) @@ -419,7 +509,9 @@ namespace RTC MS_TRACE(); if (!IsActive()) + { return; + } // May emit 'trace' event. EmitTraceEventNackType(); @@ -454,7 +546,9 @@ namespace RTC this->rtpStream->ReceiveKeyFrameRequest(messageType); if (IsActive()) + { RequestKeyFrame(); + } } void SimpleConsumer::ReceiveRtcpReceiverReport(RTC::RTCP::ReceiverReport* report) @@ -476,7 +570,9 @@ namespace RTC MS_TRACE(); if (!IsActive()) + { return 0u; + } return this->rtpStream->GetBitrate(nowMs); } @@ -495,7 +591,9 @@ namespace RTC this->syncRequired = true; if (IsActive()) + { RequestKeyFrame(); + } } void SimpleConsumer::UserOnTransportDisconnected() @@ -512,7 +610,9 @@ namespace RTC this->rtpStream->Pause(); if (this->externallyManagedBitrate && this->kind == RTC::Media::Kind::VIDEO) + { this->listener->OnConsumerNeedZeroBitrate(this); + } } void SimpleConsumer::UserOnResumed() @@ -522,7 +622,9 @@ namespace RTC this->syncRequired = true; if (IsActive()) + { RequestKeyFrame(); + } } void SimpleConsumer::CreateRtpStream() @@ -595,12 +697,16 @@ namespace RTC // If the Consumer is paused, tell the RtpStreamSend. if (IsPaused() || IsProducerPaused()) + { this->rtpStream->Pause(); + } const auto* rtxCodec = this->rtpParameters.GetRtxCodecForEncoding(encoding); if (rtxCodec && encoding.hasRtx) + { this->rtpStream->SetRtx(rtxCodec->payloadType, encoding.rtx.ssrc); + } } void SimpleConsumer::RequestKeyFrame() @@ -608,7 +714,9 @@ namespace RTC MS_TRACE(); if (this->kind != RTC::Media::Kind::VIDEO) + { return; + } auto mappedSsrc = this->consumableRtpEncodings[0].ssrc; @@ -619,11 +727,16 @@ namespace RTC { MS_TRACE(); - json data = json::object(); + auto scoreOffset = FillBufferScore(this->shared->channelNotifier->GetBufferBuilder()); - FillJsonScore(data); + auto notificationOffset = FBS::Consumer::CreateScoreNotification( + this->shared->channelNotifier->GetBufferBuilder(), scoreOffset); - this->shared->channelNotifier->Emit(this->id, "score", data); + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::CONSUMER_SCORE, + FBS::Notification::Body::Consumer_ScoreNotification, + notificationOffset); } inline void SimpleConsumer::OnRtpStreamScore( diff --git a/worker/src/RTC/SimulcastConsumer.cpp b/worker/src/RTC/SimulcastConsumer.cpp index a129555bb9..5ace36df3d 100644 --- a/worker/src/RTC/SimulcastConsumer.cpp +++ b/worker/src/RTC/SimulcastConsumer.cpp @@ -24,15 +24,16 @@ namespace RTC const std::string& id, const std::string& producerId, RTC::Consumer::Listener* listener, - json& data) + const FBS::Transport::ConsumeRequest* data) : RTC::Consumer::Consumer( shared, id, producerId, listener, data, RTC::RtpParameters::Type::SIMULCAST) { MS_TRACE(); - // Ensure there are N > 1 encodings. - if (this->consumableRtpEncodings.size() <= 1u) - MS_THROW_TYPE_ERROR("invalid consumableRtpEncodings with size <= 1"); + // We allow a single encoding in simulcast (so we can enable temporal layers + // with a single simulcast stream). + // NOTE: No need to check this->consumableRtpEncodings.size() > 0 here since + // it's already done in Consumer constructor. auto& encoding = this->rtpParameters.encodings[0]; @@ -42,8 +43,6 @@ namespace RTC MS_THROW_TYPE_ERROR("encoding.spatialLayers does not match number of consumableRtpEncodings"); } - auto jsonPreferredLayersIt = data.find("preferredLayers"); - // Fill mapMappedSsrcSpatialLayer. for (size_t idx{ 0u }; idx < this->consumableRtpEncodings.size(); ++idx) { @@ -53,37 +52,25 @@ namespace RTC } // Set preferredLayers (if given). - if (jsonPreferredLayersIt != data.end() && jsonPreferredLayersIt->is_object()) + if (flatbuffers::IsFieldPresent(data, FBS::Transport::ConsumeRequest::VT_PREFERREDLAYERS)) { - auto jsonSpatialLayerIt = jsonPreferredLayersIt->find("spatialLayer"); - auto jsonTemporalLayerIt = jsonPreferredLayersIt->find("temporalLayer"); - - // clang-format off - if ( - jsonSpatialLayerIt == jsonPreferredLayersIt->end() || - !Utils::Json::IsPositiveInteger(*jsonSpatialLayerIt) - ) - // clang-format on - { - MS_THROW_TYPE_ERROR("missing preferredLayers.spatialLayer"); - } + const auto* preferredLayers = data->preferredLayers(); - this->preferredSpatialLayer = jsonSpatialLayerIt->get(); + this->preferredSpatialLayer = preferredLayers->spatialLayer(); if (this->preferredSpatialLayer > encoding.spatialLayers - 1) + { this->preferredSpatialLayer = encoding.spatialLayers - 1; + } - // clang-format off - if ( - jsonTemporalLayerIt != jsonPreferredLayersIt->end() && - Utils::Json::IsPositiveInteger(*jsonTemporalLayerIt) - ) - // clang-format on + if (preferredLayers->temporalLayer().has_value()) { - this->preferredTemporalLayer = jsonTemporalLayerIt->get(); + this->preferredTemporalLayer = preferredLayers->temporalLayer().value(); if (this->preferredTemporalLayer > encoding.temporalLayers - 1) + { this->preferredTemporalLayer = encoding.temporalLayers - 1; + } } else { @@ -112,6 +99,14 @@ namespace RTC "%s codec not supported for simulcast", mediaCodec->mimeType.ToString().c_str()); } + // Let's chosee an initial output seq number between 1000 and 32768 to avoid + // libsrtp bug: + // https://github.com/versatica/mediasoup/issues/1437 + const uint16_t initialOutputSeq = + Utils::Crypto::GetRandomUInt(1000u, std::numeric_limits::max() / 2); + + this->rtpSeqManager.reset(new RTC::SeqManager(initialOutputSeq)); + RTC::Codecs::EncodingContext::Params params; params.spatialLayers = encoding.spatialLayers; @@ -128,8 +123,7 @@ namespace RTC this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ nullptr, - /*payloadChannelNotificationHandler*/ nullptr); + /*channelRequestHandler*/ nullptr); } SimulcastConsumer::~SimulcastConsumer() @@ -141,54 +135,54 @@ namespace RTC delete this->rtpStream; } - void SimulcastConsumer::FillJson(json& jsonObject) const + flatbuffers::Offset SimulcastConsumer::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); // Call the parent method. - RTC::Consumer::FillJson(jsonObject); - + auto base = RTC::Consumer::FillBuffer(builder); // Add rtpStream. - this->rtpStream->FillJson(jsonObject["rtpStream"]); - - // Add preferredSpatialLayer. - jsonObject["preferredSpatialLayer"] = this->preferredSpatialLayer; - - // Add targetSpatialLayer. - jsonObject["targetSpatialLayer"] = this->targetSpatialLayer; - - // Add currentSpatialLayer. - jsonObject["currentSpatialLayer"] = this->currentSpatialLayer; - - // Add preferredTemporalLayer. - jsonObject["preferredTemporalLayer"] = this->preferredTemporalLayer; - - // Add targetTemporalLayer. - jsonObject["targetTemporalLayer"] = this->targetTemporalLayer; + std::vector> rtpStreams; + rtpStreams.emplace_back(this->rtpStream->FillBuffer(builder)); + + auto dump = FBS::Consumer::CreateConsumerDumpDirect( + builder, + base, + &rtpStreams, + this->preferredSpatialLayer, + this->targetSpatialLayer, + this->currentSpatialLayer, + this->preferredTemporalLayer, + this->targetTemporalLayer, + this->encodingContext->GetCurrentTemporalLayer()); - // Add currentTemporalLayer. - jsonObject["currentTemporalLayer"] = this->encodingContext->GetCurrentTemporalLayer(); + return FBS::Consumer::CreateDumpResponse(builder, dump); } - void SimulcastConsumer::FillJsonStats(json& jsonArray) const + flatbuffers::Offset SimulcastConsumer::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) { MS_TRACE(); + std::vector> rtpStreams; + // Add stats of our send stream. - jsonArray.emplace_back(json::value_t::object); - this->rtpStream->FillJsonStats(jsonArray[0]); + rtpStreams.emplace_back(this->rtpStream->FillBufferStats(builder)); - // Add stats of our recv stream. auto* producerCurrentRtpStream = GetProducerCurrentRtpStream(); + // Add stats of our recv stream. if (producerCurrentRtpStream) { - jsonArray.emplace_back(json::value_t::object); - producerCurrentRtpStream->FillJsonStats(jsonArray[1]); + rtpStreams.emplace_back(producerCurrentRtpStream->FillBufferStats(builder)); } + + return FBS::Consumer::CreateGetStatsResponseDirect(builder, &rtpStreams); } - void SimulcastConsumer::FillJsonScore(json& jsonObject) const + flatbuffers::Offset SimulcastConsumer::FillBufferScore( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); @@ -196,68 +190,73 @@ namespace RTC auto* producerCurrentRtpStream = GetProducerCurrentRtpStream(); - jsonObject["score"] = this->rtpStream->GetScore(); + uint8_t producerScore{ 0 }; if (producerCurrentRtpStream) - jsonObject["producerScore"] = producerCurrentRtpStream->GetScore(); + { + producerScore = producerCurrentRtpStream->GetScore(); + } else - jsonObject["producerScore"] = 0; + { + producerScore = 0; + } - jsonObject["producerScores"] = *this->producerRtpStreamScores; + return FBS::Consumer::CreateConsumerScoreDirect( + builder, this->rtpStream->GetScore(), producerScore, this->producerRtpStreamScores); } void SimulcastConsumer::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::CONSUMER_REQUEST_KEY_FRAME: + case Channel::ChannelRequest::Method::CONSUMER_DUMP: + { + auto dumpOffset = FillBuffer(request->GetBufferBuilder()); + + request->Accept(FBS::Response::Body::Consumer_DumpResponse, dumpOffset); + + break; + } + + case Channel::ChannelRequest::Method::CONSUMER_REQUEST_KEY_FRAME: { if (IsActive()) + { RequestKeyFrames(); + } request->Accept(); break; } - case Channel::ChannelRequest::MethodId::CONSUMER_SET_PREFERRED_LAYERS: + case Channel::ChannelRequest::Method::CONSUMER_SET_PREFERRED_LAYERS: { auto previousPreferredSpatialLayer = this->preferredSpatialLayer; auto previousPreferredTemporalLayer = this->preferredTemporalLayer; - auto jsonSpatialLayerIt = request->data.find("spatialLayer"); - auto jsonTemporalLayerIt = request->data.find("temporalLayer"); + const auto* body = request->data->body_as(); + const auto* preferredLayers = body->preferredLayers(); // Spatial layer. - // clang-format off - if ( - jsonSpatialLayerIt == request->data.end() || - !Utils::Json::IsPositiveInteger(*jsonSpatialLayerIt) - ) - // clang-format on - { - MS_THROW_TYPE_ERROR("missing spatialLayer"); - } - - this->preferredSpatialLayer = jsonSpatialLayerIt->get(); + this->preferredSpatialLayer = preferredLayers->spatialLayer(); if (this->preferredSpatialLayer > this->rtpStream->GetSpatialLayers() - 1) + { this->preferredSpatialLayer = this->rtpStream->GetSpatialLayers() - 1; + } // preferredTemporaLayer is optional. - // clang-format off - if ( - jsonTemporalLayerIt != request->data.end() && - Utils::Json::IsPositiveInteger(*jsonTemporalLayerIt) - ) - // clang-format on + if (preferredLayers->temporalLayer().has_value()) { - this->preferredTemporalLayer = jsonTemporalLayerIt->get(); + this->preferredTemporalLayer = preferredLayers->temporalLayer().value(); if (this->preferredTemporalLayer > this->rtpStream->GetTemporalLayers() - 1) + { this->preferredTemporalLayer = this->rtpStream->GetTemporalLayers() - 1; + } } else { @@ -270,12 +269,13 @@ namespace RTC this->preferredTemporalLayer, this->id.c_str()); - json data = json::object(); + const flatbuffers::Optional preferredTemporalLayer{ this->preferredTemporalLayer }; + auto preferredLayersOffset = FBS::Consumer::CreateConsumerLayers( + request->GetBufferBuilder(), this->preferredSpatialLayer, preferredTemporalLayer); + auto responseOffset = FBS::Consumer::CreateSetPreferredLayersResponse( + request->GetBufferBuilder(), preferredLayersOffset); - data["spatialLayer"] = this->preferredSpatialLayer; - data["temporalLayer"] = this->preferredTemporalLayer; - - request->Accept(data); + request->Accept(FBS::Response::Body::Consumer_SetPreferredLayersResponse, responseOffset); // clang-format off if ( @@ -301,7 +301,7 @@ namespace RTC } } - void SimulcastConsumer::ProducerRtpStream(RTC::RtpStream* rtpStream, uint32_t mappedSsrc) + void SimulcastConsumer::ProducerRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) { MS_TRACE(); @@ -309,12 +309,12 @@ namespace RTC MS_ASSERT(it != this->mapMappedSsrcSpatialLayer.end(), "unknown mappedSsrc"); - int16_t spatialLayer = it->second; + const int16_t spatialLayer = it->second; this->producerRtpStreams[spatialLayer] = rtpStream; } - void SimulcastConsumer::ProducerNewRtpStream(RTC::RtpStream* rtpStream, uint32_t mappedSsrc) + void SimulcastConsumer::ProducerNewRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) { MS_TRACE(); @@ -322,7 +322,7 @@ namespace RTC MS_ASSERT(it != this->mapMappedSsrcSpatialLayer.end(), "unknown mappedSsrc"); - int16_t spatialLayer = it->second; + const int16_t spatialLayer = it->second; this->producerRtpStreams[spatialLayer] = rtpStream; @@ -330,11 +330,13 @@ namespace RTC EmitScore(); if (IsActive()) + { MayChangeLayers(); + } } void SimulcastConsumer::ProducerRtpStreamScore( - RTC::RtpStream* /*rtpStream*/, uint8_t score, uint8_t previousScore) + RTC::RtpStreamRecv* /*rtpStream*/, uint8_t score, uint8_t previousScore) { MS_TRACE(); @@ -343,9 +345,14 @@ namespace RTC if (RTC::Consumer::IsActive()) { + // All Producer streams are dead. + if (!IsActive()) + { + UpdateTargetLayers(-1, -1); + } // Just check target layers if the stream has died or reborned. // clang-format off - if ( + else if ( !this->externallyManagedBitrate || (score == 0u || previousScore == 0u) ) @@ -356,25 +363,31 @@ namespace RTC } } - void SimulcastConsumer::ProducerRtcpSenderReport(RTC::RtpStream* rtpStream, bool first) + void SimulcastConsumer::ProducerRtcpSenderReport(RTC::RtpStreamRecv* rtpStream, bool first) { MS_TRACE(); // Just interested if this is the first Sender Report for a RTP stream. if (!first) + { return; + } MS_DEBUG_TAG(simulcast, "first SenderReport [ssrc:%" PRIu32 "]", rtpStream->GetSsrc()); - // If our current selected RTP stream does not yet have SR, do nothing since - // we know we won't be able to switch. - auto* producerCurrentRtpStream = GetProducerCurrentRtpStream(); + // If our RTP timestamp reference stream does not yet have SR, do nothing + // since we know we won't be able to switch. + auto* producerTsReferenceRtpStream = GetProducerTsReferenceRtpStream(); - if (!producerCurrentRtpStream || !producerCurrentRtpStream->GetSenderReportNtpMs()) + if (!producerTsReferenceRtpStream || !producerTsReferenceRtpStream->GetSenderReportNtpMs()) + { return; + } if (IsActive()) + { MayChangeLayers(); + } } uint8_t SimulcastConsumer::GetBitratePriority() const @@ -384,7 +397,9 @@ namespace RTC MS_ASSERT(this->externallyManagedBitrate, "bitrate is not externally managed"); if (!IsActive()) + { return 0u; + } return this->priority; } @@ -416,11 +431,17 @@ namespace RTC auto lossPercentage = this->rtpStream->GetLossPercentage(); if (lossPercentage < 2) + { virtualBitrate = 1.08 * bitrate; + } else if (lossPercentage > 10) + { virtualBitrate = (1 - 0.5 * (lossPercentage / 100)) * bitrate; + } else + { virtualBitrate = bitrate; + } } else { @@ -451,14 +472,24 @@ namespace RTC // Ignore spatial layers lower than the one we already have. if (spatialLayer < this->provisionalTargetSpatialLayer) + { continue; + } // This can be null. auto* producerRtpStream = this->producerRtpStreams.at(spatialLayer); // Producer stream does not exist. Ignore. if (!producerRtpStream) + { continue; + } + + // Ignore spatial layers (streams) with score 0. + if (producerRtpStream->GetScore() == 0) + { + continue; + } // If the stream has not been active time enough and we have an active one // already, move to the next spatial layer. @@ -476,12 +507,16 @@ namespace RTC // The stream for the current provisional spatial layer has been active // for enough time, move to the next spatial layer. if (provisionalProducerRtpStream->GetActiveMs() >= StreamMinActiveMs) + { continue; + } } // We may not yet switch to this spatial layer. if (!CanSwitchToSpatialLayer(spatialLayer)) + { continue; + } temporalLayer = 0; @@ -520,9 +555,13 @@ namespace RTC provisionalProducerRtpStream->GetBitrate(nowMs, 0, this->provisionalTargetTemporalLayer); if (requiredBitrate > provisionalRequiredBitrate) + { requiredBitrate -= provisionalRequiredBitrate; + } else + { requiredBitrate = 1u; // Don't set 0 since it would be ignored. + } } MS_DEBUG_DEV( @@ -535,25 +574,35 @@ namespace RTC // If active layer, end iterations here. Otherwise move to next spatial layer. if (requiredBitrate) + { goto done; + } else + { break; + } } // If this is the preferred or higher spatial layer, take it and exit. if (spatialLayer >= this->preferredSpatialLayer) + { break; + } } done: // No higher active layers found. if (!requiredBitrate) + { return 0u; + } // No luck. if (requiredBitrate > virtualBitrate) + { return 0u; + } // Set provisional layers. this->provisionalTargetSpatialLayer = spatialLayer; @@ -568,11 +617,17 @@ namespace RTC requiredBitrate); if (requiredBitrate <= bitrate) + { return requiredBitrate; + } else if (requiredBitrate <= virtualBitrate) + { return bitrate; + } else + { return requiredBitrate; // NOTE: This cannot happen. + } } void SimulcastConsumer::ApplyLayers() @@ -625,7 +680,9 @@ namespace RTC MS_ASSERT(this->externallyManagedBitrate, "bitrate is not externally managed"); if (!IsActive()) + { return 0u; + } auto nowMs = DepLibUV::GetTimeMs(); uint32_t desiredBitrate{ 0u }; @@ -640,12 +697,16 @@ namespace RTC auto* producerRtpStream = this->producerRtpStreams.at(sIdx); if (!producerRtpStream) + { continue; + } auto streamBitrate = producerRtpStream->GetBitrate(nowMs); if (streamBitrate > desiredBitrate) + { desiredBitrate = streamBitrate; + } } // If consumer.rtpParameters.encodings[0].maxBitrate was given and it's @@ -653,7 +714,9 @@ namespace RTC auto maxBitrate = this->rtpParameters.encodings[0].maxBitrate; if (maxBitrate > desiredBitrate) + { desiredBitrate = maxBitrate; + } return desiredBitrate; } @@ -663,11 +726,27 @@ namespace RTC { MS_TRACE(); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.consumerId = this->id; +#endif + if (!IsActive()) + { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::CONSUMER_INACTIVE); +#endif + return; + } if (this->targetTemporalLayer == -1) + { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::INVALID_TARGET_LAYER); +#endif + return; + } auto payloadType = packet->GetPayloadType(); @@ -677,6 +756,10 @@ namespace RTC { MS_DEBUG_DEV("payload type not supported [payloadType:%" PRIu8 "]", payloadType); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::UNSUPPORTED_PAYLOAD_TYPE); +#endif + return; } @@ -689,7 +772,13 @@ namespace RTC { // Ignore if not a key frame. if (!packet->IsKeyFrame()) + { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::NOT_A_KEYFRAME); +#endif + return; + } shouldSwitchCurrentSpatialLayer = true; @@ -701,12 +790,35 @@ namespace RTC // drop it. else if (spatialLayer != this->currentSpatialLayer) { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::SPATIAL_LAYER_MISMATCH); +#endif + return; } // If we need to sync and this is not a key frame, ignore the packet. if (this->syncRequired && !packet->IsKeyFrame()) + { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::NOT_A_KEYFRAME); +#endif + + return; + } + + // If the packet belongs to current spatial layer being sent and packet does + // not have payload other than padding, then drop it. + if (spatialLayer == this->currentSpatialLayer && packet->GetPayloadLength() == 0) + { + this->rtpSeqManager->Drop(packet->GetSequenceNumber()); + +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::EMPTY_PAYLOAD); +#endif + return; + } // Whether this is the first packet after re-sync. const bool isSyncPacket = this->syncRequired; @@ -715,7 +827,9 @@ namespace RTC if (isSyncPacket && (this->spatialLayerToSync == -1 || this->spatialLayerToSync == spatialLayer)) { if (packet->IsKeyFrame()) + { MS_DEBUG_TAG(rtp, "sync key frame received"); + } uint32_t tsOffset{ 0u }; @@ -746,11 +860,15 @@ namespace RTC int64_t diffMs; if (ntpMs2 >= ntpMs1) + { diffMs = ntpMs2 - ntpMs1; + } else + { diffMs = -1 * (ntpMs1 - ntpMs2); + } - int64_t diffTs = diffMs * this->rtpStream->GetClockRate() / 1000; + const int64_t diffTs = diffMs * this->rtpStream->GetClockRate() / 1000; const uint32_t newTs2 = ts2 - diffTs; // Apply offset. This is the difference that later must be removed from the @@ -778,8 +896,8 @@ namespace RTC // Apply an expected offset for a new frame in a 30fps stream. static const uint8_t MsOffset{ 33u }; // (1 / 30 * 1000). - int64_t maxTsExtraOffset = MaxExtraOffsetMs * this->rtpStream->GetClockRate() / 1000; - uint32_t tsExtraOffset = this->rtpStream->GetMaxPacketTs() - packet->GetTimestamp() + + const int64_t maxTsExtraOffset = MaxExtraOffsetMs * this->rtpStream->GetClockRate() / 1000; + uint32_t tsExtraOffset = this->rtpStream->GetMaxPacketTs() - packet->GetTimestamp() + tsOffset + MsOffset * this->rtpStream->GetClockRate() / 1000; // NOTE: Don't ask for a key frame if already done. @@ -813,6 +931,10 @@ namespace RTC this->syncRequired = false; this->spatialLayerToSync = -1; +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::TOO_HIGH_TIMESTAMP_EXTRA_NEEDED); +#endif + return; } @@ -838,7 +960,7 @@ namespace RTC // 'packet->GetSequenceNumber() -2' may increase SeqManager::base and // increase the output sequence number. // https://github.com/versatica/mediasoup/issues/408 - this->rtpSeqManager.Sync(packet->GetSequenceNumber() - (this->lastSentPacketHasMarker ? 1 : 2)); + this->rtpSeqManager->Sync(packet->GetSequenceNumber() - (this->lastSentPacketHasMarker ? 1 : 2)); this->encodingContext->SyncRequired(); @@ -853,6 +975,11 @@ namespace RTC if (SeqManager::IsSeqLowerThan( packet->GetSequenceNumber(), this->snReferenceSpatialLayer)) { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped( + RtcLogger::RtpPacket::DropReason::PACKET_PREVIOUS_TO_SPATIAL_LAYER_SWITCH); +#endif + return; } else if (SeqManager::IsSeqHigherThan( @@ -895,20 +1022,26 @@ namespace RTC // Rewrite payload if needed. Drop packet if necessary. if (!packet->ProcessPayload(this->encodingContext.get(), marker)) { - this->rtpSeqManager.Drop(packet->GetSequenceNumber()); + this->rtpSeqManager->Drop(packet->GetSequenceNumber()); + +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::DROPPED_BY_CODEC); +#endif return; } if (previousTemporalLayer != this->encodingContext->GetCurrentTemporalLayer()) + { EmitLayersChange(); + } } // Update RTP seq number and timestamp based on NTP offset. uint16_t seq; const uint32_t timestamp = packet->GetTimestamp() - this->tsOffset; - this->rtpSeqManager.Input(packet->GetSequenceNumber(), seq); + this->rtpSeqManager->Input(packet->GetSequenceNumber(), seq); // Save original packet fields. auto origSsrc = packet->GetSsrc(); @@ -920,6 +1053,11 @@ namespace RTC packet->SetSequenceNumber(seq); packet->SetTimestamp(timestamp); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.sendRtpTimestamp = timestamp; + packet->logger.sendSeqNumber = seq; +#endif + if (isSyncPacket) { MS_DEBUG_TAG( @@ -937,8 +1075,10 @@ namespace RTC // Process the packet. if (this->rtpStream->ReceivePacket(packet, sharedPacket)) { - if (this->rtpSeqManager.GetMaxOutput() == packet->GetSequenceNumber()) + if (this->rtpSeqManager->GetMaxOutput() == packet->GetSequenceNumber()) + { this->lastSentPacketHasMarker = packet->HasMarker(); + } // Send the packet. this->listener->OnConsumerSendRtpPacket(this, packet); @@ -958,6 +1098,10 @@ namespace RTC origSsrc, origSeq, origTimestamp); + +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::SEND_RTP_STREAM_DISCARDED); +#endif } // Restore packet fields. @@ -974,29 +1118,27 @@ namespace RTC MS_TRACE(); if (static_cast((nowMs - this->lastRtcpSentTime) * 1.15) < this->maxRtcpInterval) + { return true; + } auto* senderReport = this->rtpStream->GetRtcpSenderReport(nowMs); if (!senderReport) + { return true; + } // Build SDES chunk for this sender. auto* sdesChunk = this->rtpStream->GetRtcpSdesChunk(); - RTC::RTCP::DelaySinceLastRr* delaySinceLastRrReport{ nullptr }; - - auto* dlrr = this->rtpStream->GetRtcpXrDelaySinceLastRr(nowMs); - - if (dlrr) - { - delaySinceLastRrReport = new RTC::RTCP::DelaySinceLastRr(); - delaySinceLastRrReport->AddSsrcInfo(dlrr); - } + auto* delaySinceLastRrSsrcInfo = this->rtpStream->GetRtcpXrDelaySinceLastRrSsrcInfo(nowMs); // RTCP Compound packet buffer cannot hold the data. - if (!packet->Add(senderReport, sdesChunk, delaySinceLastRrReport)) + if (!packet->Add(senderReport, sdesChunk, delaySinceLastRrSsrcInfo)) + { return false; + } this->lastRtcpSentTime = nowMs; @@ -1009,13 +1151,17 @@ namespace RTC MS_TRACE(); if (!IsActive()) + { return; + } auto fractionLost = this->rtpStream->GetFractionLost(); // If our fraction lost is worse than the given one, update it. if (fractionLost > worstRemoteFractionLost) + { worstRemoteFractionLost = fractionLost; + } } void SimulcastConsumer::ReceiveNack(RTC::RTCP::FeedbackRtpNackPacket* nackPacket) @@ -1023,7 +1169,9 @@ namespace RTC MS_TRACE(); if (!IsActive()) + { return; + } // May emit 'trace' event. EmitTraceEventNackType(); @@ -1058,7 +1206,9 @@ namespace RTC this->rtpStream->ReceiveKeyFrameRequest(messageType); if (IsActive()) + { RequestKeyFrameForCurrentSpatialLayer(); + } } void SimulcastConsumer::ReceiveRtcpReceiverReport(RTC::RTCP::ReceiverReport* report) @@ -1080,7 +1230,9 @@ namespace RTC MS_TRACE(); if (!IsActive()) + { return 0u; + } return this->rtpStream->GetBitrate(nowMs); } @@ -1101,7 +1253,9 @@ namespace RTC this->keyFrameForTsOffsetRequested = false; if (IsActive()) + { MayChangeLayers(); + } } void SimulcastConsumer::UserOnTransportDisconnected() @@ -1126,7 +1280,9 @@ namespace RTC UpdateTargetLayers(-1, -1); if (this->externallyManagedBitrate) + { this->listener->OnConsumerNeedZeroBitrate(this); + } } void SimulcastConsumer::UserOnResumed() @@ -1139,7 +1295,9 @@ namespace RTC this->checkingForOldPacketsInSpatialLayer = false; if (IsActive()) + { MayChangeLayers(); + } } void SimulcastConsumer::CreateRtpStream() @@ -1214,12 +1372,16 @@ namespace RTC // If the Consumer is paused, tell the RtpStreamSend. if (IsPaused() || IsProducerPaused()) + { this->rtpStream->Pause(); + } const auto* rtxCodec = this->rtpParameters.GetRtxCodecForEncoding(encoding); if (rtxCodec && encoding.hasRtx) + { this->rtpStream->SetRtx(rtxCodec->payloadType, encoding.rtx.ssrc); + } } void SimulcastConsumer::RequestKeyFrames() @@ -1227,7 +1389,9 @@ namespace RTC MS_TRACE(); if (this->kind != RTC::Media::Kind::VIDEO) + { return; + } auto* producerTargetRtpStream = GetProducerTargetRtpStream(); auto* producerCurrentRtpStream = GetProducerCurrentRtpStream(); @@ -1252,12 +1416,16 @@ namespace RTC MS_TRACE(); if (this->kind != RTC::Media::Kind::VIDEO) + { return; + } auto* producerTargetRtpStream = GetProducerTargetRtpStream(); if (!producerTargetRtpStream) + { return; + } auto mappedSsrc = this->consumableRtpEncodings[this->targetSpatialLayer].ssrc; @@ -1269,12 +1437,16 @@ namespace RTC MS_TRACE(); if (this->kind != RTC::Media::Kind::VIDEO) + { return; + } auto* producerCurrentRtpStream = GetProducerCurrentRtpStream(); if (!producerCurrentRtpStream) + { return; + } auto mappedSsrc = this->consumableRtpEncodings[this->currentSpatialLayer].ssrc; @@ -1298,7 +1470,9 @@ namespace RTC if (this->externallyManagedBitrate) { if (newTargetSpatialLayer != this->targetSpatialLayer || force) + { this->listener->OnConsumerNeedBitrateChange(this); + } } else { @@ -1329,13 +1503,17 @@ namespace RTC if (nowMs - this->lastBweDowngradeAtMs < BweDowngradeConservativeMs) { if (newTargetSpatialLayer > -1 && spatialLayer > this->currentSpatialLayer) + { continue; + } } // Ignore spatial layers for non existing Producer streams or for those // with score 0. if (producerScore == 0u) + { continue; + } // If the stream has not been active time enough and we have an active one // already, move to the next spatial layer. @@ -1353,23 +1531,33 @@ namespace RTC // We may not yet switch to this spatial layer. if (!CanSwitchToSpatialLayer(spatialLayer)) + { continue; + } newTargetSpatialLayer = spatialLayer; // If this is the preferred or higher spatial layer take it and exit. if (spatialLayer >= this->preferredSpatialLayer) + { break; + } } if (newTargetSpatialLayer != -1) { if (newTargetSpatialLayer == this->preferredSpatialLayer) + { newTargetTemporalLayer = this->preferredTemporalLayer; + } else if (newTargetSpatialLayer < this->preferredSpatialLayer) + { newTargetTemporalLayer = this->rtpStream->GetTemporalLayers() - 1; + } else + { newTargetTemporalLayer = 0; + } } // Return true if any target layer changed. @@ -1386,7 +1574,9 @@ namespace RTC MS_TRACE(); // If we don't have yet a RTP timestamp reference, set it now. - if (newTargetSpatialLayer != -1 && this->tsReferenceSpatialLayer == -1) + if ( + newTargetSpatialLayer != -1 && (this->tsReferenceSpatialLayer == -1 || + !GetProducerTsReferenceRtpStream()->GetSenderReportNtpMs())) { MS_DEBUG_TAG( simulcast, "using spatial layer %" PRIi16 " as RTP timestamp reference", newTargetSpatialLayer); @@ -1418,7 +1608,9 @@ namespace RTC // If the new target spatial layer matches the current one, apply the new // target temporal layer now. if (this->targetSpatialLayer == this->currentSpatialLayer) + { this->encodingContext->SetTargetTemporalLayer(this->targetTemporalLayer); + } MS_DEBUG_TAG( simulcast, @@ -1430,7 +1622,9 @@ namespace RTC // If the target spatial layer is different than the current one, request // a key frame. if (this->targetSpatialLayer != this->currentSpatialLayer) + { RequestKeyFrameForTargetSpatialLayer(); + } } inline bool SimulcastConsumer::CanSwitchToSpatialLayer(int16_t spatialLayer) const @@ -1454,10 +1648,7 @@ namespace RTC return ( this->tsReferenceSpatialLayer == -1 || spatialLayer == this->tsReferenceSpatialLayer || - ( - GetProducerTsReferenceRtpStream()->GetSenderReportNtpMs() && - this->producerRtpStreams.at(spatialLayer)->GetSenderReportNtpMs() - ) + this->producerRtpStreams.at(spatialLayer)->GetSenderReportNtpMs() ); // clang-format on } @@ -1466,11 +1657,16 @@ namespace RTC { MS_TRACE(); - json data = json::object(); + auto scoreOffset = FillBufferScore(this->shared->channelNotifier->GetBufferBuilder()); - FillJsonScore(data); + auto notificationOffset = FBS::Consumer::CreateScoreNotification( + this->shared->channelNotifier->GetBufferBuilder(), scoreOffset); - this->shared->channelNotifier->Emit(this->id, "score", data); + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::CONSUMER_SCORE, + FBS::Notification::Body::Consumer_ScoreNotification, + notificationOffset); } inline void SimulcastConsumer::EmitLayersChange() const @@ -1483,49 +1679,60 @@ namespace RTC this->encodingContext->GetCurrentTemporalLayer(), this->id.c_str()); - json data = json::object(); + flatbuffers::Offset layersOffset; if (this->currentSpatialLayer >= 0) { - data["spatialLayer"] = this->currentSpatialLayer; - data["temporalLayer"] = this->encodingContext->GetCurrentTemporalLayer(); - } - else - { - data = nullptr; + layersOffset = FBS::Consumer::CreateConsumerLayers( + this->shared->channelNotifier->GetBufferBuilder(), + this->currentSpatialLayer, + this->encodingContext->GetCurrentTemporalLayer()); } - this->shared->channelNotifier->Emit(this->id, "layerschange", data); + auto notificationOffset = FBS::Consumer::CreateLayersChangeNotification( + this->shared->channelNotifier->GetBufferBuilder(), layersOffset); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::CONSUMER_LAYERS_CHANGE, + FBS::Notification::Body::Consumer_LayersChangeNotification, + notificationOffset); } - inline RTC::RtpStream* SimulcastConsumer::GetProducerCurrentRtpStream() const + inline RTC::RtpStreamRecv* SimulcastConsumer::GetProducerCurrentRtpStream() const { MS_TRACE(); if (this->currentSpatialLayer == -1) + { return nullptr; + } // This may return nullptr. return this->producerRtpStreams.at(this->currentSpatialLayer); } - inline RTC::RtpStream* SimulcastConsumer::GetProducerTargetRtpStream() const + inline RTC::RtpStreamRecv* SimulcastConsumer::GetProducerTargetRtpStream() const { MS_TRACE(); if (this->targetSpatialLayer == -1) + { return nullptr; + } // This may return nullptr. return this->producerRtpStreams.at(this->targetSpatialLayer); } - inline RTC::RtpStream* SimulcastConsumer::GetProducerTsReferenceRtpStream() const + inline RTC::RtpStreamRecv* SimulcastConsumer::GetProducerTsReferenceRtpStream() const { MS_TRACE(); if (this->tsReferenceSpatialLayer == -1) + { return nullptr; + } // This may return nullptr. return this->producerRtpStreams.at(this->tsReferenceSpatialLayer); @@ -1545,7 +1752,9 @@ namespace RTC // NOTE: For now this is a bit useless since, when locally managed, we do // not check the Consumer score at all. if (!this->externallyManagedBitrate) + { MayChangeLayers(); + } } } diff --git a/worker/src/RTC/SrtpSession.cpp b/worker/src/RTC/SrtpSession.cpp index 97051e0cfd..43eb597c15 100644 --- a/worker/src/RTC/SrtpSession.cpp +++ b/worker/src/RTC/SrtpSession.cpp @@ -3,6 +3,9 @@ #include "RTC/SrtpSession.hpp" #include "DepLibSRTP.hpp" +#ifdef MS_LIBURING_SUPPORTED +#include "DepLibUring.hpp" +#endif #include "Logger.hpp" #include "MediaSoupErrors.hpp" #include // std::memset(), std::memcpy() @@ -28,6 +31,42 @@ namespace RTC } } + FBS::SrtpParameters::SrtpCryptoSuite SrtpSession::CryptoSuiteToFbs(CryptoSuite cryptoSuite) + { + switch (cryptoSuite) + { + case SrtpSession::CryptoSuite::AEAD_AES_256_GCM: + return FBS::SrtpParameters::SrtpCryptoSuite::AEAD_AES_256_GCM; + + case SrtpSession::CryptoSuite::AEAD_AES_128_GCM: + return FBS::SrtpParameters::SrtpCryptoSuite::AEAD_AES_128_GCM; + + case SrtpSession::CryptoSuite::AES_CM_128_HMAC_SHA1_80: + return FBS::SrtpParameters::SrtpCryptoSuite::AES_CM_128_HMAC_SHA1_80; + + case SrtpSession::CryptoSuite::AES_CM_128_HMAC_SHA1_32: + return FBS::SrtpParameters::SrtpCryptoSuite::AES_CM_128_HMAC_SHA1_32; + } + } + + SrtpSession::CryptoSuite SrtpSession::CryptoSuiteFromFbs(FBS::SrtpParameters::SrtpCryptoSuite cryptoSuite) + { + switch (cryptoSuite) + { + case FBS::SrtpParameters::SrtpCryptoSuite::AEAD_AES_256_GCM: + return SrtpSession::CryptoSuite::AEAD_AES_256_GCM; + + case FBS::SrtpParameters::SrtpCryptoSuite::AEAD_AES_128_GCM: + return SrtpSession::CryptoSuite::AEAD_AES_128_GCM; + + case FBS::SrtpParameters::SrtpCryptoSuite::AES_CM_128_HMAC_SHA1_80: + return SrtpSession::CryptoSuite::AES_CM_128_HMAC_SHA1_80; + + case FBS::SrtpParameters::SrtpCryptoSuite::AES_CM_128_HMAC_SHA1_32: + return SrtpSession::CryptoSuite::AES_CM_128_HMAC_SHA1_32; + } + } + void SrtpSession::OnSrtpEvent(srtp_event_data_t* data) { MS_TRACE(); @@ -130,7 +169,9 @@ namespace RTC const srtp_err_status_t err = srtp_create(&this->session, &policy); if (DepLibSRTP::IsError(err)) + { MS_THROW_ERROR("srtp_create() failed: %s", DepLibSRTP::GetErrorString(err)); + } } SrtpSession::~SrtpSession() @@ -142,25 +183,49 @@ namespace RTC const srtp_err_status_t err = srtp_dealloc(this->session); if (DepLibSRTP::IsError(err)) + { MS_ABORT("srtp_dealloc() failed: %s", DepLibSRTP::GetErrorString(err)); + } } } - bool SrtpSession::EncryptRtp(const uint8_t** data, int* len) + bool SrtpSession::EncryptRtp(const uint8_t** data, size_t* len) { MS_TRACE(); // Ensure that the resulting SRTP packet fits into the encrypt buffer. - if (static_cast(*len) + SRTP_MAX_TRAILER_LEN > EncryptBufferSize) + if (*len + SRTP_MAX_TRAILER_LEN > EncryptBufferSize) { - MS_WARN_TAG(srtp, "cannot encrypt RTP packet, size too big (%i bytes)", *len); + MS_WARN_TAG(srtp, "cannot encrypt RTP packet, size too big (%zu bytes)", *len); return false; } - std::memcpy(EncryptBuffer, *data, *len); + uint8_t* encryptBuffer = EncryptBuffer; + +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + if (!DepLibUring::IsActive()) + { + goto protect; + } - const srtp_err_status_t err = srtp_protect(this->session, static_cast(EncryptBuffer), len); + // Use a preallocated buffer, if available. + auto* sendBuffer = DepLibUring::GetSendBuffer(); + + if (sendBuffer) + { + encryptBuffer = sendBuffer; + } + } + + protect: +#endif + + std::memcpy(encryptBuffer, *data, *len); + + const srtp_err_status_t err = srtp_protect(this->session, encryptBuffer, len); if (DepLibSRTP::IsError(err)) { @@ -170,16 +235,16 @@ namespace RTC } // Update the given data pointer. - *data = (const uint8_t*)EncryptBuffer; + *data = const_cast(encryptBuffer); return true; } - bool SrtpSession::DecryptSrtp(uint8_t* data, int* len) + bool SrtpSession::DecryptSrtp(uint8_t* data, size_t* len) { MS_TRACE(); - const srtp_err_status_t err = srtp_unprotect(this->session, static_cast(data), len); + const srtp_err_status_t err = srtp_unprotect(this->session, data, len); if (DepLibSRTP::IsError(err)) { @@ -191,22 +256,21 @@ namespace RTC return true; } - bool SrtpSession::EncryptRtcp(const uint8_t** data, int* len) + bool SrtpSession::EncryptRtcp(const uint8_t** data, size_t* len) { MS_TRACE(); // Ensure that the resulting SRTCP packet fits into the encrypt buffer. - if (static_cast(*len) + SRTP_MAX_TRAILER_LEN > EncryptBufferSize) + if (*len + SRTP_MAX_TRAILER_LEN > EncryptBufferSize) { - MS_WARN_TAG(srtp, "cannot encrypt RTCP packet, size too big (%i bytes)", *len); + MS_WARN_TAG(srtp, "cannot encrypt RTCP packet, size too big (%zu bytes)", *len); return false; } std::memcpy(EncryptBuffer, *data, *len); - const srtp_err_status_t err = - srtp_protect_rtcp(this->session, static_cast(EncryptBuffer), len); + const srtp_err_status_t err = srtp_protect_rtcp(this->session, EncryptBuffer, len); if (DepLibSRTP::IsError(err)) { @@ -221,11 +285,11 @@ namespace RTC return true; } - bool SrtpSession::DecryptSrtcp(uint8_t* data, int* len) + bool SrtpSession::DecryptSrtcp(uint8_t* data, size_t* len) { MS_TRACE(); - const srtp_err_status_t err = srtp_unprotect_rtcp(this->session, static_cast(data), len); + const srtp_err_status_t err = srtp_unprotect_rtcp(this->session, data, len); if (DepLibSRTP::IsError(err)) { diff --git a/worker/src/RTC/StunPacket.cpp b/worker/src/RTC/StunPacket.cpp index 6a453bca24..518f8d8789 100644 --- a/worker/src/RTC/StunPacket.cpp +++ b/worker/src/RTC/StunPacket.cpp @@ -11,7 +11,7 @@ namespace RTC { /* Class variables. */ - const uint8_t StunPacket::magicCookie[] = { 0x21, 0x12, 0xA4, 0x42 }; + const uint8_t StunPacket::MagicCookie[] = { 0x21, 0x12, 0xA4, 0x42 }; /* Class methods. */ @@ -20,25 +20,25 @@ namespace RTC MS_TRACE(); if (!StunPacket::IsStun(data, len)) + { return nullptr; + } - /* - The message type field is decomposed further into the following - structure: - - 0 1 - 2 3 4 5 6 7 8 9 0 1 2 3 4 5 - +--+--+-+-+-+-+-+-+-+-+-+-+-+-+ - |M |M |M|M|M|C|M|M|M|C|M|M|M|M| - |11|10|9|8|7|1|6|5|4|0|3|2|1|0| - +--+--+-+-+-+-+-+-+-+-+-+-+-+-+ - - Figure 3: Format of STUN Message Type Field - - Here the bits in the message type field are shown as most significant - (M11) through least significant (M0). M11 through M0 represent a 12- - bit encoding of the method. C1 and C0 represent a 2-bit encoding of - the class. + /** + * The message type field is decomposed further into the following + * structure: + * + * 0 1 + * 2 3 4 5 6 7 8 9 0 1 2 3 4 5 + * +--+--+-+-+-+-+-+-+-+-+-+-+-+-+ + * |M |M |M|M|M|C|M|M|M|C|M|M|M|M| + * |11|10|9|8|7|1|6|5|4|0|3|2|1|0| + * +--+--+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * Here the bits in the message type field are shown as most significant + * (M11) through least significant (M0). M11 through M0 represent a 12-bit + * encoding of the method. C1 and C0 represent a 2-bit encoding of the + * class. */ // Get type field. @@ -47,7 +47,8 @@ namespace RTC // Get length field. const uint16_t msgLength = Utils::Byte::Get2Bytes(data, 2); - // length field must be total size minus header's 20 bytes, and must be multiple of 4 Bytes. + // length field must be total size minus header's 20 bytes, and must be + // multiple of 4 Bytes. if ((static_cast(msgLength) != len - 20) || ((msgLength & 0x03) != 0)) { MS_WARN_TAG( @@ -65,35 +66,38 @@ namespace RTC // Get STUN class. const uint16_t msgClass = ((data[0] & 0x01) << 1) | ((data[1] & 0x10) >> 4); - // Create a new StunPacket (data + 8 points to the received TransactionID field). + // Create a new StunPacket (data + 8 points to the received TransactionID + // field). auto* packet = new StunPacket( static_cast(msgClass), static_cast(msgMethod), data + 8, data, len); - /* - STUN Attributes - - After the STUN header are zero or more attributes. Each attribute - MUST be TLV encoded, with a 16-bit type, 16-bit length, and value. - Each STUN attribute MUST end on a 32-bit boundary. As mentioned - above, all fields in an attribute are transmitted most significant - bit first. - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Value (variable) .... - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /** + * STUN Attributes + * + * After the STUN header are zero or more attributes. Each attribute MUST + * be TLV encoded, with a 16-bit type, 16-bit length, and value. Each STUN + * attribute MUST end on a 32-bit boundary. As mentioned above, all fields + * in an attribute are transmitted most significant bit first. + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Type | Length | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Value (variable) .... + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ // Start looking for attributes after STUN header (Byte #20). size_t pos{ 20 }; - // Flags (positions) for special MESSAGE-INTEGRITY and FINGERPRINT attributes. + // Flags (positions) for special MESSAGE-INTEGRITY and FINGERPRINT + // attributes. bool hasMessageIntegrity{ false }; bool hasFingerprint{ false }; - size_t fingerprintAttrPos; // Will point to the beginning of the attribute. - uint32_t fingerprint; // Holds the value of the FINGERPRINT attribute. + // Will point to the beginning of the attribute. + size_t fingerprintAttrPos; + // Holds the value of the FINGERPRINT attribute. + uint32_t fingerprint; // Ensure there are at least 4 remaining bytes (attribute with 0 length). while (pos + 4 <= len) @@ -283,6 +287,24 @@ namespace RTC break; } + case Attribute::SOFTWARE: + { + // Ensure attribute length is less than 763 bytes. + if (attrLength >= 763) + { + MS_WARN_TAG( + ice, "attribute SOFTWARE must be less than 763 bytes length, packet discarded"); + + delete packet; + return nullptr; + } + + packet->SetSoftware( + reinterpret_cast(attrValuePos), static_cast(attrLength)); + + break; + } + default:; } @@ -349,16 +371,16 @@ namespace RTC switch (this->klass) { case Class::REQUEST: - klass = "Request"; + klass = "request"; break; case Class::INDICATION: - klass = "Indication"; + klass = "indication"; break; case Class::SUCCESS_RESPONSE: - klass = "SuccessResponse"; + klass = "success response"; break; case Class::ERROR_RESPONSE: - klass = "ErrorResponse"; + klass = "error response"; break; } if (this->method == Method::BINDING) @@ -372,26 +394,39 @@ namespace RTC } MS_DUMP(" size: %zu bytes", this->size); - char transactionId[25]; + auto transactionId1 = Utils::Byte::Get4Bytes(this->transactionId, 0); + auto transactionId2 = Utils::Byte::Get8Bytes(this->transactionId, 4); - for (int i{ 0 }; i < 12; ++i) - { - // NOTE: n must be 3 because snprintf adds a \0 after printed chars. - std::snprintf(transactionId + (i * 2), 3, "%.2x", this->transactionId[i]); - } - MS_DUMP(" transactionId: %s", transactionId); + MS_DUMP(" transactionId (first 4 bytes): %" PRIu32, transactionId1); + MS_DUMP(" transactionId (last 8 bytes): %" PRIu64, transactionId2); if (this->errorCode != 0u) + { MS_DUMP(" errorCode: %" PRIu16, this->errorCode); + } if (!this->username.empty()) + { MS_DUMP(" username: %s", this->username.c_str()); + } if (this->priority != 0u) + { MS_DUMP(" priority: %" PRIu32, this->priority); + } if (this->iceControlling != 0u) + { MS_DUMP(" iceControlling: %" PRIu64, this->iceControlling); + } if (this->iceControlled != 0u) + { MS_DUMP(" iceControlled: %" PRIu64, this->iceControlled); + } if (this->hasUseCandidate) - MS_DUMP(" useCandidate"); + { + MS_DUMP(" useCandidate: yes"); + } + if (!this->software.empty()) + { + MS_DUMP(" software: %s", this->software.c_str()); + } if (this->xorMappedAddress != nullptr) { int family; @@ -414,13 +449,28 @@ namespace RTC MS_DUMP(" messageIntegrity: %s", messageIntegrity); } if (this->hasFingerprint) - MS_DUMP(" has fingerprint"); + { + MS_DUMP(" fingerprint: yes"); + } MS_DUMP(""); } + void StunPacket::SetPassword(const std::string& password) + { + // Just for request, indication and success response messages. + if (this->klass == Class::ERROR_RESPONSE) + { + MS_ERROR("cannot set password for error responses"); + + return; + } + + this->password = password; + } + StunPacket::Authentication StunPacket::CheckAuthentication( - const std::string& localUsername, const std::string& localPassword) + const std::string& usernameFragment1, const std::string& password) { MS_TRACE(); @@ -429,54 +479,103 @@ namespace RTC case Class::REQUEST: case Class::INDICATION: { - // Both USERNAME and MESSAGE-INTEGRITY must be present. - if (!this->messageIntegrity || this->username.empty()) - return Authentication::BAD_REQUEST; + // usernameFragment1 must be given. + if (usernameFragment1.empty()) + { + MS_WARN_TAG(ice, "usernameFragment1 not given, cannot authenticate request or indication"); - // Check that USERNAME attribute begins with our local username plus ":". - size_t localUsernameLen = localUsername.length(); + return Authentication::BAD_MESSAGE; + } + + // USERNAME attribute must be present. + if (this->username.empty()) + { + MS_WARN_TAG(ice, "missing USERNAME attribute, cannot authenticate request or indication"); + + return Authentication::BAD_MESSAGE; + } + + // MESSAGE-INTEGRITY attribute must be present. + if (!this->messageIntegrity) + { + MS_WARN_TAG( + ice, "missing MESSAGE-INTEGRITY attribute, cannot authenticate request or indication"); + + return Authentication::BAD_MESSAGE; + } + + // Check that the USERNAME attribute begins with the first username + // fragment plus ":". + const size_t usernameFragment1Len = usernameFragment1.length(); if ( - this->username.length() <= localUsernameLen || this->username.at(localUsernameLen) != ':' || - (this->username.compare(0, localUsernameLen, localUsername) != 0)) + this->username.length() <= usernameFragment1Len || + this->username.at(usernameFragment1Len) != ':' || + this->username.compare(0, usernameFragment1Len, usernameFragment1) != 0) { return Authentication::UNAUTHORIZED; } break; } - // This method cannot check authentication in received responses (as we - // are ICE-Lite and don't generate requests). + case Class::SUCCESS_RESPONSE: case Class::ERROR_RESPONSE: { - MS_ERROR("cannot check authentication for a STUN response"); + // MESSAGE-INTEGRITY attribute must be present. + if (!this->messageIntegrity) + { + MS_WARN_TAG(ice, "missing MESSAGE-INTEGRITY attribute, cannot authenticate response"); + + return Authentication::BAD_MESSAGE; + } + + break; + } + + default: + { + MS_WARN_TAG( + ice, + "unknown STUN class %" PRIu16 ", cannot authenticate", + static_cast(this->klass)); - return Authentication::BAD_REQUEST; + return Authentication::BAD_MESSAGE; } } - // If there is FINGERPRINT it must be discarded for MESSAGE-INTEGRITY calculation, - // so the header length field must be modified (and later restored). + // If there is FINGERPRINT it must be discarded for MESSAGE-INTEGRITY + // calculation, so the header length field must be modified (and later + // restored). if (this->hasFingerprint) - // Set the header length field: full size - header length (20) - FINGERPRINT length (8). + { + // Set the header length field: full size - header length (20) - + // FINGERPRINT length (8). Utils::Byte::Set2Bytes(this->data, 2, static_cast(this->size - 20 - 8)); + } - // Calculate the HMAC-SHA1 of the message according to MESSAGE-INTEGRITY rules. - const uint8_t* computedMessageIntegrity = Utils::Crypto::GetHmacSha1( - localPassword, this->data, (this->messageIntegrity - 4) - this->data); + // Calculate the HMAC-SHA1 of the message according to MESSAGE-INTEGRITY + // rules. + const uint8_t* computedMessageIntegrity = + Utils::Crypto::GetHmacSha1(password, this->data, (this->messageIntegrity - 4) - this->data); Authentication result; // Compare the computed HMAC-SHA1 with the MESSAGE-INTEGRITY in the packet. if (std::memcmp(this->messageIntegrity, computedMessageIntegrity, 20) == 0) + { result = Authentication::OK; + } else + { result = Authentication::UNAUTHORIZED; + } // Restore the header length field. if (this->hasFingerprint) + { Utils::Byte::Set2Bytes(this->data, 2, static_cast(this->size - 20)); + } return result; } @@ -487,7 +586,7 @@ namespace RTC MS_ASSERT( this->klass == Class::REQUEST, - "attempt to create a success response for a non Request STUN packet"); + "attempt to create a success response for a non request STUN packet"); return new StunPacket(Class::SUCCESS_RESPONSE, this->method, this->transactionId, nullptr, 0); } @@ -498,7 +597,7 @@ namespace RTC MS_ASSERT( this->klass == Class::REQUEST, - "attempt to create an error response for a non Request STUN packet"); + "attempt to create an error response for a non request STUN packet"); auto* response = new StunPacket(Class::ERROR_RESPONSE, this->method, this->transactionId, nullptr, 0); @@ -508,19 +607,6 @@ namespace RTC return response; } - void StunPacket::Authenticate(const std::string& password) - { - // Just for Request, Indication and SuccessResponse messages. - if (this->klass == Class::ERROR_RESPONSE) - { - MS_ERROR("cannot set password for ErrorResponse messages"); - - return; - } - - this->password = password; - } - void StunPacket::Serialize(uint8_t* buffer) { MS_TRACE(); @@ -531,8 +617,9 @@ namespace RTC bool addXorMappedAddress = ((this->xorMappedAddress != nullptr) && this->method == StunPacket::Method::BINDING && this->klass == Class::SUCCESS_RESPONSE); - bool addErrorCode = ((this->errorCode != 0u) && this->klass == Class::ERROR_RESPONSE); - bool addMessageIntegrity = (this->klass != Class::ERROR_RESPONSE && !this->password.empty()); + const bool addErrorCode = ((this->errorCode != 0u) && this->klass == Class::ERROR_RESPONSE); + const bool addMessageIntegrity = + (this->klass != Class::ERROR_RESPONSE && !this->password.empty()); const bool addFingerprint{ true }; // Do always. // Update data pointer. @@ -548,16 +635,24 @@ namespace RTC } if (this->priority != 0u) + { this->size += 4 + 4; + } if (this->iceControlling != 0u) + { this->size += 4 + 8; + } if (this->iceControlled != 0u) + { this->size += 4 + 8; + } if (this->hasUseCandidate) + { this->size += 4; + } if (addXorMappedAddress) { @@ -589,13 +684,19 @@ namespace RTC } if (addErrorCode) + { this->size += 4 + 4; + } if (addMessageIntegrity) + { this->size += 4 + 20; + } if (addFingerprint) + { this->size += 4 + 4; + } // Merge class and method fields into type. uint16_t typeField = (static_cast(this->method) & 0x0f80) << 2; @@ -610,7 +711,7 @@ namespace RTC // Set length field. Utils::Byte::Set2Bytes(buffer, 2, static_cast(this->size) - 20); // Set magic cookie. - std::memcpy(buffer + 4, StunPacket::magicCookie, 4); + std::memcpy(buffer + 4, StunPacket::MagicCookie, 4); // Set TransactionId field. std::memcpy(buffer + 8, this->transactionId, 12); // Update the transaction ID pointer. @@ -683,17 +784,17 @@ namespace RTC attrValue + 2, &(reinterpret_cast(this->xorMappedAddress))->sin_port, 2); - attrValue[2] ^= StunPacket::magicCookie[0]; - attrValue[3] ^= StunPacket::magicCookie[1]; + attrValue[2] ^= StunPacket::MagicCookie[0]; + attrValue[3] ^= StunPacket::MagicCookie[1]; // Set address and XOR it. std::memcpy( attrValue + 4, &(reinterpret_cast(this->xorMappedAddress))->sin_addr.s_addr, 4); - attrValue[4] ^= StunPacket::magicCookie[0]; - attrValue[5] ^= StunPacket::magicCookie[1]; - attrValue[6] ^= StunPacket::magicCookie[2]; - attrValue[7] ^= StunPacket::magicCookie[3]; + attrValue[4] ^= StunPacket::MagicCookie[0]; + attrValue[5] ^= StunPacket::MagicCookie[1]; + attrValue[6] ^= StunPacket::MagicCookie[2]; + attrValue[7] ^= StunPacket::MagicCookie[3]; pos += 4 + 8; @@ -711,17 +812,17 @@ namespace RTC attrValue + 2, &(reinterpret_cast(this->xorMappedAddress))->sin6_port, 2); - attrValue[2] ^= StunPacket::magicCookie[0]; - attrValue[3] ^= StunPacket::magicCookie[1]; + attrValue[2] ^= StunPacket::MagicCookie[0]; + attrValue[3] ^= StunPacket::MagicCookie[1]; // Set address and XOR it. std::memcpy( attrValue + 4, &(reinterpret_cast(this->xorMappedAddress))->sin6_addr.s6_addr, 16); - attrValue[4] ^= StunPacket::magicCookie[0]; - attrValue[5] ^= StunPacket::magicCookie[1]; - attrValue[6] ^= StunPacket::magicCookie[2]; - attrValue[7] ^= StunPacket::magicCookie[3]; + attrValue[4] ^= StunPacket::MagicCookie[0]; + attrValue[5] ^= StunPacket::MagicCookie[1]; + attrValue[6] ^= StunPacket::MagicCookie[2]; + attrValue[7] ^= StunPacket::MagicCookie[3]; attrValue[8] ^= this->transactionId[0]; attrValue[9] ^= this->transactionId[1]; attrValue[10] ^= this->transactionId[2]; @@ -748,8 +849,8 @@ namespace RTC Utils::Byte::Set2Bytes(buffer, pos, static_cast(Attribute::ERROR_CODE)); Utils::Byte::Set2Bytes(buffer, pos + 2, 4); - auto codeClass = static_cast(this->errorCode / 100); - uint8_t codeNumber = static_cast(this->errorCode) - (codeClass * 100); + auto codeClass = static_cast(this->errorCode / 100); + const uint8_t codeNumber = static_cast(this->errorCode) - (codeClass * 100); Utils::Byte::Set2Bytes(buffer, pos + 4, 0); Utils::Byte::Set1Byte(buffer, pos + 6, codeClass); @@ -762,9 +863,12 @@ namespace RTC { // Ignore FINGERPRINT. if (addFingerprint) + { Utils::Byte::Set2Bytes(buffer, 2, static_cast(this->size - 20 - 8)); + } - // Calculate the HMAC-SHA1 of the packet according to MESSAGE-INTEGRITY rules. + // Calculate the HMAC-SHA1 of the packet according to MESSAGE-INTEGRITY + // rules. const uint8_t* computedMessageIntegrity = Utils::Crypto::GetHmacSha1(this->password, buffer, pos); @@ -778,7 +882,9 @@ namespace RTC // Restore length field. if (addFingerprint) + { Utils::Byte::Set2Bytes(buffer, 2, static_cast(this->size - 20)); + } } else { diff --git a/worker/src/RTC/SvcConsumer.cpp b/worker/src/RTC/SvcConsumer.cpp index f9a84da769..d5fb24d750 100644 --- a/worker/src/RTC/SvcConsumer.cpp +++ b/worker/src/RTC/SvcConsumer.cpp @@ -22,55 +22,42 @@ namespace RTC const std::string& id, const std::string& producerId, RTC::Consumer::Listener* listener, - json& data) + const FBS::Transport::ConsumeRequest* data) : RTC::Consumer::Consumer(shared, id, producerId, listener, data, RTC::RtpParameters::Type::SVC) { MS_TRACE(); // Ensure there is a single encoding. if (this->consumableRtpEncodings.size() != 1u) + { MS_THROW_TYPE_ERROR("invalid consumableRtpEncodings with size != 1"); + } auto& encoding = this->rtpParameters.encodings[0]; // Ensure there are multiple spatial or temporal layers. if (encoding.spatialLayers < 2u && encoding.temporalLayers < 2u) + { MS_THROW_TYPE_ERROR("invalid number of layers"); - - auto jsonPreferredLayersIt = data.find("preferredLayers"); + } // Set preferredLayers (if given). - if (jsonPreferredLayersIt != data.end() && jsonPreferredLayersIt->is_object()) + if (flatbuffers::IsFieldPresent(data, FBS::Transport::ConsumeRequest::VT_PREFERREDLAYERS)) { - auto jsonSpatialLayerIt = jsonPreferredLayersIt->find("spatialLayer"); - auto jsonTemporalLayerIt = jsonPreferredLayersIt->find("temporalLayer"); - - // clang-format off - if ( - jsonSpatialLayerIt == jsonPreferredLayersIt->end() || - !Utils::Json::IsPositiveInteger(*jsonSpatialLayerIt) - ) - // clang-format on - { - MS_THROW_TYPE_ERROR("missing preferredLayers.spatialLayer"); - } - - this->preferredSpatialLayer = jsonSpatialLayerIt->get(); + this->preferredSpatialLayer = data->preferredLayers()->spatialLayer(); if (this->preferredSpatialLayer > encoding.spatialLayers - 1) + { this->preferredSpatialLayer = encoding.spatialLayers - 1; + } - // clang-format off - if ( - jsonTemporalLayerIt != jsonPreferredLayersIt->end() && - Utils::Json::IsPositiveInteger(*jsonTemporalLayerIt) - ) - // clang-format on + if (flatbuffers::IsFieldPresent( + data->preferredLayers(), FBS::Consumer::ConsumerLayers::VT_TEMPORALLAYER)) { - this->preferredTemporalLayer = jsonTemporalLayerIt->get(); - if (this->preferredTemporalLayer > encoding.temporalLayers - 1) + { this->preferredTemporalLayer = encoding.temporalLayers - 1; + } } else { @@ -93,6 +80,14 @@ namespace RTC MS_THROW_TYPE_ERROR("%s codec not supported for svc", mediaCodec->mimeType.ToString().c_str()); } + // Let's chosee an initial output seq number between 1000 and 32768 to avoid + // libsrtp bug: + // https://github.com/versatica/mediasoup/issues/1437 + const uint16_t initialOutputSeq = + Utils::Crypto::GetRandomUInt(1000u, std::numeric_limits::max() / 2); + + this->rtpSeqManager.reset(new RTC::SeqManager(initialOutputSeq)); + RTC::Codecs::EncodingContext::Params params; params.spatialLayers = encoding.spatialLayers; @@ -110,8 +105,7 @@ namespace RTC this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ nullptr, - /*payloadChannelNotificationHandler*/ nullptr); + /*channelNotificationHandler*/ nullptr); } SvcConsumer::~SvcConsumer() @@ -123,119 +117,124 @@ namespace RTC delete this->rtpStream; } - void SvcConsumer::FillJson(json& jsonObject) const + flatbuffers::Offset SvcConsumer::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); // Call the parent method. - RTC::Consumer::FillJson(jsonObject); - + auto base = RTC::Consumer::FillBuffer(builder); // Add rtpStream. - this->rtpStream->FillJson(jsonObject["rtpStream"]); - - // Add preferredSpatialLayer. - jsonObject["preferredSpatialLayer"] = this->preferredSpatialLayer; - - // Add targetSpatialLayer. - jsonObject["targetSpatialLayer"] = this->encodingContext->GetTargetSpatialLayer(); - - // Add currentSpatialLayer. - jsonObject["currentSpatialLayer"] = this->encodingContext->GetCurrentSpatialLayer(); - - // Add preferredTemporalLayer. - jsonObject["preferredTemporalLayer"] = this->preferredTemporalLayer; - - // Add targetTemporalLayer. - jsonObject["targetTemporalLayer"] = this->encodingContext->GetTargetTemporalLayer(); + std::vector> rtpStreams; + rtpStreams.emplace_back(this->rtpStream->FillBuffer(builder)); + + auto dump = FBS::Consumer::CreateConsumerDumpDirect( + builder, + base, + &rtpStreams, + this->preferredSpatialLayer, + this->encodingContext->GetTargetSpatialLayer(), + this->encodingContext->GetCurrentSpatialLayer(), + this->preferredTemporalLayer, + this->encodingContext->GetTargetTemporalLayer(), + this->encodingContext->GetCurrentTemporalLayer()); - // Add currentTemporalLayer. - jsonObject["currentTemporalLayer"] = this->encodingContext->GetCurrentTemporalLayer(); + return FBS::Consumer::CreateDumpResponse(builder, dump); } - void SvcConsumer::FillJsonStats(json& jsonArray) const + flatbuffers::Offset SvcConsumer::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) { MS_TRACE(); + std::vector> rtpStreams; + // Add stats of our send stream. - jsonArray.emplace_back(json::value_t::object); - this->rtpStream->FillJsonStats(jsonArray[0]); + rtpStreams.emplace_back(this->rtpStream->FillBufferStats(builder)); // Add stats of our recv stream. if (this->producerRtpStream) { - jsonArray.emplace_back(json::value_t::object); - this->producerRtpStream->FillJsonStats(jsonArray[1]); + rtpStreams.emplace_back(producerRtpStream->FillBufferStats(builder)); } + + return FBS::Consumer::CreateGetStatsResponseDirect(builder, &rtpStreams); } - void SvcConsumer::FillJsonScore(json& jsonObject) const + flatbuffers::Offset SvcConsumer::FillBufferScore( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); MS_ASSERT(this->producerRtpStreamScores, "producerRtpStreamScores not set"); - jsonObject["score"] = this->rtpStream->GetScore(); + uint8_t producerScore{ 0 }; if (this->producerRtpStream) - jsonObject["producerScore"] = this->producerRtpStream->GetScore(); + { + producerScore = this->producerRtpStream->GetScore(); + } else - jsonObject["producerScore"] = 0; + { + producerScore = 0; + } - jsonObject["producerScores"] = *this->producerRtpStreamScores; + return FBS::Consumer::CreateConsumerScoreDirect( + builder, this->rtpStream->GetScore(), producerScore, this->producerRtpStreamScores); } void SvcConsumer::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::CONSUMER_REQUEST_KEY_FRAME: + case Channel::ChannelRequest::Method::CONSUMER_DUMP: + { + auto dumpOffset = FillBuffer(request->GetBufferBuilder()); + + request->Accept(FBS::Response::Body::Consumer_DumpResponse, dumpOffset); + + break; + } + + case Channel::ChannelRequest::Method::CONSUMER_REQUEST_KEY_FRAME: { if (IsActive()) + { RequestKeyFrame(); + } request->Accept(); break; } - case Channel::ChannelRequest::MethodId::CONSUMER_SET_PREFERRED_LAYERS: + case Channel::ChannelRequest::Method::CONSUMER_SET_PREFERRED_LAYERS: { auto previousPreferredSpatialLayer = this->preferredSpatialLayer; auto previousPreferredTemporalLayer = this->preferredTemporalLayer; - auto jsonSpatialLayerIt = request->data.find("spatialLayer"); - auto jsonTemporalLayerIt = request->data.find("temporalLayer"); + const auto* body = request->data->body_as(); + const auto* preferredLayers = body->preferredLayers(); // Spatial layer. - // clang-format off - if ( - jsonSpatialLayerIt == request->data.end() || - !Utils::Json::IsPositiveInteger(*jsonSpatialLayerIt) - ) - // clang-format on - { - MS_THROW_TYPE_ERROR("missing spatialLayer"); - } - - this->preferredSpatialLayer = jsonSpatialLayerIt->get(); + this->preferredSpatialLayer = preferredLayers->spatialLayer(); if (this->preferredSpatialLayer > this->rtpStream->GetSpatialLayers() - 1) + { this->preferredSpatialLayer = this->rtpStream->GetSpatialLayers() - 1; + } // preferredTemporaLayer is optional. - // clang-format off - if ( - jsonTemporalLayerIt != request->data.end() && - Utils::Json::IsPositiveInteger(*jsonTemporalLayerIt) - ) - // clang-format on + if (preferredLayers->temporalLayer().has_value()) { - this->preferredTemporalLayer = jsonTemporalLayerIt->get(); + this->preferredTemporalLayer = preferredLayers->temporalLayer().value(); if (this->preferredTemporalLayer > this->rtpStream->GetTemporalLayers() - 1) + { this->preferredTemporalLayer = this->rtpStream->GetTemporalLayers() - 1; + } } else { @@ -248,12 +247,13 @@ namespace RTC this->preferredTemporalLayer, this->id.c_str()); - json data = json::object(); - - data["spatialLayer"] = this->preferredSpatialLayer; - data["temporalLayer"] = this->preferredTemporalLayer; + const flatbuffers::Optional preferredTemporalLayer{ this->preferredTemporalLayer }; + auto preferredLayersOffset = FBS::Consumer::CreateConsumerLayers( + request->GetBufferBuilder(), this->preferredSpatialLayer, preferredTemporalLayer); + auto responseOffset = FBS::Consumer::CreateSetPreferredLayersResponse( + request->GetBufferBuilder(), preferredLayersOffset); - request->Accept(data); + request->Accept(FBS::Response::Body::Consumer_SetPreferredLayersResponse, responseOffset); // clang-format off if ( @@ -279,14 +279,14 @@ namespace RTC } } - void SvcConsumer::ProducerRtpStream(RTC::RtpStream* rtpStream, uint32_t /*mappedSsrc*/) + void SvcConsumer::ProducerRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t /*mappedSsrc*/) { MS_TRACE(); this->producerRtpStream = rtpStream; } - void SvcConsumer::ProducerNewRtpStream(RTC::RtpStream* rtpStream, uint32_t /*mappedSsrc*/) + void SvcConsumer::ProducerNewRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t /*mappedSsrc*/) { MS_TRACE(); @@ -296,11 +296,13 @@ namespace RTC EmitScore(); if (IsActive()) + { MayChangeLayers(); + } } void SvcConsumer::ProducerRtpStreamScore( - RTC::RtpStream* /*rtpStream*/, uint8_t score, uint8_t previousScore) + RTC::RtpStreamRecv* /*rtpStream*/, uint8_t score, uint8_t previousScore) { MS_TRACE(); @@ -322,7 +324,7 @@ namespace RTC } } - void SvcConsumer::ProducerRtcpSenderReport(RTC::RtpStream* /*rtpStream*/, bool /*first*/) + void SvcConsumer::ProducerRtcpSenderReport(RTC::RtpStreamRecv* /*rtpStream*/, bool /*first*/) { MS_TRACE(); @@ -336,7 +338,9 @@ namespace RTC MS_ASSERT(this->externallyManagedBitrate, "bitrate is not externally managed"); if (!IsActive()) + { return 0u; + } return this->priority; } @@ -349,7 +353,9 @@ namespace RTC MS_ASSERT(IsActive(), "should be active"); if (this->producerRtpStream->GetScore() == 0u) + { return 0u; + } // If already in the preferred layers, do nothing. // clang-format off @@ -371,11 +377,17 @@ namespace RTC auto lossPercentage = this->rtpStream->GetLossPercentage(); if (lossPercentage < 2) + { virtualBitrate = 1.08 * bitrate; + } else if (lossPercentage > 10) + { virtualBitrate = (1 - 0.5 * (lossPercentage / 100)) * bitrate; + } else + { virtualBitrate = bitrate; + } } else { @@ -404,7 +416,9 @@ namespace RTC // Ignore spatial layers lower than the one we already have. if (spatialLayer < this->provisionalTargetSpatialLayer) + { continue; + } temporalLayer = 0; @@ -426,6 +440,32 @@ namespace RTC requiredBitrate = this->producerRtpStream->GetLayerBitrate(nowMs, spatialLayer, temporalLayer); + // When using K-SVC we must subtract the bitrate of the current used layer + // if the new layer is the temporal layer 0 of an higher spatial layer. + // + // clang-format off + if ( + this->encodingContext->IsKSvc() && + requiredBitrate && + temporalLayer == 0 && + this->provisionalTargetSpatialLayer > -1 && + spatialLayer > this->provisionalTargetSpatialLayer + ) + // clang-format on + { + auto provisionalRequiredBitrate = this->producerRtpStream->GetBitrate( + nowMs, this->provisionalTargetSpatialLayer, this->provisionalTargetTemporalLayer); + + if (requiredBitrate > provisionalRequiredBitrate) + { + requiredBitrate -= provisionalRequiredBitrate; + } + else + { + requiredBitrate = 1u; // Don't set 0 since it would be ignored. + } + } + MS_DEBUG_DEV( "testing layers %" PRIi16 ":%" PRIi16 " [virtual bitrate:%" PRIu32 ", required bitrate:%" PRIu32 "]", @@ -436,25 +476,35 @@ namespace RTC // If active layer, end iterations here. Otherwise move to next spatial layer. if (requiredBitrate) + { goto done; + } else + { break; + } } // If this is the preferred or higher spatial layer, take it and exit. if (spatialLayer >= this->preferredSpatialLayer) + { break; + } } done: // No higher active layers found. if (!requiredBitrate) + { return 0u; + } // No luck. if (requiredBitrate > virtualBitrate) + { return 0u; + } // Set provisional layers. this->provisionalTargetSpatialLayer = spatialLayer; @@ -469,11 +519,17 @@ namespace RTC requiredBitrate); if (requiredBitrate <= bitrate) + { return requiredBitrate; + } else if (requiredBitrate <= virtualBitrate) + { return bitrate; + } else + { return requiredBitrate; // NOTE: This cannot happen. + } } void SvcConsumer::ApplyLayers() @@ -491,7 +547,9 @@ namespace RTC this->provisionalTargetTemporalLayer = -1; if (!IsActive()) + { return; + } // clang-format off if ( @@ -529,17 +587,46 @@ namespace RTC MS_ASSERT(this->externallyManagedBitrate, "bitrate is not externally managed"); if (!IsActive()) + { return 0u; + } + + auto nowMs = DepLibUV::GetTimeMs(); + uint32_t desiredBitrate{ 0u }; - auto nowMs = DepLibUV::GetTimeMs(); - uint32_t desiredBitrate = this->producerRtpStream->GetBitrate(nowMs); + // When using K-SVC each spatial layer is independent of the others. + if (this->encodingContext->IsKSvc()) + { + // Let's iterate all spatial layers of the Producer (from highest to lowest) and + // obtain their bitrate. Choose the highest one. + // NOTE: When the Producer enables a higher spatial layer, initially the bitrate + // oft could be less than the bitrate of a lower one. That's why we iterate all + // spatial layers here anyway. + for (auto spatialLayer{ this->producerRtpStream->GetSpatialLayers() - 1 }; spatialLayer >= 0; + --spatialLayer) + { + auto spatialLayerBitrate = + this->producerRtpStream->GetSpatialLayerBitrate(nowMs, spatialLayer); + + if (spatialLayerBitrate > desiredBitrate) + { + desiredBitrate = spatialLayerBitrate; + } + } + } + else + { + desiredBitrate = this->producerRtpStream->GetBitrate(nowMs); + } // If consumer.rtpParameters.encodings[0].maxBitrate was given and it's // greater than computed one, then use it. auto maxBitrate = this->rtpParameters.encodings[0].maxBitrate; if (maxBitrate > desiredBitrate) + { desiredBitrate = maxBitrate; + } return desiredBitrate; } @@ -548,8 +635,18 @@ namespace RTC { MS_TRACE(); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.consumerId = this->id; +#endif + if (!IsActive()) + { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::CONSUMER_INACTIVE); +#endif + return; + } // clang-format off if ( @@ -558,6 +655,10 @@ namespace RTC ) // clang-format on { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::INVALID_TARGET_LAYER); +#endif + return; } @@ -569,12 +670,34 @@ namespace RTC { MS_DEBUG_DEV("payload type not supported [payloadType:%" PRIu8 "]", payloadType); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::UNSUPPORTED_PAYLOAD_TYPE); +#endif + return; } // If we need to sync and this is not a key frame, ignore the packet. if (this->syncRequired && !packet->IsKeyFrame()) + { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::NOT_A_KEYFRAME); +#endif + + return; + } + + // Packets with only padding are not forwarded. + if (packet->GetPayloadLength() == 0) + { + this->rtpSeqManager->Drop(packet->GetSequenceNumber()); + +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::EMPTY_PAYLOAD); +#endif + return; + } // Whether this is the first packet after re-sync. const bool isSyncPacket = this->syncRequired; @@ -583,9 +706,11 @@ namespace RTC if (isSyncPacket) { if (packet->IsKeyFrame()) + { MS_DEBUG_TAG(rtp, "sync key frame received"); + } - this->rtpSeqManager.Sync(packet->GetSequenceNumber() - 1); + this->rtpSeqManager->Sync(packet->GetSequenceNumber() - 1); this->encodingContext->SyncRequired(); this->syncRequired = false; @@ -599,7 +724,11 @@ namespace RTC if (!packet->ProcessPayload(this->encodingContext.get(), marker)) { - this->rtpSeqManager.Drop(packet->GetSequenceNumber()); + this->rtpSeqManager->Drop(packet->GetSequenceNumber()); + +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::DROPPED_BY_CODEC); +#endif return; } @@ -618,7 +747,7 @@ namespace RTC // Update RTP seq number and timestamp based on NTP offset. uint16_t seq; - this->rtpSeqManager.Input(packet->GetSequenceNumber(), seq); + this->rtpSeqManager->Input(packet->GetSequenceNumber(), seq); // Save original packet fields. auto origSsrc = packet->GetSsrc(); @@ -628,6 +757,11 @@ namespace RTC packet->SetSsrc(this->rtpParameters.encodings[0].ssrc); packet->SetSequenceNumber(seq); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.sendRtpTimestamp = packet->GetTimestamp(); + packet->logger.sendSeqNumber = seq; +#endif + if (marker) { packet->SetMarker(true); @@ -681,29 +815,27 @@ namespace RTC MS_TRACE(); if (static_cast((nowMs - this->lastRtcpSentTime) * 1.15) < this->maxRtcpInterval) + { return true; + } auto* senderReport = this->rtpStream->GetRtcpSenderReport(nowMs); if (!senderReport) + { return true; + } // Build SDES chunk for this sender. auto* sdesChunk = this->rtpStream->GetRtcpSdesChunk(); - RTC::RTCP::DelaySinceLastRr* delaySinceLastRrReport{ nullptr }; - - auto* dlrr = this->rtpStream->GetRtcpXrDelaySinceLastRr(nowMs); - - if (dlrr) - { - delaySinceLastRrReport = new RTC::RTCP::DelaySinceLastRr(); - delaySinceLastRrReport->AddSsrcInfo(dlrr); - } + auto* delaySinceLastRrSsrcInfo = this->rtpStream->GetRtcpXrDelaySinceLastRrSsrcInfo(nowMs); // RTCP Compound packet buffer cannot hold the data. - if (!packet->Add(senderReport, sdesChunk, delaySinceLastRrReport)) + if (!packet->Add(senderReport, sdesChunk, delaySinceLastRrSsrcInfo)) + { return false; + } this->lastRtcpSentTime = nowMs; @@ -715,13 +847,17 @@ namespace RTC MS_TRACE(); if (!IsActive()) + { return; + } auto fractionLost = this->rtpStream->GetFractionLost(); // If our fraction lost is worse than the given one, update it. if (fractionLost > worstRemoteFractionLost) + { worstRemoteFractionLost = fractionLost; + } } void SvcConsumer::ReceiveNack(RTC::RTCP::FeedbackRtpNackPacket* nackPacket) @@ -729,7 +865,9 @@ namespace RTC MS_TRACE(); if (!IsActive()) + { return; + } // May emit 'trace' event. EmitTraceEventNackType(); @@ -763,7 +901,9 @@ namespace RTC this->rtpStream->ReceiveKeyFrameRequest(messageType); if (IsActive()) + { RequestKeyFrame(); + } } void SvcConsumer::ReceiveRtcpReceiverReport(RTC::RTCP::ReceiverReport* report) @@ -785,7 +925,9 @@ namespace RTC MS_TRACE(); if (!IsActive()) + { return 0u; + } return this->rtpStream->GetBitrate(nowMs); } @@ -804,7 +946,9 @@ namespace RTC this->syncRequired = true; if (IsActive()) + { MayChangeLayers(); + } } void SvcConsumer::UserOnTransportDisconnected() @@ -829,7 +973,9 @@ namespace RTC UpdateTargetLayers(-1, -1); if (this->externallyManagedBitrate) + { this->listener->OnConsumerNeedZeroBitrate(this); + } } void SvcConsumer::UserOnResumed() @@ -839,7 +985,9 @@ namespace RTC this->syncRequired = true; if (IsActive()) + { MayChangeLayers(); + } } void SvcConsumer::CreateRtpStream() @@ -914,12 +1062,16 @@ namespace RTC // If the Consumer is paused, tell the RtpStreamSend. if (IsPaused() || IsProducerPaused()) + { this->rtpStream->Pause(); + } const auto* rtxCodec = this->rtpParameters.GetRtxCodecForEncoding(encoding); if (rtxCodec && encoding.hasRtx) + { this->rtpStream->SetRtx(rtxCodec->payloadType, encoding.rtx.ssrc); + } } void SvcConsumer::RequestKeyFrame() @@ -927,7 +1079,9 @@ namespace RTC MS_TRACE(); if (this->kind != RTC::Media::Kind::VIDEO) + { return; + } auto mappedSsrc = this->consumableRtpEncodings[0].ssrc; @@ -951,7 +1105,9 @@ namespace RTC if (this->externallyManagedBitrate) { if (newTargetSpatialLayer != this->encodingContext->GetTargetSpatialLayer() || force) + { this->listener->OnConsumerNeedBitrateChange(this); + } } else { @@ -973,10 +1129,14 @@ namespace RTC int16_t spatialLayer{ 0 }; if (!this->producerRtpStream) + { goto done; + } if (this->producerRtpStream->GetScore() == 0u) + { goto done; + } for (; spatialLayer < this->producerRtpStream->GetSpatialLayers(); ++spatialLayer) { @@ -985,28 +1145,40 @@ namespace RTC if (nowMs - this->lastBweDowngradeAtMs < BweDowngradeConservativeMs) { if (newTargetSpatialLayer > -1 && spatialLayer > this->encodingContext->GetCurrentSpatialLayer()) + { continue; + } } if (!this->producerRtpStream->GetSpatialLayerBitrate(nowMs, spatialLayer)) + { continue; + } newTargetSpatialLayer = spatialLayer; // If this is the preferred or higher spatial layer and has bitrate, // take it and exit. if (spatialLayer >= this->preferredSpatialLayer) + { break; + } } if (newTargetSpatialLayer != -1) { if (newTargetSpatialLayer == this->preferredSpatialLayer) + { newTargetTemporalLayer = this->preferredTemporalLayer; + } else if (newTargetSpatialLayer < this->preferredSpatialLayer) + { newTargetTemporalLayer = this->rtpStream->GetTemporalLayers() - 1; + } else + { newTargetTemporalLayer = 0; + } } done: @@ -1075,11 +1247,16 @@ namespace RTC { MS_TRACE(); - json data = json::object(); + auto scoreOffset = FillBufferScore(this->shared->channelNotifier->GetBufferBuilder()); - FillJsonScore(data); + auto notificationOffset = FBS::Consumer::CreateScoreNotification( + this->shared->channelNotifier->GetBufferBuilder(), scoreOffset); - this->shared->channelNotifier->Emit(this->id, "score", data); + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::CONSUMER_SCORE, + FBS::Notification::Body::Consumer_ScoreNotification, + notificationOffset); } inline void SvcConsumer::EmitLayersChange() const @@ -1092,19 +1269,24 @@ namespace RTC this->encodingContext->GetCurrentTemporalLayer(), this->id.c_str()); - json data = json::object(); + flatbuffers::Offset layersOffset; if (this->encodingContext->GetCurrentSpatialLayer() >= 0) { - data["spatialLayer"] = this->encodingContext->GetCurrentSpatialLayer(); - data["temporalLayer"] = this->encodingContext->GetCurrentTemporalLayer(); - } - else - { - data = nullptr; + layersOffset = FBS::Consumer::CreateConsumerLayers( + this->shared->channelNotifier->GetBufferBuilder(), + this->encodingContext->GetCurrentSpatialLayer(), + this->encodingContext->GetCurrentTemporalLayer()); } - this->shared->channelNotifier->Emit(this->id, "layerschange", data); + auto notificationOffset = FBS::Consumer::CreateLayersChangeNotification( + this->shared->channelNotifier->GetBufferBuilder(), layersOffset); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::CONSUMER_LAYERS_CHANGE, + FBS::Notification::Body::Consumer_LayersChangeNotification, + notificationOffset); } inline void SvcConsumer::OnRtpStreamScore( @@ -1121,7 +1303,9 @@ namespace RTC // NOTE: For now this is a bit useless since, when locally managed, we do // not check the Consumer score at all. if (!this->externallyManagedBitrate) + { MayChangeLayers(); + } } } diff --git a/worker/src/RTC/TcpConnection.cpp b/worker/src/RTC/TcpConnection.cpp index b13cfd430c..da26865379 100644 --- a/worker/src/RTC/TcpConnection.cpp +++ b/worker/src/RTC/TcpConnection.cpp @@ -16,7 +16,7 @@ namespace RTC /* Instance methods. */ TcpConnection::TcpConnection(Listener* listener, size_t bufferSize) - : ::TcpConnectionHandler::TcpConnectionHandler(bufferSize), listener(listener) + : ::TcpConnectionHandle::TcpConnectionHandle(bufferSize), listener(listener) { MS_TRACE(); } @@ -60,13 +60,17 @@ namespace RTC // a DTLS Close Alert this would be closed (Close() called) so we cannot call // our listeners anymore. if (IsClosed()) + { return; + } const size_t dataLen = this->bufferDataLen - this->frameStart; size_t packetLen; if (dataLen >= 2) + { packetLen = size_t{ Utils::Byte::Get2Bytes(this->buffer + this->frameStart, 0) }; + } // We have packetLen bytes. if (dataLen >= 2 && dataLen >= 2 + packetLen) @@ -154,7 +158,7 @@ namespace RTC } } - void TcpConnection::Send(const uint8_t* data, size_t len, ::TcpConnectionHandler::onSendCallback* cb) + void TcpConnection::Send(const uint8_t* data, size_t len, ::TcpConnectionHandle::onSendCallback* cb) { MS_TRACE(); @@ -163,6 +167,6 @@ namespace RTC uint8_t frameLen[2]; Utils::Byte::Set2Bytes(frameLen, 0, len); - ::TcpConnectionHandler::Write(frameLen, 2, data, len, cb); + ::TcpConnectionHandle::Write(frameLen, 2, data, len, cb); } } // namespace RTC diff --git a/worker/src/RTC/TcpServer.cpp b/worker/src/RTC/TcpServer.cpp index 3d502c9d69..9ae9b49dfe 100644 --- a/worker/src/RTC/TcpServer.cpp +++ b/worker/src/RTC/TcpServer.cpp @@ -10,30 +10,44 @@ namespace RTC { /* Instance methods. */ - TcpServer::TcpServer(Listener* listener, RTC::TcpConnection::Listener* connListener, std::string& ip) + TcpServer::TcpServer( + Listener* listener, + RTC::TcpConnection::Listener* connListener, + std::string& ip, + uint16_t port, + RTC::Transport::SocketFlags& flags) : // This may throw. - ::TcpServerHandler::TcpServerHandler(RTC::PortManager::BindTcp(ip)), listener(listener), - connListener(connListener) + ::TcpServerHandle::TcpServerHandle(RTC::PortManager::BindTcp(ip, port, flags)), + listener(listener), connListener(connListener), fixedPort(true) { MS_TRACE(); } TcpServer::TcpServer( - Listener* listener, RTC::TcpConnection::Listener* connListener, std::string& ip, uint16_t port) + Listener* listener, + RTC::TcpConnection::Listener* connListener, + std::string& ip, + uint16_t minPort, + uint16_t maxPort, + RTC::Transport::SocketFlags& flags, + uint64_t& portRangeHash) : // This may throw. - ::TcpServerHandler::TcpServerHandler(RTC::PortManager::BindTcp(ip, port)), listener(listener), - connListener(connListener), fixedPort(true) + ::TcpServerHandle::TcpServerHandle( + RTC::PortManager::BindTcp(ip, minPort, maxPort, flags, portRangeHash)), + listener(listener), connListener(connListener), fixedPort(false) { MS_TRACE(); + + this->portRangeHash = portRangeHash; } TcpServer::~TcpServer() { MS_TRACE(); - if (!fixedPort) + if (!this->fixedPort) { - RTC::PortManager::UnbindTcp(this->localIp, this->localPort); + RTC::PortManager::Unbind(this->portRangeHash, this->localPort); } } @@ -48,7 +62,7 @@ namespace RTC AcceptTcpConnection(connection); } - void TcpServer::UserOnTcpConnectionClosed(::TcpConnectionHandler* connection) + void TcpServer::UserOnTcpConnectionClosed(::TcpConnectionHandle* connection) { MS_TRACE(); diff --git a/worker/src/RTC/Transport.cpp b/worker/src/RTC/Transport.cpp index efcbe6815d..eb469e70fb 100644 --- a/worker/src/RTC/Transport.cpp +++ b/worker/src/RTC/Transport.cpp @@ -1,10 +1,15 @@ +#include "flatbuffers/stl_emulation.h" #define MS_CLASS "RTC::Transport" // #define MS_LOG_DEV_LEVEL 3 #include "RTC/Transport.hpp" +#ifdef MS_LIBURING_SUPPORTED +#include "DepLibUring.hpp" +#endif #include "Logger.hpp" #include "MediaSoupErrors.hpp" #include "Utils.hpp" +#include "FBS/transport.h" #include "RTC/BweType.hpp" #include "RTC/PipeConsumer.hpp" #include "RTC/RTCP/FeedbackPs.hpp" @@ -21,166 +26,95 @@ #include // webrtc::RtpPacketSendInfo #include // std::ostream_iterator #include // std::multimap -#include // std::ostringstream namespace RTC { - static size_t DefaultSctpSendBufferSize{ 262144 }; // 2^18. - static size_t MaxSctpSendBufferSize{ 268435456 }; // 2^28. + static const size_t DefaultSctpSendBufferSize{ 262144 }; // 2^18. + static const size_t MaxSctpSendBufferSize{ 268435456 }; // 2^28. /* Instance methods. */ - Transport::Transport(RTC::Shared* shared, const std::string& id, Listener* listener, json& data) + Transport::Transport( + RTC::Shared* shared, + const std::string& id, + RTC::Transport::Listener* listener, + const FBS::Transport::Options* options) : id(id), shared(shared), listener(listener), recvRtxTransmission(1000u), sendRtxTransmission(1000u), sendProbationTransmission(100u) { MS_TRACE(); - auto jsonDirectIt = data.find("direct"); - - // clang-format off - if ( - jsonDirectIt != data.end() && - jsonDirectIt->is_boolean() && - jsonDirectIt->get() - ) - // clang-format on + if (options->direct()) { this->direct = true; - auto jsonMaxMessageSizeIt = data.find("maxMessageSize"); - - // maxMessageSize is mandatory for direct Transports. - // clang-format off - if ( - jsonMaxMessageSizeIt == data.end() || - !Utils::Json::IsPositiveInteger(*jsonMaxMessageSizeIt) - ) - // clang-format on + if (options->maxMessageSize().has_value()) { - MS_THROW_TYPE_ERROR("wrong maxMessageSize (not a number)"); + this->maxMessageSize = options->maxMessageSize().value(); } - - this->maxMessageSize = jsonMaxMessageSizeIt->get(); } - auto jsonInitialAvailableOutgoingBitrateIt = data.find("initialAvailableOutgoingBitrate"); - - if (jsonInitialAvailableOutgoingBitrateIt != data.end()) + if (options->initialAvailableOutgoingBitrate().has_value()) { - if (!Utils::Json::IsPositiveInteger(*jsonInitialAvailableOutgoingBitrateIt)) - MS_THROW_TYPE_ERROR("wrong initialAvailableOutgoingBitrate (not a number)"); - - this->initialAvailableOutgoingBitrate = jsonInitialAvailableOutgoingBitrateIt->get(); + this->initialAvailableOutgoingBitrate = options->initialAvailableOutgoingBitrate().value(); } - auto jsonEnableSctpIt = data.find("enableSctp"); - - // clang-format off - if ( - jsonEnableSctpIt != data.end() && - jsonEnableSctpIt->is_boolean() && - jsonEnableSctpIt->get() - ) - // clang-format on + if (options->enableSctp()) { if (this->direct) { MS_THROW_TYPE_ERROR("cannot enable SCTP in a direct Transport"); } - auto jsonNumSctpStreamsIt = data.find("numSctpStreams"); - auto jsonMaxSctpMessageSizeIt = data.find("maxSctpMessageSize"); - auto jsonSctpSendBufferSizeIt = data.find("sctpSendBufferSize"); - auto jsonIsDataChannelIt = data.find("isDataChannel"); - // numSctpStreams is mandatory. - // clang-format off - if ( - jsonNumSctpStreamsIt == data.end() || - !jsonNumSctpStreamsIt->is_object() - ) - // clang-format on + if (!flatbuffers::IsFieldPresent(options, FBS::Transport::Options::VT_NUMSCTPSTREAMS)) { - MS_THROW_TYPE_ERROR("wrong numSctpStreams (not an object)"); + MS_THROW_TYPE_ERROR("numSctpStreams missing"); } - auto jsonOSIt = jsonNumSctpStreamsIt->find("OS"); - auto jsonMISIt = jsonNumSctpStreamsIt->find("MIS"); - - // numSctpStreams.OS and numSctpStreams.MIS are mandatory. - // clang-format off - if ( - jsonOSIt == jsonNumSctpStreamsIt->end() || - !Utils::Json::IsPositiveInteger(*jsonOSIt) || - jsonMISIt == jsonNumSctpStreamsIt->end() || - !Utils::Json::IsPositiveInteger(*jsonMISIt) - ) - // clang-format on - { - MS_THROW_TYPE_ERROR("wrong numSctpStreams.OS and/or numSctpStreams.MIS (not a number)"); - } - - auto os = jsonOSIt->get(); - auto mis = jsonMISIt->get(); - // maxSctpMessageSize is mandatory. - // clang-format off - if ( - jsonMaxSctpMessageSizeIt == data.end() || - !Utils::Json::IsPositiveInteger(*jsonMaxSctpMessageSizeIt) - ) - // clang-format on + if (!flatbuffers::IsFieldPresent(options, FBS::Transport::Options::VT_MAXSCTPMESSAGESIZE)) { - MS_THROW_TYPE_ERROR("wrong maxSctpMessageSize (not a number)"); + MS_THROW_TYPE_ERROR("maxSctpMessageSize missing"); } - this->maxMessageSize = jsonMaxSctpMessageSizeIt->get(); + this->maxMessageSize = options->maxSctpMessageSize(); size_t sctpSendBufferSize; // sctpSendBufferSize is optional. - if (jsonSctpSendBufferSizeIt != data.end()) + if (flatbuffers::IsFieldPresent(options, FBS::Transport::Options::VT_SCTPSENDBUFFERSIZE)) { - if (!Utils::Json::IsPositiveInteger(*jsonSctpSendBufferSizeIt)) - { - MS_THROW_TYPE_ERROR("wrong sctpSendBufferSize (not a number)"); - } - - sctpSendBufferSize = jsonSctpSendBufferSizeIt->get(); - - if (sctpSendBufferSize > MaxSctpSendBufferSize) + if (options->sctpSendBufferSize() > MaxSctpSendBufferSize) { MS_THROW_TYPE_ERROR("wrong sctpSendBufferSize (maximum value exceeded)"); } + + sctpSendBufferSize = options->sctpSendBufferSize(); } else { sctpSendBufferSize = DefaultSctpSendBufferSize; } - // isDataChannel is optional. - bool isDataChannel{ false }; - - if (jsonIsDataChannelIt != data.end() && jsonIsDataChannelIt->is_boolean()) - isDataChannel = jsonIsDataChannelIt->get(); - // This may throw. this->sctpAssociation = new RTC::SctpAssociation( - this, os, mis, this->maxMessageSize, sctpSendBufferSize, isDataChannel); + this, + options->numSctpStreams()->os(), + options->numSctpStreams()->mis(), + this->maxMessageSize, + sctpSendBufferSize, + options->isDataChannel()); } // Create the RTCP timer. - this->rtcpTimer = new Timer(this); + this->rtcpTimer = new TimerHandle(this); } Transport::~Transport() { MS_TRACE(); - // Set the destroying flag. - this->destroying = true; - // The destructor must delete and clear everything silently. // Delete all Producers. @@ -299,174 +233,185 @@ namespace RTC this->listener->OnTransportListenServerClosed(this); } - void Transport::FillJson(json& jsonObject) const + flatbuffers::Offset Transport::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Add id. - jsonObject["id"] = this->id; - - // Add direct. - jsonObject["direct"] = this->direct; - // Add producerIds. - jsonObject["producerIds"] = json::array(); - auto jsonProducerIdsIt = jsonObject.find("producerIds"); + std::vector> producerIds; for (const auto& kv : this->mapProducers) { const auto& producerId = kv.first; - jsonProducerIdsIt->emplace_back(producerId); + producerIds.emplace_back(builder.CreateString(producerId)); } // Add consumerIds. - jsonObject["consumerIds"] = json::array(); - auto jsonConsumerIdsIt = jsonObject.find("consumerIds"); + std::vector> consumerIds; for (const auto& kv : this->mapConsumers) { const auto& consumerId = kv.first; - jsonConsumerIdsIt->emplace_back(consumerId); + consumerIds.emplace_back(builder.CreateString(consumerId)); } // Add mapSsrcConsumerId. - jsonObject["mapSsrcConsumerId"] = json::object(); - auto jsonMapSsrcConsumerId = jsonObject.find("mapSsrcConsumerId"); + std::vector> mapSsrcConsumerId; for (const auto& kv : this->mapSsrcConsumer) { auto ssrc = kv.first; auto* consumer = kv.second; - (*jsonMapSsrcConsumerId)[std::to_string(ssrc)] = consumer->id; + mapSsrcConsumerId.emplace_back( + FBS::Common::CreateUint32StringDirect(builder, ssrc, consumer->id.c_str())); } // Add mapRtxSsrcConsumerId. - jsonObject["mapRtxSsrcConsumerId"] = json::object(); - auto jsonMapRtxSsrcConsumerId = jsonObject.find("mapRtxSsrcConsumerId"); + std::vector> mapRtxSsrcConsumerId; for (const auto& kv : this->mapRtxSsrcConsumer) { auto ssrc = kv.first; auto* consumer = kv.second; - (*jsonMapRtxSsrcConsumerId)[std::to_string(ssrc)] = consumer->id; + mapRtxSsrcConsumerId.emplace_back( + FBS::Common::CreateUint32StringDirect(builder, ssrc, consumer->id.c_str())); } // Add dataProducerIds. - jsonObject["dataProducerIds"] = json::array(); - auto jsonDataProducerIdsIt = jsonObject.find("dataProducerIds"); + std::vector> dataProducerIds; for (const auto& kv : this->mapDataProducers) { const auto& dataProducerId = kv.first; - jsonDataProducerIdsIt->emplace_back(dataProducerId); + dataProducerIds.emplace_back(builder.CreateString(dataProducerId)); } // Add dataConsumerIds. - jsonObject["dataConsumerIds"] = json::array(); - auto jsonDataConsumerIdsIt = jsonObject.find("dataConsumerIds"); + std::vector> dataConsumerIds; for (const auto& kv : this->mapDataConsumers) { const auto& dataConsumerId = kv.first; - jsonDataConsumerIdsIt->emplace_back(dataConsumerId); + dataConsumerIds.emplace_back(builder.CreateString(dataConsumerId)); } // Add headerExtensionIds. - jsonObject["recvRtpHeaderExtensions"] = json::object(); - auto jsonRtpHeaderExtensionsIt = jsonObject.find("recvRtpHeaderExtensions"); - - if (this->recvRtpHeaderExtensionIds.mid != 0u) - (*jsonRtpHeaderExtensionsIt)["mid"] = this->recvRtpHeaderExtensionIds.mid; - - if (this->recvRtpHeaderExtensionIds.rid != 0u) - (*jsonRtpHeaderExtensionsIt)["rid"] = this->recvRtpHeaderExtensionIds.rid; - - if (this->recvRtpHeaderExtensionIds.rrid != 0u) - (*jsonRtpHeaderExtensionsIt)["rrid"] = this->recvRtpHeaderExtensionIds.rrid; - - if (this->recvRtpHeaderExtensionIds.absSendTime != 0u) - (*jsonRtpHeaderExtensionsIt)["absSendTime"] = this->recvRtpHeaderExtensionIds.absSendTime; - - if (this->recvRtpHeaderExtensionIds.transportWideCc01 != 0u) - (*jsonRtpHeaderExtensionsIt)["transportWideCc01"] = - this->recvRtpHeaderExtensionIds.transportWideCc01; - - // Add rtpListener. - this->rtpListener.FillJson(jsonObject["rtpListener"]); - - // Add maxMessageSize. - jsonObject["maxMessageSize"] = this->maxMessageSize; + auto recvRtpHeaderExtensions = FBS::Transport::CreateRecvRtpHeaderExtensions( + builder, + this->recvRtpHeaderExtensionIds.mid != 0u + ? flatbuffers::Optional(this->recvRtpHeaderExtensionIds.mid) + : flatbuffers::nullopt, + this->recvRtpHeaderExtensionIds.rid != 0u + ? flatbuffers::Optional(this->recvRtpHeaderExtensionIds.rid) + : flatbuffers::nullopt, + this->recvRtpHeaderExtensionIds.rrid != 0u + ? flatbuffers::Optional(this->recvRtpHeaderExtensionIds.rrid) + : flatbuffers::nullopt, + this->recvRtpHeaderExtensionIds.absSendTime != 0u + ? flatbuffers::Optional(this->recvRtpHeaderExtensionIds.absSendTime) + : flatbuffers::nullopt, + this->recvRtpHeaderExtensionIds.transportWideCc01 != 0u + ? flatbuffers::Optional(this->recvRtpHeaderExtensionIds.transportWideCc01) + : flatbuffers::nullopt); + + auto rtpListenerOffset = this->rtpListener.FillBuffer(builder); + + // Add sctpParameters. + flatbuffers::Offset sctpParameters; + // Add sctpState. + FBS::SctpAssociation::SctpState sctpState; + // Add sctpListener. + flatbuffers::Offset sctpListener; if (this->sctpAssociation) { // Add sctpParameters. - this->sctpAssociation->FillJson(jsonObject["sctpParameters"]); + sctpParameters = this->sctpAssociation->FillBuffer(builder); - // Add sctpState. switch (this->sctpAssociation->GetState()) { case RTC::SctpAssociation::SctpState::NEW: - jsonObject["sctpState"] = "new"; + { + sctpState = FBS::SctpAssociation::SctpState::NEW; break; + } + case RTC::SctpAssociation::SctpState::CONNECTING: - jsonObject["sctpState"] = "connecting"; + { + sctpState = FBS::SctpAssociation::SctpState::CONNECTING; break; + } + case RTC::SctpAssociation::SctpState::CONNECTED: - jsonObject["sctpState"] = "connected"; + { + sctpState = FBS::SctpAssociation::SctpState::CONNECTED; break; + } + case RTC::SctpAssociation::SctpState::FAILED: - jsonObject["sctpState"] = "failed"; + { + sctpState = FBS::SctpAssociation::SctpState::FAILED; break; + } + case RTC::SctpAssociation::SctpState::CLOSED: - jsonObject["sctpState"] = "closed"; + { + sctpState = FBS::SctpAssociation::SctpState::CLOSED; break; + } } - // Add sctpListener. - this->sctpListener.FillJson(jsonObject["sctpListener"]); + sctpListener = this->sctpListener.FillBuffer(builder); } // Add traceEventTypes. - std::vector traceEventTypes; - std::ostringstream traceEventTypesStream; + std::vector traceEventTypes; if (this->traceEventTypes.probation) - traceEventTypes.emplace_back("probation"); - if (this->traceEventTypes.bwe) - traceEventTypes.emplace_back("bwe"); - - if (!traceEventTypes.empty()) { - std::copy( - traceEventTypes.begin(), - traceEventTypes.end() - 1, - std::ostream_iterator(traceEventTypesStream, ",")); - traceEventTypesStream << traceEventTypes.back(); + traceEventTypes.emplace_back(FBS::Transport::TraceEventType::PROBATION); } - - jsonObject["traceEventTypes"] = traceEventTypesStream.str(); + if (this->traceEventTypes.bwe) + { + traceEventTypes.emplace_back(FBS::Transport::TraceEventType::BWE); + } + + return FBS::Transport::CreateDumpDirect( + builder, + this->id.c_str(), + this->direct, + &producerIds, + &consumerIds, + &mapSsrcConsumerId, + &mapRtxSsrcConsumerId, + &dataProducerIds, + &dataConsumerIds, + recvRtpHeaderExtensions, + rtpListenerOffset, + this->maxMessageSize, + sctpParameters, + this->sctpAssociation ? flatbuffers::Optional(sctpState) + : flatbuffers::nullopt, + sctpListener, + &traceEventTypes); } - void Transport::FillJsonStats(json& jsonArray) + flatbuffers::Offset Transport::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) { MS_TRACE(); auto nowMs = DepLibUV::GetTimeMs(); - jsonArray.emplace_back(json::value_t::object); - auto& jsonObject = jsonArray[0]; - - // Add transportId. - jsonObject["transportId"] = this->id; - - // Add timestamp. - jsonObject["timestamp"] = nowMs; + // Add sctpState. + FBS::SctpAssociation::SctpState sctpState; if (this->sctpAssociation) { @@ -474,176 +419,192 @@ namespace RTC switch (this->sctpAssociation->GetState()) { case RTC::SctpAssociation::SctpState::NEW: - jsonObject["sctpState"] = "new"; + { + sctpState = FBS::SctpAssociation::SctpState::NEW; break; + } + case RTC::SctpAssociation::SctpState::CONNECTING: - jsonObject["sctpState"] = "connecting"; + { + sctpState = FBS::SctpAssociation::SctpState::CONNECTING; break; + } + case RTC::SctpAssociation::SctpState::CONNECTED: - jsonObject["sctpState"] = "connected"; + { + sctpState = FBS::SctpAssociation::SctpState::CONNECTED; break; + } + case RTC::SctpAssociation::SctpState::FAILED: - jsonObject["sctpState"] = "failed"; + { + sctpState = FBS::SctpAssociation::SctpState::FAILED; break; + } + case RTC::SctpAssociation::SctpState::CLOSED: - jsonObject["sctpState"] = "closed"; + { + sctpState = FBS::SctpAssociation::SctpState::CLOSED; break; + } } } - // Add bytesReceived. - jsonObject["bytesReceived"] = this->recvTransmission.GetBytes(); - - // Add recvBitrate. - jsonObject["recvBitrate"] = this->recvTransmission.GetRate(nowMs); - - // Add bytesSent. - jsonObject["bytesSent"] = this->sendTransmission.GetBytes(); - - // Add sendBitrate. - jsonObject["sendBitrate"] = this->sendTransmission.GetRate(nowMs); - - // Add rtpBytesReceived. - jsonObject["rtpBytesReceived"] = this->recvRtpTransmission.GetBytes(); - - // Add rtpRecvBitrate. - jsonObject["rtpRecvBitrate"] = this->recvRtpTransmission.GetBitrate(nowMs); - - // Add rtpBytesSent. - jsonObject["rtpBytesSent"] = this->sendRtpTransmission.GetBytes(); - - // Add rtpSendBitrate. - jsonObject["rtpSendBitrate"] = this->sendRtpTransmission.GetBitrate(nowMs); - - // Add rtxBytesReceived. - jsonObject["rtxBytesReceived"] = this->recvRtxTransmission.GetBytes(); - - // Add rtxRecvBitrate. - jsonObject["rtxRecvBitrate"] = this->recvRtxTransmission.GetBitrate(nowMs); - - // Add rtxBytesSent. - jsonObject["rtxBytesSent"] = this->sendRtxTransmission.GetBytes(); - - // Add rtxSendBitrate. - jsonObject["rtxSendBitrate"] = this->sendRtxTransmission.GetBitrate(nowMs); - - // Add probationBytesSent. - jsonObject["probationBytesSent"] = this->sendProbationTransmission.GetBytes(); - - // Add probationSendBitrate. - jsonObject["probationSendBitrate"] = this->sendProbationTransmission.GetBitrate(nowMs); - - // Add availableOutgoingBitrate. - if (this->tccClient) - jsonObject["availableOutgoingBitrate"] = this->tccClient->GetAvailableBitrate(); - - // Add availableIncomingBitrate. - if (this->tccServer && this->tccServer->GetAvailableBitrate() != 0u) - jsonObject["availableIncomingBitrate"] = this->tccServer->GetAvailableBitrate(); - - // Add maxIncomingBitrate. - if (this->maxIncomingBitrate != 0u) - jsonObject["maxIncomingBitrate"] = this->maxIncomingBitrate; - - // Add packetLossReceived. - if (this->tccServer) - jsonObject["rtpPacketLossReceived"] = this->tccServer->GetPacketLoss(); - - // Add packetLossSent. - if (this->tccClient) - jsonObject["rtpPacketLossSent"] = this->tccClient->GetPacketLoss(); + return FBS::Transport::CreateStatsDirect( + builder, + // transportId. + this->id.c_str(), + // timestamp. + nowMs, + // sctpState. + this->sctpAssociation ? flatbuffers::Optional(sctpState) + : flatbuffers::nullopt, + // bytesReceived. + this->recvTransmission.GetBytes(), + // recvBitrate. + this->recvTransmission.GetRate(nowMs), + // bytesSent. + this->sendTransmission.GetBytes(), + // sendBitrate. + this->sendTransmission.GetRate(nowMs), + // rtpBytesReceived. + this->recvRtpTransmission.GetBytes(), + // rtpRecvBitrate. + this->recvRtpTransmission.GetBitrate(nowMs), + // rtpBytesSent. + this->sendRtpTransmission.GetBytes(), + // rtpSendBitrate. + this->sendRtpTransmission.GetBitrate(nowMs), + // rtxBytesReceived. + this->recvRtxTransmission.GetBytes(), + // rtxRecvBitrate. + this->recvRtxTransmission.GetBitrate(nowMs), + // rtxBytesSent. + this->sendRtxTransmission.GetBytes(), + // rtxSendBitrate. + this->sendRtxTransmission.GetBitrate(nowMs), + // probationBytesSent. + this->sendProbationTransmission.GetBytes(), + // probationSendBitrate. + this->sendProbationTransmission.GetBitrate(nowMs), + // availableOutgoingBitrate. + this->tccClient ? flatbuffers::Optional(this->tccClient->GetAvailableBitrate()) + : flatbuffers::nullopt, + // availableIncomingBitrate. + this->tccServer ? flatbuffers::Optional(this->tccServer->GetAvailableBitrate()) + : flatbuffers::nullopt, + // maxIncomingBitrate. + this->maxIncomingBitrate ? flatbuffers::Optional(this->maxIncomingBitrate) + : flatbuffers::nullopt, + // maxOutgoingBitrate. + this->maxOutgoingBitrate ? flatbuffers::Optional(this->maxOutgoingBitrate) + : flatbuffers::nullopt, + // minOutgoingBitrate. + this->minOutgoingBitrate ? flatbuffers::Optional(this->minOutgoingBitrate) + : flatbuffers::nullopt, + // rtpPacketLossReceived. + this->tccServer ? flatbuffers::Optional(this->tccServer->GetPacketLoss()) + : flatbuffers::nullopt, + // rtpPacketLossSent. + this->tccClient ? flatbuffers::Optional(this->tccClient->GetPacketLoss()) + : flatbuffers::nullopt); } void Transport::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::TRANSPORT_DUMP: + case Channel::ChannelRequest::Method::TRANSPORT_SET_MAX_INCOMING_BITRATE: { - json data = json::object(); + const auto* body = request->data->body_as(); - FillJson(data); + this->maxIncomingBitrate = body->maxIncomingBitrate(); - request->Accept(data); - - break; - } - - case Channel::ChannelRequest::MethodId::TRANSPORT_GET_STATS: - { - json data = json::array(); + MS_DEBUG_TAG(bwe, "maximum incoming bitrate set to %" PRIu32, this->maxIncomingBitrate); - FillJsonStats(data); + request->Accept(); - request->Accept(data); + if (this->tccServer) + { + this->tccServer->SetMaxIncomingBitrate(this->maxIncomingBitrate); + } break; } - case Channel::ChannelRequest::MethodId::TRANSPORT_SET_MAX_INCOMING_BITRATE: + case Channel::ChannelRequest::Method::TRANSPORT_SET_MAX_OUTGOING_BITRATE: { - auto jsonBitrateIt = request->data.find("bitrate"); + const auto* body = request->data->body_as(); + const uint32_t bitrate = body->maxOutgoingBitrate(); - // clang-format off - if ( - jsonBitrateIt == request->data.end() || - !Utils::Json::IsPositiveInteger(*jsonBitrateIt) - ) - // clang-format on + if (bitrate > 0u && bitrate < RTC::TransportCongestionControlMinOutgoingBitrate) + { + MS_THROW_TYPE_ERROR( + "bitrate must be >= %" PRIu32 " or 0 (unlimited)", + RTC::TransportCongestionControlMinOutgoingBitrate); + } + else if (bitrate > 0u && bitrate < this->minOutgoingBitrate) { - MS_THROW_TYPE_ERROR("missing bitrate"); + MS_THROW_TYPE_ERROR( + "bitrate must be >= current min outgoing bitrate (%" PRIu32 ") or 0 (unlimited)", + this->minOutgoingBitrate); } - this->maxIncomingBitrate = jsonBitrateIt->get(); + if (this->tccClient) + { + // NOTE: This may throw so don't update things before calling this + // method. + this->tccClient->SetMaxOutgoingBitrate(bitrate); + this->maxOutgoingBitrate = bitrate; - MS_DEBUG_TAG(bwe, "maximum incoming bitrate set to %" PRIu32, this->maxIncomingBitrate); + MS_DEBUG_TAG(bwe, "maximum outgoing bitrate set to %" PRIu32, this->maxOutgoingBitrate); - request->Accept(); + ComputeOutgoingDesiredBitrate(); + } + else + { + this->maxOutgoingBitrate = bitrate; + } - if (this->tccServer) - this->tccServer->SetMaxIncomingBitrate(this->maxIncomingBitrate); + request->Accept(); break; } - case Channel::ChannelRequest::MethodId::TRANSPORT_SET_MAX_OUTGOING_BITRATE: + case Channel::ChannelRequest::Method::TRANSPORT_SET_MIN_OUTGOING_BITRATE: { - auto jsonBitrateIt = request->data.find("bitrate"); + const auto* body = request->data->body_as(); + const uint32_t bitrate = body->minOutgoingBitrate(); - // clang-format off - if ( - jsonBitrateIt == request->data.end() || - !Utils::Json::IsPositiveInteger(*jsonBitrateIt) - ) - // clang-format on + if (bitrate > 0u && bitrate < RTC::TransportCongestionControlMinOutgoingBitrate) { - MS_THROW_TYPE_ERROR("missing bitrate"); + MS_THROW_TYPE_ERROR( + "bitrate must be >= %" PRIu32 " or 0 (unlimited)", + RTC::TransportCongestionControlMinOutgoingBitrate); } - - uint32_t bitrate = jsonBitrateIt->get(); - - if (bitrate < RTC::TransportCongestionControlMinOutgoingBitrate) + else if (bitrate > 0u && this->maxOutgoingBitrate > 0 && bitrate > this->maxOutgoingBitrate) { MS_THROW_TYPE_ERROR( - "bitrate must be >= %" PRIu32 " bps", RTC::TransportCongestionControlMinOutgoingBitrate); + "bitrate must be <= current max outgoing bitrate (%" PRIu32 ") or 0 (unlimited)", + this->maxOutgoingBitrate); } if (this->tccClient) { // NOTE: This may throw so don't update things before calling this // method. - this->tccClient->SetMaxOutgoingBitrate(bitrate); - this->maxOutgoingBitrate = bitrate; + this->tccClient->SetMinOutgoingBitrate(bitrate); + this->minOutgoingBitrate = bitrate; - MS_DEBUG_TAG(bwe, "maximum outgoing bitrate set to %" PRIu32, this->maxOutgoingBitrate); + MS_DEBUG_TAG(bwe, "minimum outgoing bitrate set to %" PRIu32, this->minOutgoingBitrate); ComputeOutgoingDesiredBitrate(); } else { - this->maxOutgoingBitrate = bitrate; + this->minOutgoingBitrate = bitrate; } request->Accept(); @@ -651,15 +612,18 @@ namespace RTC break; } - case Channel::ChannelRequest::MethodId::TRANSPORT_PRODUCE: + case Channel::ChannelRequest::Method::TRANSPORT_PRODUCE: { - std::string producerId; + const auto* body = request->data->body_as(); + auto producerId = body->producerId()->str(); - // This may throw. - SetNewProducerIdFromData(request->data, producerId); + if (this->mapProducers.find(producerId) != this->mapProducers.end()) + { + MS_THROW_ERROR("a Producer with same producerId already exists"); + } // This may throw. - auto* producer = new RTC::Producer(this->shared, producerId, this, request->data); + auto* producer = new RTC::Producer(this->shared, producerId, this, body); // Insert the Producer into the RtpListener. // This may throw. If so, delete the Producer and throw. @@ -727,11 +691,10 @@ namespace RTC } // Create status response. - json data = json::object(); - - data["type"] = RTC::RtpParameters::GetTypeString(producer->GetType()); + auto responseOffset = FBS::Transport::CreateProduceResponse( + request->GetBufferBuilder(), FBS::RtpParameters::Type(producer->GetType())); - request->Accept(data); + request->Accept(FBS::Response::Body::Transport_ProduceResponse, responseOffset); // Check if TransportCongestionControlServer or REMB server must be // created. @@ -799,56 +762,41 @@ namespace RTC std::make_shared(this, bweType, RTC::MtuSize); if (this->maxIncomingBitrate != 0u) + { this->tccServer->SetMaxIncomingBitrate(this->maxIncomingBitrate); + } if (IsConnected()) + { this->tccServer->TransportConnected(); + } } } break; } - case Channel::ChannelRequest::MethodId::TRANSPORT_CONSUME: + case Channel::ChannelRequest::Method::TRANSPORT_CONSUME: { - auto jsonProducerIdIt = request->data.find("producerId"); + const auto* body = request->data->body_as(); + const std::string producerId = body->producerId()->str(); + const std::string consumerId = body->consumerId()->str(); - if (jsonProducerIdIt == request->data.end() || !jsonProducerIdIt->is_string()) + if (this->mapConsumers.find(consumerId) != this->mapConsumers.end()) { - MS_THROW_TYPE_ERROR("missing producerId"); + MS_THROW_ERROR("a Consumer with same consumerId already exists"); } - std::string producerId = jsonProducerIdIt->get(); - std::string consumerId; - - // This may throw. - SetNewConsumerIdFromData(request->data, consumerId); - - // Get type. - auto jsonTypeIt = request->data.find("type"); - - if (jsonTypeIt == request->data.end() || !jsonTypeIt->is_string()) - MS_THROW_TYPE_ERROR("missing type"); - - // This may throw. - auto type = RTC::RtpParameters::GetType(jsonTypeIt->get()); + auto type = RTC::RtpParameters::Type(body->type()); RTC::Consumer* consumer{ nullptr }; switch (type) { - case RTC::RtpParameters::Type::NONE: - { - MS_THROW_TYPE_ERROR("invalid type 'none'"); - - break; - } - case RTC::RtpParameters::Type::SIMPLE: { // This may throw. - consumer = - new RTC::SimpleConsumer(this->shared, consumerId, producerId, this, request->data); + consumer = new RTC::SimpleConsumer(this->shared, consumerId, producerId, this, body); break; } @@ -856,8 +804,7 @@ namespace RTC case RTC::RtpParameters::Type::SIMULCAST: { // This may throw. - consumer = - new RTC::SimulcastConsumer(this->shared, consumerId, producerId, this, request->data); + consumer = new RTC::SimulcastConsumer(this->shared, consumerId, producerId, this, body); break; } @@ -865,8 +812,7 @@ namespace RTC case RTC::RtpParameters::Type::SVC: { // This may throw. - consumer = - new RTC::SvcConsumer(this->shared, consumerId, producerId, this, request->data); + consumer = new RTC::SvcConsumer(this->shared, consumerId, producerId, this, body); break; } @@ -874,8 +820,7 @@ namespace RTC case RTC::RtpParameters::Type::PIPE: { // This may throw. - consumer = - new RTC::PipeConsumer(this->shared, consumerId, producerId, this, request->data); + consumer = new RTC::PipeConsumer(this->shared, consumerId, producerId, this, body); break; } @@ -910,23 +855,25 @@ namespace RTC MS_DEBUG_DEV( "Consumer created [consumerId:%s, producerId:%s]", consumerId.c_str(), producerId.c_str()); - // Create status response. - json data = json::object(); - - data["paused"] = consumer->IsPaused(); - data["producerPaused"] = consumer->IsProducerPaused(); - - consumer->FillJsonScore(data["score"]); - + flatbuffers::Offset preferredLayersOffset; auto preferredLayers = consumer->GetPreferredLayers(); if (preferredLayers.spatial > -1 && preferredLayers.temporal > -1) { - data["preferredLayers"]["spatialLayer"] = preferredLayers.spatial; - data["preferredLayers"]["temporalLayer"] = preferredLayers.temporal; + const flatbuffers::Optional preferredTemporalLayer{ preferredLayers.temporal }; + preferredLayersOffset = FBS::Consumer::CreateConsumerLayers( + request->GetBufferBuilder(), preferredLayers.spatial, preferredTemporalLayer); } - request->Accept(data); + auto scoreOffset = consumer->FillBufferScore(request->GetBufferBuilder()); + auto responseOffset = FBS::Transport::CreateConsumeResponse( + request->GetBufferBuilder(), + consumer->IsPaused(), + consumer->IsProducerPaused(), + scoreOffset, + preferredLayersOffset); + + request->Accept(FBS::Response::Body::Transport_ConsumeResponse, responseOffset); // Check if Transport Congestion Control client must be created. const auto& rtpHeaderExtensionIds = consumer->GetRtpHeaderExtensionIds(); @@ -944,19 +891,19 @@ namespace RTC // - there is "transport-cc" in codecs RTCP feedback. // // clang-format off - if ( - consumer->GetKind() == RTC::Media::Kind::VIDEO && - rtpHeaderExtensionIds.transportWideCc01 != 0u && - std::any_of( - codecs.begin(), codecs.end(), [](const RTC::RtpCodecParameters& codec) - { - return std::any_of( - codec.rtcpFeedback.begin(), codec.rtcpFeedback.end(), [](const RTC::RtcpFeedback& fb) + if ( + consumer->GetKind() == RTC::Media::Kind::VIDEO && + rtpHeaderExtensionIds.transportWideCc01 != 0u && + std::any_of( + codecs.begin(), codecs.end(), [](const RTC::RtpCodecParameters& codec) { - return fb.type == "transport-cc"; - }); - }) - ) + return std::any_of( + codec.rtcpFeedback.begin(), codec.rtcpFeedback.end(), [](const RTC::RtcpFeedback& fb) + { + return fb.type == "transport-cc"; + }); + }) + ) // clang-format on { MS_DEBUG_TAG(bwe, "enabling TransportCongestionControlClient with transport-cc"); @@ -970,19 +917,19 @@ namespace RTC // - there is "remb" in codecs RTCP feedback. // // clang-format off - else if ( - consumer->GetKind() == RTC::Media::Kind::VIDEO && - rtpHeaderExtensionIds.absSendTime != 0u && - std::any_of( - codecs.begin(), codecs.end(), [](const RTC::RtpCodecParameters& codec) - { - return std::any_of( - codec.rtcpFeedback.begin(), codec.rtcpFeedback.end(), [](const RTC::RtcpFeedback& fb) + else if ( + consumer->GetKind() == RTC::Media::Kind::VIDEO && + rtpHeaderExtensionIds.absSendTime != 0u && + std::any_of( + codecs.begin(), codecs.end(), [](const RTC::RtpCodecParameters& codec) { - return fb.type == "goog-remb"; - }); - }) - ) + return std::any_of( + codec.rtcpFeedback.begin(), codec.rtcpFeedback.end(), [](const RTC::RtcpFeedback& fb) + { + return fb.type == "goog-remb"; + }); + }) + ) // clang-format on { MS_DEBUG_TAG(bwe, "enabling TransportCongestionControlClient with REMB"); @@ -1002,17 +949,25 @@ namespace RTC }; this->tccClient = std::make_shared( - this, bweType, this->initialAvailableOutgoingBitrate, this->maxOutgoingBitrate); + this, + bweType, + this->initialAvailableOutgoingBitrate, + this->maxOutgoingBitrate, + this->minOutgoingBitrate); if (IsConnected()) + { this->tccClient->TransportConnected(); + } } } // If applicable, tell the new Consumer that we are gonna manage its // bitrate. if (this->tccClient) + { consumer->SetExternallyManagedBitrate(); + } #ifdef ENABLE_RTC_SENDER_BANDWIDTH_ESTIMATOR // Create SenderBandwidthEstimator if: @@ -1022,20 +977,20 @@ namespace RTC // - there is "transport-cc" in codecs RTCP feedback. // // clang-format off - if ( - !this->senderBwe && - consumer->GetKind() == RTC::Media::Kind::VIDEO && - rtpHeaderExtensionIds.transportWideCc01 != 0u && - std::any_of( - codecs.begin(), codecs.end(), [](const RTC::RtpCodecParameters& codec) - { - return std::any_of( - codec.rtcpFeedback.begin(), codec.rtcpFeedback.end(), [](const RTC::RtcpFeedback& fb) + if ( + !this->senderBwe && + consumer->GetKind() == RTC::Media::Kind::VIDEO && + rtpHeaderExtensionIds.transportWideCc01 != 0u && + std::any_of( + codecs.begin(), codecs.end(), [](const RTC::RtpCodecParameters& codec) { - return fb.type == "transport-cc"; - }); - }) - ) + return std::any_of( + codec.rtcpFeedback.begin(), codec.rtcpFeedback.end(), [](const RTC::RtcpFeedback& fb) + { + return fb.type == "transport-cc"; + }); + }) + ) // clang-format on { MS_DEBUG_TAG(bwe, "enabling SenderBandwidthEstimator"); @@ -1052,22 +1007,28 @@ namespace RTC this, this->initialAvailableOutgoingBitrate); if (IsConnected()) + { this->senderBwe->TransportConnected(); + } } // If applicable, tell the new Consumer that we are gonna manage its // bitrate. if (this->senderBwe) + { consumer->SetExternallyManagedBitrate(); + } #endif if (IsConnected()) + { consumer->TransportConnected(); + } break; } - case Channel::ChannelRequest::MethodId::TRANSPORT_PRODUCE_DATA: + case Channel::ChannelRequest::Method::TRANSPORT_PRODUCE_DATA: { // Early check. The Transport must support SCTP or be direct. if (!this->sctpAssociation && !this->direct) @@ -1075,14 +1036,16 @@ namespace RTC MS_THROW_ERROR("SCTP not enabled and not a direct Transport"); } - std::string dataProducerId; + const auto* body = request->data->body_as(); + + auto dataProducerId = body->dataProducerId()->str(); // This may throw. - SetNewDataProducerIdFromData(request->data, dataProducerId); + CheckNoDataProducer(dataProducerId); // This may throw. - auto* dataProducer = new RTC::DataProducer( - this->shared, dataProducerId, this->maxMessageSize, this, request->data); + auto* dataProducer = + new RTC::DataProducer(this->shared, dataProducerId, this->maxMessageSize, this, body); // Verify the type of the DataProducer. switch (dataProducer->GetType()) @@ -1155,16 +1118,14 @@ namespace RTC MS_DEBUG_DEV("DataProducer created [dataProducerId:%s]", dataProducerId.c_str()); - json data = json::object(); - - dataProducer->FillJson(data); + auto dumpOffset = dataProducer->FillBuffer(request->GetBufferBuilder()); - request->Accept(data); + request->Accept(FBS::Response::Body::DataProducer_DumpResponse, dumpOffset); break; } - case Channel::ChannelRequest::MethodId::TRANSPORT_CONSUME_DATA: + case Channel::ChannelRequest::Method::TRANSPORT_CONSUME_DATA: { // Early check. The Transport must support SCTP or be direct. if (!this->sctpAssociation && !this->direct) @@ -1172,18 +1133,13 @@ namespace RTC MS_THROW_ERROR("SCTP not enabled and not a direct Transport"); } - auto jsonDataProducerIdIt = request->data.find("dataProducerId"); + const auto* body = request->data->body_as(); - if (jsonDataProducerIdIt == request->data.end() || !jsonDataProducerIdIt->is_string()) - { - MS_THROW_ERROR("missing dataProducerId"); - } - - std::string dataProducerId = jsonDataProducerIdIt->get(); - std::string dataConsumerId; + auto dataProducerId = body->dataProducerId()->str(); + auto dataConsumerId = body->dataConsumerId()->str(); // This may throw. - SetNewDataConsumerIdFromData(request->data, dataConsumerId); + CheckNoDataConsumer(dataConsumerId); // This may throw. auto* dataConsumer = new RTC::DataConsumer( @@ -1192,7 +1148,7 @@ namespace RTC dataProducerId, this->sctpAssociation, this, - request->data, + body, this->maxMessageSize); // Verify the type of the DataConsumer. @@ -1248,14 +1204,14 @@ namespace RTC dataConsumerId.c_str(), dataProducerId.c_str()); - json data = json::object(); - - dataConsumer->FillJson(data); + auto dumpOffset = dataConsumer->FillBuffer(request->GetBufferBuilder()); - request->Accept(data); + request->Accept(FBS::Response::Body::DataConsumer_DumpResponse, dumpOffset); if (IsConnected()) + { dataConsumer->TransportConnected(); + } if (dataConsumer->GetType() == RTC::DataConsumer::Type::SCTP) { @@ -1271,28 +1227,31 @@ namespace RTC break; } - case Channel::ChannelRequest::MethodId::TRANSPORT_ENABLE_TRACE_EVENT: + case Channel::ChannelRequest::Method::TRANSPORT_ENABLE_TRACE_EVENT: { - auto jsonTypesIt = request->data.find("types"); - - // Disable all if no entries. - if (jsonTypesIt == request->data.end() || !jsonTypesIt->is_array()) - MS_THROW_TYPE_ERROR("wrong types (not an array)"); + const auto* body = request->data->body_as(); // Reset traceEventTypes. struct TraceEventTypes newTraceEventTypes; - for (const auto& type : *jsonTypesIt) + for (const auto& type : *body->events()) { - if (!type.is_string()) - MS_THROW_TYPE_ERROR("wrong type (not a string)"); + switch (type) + { + case FBS::Transport::TraceEventType::PROBATION: + { + newTraceEventTypes.probation = true; + + break; + } - std::string typeStr = type.get(); + case FBS::Transport::TraceEventType::BWE: + { + newTraceEventTypes.bwe = true; - if (typeStr == "probation") - newTraceEventTypes.probation = true; - if (typeStr == "bwe") - newTraceEventTypes.bwe = true; + break; + } + } } this->traceEventTypes = newTraceEventTypes; @@ -1302,10 +1261,12 @@ namespace RTC break; } - case Channel::ChannelRequest::MethodId::TRANSPORT_CLOSE_PRODUCER: + case Channel::ChannelRequest::Method::TRANSPORT_CLOSE_PRODUCER: { + const auto* body = request->data->body_as(); + // This may throw. - RTC::Producer* producer = GetProducerFromData(request->data); + RTC::Producer* producer = GetProducerById(body->producerId()->str()); // Remove it from the RtpListener. this->rtpListener.RemoveProducer(producer); @@ -1321,7 +1282,9 @@ namespace RTC RecvStreamClosed(rtpStream->GetSsrc()); if (rtpStream->HasRtx()) + { RecvStreamClosed(rtpStream->GetRtxSsrc()); + } } // Notify the listener. @@ -1337,10 +1300,12 @@ namespace RTC break; } - case Channel::ChannelRequest::MethodId::TRANSPORT_CLOSE_CONSUMER: + case Channel::ChannelRequest::Method::TRANSPORT_CLOSE_CONSUMER: { + const auto* body = request->data->body_as(); + // This may throw. - RTC::Consumer* consumer = GetConsumerFromData(request->data); + RTC::Consumer* consumer = GetConsumerById(body->consumerId()->str()); // Remove it from the maps. this->mapConsumers.erase(consumer->id); @@ -1371,17 +1336,22 @@ namespace RTC request->Accept(); - // This may be the latest active Consumer with BWE. If so we have to stop probation. + // This may be the latest active Consumer with BWE. If so we have to stop + // probation. if (this->tccClient) + { ComputeOutgoingDesiredBitrate(/*forceBitrate*/ true); + } break; } - case Channel::ChannelRequest::MethodId::TRANSPORT_CLOSE_DATA_PRODUCER: + case Channel::ChannelRequest::Method::TRANSPORT_CLOSE_DATAPRODUCER: { + const auto* body = request->data->body_as(); + // This may throw. - RTC::DataProducer* dataProducer = GetDataProducerFromData(request->data); + RTC::DataProducer* dataProducer = GetDataProducerById(body->dataProducerId()->str()); if (dataProducer->GetType() == RTC::DataProducer::Type::SCTP) { @@ -1411,10 +1381,12 @@ namespace RTC break; } - case Channel::ChannelRequest::MethodId::TRANSPORT_CLOSE_DATA_CONSUMER: + case Channel::ChannelRequest::Method::TRANSPORT_CLOSE_DATACONSUMER: { + const auto* body = request->data->body_as(); + // This may throw. - RTC::DataConsumer* dataConsumer = GetDataConsumerFromData(request->data); + RTC::DataConsumer* dataConsumer = GetDataConsumerById(body->dataConsumerId()->str()); // Remove it from the maps. this->mapDataConsumers.erase(dataConsumer->id); @@ -1440,37 +1412,41 @@ namespace RTC default: { - MS_THROW_ERROR("unknown method '%s'", request->method.c_str()); + MS_THROW_ERROR("unknown method '%s'", request->methodCStr); } } - } - void Transport::HandleRequest(PayloadChannel::PayloadChannelRequest* request) - { - MS_TRACE(); + return; - switch (request->methodId) + switch (request->method) { default: { - MS_THROW_ERROR("unknown method '%s'", request->method.c_str()); + MS_ERROR("unknown method"); } } } - void Transport::HandleNotification(PayloadChannel::PayloadChannelNotification* notification) + void Transport::HandleNotification(Channel::ChannelNotification* notification) { MS_TRACE(); - switch (notification->eventId) + switch (notification->event) { default: { - MS_ERROR("unknown event '%s'", notification->event.c_str()); + MS_ERROR("unknown event '%s'", notification->eventCStr); } } } + void Transport::Destroying() + { + MS_TRACE(); + + this->destroying = true; + } + void Transport::Connected() { MS_TRACE(); @@ -1493,23 +1469,31 @@ namespace RTC // Tell the SctpAssociation. if (this->sctpAssociation) + { this->sctpAssociation->TransportConnected(); + } // Start the RTCP timer. this->rtcpTimer->Start(static_cast(RTC::RTCP::MaxVideoIntervalMs / 2)); // Tell the TransportCongestionControlClient. if (this->tccClient) + { this->tccClient->TransportConnected(); + } // Tell the TransportCongestionControlServer. if (this->tccServer) + { this->tccServer->TransportConnected(); + } #ifdef ENABLE_RTC_SENDER_BANDWIDTH_ESTIMATOR // Tell the SenderBandwidthEstimator. if (this->senderBwe) + { this->senderBwe->TransportConnected(); + } #endif } @@ -1538,16 +1522,22 @@ namespace RTC // Tell the TransportCongestionControlClient. if (this->tccClient) + { this->tccClient->TransportDisconnected(); + } // Tell the TransportCongestionControlServer. if (this->tccServer) + { this->tccServer->TransportDisconnected(); + } #ifdef ENABLE_RTC_SENDER_BANDWIDTH_ESTIMATOR // Tell the SenderBandwidthEstimator. if (this->senderBwe) + { this->senderBwe->TransportDisconnected(); + } #endif } @@ -1555,6 +1545,10 @@ namespace RTC { MS_TRACE(); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.recvTransportId = this->id; +#endif + // Apply the Transport RTP header extension ids so the RTP listener can use them. packet->SetMidExtensionId(this->recvRtpHeaderExtensionIds.mid); packet->SetRidExtensionId(this->recvRtpHeaderExtensionIds.rid); @@ -1566,13 +1560,19 @@ namespace RTC // Feed the TransportCongestionControlServer. if (this->tccServer) + { this->tccServer->IncomingPacket(nowMs, packet); + } // Get the associated Producer. RTC::Producer* producer = this->rtpListener.GetProducer(packet); if (!producer) { +#ifdef MS_RTC_LOGGER_RTP + packet->logger.Dropped(RtcLogger::RtpPacket::DropReason::PRODUCER_NOT_FOUND); +#endif + MS_WARN_TAG( rtp, "no suitable Producer for received RTP packet [ssrc:%" PRIu32 ", payloadType:%" PRIu8 "]", @@ -1646,84 +1646,50 @@ namespace RTC this->sctpAssociation->ProcessSctpData(data, len); } - void Transport::SetNewProducerIdFromData(json& data, std::string& producerId) const + void Transport::CheckNoDataProducer(const std::string& dataProducerId) const { - MS_TRACE(); - - auto jsonProducerIdIt = data.find("producerId"); - - if (jsonProducerIdIt == data.end() || !jsonProducerIdIt->is_string()) - { - MS_THROW_TYPE_ERROR("missing producerId"); - } - - producerId.assign(jsonProducerIdIt->get()); - - if (this->mapProducers.find(producerId) != this->mapProducers.end()) + if (this->mapDataProducers.find(dataProducerId) != this->mapDataProducers.end()) { - MS_THROW_ERROR("a Producer with same producerId already exists"); + MS_THROW_ERROR("a DataProducer with same dataProducerId already exists"); } } - RTC::Producer* Transport::GetProducerFromData(json& data) const + void Transport::CheckNoDataConsumer(const std::string& dataConsumerId) const { MS_TRACE(); - auto jsonProducerIdIt = data.find("producerId"); - - if (jsonProducerIdIt == data.end() || !jsonProducerIdIt->is_string()) + if (this->mapDataConsumers.find(dataConsumerId) != this->mapDataConsumers.end()) { - MS_THROW_TYPE_ERROR("missing producerId"); + MS_THROW_ERROR("a DataConsumer with same dataConsumerId already exists"); } - - auto it = this->mapProducers.find(jsonProducerIdIt->get()); - - if (it == this->mapProducers.end()) - MS_THROW_ERROR("Producer not found"); - - RTC::Producer* producer = it->second; - - return producer; } - void Transport::SetNewConsumerIdFromData(json& data, std::string& consumerId) const + RTC::Producer* Transport::GetProducerById(const std::string& producerId) const { MS_TRACE(); - auto jsonConsumerIdIt = data.find("consumerId"); + auto it = this->mapProducers.find(producerId); - if (jsonConsumerIdIt == data.end() || !jsonConsumerIdIt->is_string()) + if (it == this->mapProducers.end()) { - MS_THROW_TYPE_ERROR("missing consumerId"); + MS_THROW_ERROR("Producer not found"); } - consumerId.assign(jsonConsumerIdIt->get()); - - if (this->mapConsumers.find(consumerId) != this->mapConsumers.end()) - { - MS_THROW_ERROR("a Consumer with same consumerId already exists"); - } + return it->second; } - RTC::Consumer* Transport::GetConsumerFromData(json& data) const + RTC::Consumer* Transport::GetConsumerById(const std::string& consumerId) const { MS_TRACE(); - auto jsonConsumerIdIt = data.find("consumerId"); - - if (jsonConsumerIdIt == data.end() || !jsonConsumerIdIt->is_string()) - { - MS_THROW_TYPE_ERROR("missing consumerId"); - } - - auto it = this->mapConsumers.find(jsonConsumerIdIt->get()); + auto it = this->mapConsumers.find(consumerId); if (it == this->mapConsumers.end()) + { MS_THROW_ERROR("Consumer not found"); + } - RTC::Consumer* consumer = it->second; - - return consumer; + return it->second; } inline RTC::Consumer* Transport::GetConsumerByMediaSsrc(uint32_t ssrc) const @@ -1733,7 +1699,9 @@ namespace RTC auto mapSsrcConsumerIt = this->mapSsrcConsumer.find(ssrc); if (mapSsrcConsumerIt == this->mapSsrcConsumer.end()) + { return nullptr; + } auto* consumer = mapSsrcConsumerIt->second; @@ -1747,91 +1715,41 @@ namespace RTC auto mapRtxSsrcConsumerIt = this->mapRtxSsrcConsumer.find(ssrc); if (mapRtxSsrcConsumerIt == this->mapRtxSsrcConsumer.end()) + { return nullptr; + } auto* consumer = mapRtxSsrcConsumerIt->second; return consumer; } - void Transport::SetNewDataProducerIdFromData(json& data, std::string& dataProducerId) const - { - MS_TRACE(); - - auto jsonDataProducerIdIt = data.find("dataProducerId"); - - if (jsonDataProducerIdIt == data.end() || !jsonDataProducerIdIt->is_string()) - { - MS_THROW_TYPE_ERROR("missing dataProducerId"); - } - - dataProducerId.assign(jsonDataProducerIdIt->get()); - - if (this->mapDataProducers.find(dataProducerId) != this->mapDataProducers.end()) - { - MS_THROW_ERROR("a DataProducer with same dataProducerId already exists"); - } - } - - RTC::DataProducer* Transport::GetDataProducerFromData(json& data) const + RTC::DataProducer* Transport::GetDataProducerById(const std::string& dataProducerId) const { MS_TRACE(); - auto jsonDataProducerIdIt = data.find("dataProducerId"); - - if (jsonDataProducerIdIt == data.end() || !jsonDataProducerIdIt->is_string()) - { - MS_THROW_TYPE_ERROR("missing dataProducerId"); - } - - auto it = this->mapDataProducers.find(jsonDataProducerIdIt->get()); + auto it = this->mapDataProducers.find(dataProducerId); if (it == this->mapDataProducers.end()) - MS_THROW_ERROR("DataProducer not found"); - - RTC::DataProducer* dataProducer = it->second; - - return dataProducer; - } - - void Transport::SetNewDataConsumerIdFromData(json& data, std::string& dataConsumerId) const - { - MS_TRACE(); - - auto jsonDataConsumerIdIt = data.find("dataConsumerId"); - - if (jsonDataConsumerIdIt == data.end() || !jsonDataConsumerIdIt->is_string()) { - MS_THROW_TYPE_ERROR("missing dataConsumerId"); + MS_THROW_ERROR("DataProducer not found"); } - dataConsumerId.assign(jsonDataConsumerIdIt->get()); - - if (this->mapDataConsumers.find(dataConsumerId) != this->mapDataConsumers.end()) - { - MS_THROW_ERROR("a DataConsumer with same dataConsumerId already exists"); - } + return it->second; } - RTC::DataConsumer* Transport::GetDataConsumerFromData(json& data) const + RTC::DataConsumer* Transport::GetDataConsumerById(const std::string& dataConsumerId) const { MS_TRACE(); - auto jsonDataConsumerIdIt = data.find("dataConsumerId"); - - if (jsonDataConsumerIdIt == data.end() || !jsonDataConsumerIdIt->is_string()) - { - MS_THROW_TYPE_ERROR("missing dataConsumerId"); - } - - auto it = this->mapDataConsumers.find(jsonDataConsumerIdIt->get()); + auto it = this->mapDataConsumers.find(dataConsumerId); if (it == this->mapDataConsumers.end()) + { MS_THROW_ERROR("DataConsumer not found"); + } - RTC::DataConsumer* dataConsumer = it->second; - - return dataConsumer; + return it->second; } void Transport::HandleRtcpPacket(RTC::RTCP::Packet* packet) @@ -2085,12 +2003,16 @@ namespace RTC auto* feedback = static_cast(packet); if (this->tccClient) + { this->tccClient->ReceiveRtcpTransportFeedback(feedback); + } #ifdef ENABLE_RTC_SENDER_BANDWIDTH_ESTIMATOR // Pass it to the SenderBandwidthEstimator client. if (this->senderBwe) + { this->senderBwe->ReceiveRtcpTransportFeedback(feedback); + } #endif break; @@ -2176,7 +2098,9 @@ namespace RTC // SSRC should be filled in the sub-block. if (ssrcInfo->GetSsrc() == 0) + { ssrcInfo->SetSsrc(xr->GetSsrc()); + } auto* producer = this->rtpListener.GetProducer(ssrcInfo->GetSsrc()); @@ -2233,6 +2157,14 @@ namespace RTC std::unique_ptr packet{ new RTC::RTCP::CompoundPacket() }; +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + // Activate liburing usage. + DepLibUring::SetActive(); + } +#endif + for (auto& kv : this->mapConsumers) { auto* consumer = kv.second; @@ -2276,6 +2208,14 @@ namespace RTC { SendRtcpCompoundPacket(packet.get()); } + +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + // Submit all prepared submission entries. + DepLibUring::Submit(); + } +#endif } void Transport::DistributeAvailableOutgoingBitrate() @@ -2293,12 +2233,16 @@ namespace RTC auto priority = consumer->GetBitratePriority(); if (priority > 0u) + { multimapPriorityConsumer.emplace(priority, consumer); + } } // Nobody wants bitrate. Exit. if (multimapPriorityConsumer.empty()) + { return; + } bool baseAllocation = true; uint32_t availableBitrate = this->tccClient->GetAvailableBitrate(); @@ -2321,10 +2265,11 @@ namespace RTC auto* consumer = it->second; auto bweType = this->tccClient->GetBweType(); + // NOLINTNEXTLINE(bugprone-too-small-loop-variable) for (uint8_t i{ 1u }; i <= (baseAllocation ? 1u : priority); ++i) { uint32_t usedBitrate{ 0u }; - bool considerLoss = (bweType == RTC::BweType::REMB); + const bool considerLoss = (bweType == RTC::BweType::REMB); usedBitrate = consumer->IncreaseLayer(availableBitrate, considerLoss); @@ -2334,13 +2279,17 @@ namespace RTC // Exit the loop fast if used bitrate is 0. if (usedBitrate == 0u) + { break; + } } } // If no Consumer used bitrate, exit the loop. if (availableBitrate == previousAvailableBitrate) + { break; + } baseAllocation = false; } @@ -2377,22 +2326,27 @@ namespace RTC this->tccClient->SetDesiredBitrate(totalDesiredBitrate, forceBitrate); } - inline void Transport::EmitTraceEventProbationType(RTC::RtpPacket* packet) const + inline void Transport::EmitTraceEventProbationType(RTC::RtpPacket* /*packet*/) const { MS_TRACE(); if (!this->traceEventTypes.probation) + { return; + } - json data = json::object(); - - data["type"] = "probation"; - data["timestamp"] = DepLibUV::GetTimeMs(); - data["direction"] = "out"; - - packet->FillJson(data["info"]); + // TODO: Missing trace info (RTP packet dump). + auto notification = FBS::Transport::CreateTraceNotification( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::Transport::TraceEventType::PROBATION, + DepLibUV::GetTimeMs(), + FBS::Common::TraceDirection::DIRECTION_OUT); - this->shared->channelNotifier->Emit(this->id, "trace", data); + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::TRANSPORT_TRACE, + FBS::Notification::Body::Transport_TraceNotification, + notification); } inline void Transport::EmitTraceEventBweType( @@ -2401,32 +2355,36 @@ namespace RTC MS_TRACE(); if (!this->traceEventTypes.bwe) - return; - - json data = json::object(); - - data["type"] = "bwe"; - data["timestamp"] = DepLibUV::GetTimeMs(); - data["direction"] = "out"; - data["info"]["desiredBitrate"] = bitrates.desiredBitrate; - data["info"]["effectiveDesiredBitrate"] = bitrates.effectiveDesiredBitrate; - data["info"]["minBitrate"] = bitrates.minBitrate; - data["info"]["maxBitrate"] = bitrates.maxBitrate; - data["info"]["startBitrate"] = bitrates.startBitrate; - data["info"]["maxPaddingBitrate"] = bitrates.maxPaddingBitrate; - data["info"]["availableBitrate"] = bitrates.availableBitrate; - - switch (this->tccClient->GetBweType()) { - case RTC::BweType::TRANSPORT_CC: - data["info"]["type"] = "transport-cc"; - break; - case RTC::BweType::REMB: - data["info"]["type"] = "remb"; - break; + return; } - this->shared->channelNotifier->Emit(this->id, "trace", data); + auto traceInfo = FBS::Transport::CreateBweTraceInfo( + this->shared->channelNotifier->GetBufferBuilder(), + this->tccClient->GetBweType() == RTC::BweType::TRANSPORT_CC + ? FBS::Transport::BweType::TRANSPORT_CC + : FBS::Transport::BweType::REMB, + bitrates.desiredBitrate, + bitrates.effectiveDesiredBitrate, + bitrates.minBitrate, + bitrates.maxBitrate, + bitrates.startBitrate, + bitrates.maxPaddingBitrate, + bitrates.availableBitrate); + + auto notification = FBS::Transport::CreateTraceNotification( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::Transport::TraceEventType::BWE, + DepLibUV::GetTimeMs(), + FBS::Common::TraceDirection::DIRECTION_OUT, + FBS::Transport::TraceInfo::BweTraceInfo, + traceInfo.Union()); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::TRANSPORT_TRACE, + FBS::Notification::Body::Transport_TraceNotification, + notification); } inline void Transport::OnProducerPaused(RTC::Producer* producer) @@ -2444,7 +2402,7 @@ namespace RTC } inline void Transport::OnProducerNewRtpStream( - RTC::Producer* producer, RTC::RtpStream* rtpStream, uint32_t mappedSsrc) + RTC::Producer* producer, RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) { MS_TRACE(); @@ -2452,7 +2410,7 @@ namespace RTC } inline void Transport::OnProducerRtpStreamScore( - RTC::Producer* producer, RTC::RtpStream* rtpStream, uint8_t score, uint8_t previousScore) + RTC::Producer* producer, RTC::RtpStreamRecv* rtpStream, uint8_t score, uint8_t previousScore) { MS_TRACE(); @@ -2460,7 +2418,7 @@ namespace RTC } inline void Transport::OnProducerRtcpSenderReport( - RTC::Producer* producer, RTC::RtpStream* rtpStream, bool first) + RTC::Producer* producer, RTC::RtpStreamRecv* rtpStream, bool first) { MS_TRACE(); @@ -2494,6 +2452,11 @@ namespace RTC { MS_TRACE(); +#ifdef MS_RTC_LOGGER_RTP + packet->logger.sendTransportId = this->id; + packet->logger.Sent(); +#endif + // Update abs-send-time if present. packet->UpdateAbsSendTime(DepLibUV::GetTimeMs()); @@ -2525,7 +2488,7 @@ namespace RTC // invoked *after* the WebRtcTransport has been closed (freed). To avoid // invalid memory access we need to use weak_ptr. Same applies in other // send callbacks. - std::weak_ptr tccClientWeakPtr(this->tccClient); + const std::weak_ptr tccClientWeakPtr(this->tccClient); #ifdef ENABLE_RTC_SENDER_BANDWIDTH_ESTIMATOR std::weak_ptr senderBweWeakPtr(this->senderBwe); @@ -2536,7 +2499,7 @@ namespace RTC sentInfo.sendingAtMs = DepLibUV::GetTimeMs(); auto* cb = new onSendCallback( - [tccClientWeakPtr, &packetInfo, senderBweWeakPtr, &sentInfo](bool sent) + [tccClientWeakPtr, packetInfo, senderBweWeakPtr, sentInfo](bool sent) { if (sent) { @@ -2560,7 +2523,7 @@ namespace RTC SendRtpPacket(consumer, packet, cb); #else const auto* cb = new onSendCallback( - [tccClientWeakPtr, &packetInfo](bool sent) + [tccClientWeakPtr, packetInfo](bool sent) { if (sent) { @@ -2614,7 +2577,7 @@ namespace RTC // Indicate the pacer (and prober) that a packet is to be sent. this->tccClient->InsertPacket(packetInfo); - std::weak_ptr tccClientWeakPtr(this->tccClient); + const std::weak_ptr tccClientWeakPtr(this->tccClient); #ifdef ENABLE_RTC_SENDER_BANDWIDTH_ESTIMATOR std::weak_ptr senderBweWeakPtr = this->senderBwe; @@ -2625,7 +2588,7 @@ namespace RTC sentInfo.sendingAtMs = DepLibUV::GetTimeMs(); auto* cb = new onSendCallback( - [tccClientWeakPtr, &packetInfo, senderBweWeakPtr, &sentInfo](bool sent) + [tccClientWeakPtr, packetInfo, senderBweWeakPtr, sentInfo](bool sent) { if (sent) { @@ -2649,7 +2612,7 @@ namespace RTC SendRtpPacket(consumer, packet, cb); #else const auto* cb = new onSendCallback( - [tccClientWeakPtr, &packetInfo](bool sent) + [tccClientWeakPtr, packetInfo](bool sent) { if (sent) { @@ -2740,23 +2703,45 @@ namespace RTC // This may be the latest active Consumer with BWE. If so we have to stop probation. if (this->tccClient) + { ComputeOutgoingDesiredBitrate(/*forceBitrate*/ true); + } } inline void Transport::OnDataProducerMessageReceived( - RTC::DataProducer* dataProducer, uint32_t ppid, const uint8_t* msg, size_t len) + RTC::DataProducer* dataProducer, + const uint8_t* msg, + size_t len, + uint32_t ppid, + std::vector& subchannels, + std::optional requiredSubchannel) + { + MS_TRACE(); + + this->listener->OnTransportDataProducerMessageReceived( + this, dataProducer, msg, len, ppid, subchannels, requiredSubchannel); + } + + inline void Transport::OnDataProducerPaused(RTC::DataProducer* dataProducer) { MS_TRACE(); - this->listener->OnTransportDataProducerMessageReceived(this, dataProducer, ppid, msg, len); + this->listener->OnTransportDataProducerPaused(this, dataProducer); + } + + inline void Transport::OnDataProducerResumed(RTC::DataProducer* dataProducer) + { + MS_TRACE(); + + this->listener->OnTransportDataProducerResumed(this, dataProducer); } inline void Transport::OnDataConsumerSendMessage( - RTC::DataConsumer* dataConsumer, uint32_t ppid, const uint8_t* msg, size_t len, onQueuedCallback* cb) + RTC::DataConsumer* dataConsumer, const uint8_t* msg, size_t len, uint32_t ppid, onQueuedCallback* cb) { MS_TRACE(); - SendMessage(dataConsumer, ppid, msg, len, cb); + SendMessage(dataConsumer, msg, len, ppid, cb); } inline void Transport::OnDataConsumerDataProducerClosed(RTC::DataConsumer* dataConsumer) @@ -2784,11 +2769,14 @@ namespace RTC MS_TRACE(); // Notify the Node Transport. - json data = json::object(); - - data["sctpState"] = "connecting"; - - this->shared->channelNotifier->Emit(this->id, "sctpstatechange", data); + auto sctpStateChangeOffset = FBS::Transport::CreateSctpStateChangeNotification( + this->shared->channelNotifier->GetBufferBuilder(), FBS::SctpAssociation::SctpState::CONNECTING); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::TRANSPORT_SCTP_STATE_CHANGE, + FBS::Notification::Body::Transport_SctpStateChangeNotification, + sctpStateChangeOffset); } inline void Transport::OnSctpAssociationConnected(RTC::SctpAssociation* /*sctpAssociation*/) @@ -2807,11 +2795,14 @@ namespace RTC } // Notify the Node Transport. - json data = json::object(); - - data["sctpState"] = "connected"; - - this->shared->channelNotifier->Emit(this->id, "sctpstatechange", data); + auto sctpStateChangeOffset = FBS::Transport::CreateSctpStateChangeNotification( + this->shared->channelNotifier->GetBufferBuilder(), FBS::SctpAssociation::SctpState::CONNECTED); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::TRANSPORT_SCTP_STATE_CHANGE, + FBS::Notification::Body::Transport_SctpStateChangeNotification, + sctpStateChangeOffset); } inline void Transport::OnSctpAssociationFailed(RTC::SctpAssociation* /*sctpAssociation*/) @@ -2830,11 +2821,14 @@ namespace RTC } // Notify the Node Transport. - json data = json::object(); - - data["sctpState"] = "failed"; - - this->shared->channelNotifier->Emit(this->id, "sctpstatechange", data); + auto sctpStateChangeOffset = FBS::Transport::CreateSctpStateChangeNotification( + this->shared->channelNotifier->GetBufferBuilder(), FBS::SctpAssociation::SctpState::FAILED); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::TRANSPORT_SCTP_STATE_CHANGE, + FBS::Notification::Body::Transport_SctpStateChangeNotification, + sctpStateChangeOffset); } inline void Transport::OnSctpAssociationClosed(RTC::SctpAssociation* /*sctpAssociation*/) @@ -2853,11 +2847,14 @@ namespace RTC } // Notify the Node Transport. - json data = json::object(); - - data["sctpState"] = "closed"; - - this->shared->channelNotifier->Emit(this->id, "sctpstatechange", data); + auto sctpStateChangeOffset = FBS::Transport::CreateSctpStateChangeNotification( + this->shared->channelNotifier->GetBufferBuilder(), FBS::SctpAssociation::SctpState::CLOSED); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::TRANSPORT_SCTP_STATE_CHANGE, + FBS::Notification::Body::Transport_SctpStateChangeNotification, + sctpStateChangeOffset); } inline void Transport::OnSctpAssociationSendData( @@ -2870,18 +2867,22 @@ namespace RTC // its destructor is called first and then the parent Transport's destructor, // and we would end here calling SendSctpData() which is an abstract method. if (this->destroying) + { return; + } if (this->sctpAssociation) + { SendSctpData(data, len); + } } inline void Transport::OnSctpAssociationMessageReceived( RTC::SctpAssociation* /*sctpAssociation*/, uint16_t streamId, - uint32_t ppid, const uint8_t* msg, - size_t len) + size_t len, + uint32_t ppid) { MS_TRACE(); @@ -2898,7 +2899,10 @@ namespace RTC // Pass the SCTP message to the corresponding DataProducer. try { - dataProducer->ReceiveMessage(ppid, msg, len); + static std::vector emptySubchannels; + + dataProducer->ReceiveMessage( + msg, len, ppid, emptySubchannels, /*requiredSubchannel*/ std::nullopt); } catch (std::exception& error) { @@ -2916,7 +2920,9 @@ namespace RTC auto* dataConsumer = kv.second; if (dataConsumer->GetType() == RTC::DataConsumer::Type::SCTP) + { dataConsumer->SctpAssociationBufferedAmount(bufferedAmount); + } } } @@ -2936,7 +2942,7 @@ namespace RTC } inline void Transport::OnTransportCongestionControlClientSendRtpPacket( - RTC::TransportCongestionControlClient* tccClient, + RTC::TransportCongestionControlClient* /*tccClient*/, RTC::RtpPacket* packet, const webrtc::PacedPacketInfo& pacingInfo) { @@ -2970,7 +2976,7 @@ namespace RTC // Indicate the pacer (and prober) that a packet is to be sent. this->tccClient->InsertPacket(packetInfo); - std::weak_ptr tccClientWeakPtr(this->tccClient); + const std::weak_ptr tccClientWeakPtr(this->tccClient); #ifdef ENABLE_RTC_SENDER_BANDWIDTH_ESTIMATOR std::weak_ptr senderBweWeakPtr = this->senderBwe; @@ -2982,7 +2988,7 @@ namespace RTC sentInfo.sendingAtMs = DepLibUV::GetTimeMs(); auto* cb = new onSendCallback( - [tccClientWeakPtr, &packetInfo, senderBweWeakPtr, &sentInfo](bool sent) + [tccClientWeakPtr, packetInfo, senderBweWeakPtr, sentInfo](bool sent) { if (sent) { @@ -3006,7 +3012,7 @@ namespace RTC SendRtpPacket(nullptr, packet, cb); #else const auto* cb = new onSendCallback( - [tccClientWeakPtr, &packetInfo](bool sent) + [tccClientWeakPtr, packetInfo](bool sent) { if (sent) { @@ -3069,7 +3075,7 @@ namespace RTC } #endif - inline void Transport::OnTimer(Timer* timer) + inline void Transport::OnTimer(TimerHandle* timer) { MS_TRACE(); @@ -3083,8 +3089,8 @@ namespace RTC /* * The interval between RTCP packets is varied randomly over the range - * [1.0,1.5] times the calculated interval to avoid unintended synchronization - * of all participants. + * [1.0, 1.5] times the calculated interval to avoid unintended + * synchronization of all participants. */ interval *= static_cast(Utils::Crypto::GetRandomUInt(10, 15)) / 10; diff --git a/worker/src/RTC/TransportCongestionControlClient.cpp b/worker/src/RTC/TransportCongestionControlClient.cpp index f555bdf7a5..c8f82b64d3 100644 --- a/worker/src/RTC/TransportCongestionControlClient.cpp +++ b/worker/src/RTC/TransportCongestionControlClient.cpp @@ -5,7 +5,6 @@ #include "RTC/TransportCongestionControlClient.hpp" #include "DepLibUV.hpp" #include "Logger.hpp" -#include "MediaSoupErrors.hpp" #include // webrtc::TargetRateConstraints #include @@ -27,11 +26,12 @@ namespace RTC RTC::TransportCongestionControlClient::Listener* listener, RTC::BweType bweType, uint32_t initialAvailableBitrate, - uint32_t maxOutgoingBitrate) + uint32_t maxOutgoingBitrate, + uint32_t minOutgoingBitrate) : listener(listener), bweType(bweType), initialAvailableBitrate(std::max( initialAvailableBitrate, RTC::TransportCongestionControlMinOutgoingBitrate)), - maxOutgoingBitrate(maxOutgoingBitrate) + maxOutgoingBitrate(maxOutgoingBitrate), minOutgoingBitrate(minOutgoingBitrate) { MS_TRACE(); @@ -72,7 +72,7 @@ namespace RTC // videos are muted or using screensharing with still images) this->rtpTransportControllerSend->EnablePeriodicAlrProbing(true); - this->processTimer = new Timer(this); + this->processTimer = new TimerHandle(this); // clang-format off this->processTimer->Start(std::min( @@ -151,7 +151,8 @@ namespace RTC return this->rtpTransportControllerSend->packet_sender()->GetPacingInfo(); } - void TransportCongestionControlClient::PacketSent(webrtc::RtpPacketSendInfo& packetInfo, int64_t nowMs) + void TransportCongestionControlClient::PacketSent( + const webrtc::RtpPacketSendInfo& packetInfo, int64_t nowMs) { MS_TRACE(); @@ -214,14 +215,21 @@ namespace RTC MS_TRACE(); // Update packet loss history. - const size_t expected_packets = feedback->GetPacketStatusCount(); - size_t lost_packets = 0; + const size_t expectedPackets = feedback->GetPacketStatusCount(); + size_t lostPackets = 0; + for (const auto& result : feedback->GetPacketResults()) { if (!result.received) - lost_packets += 1; + { + lostPackets += 1; + } + } + + if (expectedPackets > 0) + { + this->UpdatePacketLoss(static_cast(lostPackets) / expectedPackets); } - this->UpdatePacketLoss(static_cast(lost_packets) / expected_packets); if (this->rtpTransportControllerSend == nullptr) { @@ -233,9 +241,11 @@ namespace RTC void TransportCongestionControlClient::UpdatePacketLoss(double packetLoss) { - // Add the score into the histogram. + // Add the lost into the histogram. if (this->packetLossHistory.size() == PacketLossHistogramLength) + { this->packetLossHistory.pop_front(); + } this->packetLossHistory.push_back(packetLoss); @@ -281,6 +291,16 @@ namespace RTC } } + void TransportCongestionControlClient::SetMinOutgoingBitrate(uint32_t minBitrate) + { + this->minOutgoingBitrate = minBitrate; + + ApplyBitrateUpdates(); + + this->bitrates.minBitrate = std::max( + this->minOutgoingBitrate, RTC::TransportCongestionControlMinOutgoingBitrate); + } + void TransportCongestionControlClient::SetDesiredBitrate(uint32_t desiredBitrate, bool force) { MS_TRACE(); @@ -292,9 +312,13 @@ namespace RTC // Manage it via trending and increase it a bit to avoid immediate oscillations. #ifdef USE_TREND_CALCULATOR if (!force) + { this->desiredBitrateTrend.Update(desiredBitrate, nowMs); + } else + { this->desiredBitrateTrend.ForceUpdate(desiredBitrate, nowMs); + } #endif this->bitrates.desiredBitrate = desiredBitrate; @@ -305,7 +329,9 @@ namespace RTC this->bitrates.effectiveDesiredBitrate = desiredBitrate; #endif - this->bitrates.minBitrate = RTC::TransportCongestionControlMinOutgoingBitrate; + this->bitrates.minBitrate = std::max( + this->minOutgoingBitrate, RTC::TransportCongestionControlMinOutgoingBitrate); + // NOTE: Setting 'startBitrate' to 'availableBitrate' has proven to generate // more stable values. this->bitrates.startBitrate = std::max( @@ -333,9 +359,10 @@ namespace RTC this->bitrates.desiredBitrate * MaxBitrateIncrementFactor); #endif - // If max bitrate requested didn't change by more than a small % keep the previous settings - // to avoid constant small fluctuations requiring extra probing and making the estimation - // less stable (requires constant redistribution of bitrate accross consumers). + // If max bitrate requested didn't change by more than a small % keep the + // previous settings to avoid constant small fluctuations requiring extra + // probing and making the estimation less stable (requires constant + // redistribution of bitrate accross consumers). auto maxBitrateMargin = newMaxBitrate * MaxBitrateMarginFactor; if (currentMaxBitrate > newMaxBitrate - maxBitrateMargin && currentMaxBitrate < newMaxBitrate + maxBitrateMargin) { @@ -358,6 +385,9 @@ namespace RTC this->bitrates.maxBitrate = newMaxBitrate; } + this->bitrates.minBitrate = std::max( + this->minOutgoingBitrate, RTC::TransportCongestionControlMinOutgoingBitrate); + MS_DEBUG_DEV( "[desiredBitrate:%" PRIu32 ", desiredBitrateTrend:%" PRIu32 ", startBitrate:%" PRIu32 ", minBitrate:%" PRIu32 ", maxBitrate:%" PRIu32 ", maxPaddingBitrate:%" PRIu32 "]", @@ -495,9 +525,13 @@ namespace RTC // Update availableBitrate. // NOTE: Just in case. if (targetTransferRate.target_rate.bps() > std::numeric_limits::max()) + { this->bitrates.availableBitrate = std::numeric_limits::max(); + } else + { this->bitrates.availableBitrate = static_cast(targetTransferRate.target_rate.bps()); + } MS_DEBUG_DEV("new available bitrate:%" PRIu32, this->bitrates.availableBitrate); @@ -522,7 +556,7 @@ namespace RTC return this->probationGenerator->GetNextPacket(size); } - void TransportCongestionControlClient::OnTimer(Timer* timer) + void TransportCongestionControlClient::OnTimer(TimerHandle* timer) { MS_TRACE(); diff --git a/worker/src/RTC/TransportCongestionControlServer.cpp b/worker/src/RTC/TransportCongestionControlServer.cpp index d171d1d99d..5beba32038 100644 --- a/worker/src/RTC/TransportCongestionControlServer.cpp +++ b/worker/src/RTC/TransportCongestionControlServer.cpp @@ -14,6 +14,7 @@ namespace RTC static constexpr uint64_t TransportCcFeedbackSendInterval{ 100u }; // In ms. static constexpr uint64_t LimitationRembInterval{ 1500u }; // In ms. + static constexpr uint64_t PacketArrivalTimestampWindow{ 500u }; // In ms. static constexpr uint8_t UnlimitedRembNumPackets{ 4u }; static constexpr size_t PacketLossHistogramLength{ 24 }; @@ -32,13 +33,10 @@ namespace RTC case RTC::BweType::TRANSPORT_CC: { // Create a feedback packet. - this->transportCcFeedbackPacket.reset(new RTC::RTCP::FeedbackRtpTransportPacket(0u, 0u)); - - // Set initial packet count. - this->transportCcFeedbackPacket->SetFeedbackPacketCount(this->transportCcFeedbackPacketCount); + ResetTransportCcFeedback(0u); // Create the feedback send periodic timer. - this->transportCcFeedbackSendPeriodicTimer = new Timer(this); + this->transportCcFeedbackSendPeriodicTimer = new TimerHandle(this); break; } @@ -93,7 +91,7 @@ namespace RTC this->transportCcFeedbackSendPeriodicTimer->Stop(); // Create a new feedback packet. - this->transportCcFeedbackPacket.reset(new RTC::RTCP::FeedbackRtpTransportPacket(0u, 0u)); + ResetTransportCcFeedback(this->transportCcFeedbackPacketCount); break; } @@ -120,63 +118,37 @@ namespace RTC uint16_t wideSeqNumber; if (!packet->ReadTransportWideCc01(wideSeqNumber)) + { break; + } - // Update the RTCP media SSRC of the ongoing Transport-CC Feedback packet. - this->transportCcFeedbackSenderSsrc = 0u; - this->transportCcFeedbackMediaSsrc = packet->GetSsrc(); - - this->transportCcFeedbackPacket->SetSenderSsrc(0u); - this->transportCcFeedbackPacket->SetMediaSsrc(this->transportCcFeedbackMediaSsrc); - - // Provide the feedback packet with the RTP packet info. If it fails, - // send current feedback and add the packet info to a new one. - auto result = - this->transportCcFeedbackPacket->AddPacket(wideSeqNumber, nowMs, this->maxRtcpPacketLen); - - switch (result) + // Only insert the packet when receiving it for the first time. + if (!this->mapPacketArrivalTimes.try_emplace(wideSeqNumber, nowMs).second) { - case RTC::RTCP::FeedbackRtpTransportPacket::AddPacketResult::SUCCESS: - { - // If the feedback packet is full, send it now. - if (this->transportCcFeedbackPacket->IsFull()) - { - MS_DEBUG_DEV("transport-cc feedback packet is full, sending feedback now"); - - SendTransportCcFeedback(); - } - - break; - } - - case RTC::RTCP::FeedbackRtpTransportPacket::AddPacketResult::MAX_SIZE_EXCEEDED: - { - // Send ongoing feedback packet and add the new packet info to the - // regenerated one. - SendTransportCcFeedback(); + break; + } - this->transportCcFeedbackPacket->AddPacket(wideSeqNumber, nowMs, this->maxRtcpPacketLen); + // We may receive packets with sequence number lower than the one in + // previous tcc feedback, these packets may have been reported as lost + // previously, therefore we need to reset the start sequence num for the + // next tcc feedback. + if ( + !this->transportWideSeqNumberReceived || + RTC::SeqManager::IsSeqLowerThan( + wideSeqNumber, this->transportCcFeedbackWideSeqNumStart)) + { + this->transportCcFeedbackWideSeqNumStart = wideSeqNumber; + } - break; - } + this->transportWideSeqNumberReceived = true; - case RTC::RTCP::FeedbackRtpTransportPacket::AddPacketResult::FATAL: - { - // Create a new feedback packet. - this->transportCcFeedbackPacket.reset(new RTC::RTCP::FeedbackRtpTransportPacket( - this->transportCcFeedbackSenderSsrc, this->transportCcFeedbackMediaSsrc)); + MayDropOldPacketArrivalTimes(wideSeqNumber, nowMs); - // Use current packet count. - // NOTE: Do not increment it since the previous ongoing feedback - // packet was not sent. - this->transportCcFeedbackPacket->SetFeedbackPacketCount( - this->transportCcFeedbackPacketCount); - - break; - } - } + // Update the RTCP media SSRC of the ongoing Transport-CC Feedback packet. + this->transportCcFeedbackSenderSsrc = 0u; + this->transportCcFeedbackMediaSsrc = packet->GetSsrc(); - MaySendLimitationRembFeedback(); + MaySendLimitationRembFeedback(nowMs); break; } @@ -186,7 +158,9 @@ namespace RTC uint32_t absSendTime; if (!packet->ReadAbsSendTime(absSendTime)) + { break; + } // NOTE: nowMs is uint64_t but we need to "convert" it to int64_t before // we give it to libwebrtc lib (althought this is implicit in the @@ -200,6 +174,101 @@ namespace RTC } } + void TransportCongestionControlServer::FillAndSendTransportCcFeedback() + { + MS_TRACE(); + + if (!this->transportWideSeqNumberReceived) + { + return; + } + + auto it = this->mapPacketArrivalTimes.lower_bound(this->transportCcFeedbackWideSeqNumStart); + + if (it == this->mapPacketArrivalTimes.end()) + { + return; + } + + for (; it != this->mapPacketArrivalTimes.end(); ++it) + { + auto sequenceNumber = it->first; + auto timestamp = it->second; + + // If the base is not set in this packet let's set it. + // NOTE: This maybe needed many times during this loop since the current + // feedback packet maybe a fresh new one if the previous one was full (so + // already sent) or failed to be built. + if (!this->transportCcFeedbackPacket->IsBaseSet()) + { + // Set base sequence num and reference time. + this->transportCcFeedbackPacket->SetBase(this->transportCcFeedbackWideSeqNumStart, timestamp); + } + + auto result = this->transportCcFeedbackPacket->AddPacket( + sequenceNumber, timestamp, this->maxRtcpPacketLen); + + switch (result) + { + case RTC::RTCP::FeedbackRtpTransportPacket::AddPacketResult::SUCCESS: + { + // If the feedback packet is full, send it now. + if (this->transportCcFeedbackPacket->IsFull()) + { + MS_DEBUG_DEV("transport-cc feedback packet is full, sending feedback now"); + + auto sent = SendTransportCcFeedback(); + + if (sent) + { + ++this->transportCcFeedbackPacketCount; + } + + // Create a new feedback packet. + ResetTransportCcFeedback(this->transportCcFeedbackPacketCount); + } + + break; + } + + case RTC::RTCP::FeedbackRtpTransportPacket::AddPacketResult::MAX_SIZE_EXCEEDED: + { + // This should not happen. + MS_WARN_TAG(rtcp, "transport-cc feedback packet is exceeded"); + + // Create a new feedback packet. + // NOTE: Do not increment packet count it since the previous ongoing + // feedback packet was not sent. + ResetTransportCcFeedback(this->transportCcFeedbackPacketCount); + + break; + } + + case RTC::RTCP::FeedbackRtpTransportPacket::AddPacketResult::FATAL: + { + // Create a new feedback packet. + // NOTE: Do not increment packet count it since the previous ongoing + // feedback packet was not sent. + ResetTransportCcFeedback(this->transportCcFeedbackPacketCount); + + break; + } + } + } + + // It may happen that the packet is empty (no deltas) but in that case + // SendTransportCcFeedback() won't send it so we are safe. + auto sent = SendTransportCcFeedback(); + + if (sent) + { + ++this->transportCcFeedbackPacketCount; + } + + // Create a new feedback packet. + ResetTransportCcFeedback(this->transportCcFeedbackPacketCount); + } + void TransportCongestionControlServer::SetMaxIncomingBitrate(uint32_t bitrate) { MS_TRACE(); @@ -213,59 +282,84 @@ namespace RTC // This is to ensure that we send N REMB packets with bitrate 0 (unlimited). this->unlimitedRembCounter = UnlimitedRembNumPackets; - MaySendLimitationRembFeedback(); + auto nowMs = DepLibUV::GetTimeMs(); + + MaySendLimitationRembFeedback(nowMs); } } - inline void TransportCongestionControlServer::SendTransportCcFeedback() + bool TransportCongestionControlServer::SendTransportCcFeedback() { MS_TRACE(); + this->transportCcFeedbackPacket->Finish(); + if (!this->transportCcFeedbackPacket->IsSerializable()) - return; + { + MS_WARN_TAG(rtcp, "couldn't send feedback-cc packet because it is not serializable"); + + return false; + } auto latestWideSeqNumber = this->transportCcFeedbackPacket->GetLatestSequenceNumber(); - auto latestTimestamp = this->transportCcFeedbackPacket->GetLatestTimestamp(); // Notify the listener. this->listener->OnTransportCongestionControlServerSendRtcpPacket( this, this->transportCcFeedbackPacket.get()); // Update packet loss history. - size_t expected_packets = this->transportCcFeedbackPacket->GetPacketStatusCount(); - size_t lost_packets = 0; + const size_t expectedPackets = this->transportCcFeedbackPacket->GetPacketStatusCount(); + size_t lostPackets = 0; + for (const auto& result : this->transportCcFeedbackPacket->GetPacketResults()) { if (!result.received) - lost_packets += 1; + { + lostPackets += 1; + } } - this->UpdatePacketLoss(static_cast(lost_packets) / expected_packets); + if (expectedPackets > 0) + { + this->UpdatePacketLoss(static_cast(lostPackets) / expectedPackets); + } - // Create a new feedback packet. - this->transportCcFeedbackPacket.reset(new RTC::RTCP::FeedbackRtpTransportPacket( - this->transportCcFeedbackSenderSsrc, this->transportCcFeedbackMediaSsrc)); + this->transportCcFeedbackWideSeqNumStart = latestWideSeqNumber + 1; - // Increment packet count. - this->transportCcFeedbackPacket->SetFeedbackPacketCount(++this->transportCcFeedbackPacketCount); + return true; + } + + void TransportCongestionControlServer::MayDropOldPacketArrivalTimes(uint16_t seqNum, uint64_t nowMs) + { + MS_TRACE(); - // Pass the latest packet info (if any) as pre base for the new feedback packet. - if (latestTimestamp > 0u) + // Ignore nowMs value if it's smaller than PacketArrivalTimestampWindow in + // order to avoid negative values (should never happen) and return early if + // the condition is met. + if (nowMs >= PacketArrivalTimestampWindow) { - this->transportCcFeedbackPacket->AddPacket( - latestWideSeqNumber, latestTimestamp, this->maxRtcpPacketLen); + uint64_t expiryTimestamp = nowMs - PacketArrivalTimestampWindow; + auto it = this->mapPacketArrivalTimes.begin(); + + while (it != this->mapPacketArrivalTimes.end() && + it->first != this->transportCcFeedbackWideSeqNumStart && + RTC::SeqManager::IsSeqLowerThan(it->first, seqNum) && + it->second <= expiryTimestamp) + { + it = this->mapPacketArrivalTimes.erase(it); + } } } - inline void TransportCongestionControlServer::MaySendLimitationRembFeedback() + void TransportCongestionControlServer::MaySendLimitationRembFeedback(uint64_t nowMs) { MS_TRACE(); - auto nowMs = DepLibUV::GetTimeMs(); - // May fix unlimitedRembCounter. if (this->unlimitedRembCounter > 0u && this->maxIncomingBitrate != 0u) + { this->unlimitedRembCounter = 0u; + } // In case this is the first unlimited REMB packet, send it fast. // clang-format off @@ -295,15 +389,19 @@ namespace RTC this->limitationRembSentAtMs = nowMs; if (this->unlimitedRembCounter > 0u) + { this->unlimitedRembCounter--; + } } } void TransportCongestionControlServer::UpdatePacketLoss(double packetLoss) { - // Add the score into the histogram. + // Add the lost into the histogram. if (this->packetLossHistory.size() == PacketLossHistogramLength) + { this->packetLossHistory.pop_front(); + } this->packetLossHistory.push_back(packetLoss); @@ -325,7 +423,17 @@ namespace RTC this->packetLoss = totalPacketLoss / samples; } - inline void TransportCongestionControlServer::OnRembServerAvailableBitrate( + void TransportCongestionControlServer::ResetTransportCcFeedback(uint8_t feedbackPacketCount) + { + MS_TRACE(); + + this->transportCcFeedbackPacket.reset(new RTC::RTCP::FeedbackRtpTransportPacket( + this->transportCcFeedbackSenderSsrc, this->transportCcFeedbackMediaSsrc)); + + this->transportCcFeedbackPacket->SetFeedbackPacketCount(feedbackPacketCount); + } + + void TransportCongestionControlServer::OnRembServerAvailableBitrate( const webrtc::RemoteBitrateEstimator* /*rembServer*/, const std::vector& ssrcs, uint32_t availableBitrate) @@ -334,7 +442,9 @@ namespace RTC // Limit announced bitrate if requested via API. if (this->maxIncomingBitrate != 0u) + { availableBitrate = std::min(availableBitrate, this->maxIncomingBitrate); + } #if MS_LOG_DEV_LEVEL == 3 std::ostringstream ssrcsStream; @@ -361,11 +471,13 @@ namespace RTC this->listener->OnTransportCongestionControlServerSendRtcpPacket(this, &packet); } - inline void TransportCongestionControlServer::OnTimer(Timer* timer) + void TransportCongestionControlServer::OnTimer(TimerHandle* timer) { MS_TRACE(); if (timer == this->transportCcFeedbackSendPeriodicTimer) - SendTransportCcFeedback(); + { + FillAndSendTransportCcFeedback(); + } } } // namespace RTC diff --git a/worker/src/RTC/TransportTuple.cpp b/worker/src/RTC/TransportTuple.cpp index ab8dce24b2..8e59399640 100644 --- a/worker/src/RTC/TransportTuple.cpp +++ b/worker/src/RTC/TransportTuple.cpp @@ -7,46 +7,75 @@ namespace RTC { - /* Instance methods. */ + /* Static methods. */ - void TransportTuple::FillJson(json& jsonObject) const + TransportTuple::Protocol TransportTuple::ProtocolFromFbs(FBS::Transport::Protocol protocol) { MS_TRACE(); - int family; - std::string ip; - uint16_t port; + switch (protocol) + { + case FBS::Transport::Protocol::UDP: + return TransportTuple::Protocol::UDP; - Utils::IP::GetAddressInfo(GetLocalAddress(), family, ip, port); + case FBS::Transport::Protocol::TCP: + return TransportTuple::Protocol::TCP; + } + } - // Add localIp. - if (this->localAnnouncedIp.empty()) - jsonObject["localIp"] = ip; - else - jsonObject["localIp"] = this->localAnnouncedIp; + FBS::Transport::Protocol TransportTuple::ProtocolToFbs(TransportTuple::Protocol protocol) + { + MS_TRACE(); - // Add localPort. - jsonObject["localPort"] = port; + switch (protocol) + { + case TransportTuple::Protocol::UDP: + return FBS::Transport::Protocol::UDP; - Utils::IP::GetAddressInfo(GetRemoteAddress(), family, ip, port); + case TransportTuple::Protocol::TCP: + return FBS::Transport::Protocol::TCP; + } + } - // Add remoteIp. - jsonObject["remoteIp"] = ip; + /* Instance methods. */ - // Add remotePort. - jsonObject["remotePort"] = port; + void TransportTuple::CloseTcpConnection() + { + MS_TRACE(); - // Add protocol. - switch (GetProtocol()) + if (this->protocol == Protocol::UDP) { - case Protocol::UDP: - jsonObject["protocol"] = "udp"; - break; - - case Protocol::TCP: - jsonObject["protocol"] = "tcp"; - break; + MS_ABORT("cannot delete a UDP socket"); } + + this->tcpConnection->TriggerClose(); + } + + flatbuffers::Offset TransportTuple::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const + { + MS_TRACE(); + + int family; + std::string localIp; + uint16_t localPort; + + Utils::IP::GetAddressInfo(GetLocalAddress(), family, localIp, localPort); + + std::string remoteIp; + uint16_t remotePort; + + Utils::IP::GetAddressInfo(GetRemoteAddress(), family, remoteIp, remotePort); + + auto protocol = TransportTuple::ProtocolToFbs(GetProtocol()); + + return FBS::Transport::CreateTupleDirect( + builder, + (this->localAnnouncedAddress.empty() ? localIp : this->localAnnouncedAddress).c_str(), + localPort, + remoteIp.c_str(), + remotePort, + protocol); } void TransportTuple::Dump() const @@ -61,25 +90,98 @@ namespace RTC Utils::IP::GetAddressInfo(GetLocalAddress(), family, ip, port); - MS_DUMP(" localIp : %s", ip.c_str()); - MS_DUMP(" localPort : %" PRIu16, port); + MS_DUMP(" localIp: %s", ip.c_str()); + MS_DUMP(" localPort: %" PRIu16, port); Utils::IP::GetAddressInfo(GetRemoteAddress(), family, ip, port); - MS_DUMP(" remoteIp : %s", ip.c_str()); - MS_DUMP(" remotePort : %" PRIu16, port); + MS_DUMP(" remoteIp: %s", ip.c_str()); + MS_DUMP(" remotePort: %" PRIu16, port); switch (GetProtocol()) { case Protocol::UDP: - MS_DUMP(" protocol : udp"); + MS_DUMP(" protocol: udp"); break; case Protocol::TCP: - MS_DUMP(" protocol : tcp"); + MS_DUMP(" protocol: tcp"); break; } MS_DUMP(""); } + + /* + * Hash for IPv4. + * + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | PORT | IP | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | IP | |F|P| + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * Hash for IPv6. + * + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | PORT | IP[0] ^ IP[1] ^ IP[2] ^ IP[3]| + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * |IP[0] ^ IP[1] ^ IP[2] ^ IP[3] | IP[0] >> 16 |F|P| + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + void TransportTuple::SetHash() + { + MS_TRACE(); + + const struct sockaddr* remoteSockAddr = GetRemoteAddress(); + + switch (remoteSockAddr->sa_family) + { + case AF_INET: + { + const auto* remoteSockAddrIn = reinterpret_cast(remoteSockAddr); + + const uint64_t address = ntohl(remoteSockAddrIn->sin_addr.s_addr); + const uint64_t port = ntohs(remoteSockAddrIn->sin_port); + + this->hash = port << 48; + this->hash |= address << 16; + this->hash |= 0x0000; // AF_INET. + + break; + } + + case AF_INET6: + { + const auto* remoteSockAddrIn6 = reinterpret_cast(remoteSockAddr); + const auto* a = + reinterpret_cast(std::addressof(remoteSockAddrIn6->sin6_addr)); + + const auto address1 = a[0] ^ a[1] ^ a[2] ^ a[3]; + const auto address2 = a[0]; + const uint64_t port = ntohs(remoteSockAddrIn6->sin6_port); + + this->hash = port << 48; + this->hash |= static_cast(address1) << 16; + this->hash |= address2 >> 16 & 0xFFFC; + this->hash |= 0x0002; // AF_INET6. + + break; + } + } + + // Override least significant bit with protocol information: + // - If UDP, start with 0. + // - If TCP, start with 1. + if (this->protocol == Protocol::UDP) + { + this->hash |= 0x0000; + } + else + { + this->hash |= 0x0001; + } + } } // namespace RTC diff --git a/worker/src/RTC/TrendCalculator.cpp b/worker/src/RTC/TrendCalculator.cpp index 4fa757dc1f..356dceb40f 100644 --- a/worker/src/RTC/TrendCalculator.cpp +++ b/worker/src/RTC/TrendCalculator.cpp @@ -34,7 +34,7 @@ namespace RTC // Otherwise decrease current value. else { - uint64_t elapsedMs = nowMs - this->highestValueUpdatedAtMs; + const uint64_t elapsedMs = nowMs - this->highestValueUpdatedAtMs; auto subtraction = static_cast(this->highestValue * this->decreaseFactor * (elapsedMs / 1000.0)); diff --git a/worker/src/RTC/UdpSocket.cpp b/worker/src/RTC/UdpSocket.cpp index e750131f71..5069688baf 100644 --- a/worker/src/RTC/UdpSocket.cpp +++ b/worker/src/RTC/UdpSocket.cpp @@ -10,28 +10,39 @@ namespace RTC { /* Instance methods. */ - UdpSocket::UdpSocket(Listener* listener, std::string& ip) + UdpSocket::UdpSocket( + Listener* listener, std::string& ip, uint16_t port, RTC::Transport::SocketFlags& flags) : // This may throw. - ::UdpSocketHandler::UdpSocketHandler(PortManager::BindUdp(ip)), listener(listener) + ::UdpSocketHandle::UdpSocketHandle(RTC::PortManager::BindUdp(ip, port, flags)), + listener(listener), fixedPort(true) { MS_TRACE(); } - UdpSocket::UdpSocket(Listener* listener, std::string& ip, uint16_t port) + UdpSocket::UdpSocket( + Listener* listener, + std::string& ip, + uint16_t minPort, + uint16_t maxPort, + RTC::Transport::SocketFlags& flags, + uint64_t& portRangeHash) : // This may throw. - ::UdpSocketHandler::UdpSocketHandler(PortManager::BindUdp(ip, port)), listener(listener), - fixedPort(true) + ::UdpSocketHandle::UdpSocketHandle( + RTC::PortManager::BindUdp(ip, minPort, maxPort, flags, portRangeHash)), + listener(listener), fixedPort(false) { MS_TRACE(); + + this->portRangeHash = portRangeHash; } UdpSocket::~UdpSocket() { MS_TRACE(); - if (!fixedPort) + if (!this->fixedPort) { - PortManager::UnbindUdp(this->localIp, this->localPort); + RTC::PortManager::Unbind(this->portRangeHash, this->localPort); } } diff --git a/worker/src/RTC/WebRtcServer.cpp b/worker/src/RTC/WebRtcServer.cpp index 25d315f98f..c088564546 100644 --- a/worker/src/RTC/WebRtcServer.cpp +++ b/worker/src/RTC/WebRtcServer.cpp @@ -4,6 +4,7 @@ #include "RTC/WebRtcServer.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" +#include "Settings.hpp" #include "Utils.hpp" #include // std::pow() @@ -25,115 +26,181 @@ namespace RTC std::pow(2, 0) * (256 - IceComponent); } - /* Instance methods. */ + /* Class methods. */ - WebRtcServer::WebRtcServer(RTC::Shared* shared, const std::string& id, json& data) - : id(id), shared(shared) + inline std::string WebRtcServer::GetLocalIceUsernameFragmentFromReceivedStunPacket( + RTC::StunPacket* packet) { MS_TRACE(); - auto jsonListenInfosIt = data.find("listenInfos"); - - if (jsonListenInfosIt == data.end()) - MS_THROW_TYPE_ERROR("missing listenInfos"); - else if (!jsonListenInfosIt->is_array()) - MS_THROW_TYPE_ERROR("wrong listenInfos (not an array)"); - else if (jsonListenInfosIt->empty()) - MS_THROW_TYPE_ERROR("wrong listenInfos (empty array)"); - else if (jsonListenInfosIt->size() > 8) - MS_THROW_TYPE_ERROR("wrong listenInfos (too many entries)"); + // Here we inspect the USERNAME attribute of a received STUN request and + // extract its remote usernameFragment (the one given to our IceServer as + // local usernameFragment) which is the first value in the attribute value + // before the ":" symbol. - std::vector listenInfos(jsonListenInfosIt->size()); + const auto& username = packet->GetUsername(); + const size_t colonPos = username.find(':'); - for (size_t i{ 0 }; i < jsonListenInfosIt->size(); ++i) + // If no colon is found just return the whole USERNAME attribute anyway. + if (colonPos == std::string::npos) { - auto& jsonListenInfo = (*jsonListenInfosIt)[i]; - auto& listenInfo = listenInfos[i]; - - if (!jsonListenInfo.is_object()) - MS_THROW_TYPE_ERROR("wrong listenInfo (not an object)"); - - auto jsonProtocolIt = jsonListenInfo.find("protocol"); - - if (jsonProtocolIt == jsonListenInfo.end()) - MS_THROW_TYPE_ERROR("missing listenInfo.protocol"); - else if (!jsonProtocolIt->is_string()) - MS_THROW_TYPE_ERROR("wrong listenInfo.protocol (not an string"); - - std::string protocolStr = jsonProtocolIt->get(); - - Utils::String::ToLowerCase(protocolStr); - - if (protocolStr == "udp") - listenInfo.protocol = RTC::TransportTuple::Protocol::UDP; - else if (protocolStr == "tcp") - listenInfo.protocol = RTC::TransportTuple::Protocol::TCP; - else - MS_THROW_TYPE_ERROR("invalid listenInfo.protocol (must be 'udp' or 'tcp'"); - - auto jsonIpIt = jsonListenInfo.find("ip"); + return username; + } - if (jsonIpIt == jsonListenInfo.end()) - MS_THROW_TYPE_ERROR("missing listenInfo.ip"); - else if (!jsonIpIt->is_string()) - MS_THROW_TYPE_ERROR("wrong listenInfo.ip (not an string"); + return username.substr(0, colonPos); + } - listenInfo.ip.assign(jsonIpIt->get()); + /* Instance methods. */ - // This may throw. - Utils::IP::NormalizeIp(listenInfo.ip); + WebRtcServer::WebRtcServer( + RTC::Shared* shared, + const std::string& id, + const flatbuffers::Vector>* listenInfos) + : id(id), shared(shared) + { + MS_TRACE(); - auto jsonAnnouncedIpIt = jsonListenInfo.find("announcedIp"); + if (listenInfos->size() == 0) + { + MS_THROW_TYPE_ERROR("wrong listenInfos (empty array)"); + } + else if (listenInfos->size() > 8) + { + MS_THROW_TYPE_ERROR("wrong listenInfos (too many entries)"); + } - if (jsonAnnouncedIpIt != jsonListenInfo.end()) + try + { + for (const auto* listenInfo : *listenInfos) { - if (!jsonAnnouncedIpIt->is_string()) - MS_THROW_TYPE_ERROR("wrong listenInfo.announcedIp (not an string)"); + auto ip = listenInfo->ip()->str(); - listenInfo.announcedIp.assign(jsonAnnouncedIpIt->get()); - } + // This may throw. + Utils::IP::NormalizeIp(ip); - uint16_t port{ 0 }; - auto jsonPortIt = jsonListenInfo.find("port"); + std::string announcedAddress; - if (jsonPortIt != jsonListenInfo.end()) - { - if (!(jsonPortIt->is_number() && Utils::Json::IsPositiveInteger(*jsonPortIt))) - MS_THROW_TYPE_ERROR("wrong port (not a positive number)"); + if (flatbuffers::IsFieldPresent(listenInfo, FBS::Transport::ListenInfo::VT_ANNOUNCEDADDRESS)) + { + announcedAddress = listenInfo->announcedAddress()->str(); + } - port = jsonPortIt->get(); - } + RTC::Transport::SocketFlags flags; - listenInfo.port = port; - } + flags.ipv6Only = listenInfo->flags()->ipv6Only(); + flags.udpReusePort = listenInfo->flags()->udpReusePort(); - try - { - for (auto& listenInfo : listenInfos) - { - if (listenInfo.protocol == RTC::TransportTuple::Protocol::UDP) + if (listenInfo->protocol() == FBS::Transport::Protocol::UDP) { // This may throw. RTC::UdpSocket* udpSocket; - if (listenInfo.port != 0) - udpSocket = new RTC::UdpSocket(this, listenInfo.ip, listenInfo.port); + if (listenInfo->portRange()->min() != 0 && listenInfo->portRange()->max() != 0) + { + uint64_t portRangeHash{ 0u }; + + udpSocket = new RTC::UdpSocket( + this, + ip, + listenInfo->portRange()->min(), + listenInfo->portRange()->max(), + flags, + portRangeHash); + } + else if (listenInfo->port() != 0) + { + udpSocket = new RTC::UdpSocket(this, ip, listenInfo->port(), flags); + } + // NOTE: This is temporal to allow deprecated usage of worker port range. + // In the future this should throw since |port| or |portRange| will be + // required. else - udpSocket = new RTC::UdpSocket(this, listenInfo.ip); - - this->udpSocketOrTcpServers.emplace_back(udpSocket, nullptr, listenInfo.announcedIp); + { + uint64_t portRangeHash{ 0u }; + + udpSocket = new RTC::UdpSocket( + this, + ip, + Settings::configuration.rtcMinPort, + Settings::configuration.rtcMaxPort, + flags, + portRangeHash); + } + + this->udpSocketOrTcpServers.emplace_back(udpSocket, nullptr, announcedAddress); + + if (listenInfo->sendBufferSize() != 0) + { + udpSocket->SetSendBufferSize(listenInfo->sendBufferSize()); + } + + if (listenInfo->recvBufferSize() != 0) + { + udpSocket->SetRecvBufferSize(listenInfo->recvBufferSize()); + } + + MS_DEBUG_TAG( + info, + "UDP socket send buffer size: %d, recv buffer size: %d", + udpSocket->GetSendBufferSize(), + udpSocket->GetRecvBufferSize()); } - else if (listenInfo.protocol == RTC::TransportTuple::Protocol::TCP) + else if (listenInfo->protocol() == FBS::Transport::Protocol::TCP) { // This may throw. RTC::TcpServer* tcpServer; - if (listenInfo.port != 0) - tcpServer = new RTC::TcpServer(this, this, listenInfo.ip, listenInfo.port); + if (listenInfo->portRange()->min() != 0 && listenInfo->portRange()->max() != 0) + { + uint64_t portRangeHash{ 0u }; + + tcpServer = new RTC::TcpServer( + this, + this, + ip, + listenInfo->portRange()->min(), + listenInfo->portRange()->max(), + flags, + portRangeHash); + } + else if (listenInfo->port() != 0) + { + tcpServer = new RTC::TcpServer(this, this, ip, listenInfo->port(), flags); + } + // NOTE: This is temporal to allow deprecated usage of worker port range. + // In the future this should throw since |port| or |portRange| will be + // required. else - tcpServer = new RTC::TcpServer(this, this, listenInfo.ip); - - this->udpSocketOrTcpServers.emplace_back(nullptr, tcpServer, listenInfo.announcedIp); + { + uint64_t portRangeHash{ 0u }; + + tcpServer = new RTC::TcpServer( + this, + this, + ip, + Settings::configuration.rtcMinPort, + Settings::configuration.rtcMaxPort, + flags, + portRangeHash); + } + + this->udpSocketOrTcpServers.emplace_back(nullptr, tcpServer, announcedAddress); + + if (listenInfo->sendBufferSize() != 0) + { + tcpServer->SetSendBufferSize(listenInfo->sendBufferSize()); + } + + if (listenInfo->recvBufferSize() != 0) + { + tcpServer->SetRecvBufferSize(listenInfo->recvBufferSize()); + } + + MS_DEBUG_TAG( + info, + "TCP server send buffer size: %d, recv buffer size: %d", + tcpServer->GetSendBufferSize(), + tcpServer->GetRecvBufferSize()); } } @@ -141,8 +208,7 @@ namespace RTC this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ nullptr, - /*payloadChannelNotificationHandler*/ nullptr); + /*channelNotificationHandler*/ nullptr); } catch (const MediaSoupError& error) { @@ -166,8 +232,19 @@ namespace RTC { MS_TRACE(); + this->closing = true; + this->shared->channelMessageRegistrator->UnregisterHandler(this->id); + // NOTE: We need to close WebRtcTransports first since they may need to + // send DTLS Close Alert so UDP sockets and TCP connections must remain + // open. + for (auto* webRtcTransport : this->webRtcTransports) + { + webRtcTransport->ListenServerClosed(); + } + this->webRtcTransports.clear(); + for (auto& item : this->udpSocketOrTcpServers) { delete item.udpSocket; @@ -177,169 +254,146 @@ namespace RTC item.tcpServer = nullptr; } this->udpSocketOrTcpServers.clear(); - - for (auto* webRtcTransport : this->webRtcTransports) - { - webRtcTransport->ListenServerClosed(); - } - this->webRtcTransports.clear(); } - void WebRtcServer::FillJson(json& jsonObject) const + flatbuffers::Offset WebRtcServer::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Add id. - jsonObject["id"] = this->id; - // Add udpSockets and tcpServers. - jsonObject["udpSockets"] = json::array(); - auto jsonUdpSocketsIt = jsonObject.find("udpSockets"); - jsonObject["tcpServers"] = json::array(); - auto jsonTcpServersIt = jsonObject.find("tcpServers"); - - size_t udpSocketIdx{ 0 }; - size_t tcpServerIdx{ 0 }; + std::vector> udpSockets; + std::vector> tcpServers; - for (auto& item : this->udpSocketOrTcpServers) + for (const auto& item : this->udpSocketOrTcpServers) { if (item.udpSocket) { - jsonUdpSocketsIt->emplace_back(json::value_t::object); - - auto& jsonEntry = (*jsonUdpSocketsIt)[udpSocketIdx]; - - jsonEntry["ip"] = item.udpSocket->GetLocalIp(); - jsonEntry["port"] = item.udpSocket->GetLocalPort(); - - ++udpSocketIdx; + udpSockets.emplace_back(FBS::WebRtcServer::CreateIpPortDirect( + builder, item.udpSocket->GetLocalIp().c_str(), item.udpSocket->GetLocalPort())); } else if (item.tcpServer) { - jsonTcpServersIt->emplace_back(json::value_t::object); - - auto& jsonEntry = (*jsonTcpServersIt)[tcpServerIdx]; - - jsonEntry["ip"] = item.tcpServer->GetLocalIp(); - jsonEntry["port"] = item.tcpServer->GetLocalPort(); - - ++tcpServerIdx; + tcpServers.emplace_back(FBS::WebRtcServer::CreateIpPortDirect( + builder, item.tcpServer->GetLocalIp().c_str(), item.tcpServer->GetLocalPort())); } } // Add webRtcTransportIds. - jsonObject["webRtcTransportIds"] = json::array(); - auto jsonWebRtcTransportIdsIt = jsonObject.find("webRtcTransportIds"); + std::vector> webRtcTransportIds; for (auto* webRtcTransport : this->webRtcTransports) { - jsonWebRtcTransportIdsIt->emplace_back(webRtcTransport->id); + webRtcTransportIds.emplace_back(builder.CreateString(webRtcTransport->id)); } - size_t idx; - // Add localIceUsernameFragments. - jsonObject["localIceUsernameFragments"] = json::array(); - auto jsonLocalIceUsernamesIt = jsonObject.find("localIceUsernameFragments"); + std::vector> localIceUsernameFragments; - idx = 0; - for (auto& kv : this->mapLocalIceUsernameFragmentWebRtcTransport) + for (const auto& kv : this->mapLocalIceUsernameFragmentWebRtcTransport) { const auto& localIceUsernameFragment = kv.first; const auto* webRtcTransport = kv.second; - jsonLocalIceUsernamesIt->emplace_back(json::value_t::object); - - auto& jsonEntry = (*jsonLocalIceUsernamesIt)[idx]; - - jsonEntry["localIceUsernameFragment"] = localIceUsernameFragment; - jsonEntry["webRtcTransportId"] = webRtcTransport->id; - - ++idx; + localIceUsernameFragments.emplace_back(FBS::WebRtcServer::CreateIceUserNameFragmentDirect( + builder, localIceUsernameFragment.c_str(), webRtcTransport->id.c_str())); } // Add tupleHashes. - jsonObject["tupleHashes"] = json::array(); - auto jsonTupleHashesIt = jsonObject.find("tupleHashes"); + std::vector> tupleHashes; - idx = 0; - for (auto& kv : this->mapTupleWebRtcTransport) + for (const auto& kv : this->mapTupleWebRtcTransport) { const auto& tupleHash = kv.first; const auto* webRtcTransport = kv.second; - jsonTupleHashesIt->emplace_back(json::value_t::object); - - auto& jsonEntry = (*jsonTupleHashesIt)[idx]; - - jsonEntry["tupleHash"] = tupleHash; - jsonEntry["webRtcTransportId"] = webRtcTransport->id; - - ++idx; + tupleHashes.emplace_back( + FBS::WebRtcServer::CreateTupleHashDirect(builder, tupleHash, webRtcTransport->id.c_str())); } + + return FBS::WebRtcServer::CreateDumpResponseDirect( + builder, + this->id.c_str(), + &udpSockets, + &tcpServers, + &webRtcTransportIds, + &localIceUsernameFragments, + &tupleHashes); } void WebRtcServer::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::WEBRTC_SERVER_DUMP: + case Channel::ChannelRequest::Method::WEBRTCSERVER_DUMP: { - json data = json::object(); - - FillJson(data); + auto dumpOffset = FillBuffer(request->GetBufferBuilder()); - request->Accept(data); + request->Accept(FBS::Response::Body::WebRtcServer_DumpResponse, dumpOffset); break; } default: { - MS_THROW_ERROR("unknown method '%s'", request->method.c_str()); + MS_THROW_ERROR("unknown method '%s'", request->methodCStr); } } } std::vector WebRtcServer::GetIceCandidates( - bool enableUdp, bool enableTcp, bool preferUdp, bool preferTcp) + bool enableUdp, bool enableTcp, bool preferUdp, bool preferTcp) const { MS_TRACE(); std::vector iceCandidates; uint16_t iceLocalPreferenceDecrement{ 0 }; - for (auto& item : this->udpSocketOrTcpServers) + for (const auto& item : this->udpSocketOrTcpServers) { if (item.udpSocket && enableUdp) { uint16_t iceLocalPreference = IceCandidateDefaultLocalPriority - iceLocalPreferenceDecrement; if (preferUdp) + { iceLocalPreference += 1000; + } - uint32_t icePriority = generateIceCandidatePriority(iceLocalPreference); + const uint32_t icePriority = generateIceCandidatePriority(iceLocalPreference); - if (item.announcedIp.empty()) + if (item.announcedAddress.empty()) + { iceCandidates.emplace_back(item.udpSocket, icePriority); + } else - iceCandidates.emplace_back(item.udpSocket, icePriority, item.announcedIp); + { + iceCandidates.emplace_back( + item.udpSocket, icePriority, const_cast(item.announcedAddress)); + } } else if (item.tcpServer && enableTcp) { uint16_t iceLocalPreference = IceCandidateDefaultLocalPriority - iceLocalPreferenceDecrement; if (preferTcp) + { iceLocalPreference += 1000; + } - uint32_t icePriority = generateIceCandidatePriority(iceLocalPreference); + const uint32_t icePriority = generateIceCandidatePriority(iceLocalPreference); - if (item.announcedIp.empty()) + if (item.announcedAddress.empty()) + { iceCandidates.emplace_back(item.tcpServer, icePriority); + } else - iceCandidates.emplace_back(item.tcpServer, icePriority, item.announcedIp); + { + iceCandidates.emplace_back( + item.tcpServer, icePriority, const_cast(item.announcedAddress)); + } } // Decrement initial ICE local preference for next IP. @@ -349,26 +403,6 @@ namespace RTC return iceCandidates; } - inline std::string WebRtcServer::GetLocalIceUsernameFragmentFromReceivedStunPacket( - RTC::StunPacket* packet) const - { - MS_TRACE(); - - // Here we inspect the USERNAME attribute of a received STUN request and - // extract its remote usernameFragment (the one given to our IceServer as - // local usernameFragment) which is the first value in the attribute value - // before the ":" symbol. - - auto& username = packet->GetUsername(); - size_t colonPos = username.find(':'); - - // If no colon is found just return the whole USERNAME attribute anyway. - if (colonPos == std::string::npos) - return username; - - return username.substr(0, colonPos); - } - inline void WebRtcServer::OnPacketReceived(RTC::TransportTuple* tuple, const uint8_t* data, size_t len) { MS_TRACE(); @@ -411,7 +445,7 @@ namespace RTC } // Otherwise try to match the local ICE username fragment. - auto key = GetLocalIceUsernameFragmentFromReceivedStunPacket(packet); + auto key = WebRtcServer::GetLocalIceUsernameFragmentFromReceivedStunPacket(packet); auto it2 = this->mapLocalIceUsernameFragmentWebRtcTransport.find(key); if (it2 == this->mapLocalIceUsernameFragmentWebRtcTransport.end()) @@ -468,7 +502,14 @@ namespace RTC this->webRtcTransports.find(webRtcTransport) != this->webRtcTransports.end(), "WebRtcTransport not handled"); - this->webRtcTransports.erase(webRtcTransport); + // NOTE: If WebRtcServer is closing then do not remove the transport from + // the set since it would modify the set while the WebRtcServer destructor + // is iterating it. + // See: https://github.com/versatica/mediasoup/pull/1369#issuecomment-2044672247 + if (!this->closing) + { + this->webRtcTransports.erase(webRtcTransport); + } } inline void WebRtcServer::OnWebRtcTransportLocalIceUsernameFragmentAdded( @@ -485,7 +526,7 @@ namespace RTC } inline void WebRtcServer::OnWebRtcTransportLocalIceUsernameFragmentRemoved( - RTC::WebRtcTransport* webRtcTransport, const std::string& usernameFragment) + RTC::WebRtcTransport* /*webRtcTransport*/, const std::string& usernameFragment) { MS_TRACE(); @@ -513,13 +554,13 @@ namespace RTC } inline void WebRtcServer::OnWebRtcTransportTransportTupleRemoved( - RTC::WebRtcTransport* webRtcTransport, RTC::TransportTuple* tuple) + RTC::WebRtcTransport* /*webRtcTransport*/, RTC::TransportTuple* tuple) { MS_TRACE(); if (this->mapTupleWebRtcTransport.find(tuple->hash) == this->mapTupleWebRtcTransport.end()) { - MS_WARN_TAG(ice, "tuple hash not found in the table"); + MS_DEBUG_TAG(ice, "tuple hash not found in the table"); return; } @@ -551,7 +592,9 @@ namespace RTC auto it = this->mapTupleWebRtcTransport.find(tuple.hash); if (it == this->mapTupleWebRtcTransport.end()) + { return; + } auto* webRtcTransport = it->second; diff --git a/worker/src/RTC/WebRtcTransport.cpp b/worker/src/RTC/WebRtcTransport.cpp index b3ce40ff55..c25ccc8430 100644 --- a/worker/src/RTC/WebRtcTransport.cpp +++ b/worker/src/RTC/WebRtcTransport.cpp @@ -4,7 +4,9 @@ #include "RTC/WebRtcTransport.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" +#include "Settings.hpp" #include "Utils.hpp" +#include "FBS/webRtcTransport.h" #include // std::pow() namespace RTC @@ -27,179 +29,191 @@ namespace RTC /* Instance methods. */ + /** + * This constructor is used when the WebRtcTransport doesn't use a WebRtcServer. + */ WebRtcTransport::WebRtcTransport( - RTC::Shared* shared, const std::string& id, RTC::Transport::Listener* listener, json& data) - : RTC::Transport::Transport(shared, id, listener, data) + RTC::Shared* shared, + const std::string& id, + RTC::Transport::Listener* listener, + const FBS::WebRtcTransport::WebRtcTransportOptions* options) + : RTC::Transport::Transport(shared, id, listener, options->base()) { MS_TRACE(); - bool enableUdp{ true }; - auto jsonEnableUdpIt = data.find("enableUdp"); - - if (jsonEnableUdpIt != data.end()) - { - if (!jsonEnableUdpIt->is_boolean()) - MS_THROW_TYPE_ERROR("wrong enableUdp (not a boolean)"); - - enableUdp = jsonEnableUdpIt->get(); - } - - bool enableTcp{ false }; - auto jsonEnableTcpIt = data.find("enableTcp"); - - if (jsonEnableTcpIt != data.end()) - { - if (!jsonEnableTcpIt->is_boolean()) - MS_THROW_TYPE_ERROR("wrong enableTcp (not a boolean)"); - - enableTcp = jsonEnableTcpIt->get(); - } - - bool preferUdp{ false }; - auto jsonPreferUdpIt = data.find("preferUdp"); - - if (jsonPreferUdpIt != data.end()) - { - if (!jsonPreferUdpIt->is_boolean()) - MS_THROW_TYPE_ERROR("wrong preferUdp (not a boolean)"); - - preferUdp = jsonPreferUdpIt->get(); - } - - bool preferTcp{ false }; - auto jsonPreferTcpIt = data.find("preferTcp"); - - if (jsonPreferTcpIt != data.end()) - { - if (!jsonPreferTcpIt->is_boolean()) - MS_THROW_TYPE_ERROR("wrong preferTcp (not a boolean)"); - - preferTcp = jsonPreferTcpIt->get(); - } - - auto jsonListenIpsIt = data.find("listenIps"); - - if (jsonListenIpsIt == data.end()) - MS_THROW_TYPE_ERROR("missing listenIps"); - else if (!jsonListenIpsIt->is_array()) - MS_THROW_TYPE_ERROR("wrong listenIps (not an array)"); - else if (jsonListenIpsIt->empty()) - MS_THROW_TYPE_ERROR("wrong listenIps (empty array)"); - else if (jsonListenIpsIt->size() > 8) - MS_THROW_TYPE_ERROR("wrong listenIps (too many IPs)"); - - std::vector listenIps(jsonListenIpsIt->size()); - - for (size_t i{ 0 }; i < jsonListenIpsIt->size(); ++i) + try { - auto& jsonListenIp = (*jsonListenIpsIt)[i]; - auto& listenIp = listenIps[i]; + const auto* listenIndividual = options->listen_as(); + const auto* listenInfos = listenIndividual->listenInfos(); + uint16_t iceLocalPreferenceDecrement{ 0u }; - if (!jsonListenIp.is_object()) - MS_THROW_TYPE_ERROR("wrong listenIp (not an object)"); + this->iceCandidates.reserve(listenInfos->size()); - auto jsonIpIt = jsonListenIp.find("ip"); - - if (jsonIpIt == jsonListenIp.end()) - MS_THROW_TYPE_ERROR("missing listenIp.ip"); - else if (!jsonIpIt->is_string()) - MS_THROW_TYPE_ERROR("wrong listenIp.ip (not an string"); - - listenIp.ip.assign(jsonIpIt->get()); - - // This may throw. - Utils::IP::NormalizeIp(listenIp.ip); - - auto jsonAnnouncedIpIt = jsonListenIp.find("announcedIp"); - - if (jsonAnnouncedIpIt != jsonListenIp.end()) + for (const auto* listenInfo : *listenInfos) { - if (!jsonAnnouncedIpIt->is_string()) - MS_THROW_TYPE_ERROR("wrong listenIp.announcedIp (not an string)"); + auto ip = listenInfo->ip()->str(); - listenIp.announcedIp.assign(jsonAnnouncedIpIt->get()); - } - } + // This may throw. + Utils::IP::NormalizeIp(ip); - uint16_t port{ 0 }; - auto jsonPortIt = data.find("port"); + std::string announcedAddress; - if (jsonPortIt != data.end()) - { - if (!(jsonPortIt->is_number() && Utils::Json::IsPositiveInteger(*jsonPortIt))) - MS_THROW_TYPE_ERROR("wrong port (not a positive number)"); + if (flatbuffers::IsFieldPresent(listenInfo, FBS::Transport::ListenInfo::VT_ANNOUNCEDADDRESS)) + { + announcedAddress = listenInfo->announcedAddress()->str(); + } - port = jsonPortIt->get(); - } + RTC::Transport::SocketFlags flags; - try - { - uint16_t iceLocalPreferenceDecrement{ 0 }; + flags.ipv6Only = listenInfo->flags()->ipv6Only(); + flags.udpReusePort = listenInfo->flags()->udpReusePort(); - if (enableUdp && enableTcp) - this->iceCandidates.reserve(2 * jsonListenIpsIt->size()); - else - this->iceCandidates.reserve(jsonListenIpsIt->size()); + const uint16_t iceLocalPreference = + IceCandidateDefaultLocalPriority - iceLocalPreferenceDecrement; + const uint32_t icePriority = generateIceCandidatePriority(iceLocalPreference); - for (auto& listenIp : listenIps) - { - if (enableUdp) + if (listenInfo->protocol() == FBS::Transport::Protocol::UDP) { - uint16_t iceLocalPreference = - IceCandidateDefaultLocalPriority - iceLocalPreferenceDecrement; - - if (preferUdp) - iceLocalPreference += 1000; - - uint32_t icePriority = generateIceCandidatePriority(iceLocalPreference); - - // This may throw. RTC::UdpSocket* udpSocket; - if (port != 0) - udpSocket = new RTC::UdpSocket(this, listenIp.ip, port); + + if (listenInfo->portRange()->min() != 0 && listenInfo->portRange()->max() != 0) + { + uint64_t portRangeHash{ 0u }; + + udpSocket = new RTC::UdpSocket( + this, + ip, + listenInfo->portRange()->min(), + listenInfo->portRange()->max(), + flags, + portRangeHash); + } + else if (listenInfo->port() != 0) + { + udpSocket = new RTC::UdpSocket(this, ip, listenInfo->port(), flags); + } + // NOTE: This is temporal to allow deprecated usage of worker port range. + // In the future this should throw since |port| or |portRange| will be + // required. else - udpSocket = new RTC::UdpSocket(this, listenIp.ip); + { + uint64_t portRangeHash{ 0u }; + + udpSocket = new RTC::UdpSocket( + this, + ip, + Settings::configuration.rtcMinPort, + Settings::configuration.rtcMaxPort, + flags, + portRangeHash); + } - this->udpSockets[udpSocket] = listenIp.announcedIp; + this->udpSockets[udpSocket] = announcedAddress; - if (listenIp.announcedIp.empty()) + if (announcedAddress.empty()) + { this->iceCandidates.emplace_back(udpSocket, icePriority); + } else - this->iceCandidates.emplace_back(udpSocket, icePriority, listenIp.announcedIp); - } - - if (enableTcp) - { - uint16_t iceLocalPreference = - IceCandidateDefaultLocalPriority - iceLocalPreferenceDecrement; + { + this->iceCandidates.emplace_back(udpSocket, icePriority, announcedAddress); + } - if (preferTcp) - iceLocalPreference += 1000; + if (listenInfo->sendBufferSize() != 0) + { + // NOTE: This may throw. + udpSocket->SetSendBufferSize(listenInfo->sendBufferSize()); + } - uint32_t icePriority = generateIceCandidatePriority(iceLocalPreference); + if (listenInfo->recvBufferSize() != 0) + { + // NOTE: This may throw. + udpSocket->SetRecvBufferSize(listenInfo->recvBufferSize()); + } - // This may throw. + MS_DEBUG_TAG( + info, + "UDP socket buffer sizes [send:%" PRIu32 ", recv:%" PRIu32 "]", + udpSocket->GetSendBufferSize(), + udpSocket->GetRecvBufferSize()); + } + else if (listenInfo->protocol() == FBS::Transport::Protocol::TCP) + { RTC::TcpServer* tcpServer; - if (port != 0) - tcpServer = new RTC::TcpServer(this, this, listenIp.ip, port); + + if (listenInfo->portRange()->min() != 0 && listenInfo->portRange()->max() != 0) + { + uint64_t portRangeHash{ 0u }; + + tcpServer = new RTC::TcpServer( + this, + this, + ip, + listenInfo->portRange()->min(), + listenInfo->portRange()->max(), + flags, + portRangeHash); + } + else if (listenInfo->port() != 0) + { + tcpServer = new RTC::TcpServer(this, this, ip, listenInfo->port(), flags); + } + // NOTE: This is temporal to allow deprecated usage of worker port range. + // In the future this should throw since |port| or |portRange| will be + // required. else - tcpServer = new RTC::TcpServer(this, this, listenIp.ip); + { + uint64_t portRangeHash{ 0u }; + + tcpServer = new RTC::TcpServer( + this, + this, + ip, + Settings::configuration.rtcMinPort, + Settings::configuration.rtcMaxPort, + flags, + portRangeHash); + } - this->tcpServers[tcpServer] = listenIp.announcedIp; + this->tcpServers[tcpServer] = announcedAddress; - if (listenIp.announcedIp.empty()) + if (announcedAddress.empty()) + { this->iceCandidates.emplace_back(tcpServer, icePriority); + } else - this->iceCandidates.emplace_back(tcpServer, icePriority, listenIp.announcedIp); + { + this->iceCandidates.emplace_back(tcpServer, icePriority, announcedAddress); + } + + if (listenInfo->sendBufferSize() != 0) + { + // NOTE: This may throw. + tcpServer->SetSendBufferSize(listenInfo->sendBufferSize()); + } + + if (listenInfo->recvBufferSize() != 0) + { + // NOTE: This may throw. + tcpServer->SetRecvBufferSize(listenInfo->recvBufferSize()); + } + + MS_DEBUG_TAG( + info, + "TCP sockets buffer sizes [send:%" PRIu32 ", recv:%" PRIu32 "]", + tcpServer->GetSendBufferSize(), + tcpServer->GetRecvBufferSize()); } // Decrement initial ICE local preference for next IP. iceLocalPreferenceDecrement += 100; } + auto iceConsentTimeout = options->iceConsentTimeout(); + // Create a ICE server. this->iceServer = new RTC::IceServer( - this, Utils::Crypto::GetRandomString(32), Utils::Crypto::GetRandomString(32)); + this, Utils::Crypto::GetRandomString(32), Utils::Crypto::GetRandomString(32), iceConsentTimeout); // Create a DTLS transport. this->dtlsTransport = new RTC::DtlsTransport(this); @@ -208,8 +222,7 @@ namespace RTC this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ this, - /*payloadChannelNotificationHandler*/ this); + /*channelNotificationHandler*/ this); } catch (const MediaSoupError& error) { @@ -251,9 +264,9 @@ namespace RTC const std::string& id, RTC::Transport::Listener* listener, WebRtcTransportListener* webRtcTransportListener, - std::vector& iceCandidates, - json& data) - : RTC::Transport::Transport(shared, id, listener, data), + const std::vector& iceCandidates, + const FBS::WebRtcTransport::WebRtcTransportOptions* options) + : RTC::Transport::Transport(shared, id, listener, options->base()), webRtcTransportListener(webRtcTransportListener), iceCandidates(iceCandidates) { MS_TRACE(); @@ -261,11 +274,15 @@ namespace RTC try { if (iceCandidates.empty()) + { MS_THROW_TYPE_ERROR("empty iceCandidates"); + } + + auto iceConsentTimeout = options->iceConsentTimeout(); // Create a ICE server. this->iceServer = new RTC::IceServer( - this, Utils::Crypto::GetRandomString(32), Utils::Crypto::GetRandomString(32)); + this, Utils::Crypto::GetRandomString(32), Utils::Crypto::GetRandomString(32), iceConsentTimeout); // Create a DTLS transport. this->dtlsTransport = new RTC::DtlsTransport(this); @@ -277,8 +294,7 @@ namespace RTC this->shared->channelMessageRegistrator->RegisterHandler( this->id, /*channelRequestHandler*/ this, - /*payloadChannelRequestHandler*/ this, - /*payloadChannelNotificationHandler*/ this); + /*channelNotificationHandler*/ this); } catch (const MediaSoupError& error) { @@ -290,8 +306,6 @@ namespace RTC delete this->iceServer; this->iceServer = nullptr; - this->iceCandidates.clear(); - throw; } } @@ -300,6 +314,11 @@ namespace RTC { MS_TRACE(); + // We need to tell the Transport parent class that we are about to destroy + // the class instance. This is because child's destructor runs before + // parent's destructor. See comment in Transport::OnSctpAssociationSendData(). + Destroying(); + this->shared->channelMessageRegistrator->UnregisterHandler(this->id); // Must delete the DTLS transport first since it will generate a DTLS alert @@ -336,261 +355,163 @@ namespace RTC // Notify the webRtcTransportListener. if (this->webRtcTransportListener) + { this->webRtcTransportListener->OnWebRtcTransportClosed(this); + } } - void WebRtcTransport::FillJson(json& jsonObject) const + flatbuffers::Offset WebRtcTransport::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); - // Call the parent method. - RTC::Transport::FillJson(jsonObject); - - // Add iceRole (we are always "controlled"). - jsonObject["iceRole"] = "controlled"; - // Add iceParameters. - jsonObject["iceParameters"] = json::object(); - auto jsonIceParametersIt = jsonObject.find("iceParameters"); - - (*jsonIceParametersIt)["usernameFragment"] = this->iceServer->GetUsernameFragment(); - (*jsonIceParametersIt)["password"] = this->iceServer->GetPassword(); - (*jsonIceParametersIt)["iceLite"] = true; + auto iceParameters = FBS::WebRtcTransport::CreateIceParametersDirect( + builder, + this->iceServer->GetUsernameFragment().c_str(), + this->iceServer->GetPassword().c_str(), + true); - // Add iceCandidates. - jsonObject["iceCandidates"] = json::array(); - auto jsonIceCandidatesIt = jsonObject.find("iceCandidates"); + std::vector> iceCandidates; + iceCandidates.reserve(this->iceCandidates.size()); - for (size_t i{ 0 }; i < this->iceCandidates.size(); ++i) + for (const auto& iceCandidate : this->iceCandidates) { - jsonIceCandidatesIt->emplace_back(json::value_t::object); - - auto& jsonEntry = (*jsonIceCandidatesIt)[i]; - auto& iceCandidate = this->iceCandidates[i]; - - iceCandidate.FillJson(jsonEntry); + iceCandidates.emplace_back(iceCandidate.FillBuffer(builder)); } // Add iceState. - switch (this->iceServer->GetState()) - { - case RTC::IceServer::IceState::NEW: - jsonObject["iceState"] = "new"; - break; - case RTC::IceServer::IceState::CONNECTED: - jsonObject["iceState"] = "connected"; - break; - case RTC::IceServer::IceState::COMPLETED: - jsonObject["iceState"] = "completed"; - break; - case RTC::IceServer::IceState::DISCONNECTED: - jsonObject["iceState"] = "disconnected"; - break; - } + auto iceState = RTC::IceServer::IceStateToFbs(this->iceServer->GetState()); // Add iceSelectedTuple. - if (this->iceServer->GetSelectedTuple()) - this->iceServer->GetSelectedTuple()->FillJson(jsonObject["iceSelectedTuple"]); + flatbuffers::Offset iceSelectedTuple; - // Add dtlsParameters. - jsonObject["dtlsParameters"] = json::object(); - auto jsonDtlsParametersIt = jsonObject.find("dtlsParameters"); + if (this->iceServer->GetSelectedTuple()) + { + iceSelectedTuple = this->iceServer->GetSelectedTuple()->FillBuffer(builder); + } // Add dtlsParameters.fingerprints. - (*jsonDtlsParametersIt)["fingerprints"] = json::array(); - auto jsonDtlsParametersFingerprintsIt = jsonDtlsParametersIt->find("fingerprints"); - auto& fingerprints = this->dtlsTransport->GetLocalFingerprints(); + std::vector> fingerprints; - for (size_t i{ 0 }; i < fingerprints.size(); ++i) + for (const auto& fingerprint : RTC::DtlsTransport::GetLocalFingerprints()) { - jsonDtlsParametersFingerprintsIt->emplace_back(json::value_t::object); - - auto& jsonEntry = (*jsonDtlsParametersFingerprintsIt)[i]; - auto& fingerprint = fingerprints[i]; + auto algorithm = DtlsTransport::AlgorithmToFbs(fingerprint.algorithm); + const auto& value = fingerprint.value; - jsonEntry["algorithm"] = - RTC::DtlsTransport::GetFingerprintAlgorithmString(fingerprint.algorithm); - jsonEntry["value"] = fingerprint.value; + fingerprints.emplace_back( + FBS::WebRtcTransport::CreateFingerprintDirect(builder, algorithm, value.c_str())); } // Add dtlsParameters.role. - switch (this->dtlsRole) - { - case RTC::DtlsTransport::Role::NONE: - (*jsonDtlsParametersIt)["role"] = "none"; - break; - case RTC::DtlsTransport::Role::AUTO: - (*jsonDtlsParametersIt)["role"] = "auto"; - break; - case RTC::DtlsTransport::Role::CLIENT: - (*jsonDtlsParametersIt)["role"] = "client"; - break; - case RTC::DtlsTransport::Role::SERVER: - (*jsonDtlsParametersIt)["role"] = "server"; - break; - } + auto dtlsRole = DtlsTransport::RoleToFbs(this->dtlsRole); + auto dtlsState = DtlsTransport::StateToFbs(this->dtlsTransport->GetState()); - // Add dtlsState. - switch (this->dtlsTransport->GetState()) - { - case RTC::DtlsTransport::DtlsState::NEW: - jsonObject["dtlsState"] = "new"; - break; - case RTC::DtlsTransport::DtlsState::CONNECTING: - jsonObject["dtlsState"] = "connecting"; - break; - case RTC::DtlsTransport::DtlsState::CONNECTED: - jsonObject["dtlsState"] = "connected"; - break; - case RTC::DtlsTransport::DtlsState::FAILED: - jsonObject["dtlsState"] = "failed"; - break; - case RTC::DtlsTransport::DtlsState::CLOSED: - jsonObject["dtlsState"] = "closed"; - break; - } + // Add base transport dump. + auto base = Transport::FillBuffer(builder); + // Add dtlsParameters. + auto dtlsParameters = + FBS::WebRtcTransport::CreateDtlsParametersDirect(builder, &fingerprints, dtlsRole); + + return FBS::WebRtcTransport::CreateDumpResponseDirect( + builder, + base, + FBS::WebRtcTransport::IceRole::CONTROLLED, + iceParameters, + &iceCandidates, + iceState, + iceSelectedTuple, + dtlsParameters, + dtlsState); } - void WebRtcTransport::FillJsonStats(json& jsonArray) + flatbuffers::Offset WebRtcTransport::FillBufferStats( + flatbuffers::FlatBufferBuilder& builder) { MS_TRACE(); - // Call the parent method. - RTC::Transport::FillJsonStats(jsonArray); - - auto& jsonObject = jsonArray[0]; - - // Add type. - jsonObject["type"] = "webrtc-transport"; - - // Add iceRole (we are always "controlled"). - jsonObject["iceRole"] = "controlled"; - // Add iceState. - switch (this->iceServer->GetState()) - { - case RTC::IceServer::IceState::NEW: - jsonObject["iceState"] = "new"; - break; - case RTC::IceServer::IceState::CONNECTED: - jsonObject["iceState"] = "connected"; - break; - case RTC::IceServer::IceState::COMPLETED: - jsonObject["iceState"] = "completed"; - break; - case RTC::IceServer::IceState::DISCONNECTED: - jsonObject["iceState"] = "disconnected"; - break; - } + auto iceState = RTC::IceServer::IceStateToFbs(this->iceServer->GetState()); + + // Add iceSelectedTuple. + flatbuffers::Offset iceSelectedTuple; if (this->iceServer->GetSelectedTuple()) { - // Add iceSelectedTuple. - this->iceServer->GetSelectedTuple()->FillJson(jsonObject["iceSelectedTuple"]); + iceSelectedTuple = this->iceServer->GetSelectedTuple()->FillBuffer(builder); } - // Add dtlsState. - switch (this->dtlsTransport->GetState()) - { - case RTC::DtlsTransport::DtlsState::NEW: - jsonObject["dtlsState"] = "new"; - break; - case RTC::DtlsTransport::DtlsState::CONNECTING: - jsonObject["dtlsState"] = "connecting"; - break; - case RTC::DtlsTransport::DtlsState::CONNECTED: - jsonObject["dtlsState"] = "connected"; - break; - case RTC::DtlsTransport::DtlsState::FAILED: - jsonObject["dtlsState"] = "failed"; - break; - case RTC::DtlsTransport::DtlsState::CLOSED: - jsonObject["dtlsState"] = "closed"; - break; - } + auto dtlsState = DtlsTransport::StateToFbs(this->dtlsTransport->GetState()); + + // Base Transport stats. + auto base = Transport::FillBufferStats(builder); + + return FBS::WebRtcTransport::CreateGetStatsResponse( + builder, + base, + // iceRole (we are always "controlled"). + FBS::WebRtcTransport::IceRole::CONTROLLED, + iceState, + iceSelectedTuple, + dtlsState); } void WebRtcTransport::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::TRANSPORT_CONNECT: + case Channel::ChannelRequest::Method::TRANSPORT_GET_STATS: { - // Ensure this method is not called twice. - if (this->connectCalled) - MS_THROW_ERROR("connect() already called"); + auto responseOffset = FillBufferStats(request->GetBufferBuilder()); - RTC::DtlsTransport::Fingerprint dtlsRemoteFingerprint; - RTC::DtlsTransport::Role dtlsRemoteRole; + request->Accept(FBS::Response::Body::WebRtcTransport_GetStatsResponse, responseOffset); - auto jsonDtlsParametersIt = request->data.find("dtlsParameters"); + break; + } - if (jsonDtlsParametersIt == request->data.end() || !jsonDtlsParametersIt->is_object()) - MS_THROW_TYPE_ERROR("missing dtlsParameters"); + case Channel::ChannelRequest::Method::TRANSPORT_DUMP: + { + auto dumpOffset = FillBuffer(request->GetBufferBuilder()); - auto jsonFingerprintsIt = jsonDtlsParametersIt->find("fingerprints"); + request->Accept(FBS::Response::Body::WebRtcTransport_DumpResponse, dumpOffset); - if (jsonFingerprintsIt == jsonDtlsParametersIt->end() || !jsonFingerprintsIt->is_array()) + break; + } + + case Channel::ChannelRequest::Method::WEBRTCTRANSPORT_CONNECT: + { + // Ensure this method is not called twice. + if (this->connectCalled) { - MS_THROW_TYPE_ERROR("missing dtlsParameters.fingerprints"); + MS_THROW_ERROR("connect() already called"); } - else if (jsonFingerprintsIt->empty()) + + const auto* body = request->data->body_as(); + + const auto* dtlsParameters = body->dtlsParameters(); + + RTC::DtlsTransport::Fingerprint dtlsRemoteFingerprint; + RTC::DtlsTransport::Role dtlsRemoteRole; + + if (dtlsParameters->fingerprints()->size() == 0) { MS_THROW_TYPE_ERROR("empty dtlsParameters.fingerprints array"); } // NOTE: Just take the first fingerprint. - for (auto& jsonFingerprint : *jsonFingerprintsIt) + for (const auto& fingerprint : *dtlsParameters->fingerprints()) { - if (!jsonFingerprint.is_object()) - MS_THROW_TYPE_ERROR("wrong entry in dtlsParameters.fingerprints (not an object)"); - - auto jsonAlgorithmIt = jsonFingerprint.find("algorithm"); + dtlsRemoteFingerprint.algorithm = DtlsTransport::AlgorithmFromFbs(fingerprint->algorithm()); - if (jsonAlgorithmIt == jsonFingerprint.end()) - MS_THROW_TYPE_ERROR("missing fingerprint.algorithm"); - else if (!jsonAlgorithmIt->is_string()) - MS_THROW_TYPE_ERROR("wrong fingerprint.algorithm (not a string)"); - - dtlsRemoteFingerprint.algorithm = - RTC::DtlsTransport::GetFingerprintAlgorithm(jsonAlgorithmIt->get()); - - if (dtlsRemoteFingerprint.algorithm == RTC::DtlsTransport::FingerprintAlgorithm::NONE) - { - MS_THROW_TYPE_ERROR("invalid fingerprint.algorithm value"); - } - - auto jsonValueIt = jsonFingerprint.find("value"); - - if (jsonValueIt == jsonFingerprint.end()) - MS_THROW_TYPE_ERROR("missing fingerprint.value"); - else if (!jsonValueIt->is_string()) - MS_THROW_TYPE_ERROR("wrong fingerprint.value (not a string)"); - - dtlsRemoteFingerprint.value = jsonValueIt->get(); + dtlsRemoteFingerprint.value = fingerprint->value()->str(); // Just use the first fingerprint. break; } - auto jsonRoleIt = jsonDtlsParametersIt->find("role"); - - if (jsonRoleIt != jsonDtlsParametersIt->end()) - { - if (!jsonRoleIt->is_string()) - MS_THROW_TYPE_ERROR("wrong dtlsParameters.role (not a string)"); - - dtlsRemoteRole = RTC::DtlsTransport::StringToRole(jsonRoleIt->get()); - - if (dtlsRemoteRole == RTC::DtlsTransport::Role::NONE) - MS_THROW_TYPE_ERROR("invalid dtlsParameters.role value"); - } - else - { - dtlsRemoteRole = RTC::DtlsTransport::Role::AUTO; - } + dtlsRemoteRole = RTC::DtlsTransport::RoleFromFbs(dtlsParameters->role()); // Set local DTLS role. switch (dtlsRemoteRole) @@ -609,10 +530,6 @@ namespace RTC break; } - case RTC::DtlsTransport::Role::NONE: - { - MS_THROW_TYPE_ERROR("invalid remote DTLS role"); - } } this->connectCalled = true; @@ -625,31 +542,20 @@ namespace RTC } // Tell the caller about the selected local DTLS role. - json data = json::object(); - - switch (this->dtlsRole) - { - case RTC::DtlsTransport::Role::CLIENT: - data["dtlsLocalRole"] = "client"; - break; - - case RTC::DtlsTransport::Role::SERVER: - data["dtlsLocalRole"] = "server"; - break; + auto dtlsLocalRole = DtlsTransport::RoleToFbs(this->dtlsRole); - default: - MS_ABORT("invalid local DTLS role"); - } + auto responseOffset = + FBS::WebRtcTransport::CreateConnectResponse(request->GetBufferBuilder(), dtlsLocalRole); - request->Accept(data); + request->Accept(FBS::Response::Body::WebRtcTransport_ConnectResponse, responseOffset); break; } - case Channel::ChannelRequest::MethodId::TRANSPORT_RESTART_ICE: + case Channel::ChannelRequest::Method::TRANSPORT_RESTART_ICE: { - std::string usernameFragment = Utils::Crypto::GetRandomString(32); - std::string password = Utils::Crypto::GetRandomString(32); + const std::string usernameFragment = Utils::Crypto::GetRandomString(32); + const std::string password = Utils::Crypto::GetRandomString(32); this->iceServer->RestartIce(usernameFragment, password); @@ -657,16 +563,14 @@ namespace RTC "WebRtcTransport ICE usernameFragment and password changed [id:%s]", this->id.c_str()); // Reply with the updated ICE local parameters. - json data = json::object(); - - data["iceParameters"] = json::object(); - auto jsonIceParametersIt = data.find("iceParameters"); - - (*jsonIceParametersIt)["usernameFragment"] = this->iceServer->GetUsernameFragment(); - (*jsonIceParametersIt)["password"] = this->iceServer->GetPassword(); - (*jsonIceParametersIt)["iceLite"] = true; + auto responseOffset = FBS::Transport::CreateRestartIceResponseDirect( + request->GetBufferBuilder(), + this->iceServer->GetUsernameFragment().c_str(), + this->iceServer->GetPassword().c_str(), + true /* iceLite */ + ); - request->Accept(data); + request->Accept(FBS::Response::Body::Transport_RestartIceResponse, responseOffset); break; } @@ -679,7 +583,7 @@ namespace RTC } } - void WebRtcTransport::HandleNotification(PayloadChannel::PayloadChannelNotification* notification) + void WebRtcTransport::HandleNotification(Channel::ChannelNotification* notification) { MS_TRACE(); @@ -754,7 +658,9 @@ namespace RTC // Do nothing if we have the same local DTLS role as the DTLS transport. // NOTE: local role in DTLS transport can be NONE, but not ours. if (this->dtlsTransport->GetLocalRole() == this->dtlsRole) + { return; + } // Check our local DTLS role. switch (this->dtlsRole) @@ -822,11 +728,6 @@ namespace RTC break; } - - case RTC::DtlsTransport::Role::NONE: - { - MS_ABORT("local DTLS role not set"); - } } } @@ -861,9 +762,9 @@ namespace RTC } const uint8_t* data = packet->GetData(); - auto intLen = static_cast(packet->GetSize()); + auto len = packet->GetSize(); - if (!this->srtpSendSession->EncryptRtp(&data, &intLen)) + if (!this->srtpSendSession->EncryptRtp(&data, &len)) { if (cb) { @@ -874,8 +775,6 @@ namespace RTC return; } - auto len = static_cast(intLen); - this->iceServer->GetSelectedTuple()->Send(data, len, cb); // Increase send transmission. @@ -887,10 +786,12 @@ namespace RTC MS_TRACE(); if (!IsConnected()) + { return; + } const uint8_t* data = packet->GetData(); - auto intLen = static_cast(packet->GetSize()); + auto len = packet->GetSize(); // Ensure there is sending SRTP session. if (!this->srtpSendSession) @@ -900,10 +801,10 @@ namespace RTC return; } - if (!this->srtpSendSession->EncryptRtcp(&data, &intLen)) + if (!this->srtpSendSession->EncryptRtcp(&data, &len)) + { return; - - auto len = static_cast(intLen); + } this->iceServer->GetSelectedTuple()->Send(data, len); @@ -916,12 +817,14 @@ namespace RTC MS_TRACE(); if (!IsConnected()) + { return; + } packet->Serialize(RTC::RTCP::Buffer); const uint8_t* data = packet->GetData(); - auto intLen = static_cast(packet->GetSize()); + auto len = packet->GetSize(); // Ensure there is sending SRTP session. if (!this->srtpSendSession) @@ -931,10 +834,10 @@ namespace RTC return; } - if (!this->srtpSendSession->EncryptRtcp(&data, &intLen)) + if (!this->srtpSendSession->EncryptRtcp(&data, &len)) + { return; - - auto len = static_cast(intLen); + } this->iceServer->GetSelectedTuple()->Send(data, len); @@ -943,11 +846,11 @@ namespace RTC } void WebRtcTransport::SendMessage( - RTC::DataConsumer* dataConsumer, uint32_t ppid, const uint8_t* msg, size_t len, onQueuedCallback* cb) + RTC::DataConsumer* dataConsumer, const uint8_t* msg, size_t len, uint32_t ppid, onQueuedCallback* cb) { MS_TRACE(); - this->sctpAssociation->SendSctpMessage(dataConsumer, ppid, msg, len, cb); + this->sctpAssociation->SendSctpMessage(dataConsumer, msg, len, ppid, cb); } void WebRtcTransport::SendSctpData(const uint8_t* data, size_t len) @@ -1053,7 +956,7 @@ namespace RTC } // Trick for clients performing aggressive ICE regardless we are ICE-Lite. - this->iceServer->ForceSelectedTuple(tuple); + this->iceServer->MayForceSelectedTuple(tuple); // Check that DTLS status is 'connecting' or 'connected'. if ( @@ -1102,11 +1005,9 @@ namespace RTC } // Decrypt the SRTP packet. - auto intLen = static_cast(len); - - if (!this->srtpRecvSession->DecryptSrtp(const_cast(data), &intLen)) + if (!this->srtpRecvSession->DecryptSrtp(const_cast(data), &len)) { - RTC::RtpPacket* packet = RTC::RtpPacket::Parse(data, static_cast(intLen)); + RTC::RtpPacket* packet = RTC::RtpPacket::Parse(data, len); if (!packet) { @@ -1127,7 +1028,7 @@ namespace RTC return; } - RTC::RtpPacket* packet = RTC::RtpPacket::Parse(data, static_cast(intLen)); + RTC::RtpPacket* packet = RTC::RtpPacket::Parse(data, len); if (!packet) { @@ -1137,7 +1038,7 @@ namespace RTC } // Trick for clients performing aggressive ICE regardless we are ICE-Lite. - this->iceServer->ForceSelectedTuple(tuple); + this->iceServer->MayForceSelectedTuple(tuple); // Pass the packet to the parent transport. RTC::Transport::ReceiveRtpPacket(packet); @@ -1173,12 +1074,12 @@ namespace RTC } // Decrypt the SRTCP packet. - auto intLen = static_cast(len); - - if (!this->srtpRecvSession->DecryptSrtcp(const_cast(data), &intLen)) + if (!this->srtpRecvSession->DecryptSrtcp(const_cast(data), &len)) + { return; + } - RTC::RTCP::Packet* packet = RTC::RTCP::Packet::Parse(data, static_cast(intLen)); + RTC::RTCP::Packet* packet = RTC::RTCP::Packet::Parse(data, len); if (!packet) { @@ -1279,9 +1180,9 @@ namespace RTC } // If this is a TCP tuple, close its underlaying TCP connection. - if (tuple->GetProtocol() == RTC::TransportTuple::Protocol::TCP && !tuple->IsClosed()) + if (tuple->GetProtocol() == RTC::TransportTuple::Protocol::TCP) { - tuple->Close(); + tuple->CloseTcpConnection(); } } @@ -1300,11 +1201,17 @@ namespace RTC MS_DEBUG_TAG(ice, "ICE selected tuple"); // Notify the Node WebRtcTransport. - json data = json::object(); + auto tuple = this->iceServer->GetSelectedTuple()->FillBuffer( + this->shared->channelNotifier->GetBufferBuilder()); - this->iceServer->GetSelectedTuple()->FillJson(data["iceSelectedTuple"]); + auto notification = FBS::WebRtcTransport::CreateIceSelectedTupleChangeNotification( + this->shared->channelNotifier->GetBufferBuilder(), tuple); - this->shared->channelNotifier->Emit(this->id, "iceselectedtuplechange", data); + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::WEBRTCTRANSPORT_ICE_SELECTED_TUPLE_CHANGE, + FBS::Notification::Body::WebRtcTransport_IceSelectedTupleChangeNotification, + notification); } inline void WebRtcTransport::OnIceServerConnected(const RTC::IceServer* /*iceServer*/) @@ -1314,11 +1221,14 @@ namespace RTC MS_DEBUG_TAG(ice, "ICE connected"); // Notify the Node WebRtcTransport. - json data = json::object(); - - data["iceState"] = "connected"; + auto iceStateChangeOffset = FBS::WebRtcTransport::CreateIceStateChangeNotification( + this->shared->channelNotifier->GetBufferBuilder(), FBS::WebRtcTransport::IceState::CONNECTED); - this->shared->channelNotifier->Emit(this->id, "icestatechange", data); + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::WEBRTCTRANSPORT_ICE_STATE_CHANGE, + FBS::Notification::Body::WebRtcTransport_IceStateChangeNotification, + iceStateChangeOffset); // If ready, run the DTLS handler. MayRunDtlsTransport(); @@ -1337,11 +1247,14 @@ namespace RTC MS_DEBUG_TAG(ice, "ICE completed"); // Notify the Node WebRtcTransport. - json data = json::object(); + auto iceStateChangeOffset = FBS::WebRtcTransport::CreateIceStateChangeNotification( + this->shared->channelNotifier->GetBufferBuilder(), FBS::WebRtcTransport::IceState::COMPLETED); - data["iceState"] = "completed"; - - this->shared->channelNotifier->Emit(this->id, "icestatechange", data); + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::WEBRTCTRANSPORT_ICE_STATE_CHANGE, + FBS::Notification::Body::WebRtcTransport_IceStateChangeNotification, + iceStateChangeOffset); // If ready, run the DTLS handler. MayRunDtlsTransport(); @@ -1360,11 +1273,15 @@ namespace RTC MS_DEBUG_TAG(ice, "ICE disconnected"); // Notify the Node WebRtcTransport. - json data = json::object(); - - data["iceState"] = "disconnected"; + auto iceStateChangeOffset = FBS::WebRtcTransport::CreateIceStateChangeNotification( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::WebRtcTransport::IceState::DISCONNECTED); - this->shared->channelNotifier->Emit(this->id, "icestatechange", data); + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::WEBRTCTRANSPORT_ICE_STATE_CHANGE, + FBS::Notification::Body::WebRtcTransport_IceStateChangeNotification, + iceStateChangeOffset); // If DTLS was already connected, notify the parent class. if (this->dtlsTransport->GetState() == RTC::DtlsTransport::DtlsState::CONNECTED) @@ -1380,11 +1297,14 @@ namespace RTC MS_DEBUG_TAG(dtls, "DTLS connecting"); // Notify the Node WebRtcTransport. - json data = json::object(); - - data["dtlsState"] = "connecting"; - - this->shared->channelNotifier->Emit(this->id, "dtlsstatechange", data); + auto dtlsStateChangeOffset = FBS::WebRtcTransport::CreateDtlsStateChangeNotification( + this->shared->channelNotifier->GetBufferBuilder(), FBS::WebRtcTransport::DtlsState::CONNECTING); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::WEBRTCTRANSPORT_DTLS_STATE_CHANGE, + FBS::Notification::Body::WebRtcTransport_DtlsStateChangeNotification, + dtlsStateChangeOffset); } inline void WebRtcTransport::OnDtlsTransportConnected( @@ -1423,12 +1343,16 @@ namespace RTC RTC::SrtpSession::Type::INBOUND, srtpCryptoSuite, srtpRemoteKey, srtpRemoteKeyLen); // Notify the Node WebRtcTransport. - json data = json::object(); - - data["dtlsState"] = "connected"; - data["dtlsRemoteCert"] = remoteCert; + auto dtlsStateChangeOffset = FBS::WebRtcTransport::CreateDtlsStateChangeNotificationDirect( + this->shared->channelNotifier->GetBufferBuilder(), + FBS::WebRtcTransport::DtlsState::CONNECTED, + remoteCert.c_str()); - this->shared->channelNotifier->Emit(this->id, "dtlsstatechange", data); + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::WEBRTCTRANSPORT_DTLS_STATE_CHANGE, + FBS::Notification::Body::WebRtcTransport_DtlsStateChangeNotification, + dtlsStateChangeOffset); // Tell the parent class. RTC::Transport::Connected(); @@ -1449,11 +1373,14 @@ namespace RTC MS_WARN_TAG(dtls, "DTLS failed"); // Notify the Node WebRtcTransport. - json data = json::object(); - - data["dtlsState"] = "failed"; - - this->shared->channelNotifier->Emit(this->id, "dtlsstatechange", data); + auto dtlsStateChangeOffset = FBS::WebRtcTransport::CreateDtlsStateChangeNotification( + this->shared->channelNotifier->GetBufferBuilder(), FBS::WebRtcTransport::DtlsState::FAILED); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::WEBRTCTRANSPORT_DTLS_STATE_CHANGE, + FBS::Notification::Body::WebRtcTransport_DtlsStateChangeNotification, + dtlsStateChangeOffset); } inline void WebRtcTransport::OnDtlsTransportClosed(const RTC::DtlsTransport* /*dtlsTransport*/) @@ -1463,11 +1390,14 @@ namespace RTC MS_WARN_TAG(dtls, "DTLS remotely closed"); // Notify the Node WebRtcTransport. - json data = json::object(); - - data["dtlsState"] = "closed"; - - this->shared->channelNotifier->Emit(this->id, "dtlsstatechange", data); + auto dtlsStateChangeOffset = FBS::WebRtcTransport::CreateDtlsStateChangeNotification( + this->shared->channelNotifier->GetBufferBuilder(), FBS::WebRtcTransport::DtlsState::CLOSED); + + this->shared->channelNotifier->Emit( + this->id, + FBS::Notification::Event::WEBRTCTRANSPORT_DTLS_STATE_CHANGE, + FBS::Notification::Body::WebRtcTransport_DtlsStateChangeNotification, + dtlsStateChangeOffset); // Tell the parent class. RTC::Transport::Disconnected(); diff --git a/worker/src/Settings.cpp b/worker/src/Settings.cpp index 1c13237150..f21c1f4759 100644 --- a/worker/src/Settings.cpp +++ b/worker/src/Settings.cpp @@ -5,10 +5,10 @@ #include "Logger.hpp" #include "MediaSoupErrors.hpp" #include "Utils.hpp" +#include #include // isprint() #include // std::ostream_iterator #include -#include #include // std::ostringstream #include extern "C" @@ -18,20 +18,20 @@ extern "C" /* Static. */ -static std::mutex globalSyncMutex; +static std::mutex GlobalSyncMutex; /* Class variables. */ thread_local struct Settings::Configuration Settings::configuration; // clang-format off -absl::flat_hash_map Settings::string2LogLevel = +absl::flat_hash_map Settings::String2LogLevel = { { "debug", LogLevel::LOG_DEBUG }, { "warn", LogLevel::LOG_WARN }, { "error", LogLevel::LOG_ERROR }, { "none", LogLevel::LOG_NONE } }; -absl::flat_hash_map Settings::logLevel2String = +absl::flat_hash_map Settings::LogLevel2String = { { LogLevel::LOG_DEBUG, "debug" }, { LogLevel::LOG_WARN, "warn" }, @@ -60,7 +60,8 @@ void Settings::SetConfiguration(int argc, char* argv[]) { "dtlsCertificateFile", optional_argument, nullptr, 'c' }, { "dtlsPrivateKeyFile", optional_argument, nullptr, 'p' }, { "libwebrtcFieldTrials", optional_argument, nullptr, 'W' }, - { nullptr, 0, nullptr, 0 } + { "disableLiburing", optional_argument, nullptr, 'd' }, + { nullptr, 0, nullptr, 0 } }; // clang-format on std::string stringValue; @@ -69,14 +70,17 @@ void Settings::SetConfiguration(int argc, char* argv[]) /* Parse command line options. */ // getopt_long_only() is not thread-safe - std::lock_guard lock(globalSyncMutex); + const std::lock_guard lock(GlobalSyncMutex); optind = 1; // Set explicitly, otherwise subsequent runs will fail. opterr = 0; // Don't allow getopt to print error messages. + while ((c = getopt_long_only(argc, argv, "", options, &optionIdx)) != -1) { if (!optarg) - MS_THROW_TYPE_ERROR("unknown configuration parameter: %s", optarg); + { + MS_THROW_TYPE_ERROR("missing value in command line argument in option '%c'", c); + } switch (c) { @@ -156,13 +160,29 @@ void Settings::SetConfiguration(int argc, char* argv[]) break; } + case 'd': + { + stringValue = std::string(optarg); + + if (stringValue == "true") + { + Settings::configuration.liburingDisabled = true; + } + + break; + } + // Invalid option. case '?': { if (isprint(optopt) != 0) + { MS_THROW_TYPE_ERROR("invalid option '-%c'", (char)optopt); + } else + { MS_THROW_TYPE_ERROR("unknown long option given as argument"); + } } // Valid option, but it requires and argument that is not given. @@ -183,11 +203,15 @@ void Settings::SetConfiguration(int argc, char* argv[]) // Set logTags. if (!logTags.empty()) + { Settings::SetLogTags(logTags); + } // Validate RTC ports. if (Settings::configuration.rtcMaxPort < Settings::configuration.rtcMinPort) + { MS_THROW_TYPE_ERROR("rtcMaxPort cannot be less than rtcMinPort"); + } // Set DTLS certificate files (if provided), Settings::SetDtlsCertificateAndPrivateKeyFiles(); @@ -201,31 +225,57 @@ void Settings::PrintConfiguration() std::ostringstream logTagsStream; if (Settings::configuration.logTags.info) + { logTags.emplace_back("info"); + } if (Settings::configuration.logTags.ice) + { logTags.emplace_back("ice"); + } if (Settings::configuration.logTags.dtls) + { logTags.emplace_back("dtls"); + } if (Settings::configuration.logTags.rtp) + { logTags.emplace_back("rtp"); + } if (Settings::configuration.logTags.srtp) + { logTags.emplace_back("srtp"); + } if (Settings::configuration.logTags.rtcp) + { logTags.emplace_back("rtcp"); + } if (Settings::configuration.logTags.rtx) + { logTags.emplace_back("rtx"); + } if (Settings::configuration.logTags.bwe) + { logTags.emplace_back("bwe"); + } if (Settings::configuration.logTags.score) + { logTags.emplace_back("score"); + } if (Settings::configuration.logTags.simulcast) + { logTags.emplace_back("simulcast"); + } if (Settings::configuration.logTags.svc) + { logTags.emplace_back("svc"); + } if (Settings::configuration.logTags.sctp) + { logTags.emplace_back("sctp"); + } if (Settings::configuration.logTags.message) + { logTags.emplace_back("message"); + } if (!logTags.empty()) { @@ -237,23 +287,20 @@ void Settings::PrintConfiguration() MS_DEBUG_TAG(info, ""); MS_DEBUG_TAG( - info, - " logLevel : %s", - Settings::logLevel2String[Settings::configuration.logLevel].c_str()); - MS_DEBUG_TAG(info, " logTags : %s", logTagsStream.str().c_str()); - MS_DEBUG_TAG(info, " rtcMinPort : %" PRIu16, Settings::configuration.rtcMinPort); - MS_DEBUG_TAG(info, " rtcMaxPort : %" PRIu16, Settings::configuration.rtcMaxPort); + info, " logLevel: %s", Settings::LogLevel2String[Settings::configuration.logLevel].c_str()); + MS_DEBUG_TAG(info, " logTags: %s", logTagsStream.str().c_str()); + MS_DEBUG_TAG(info, " rtcMinPort: %" PRIu16, Settings::configuration.rtcMinPort); + MS_DEBUG_TAG(info, " rtcMaxPort: %" PRIu16, Settings::configuration.rtcMaxPort); if (!Settings::configuration.dtlsCertificateFile.empty()) { MS_DEBUG_TAG( - info, " dtlsCertificateFile : %s", Settings::configuration.dtlsCertificateFile.c_str()); - MS_DEBUG_TAG( - info, " dtlsPrivateKeyFile : %s", Settings::configuration.dtlsPrivateKeyFile.c_str()); + info, " dtlsCertificateFile: %s", Settings::configuration.dtlsCertificateFile.c_str()); + MS_DEBUG_TAG(info, " dtlsPrivateKeyFile: %s", Settings::configuration.dtlsPrivateKeyFile.c_str()); } if (!Settings::configuration.libwebrtcFieldTrials.empty()) { MS_DEBUG_TAG( - info, " libwebrtcFieldTrials : %s", Settings::configuration.libwebrtcFieldTrials.c_str()); + info, " libwebrtcFieldTrials: %s", Settings::configuration.libwebrtcFieldTrials.c_str()); } MS_DEBUG_TAG(info, ""); @@ -263,31 +310,28 @@ void Settings::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::WORKER_UPDATE_SETTINGS: + case Channel::ChannelRequest::Method::WORKER_UPDATE_SETTINGS: { - auto jsonLogLevelIt = request->data.find("logLevel"); - auto jsonLogTagsIt = request->data.find("logTags"); + const auto* body = request->data->body_as(); - // Update logLevel if requested. - if (jsonLogLevelIt != request->data.end() && jsonLogLevelIt->is_string()) + if (flatbuffers::IsFieldPresent(body, FBS::Worker::UpdateSettingsRequest::VT_LOGLEVEL)) { - std::string logLevel = *jsonLogLevelIt; + auto logLevel = body->logLevel()->str(); // This may throw. Settings::SetLogLevel(logLevel); } // Update logTags if requested. - if (jsonLogTagsIt != request->data.end() && jsonLogTagsIt->is_array()) + if (flatbuffers::IsFieldPresent(body, FBS::Worker::UpdateSettingsRequest::VT_LOGTAGS)) { std::vector logTags; - for (const auto& logTag : *jsonLogTagsIt) + for (const auto& logTag : *body->logTags()) { - if (logTag.is_string()) - logTags.push_back(logTag); + logTags.push_back(logTag->str()); } Settings::SetLogTags(logTags); @@ -303,7 +347,7 @@ void Settings::HandleRequest(Channel::ChannelRequest* request) default: { - MS_THROW_ERROR("unknown method '%s'", request->method.c_str()); + MS_THROW_ERROR("unknown method '%s'", request->methodCStr); } } } @@ -315,10 +359,12 @@ void Settings::SetLogLevel(std::string& level) // Lowcase given level. Utils::String::ToLowerCase(level); - if (Settings::string2LogLevel.find(level) == Settings::string2LogLevel.end()) + if (Settings::String2LogLevel.find(level) == Settings::String2LogLevel.end()) + { MS_THROW_TYPE_ERROR("invalid value '%s' for logLevel", level.c_str()); + } - Settings::configuration.logLevel = Settings::string2LogLevel[level]; + Settings::configuration.logLevel = Settings::String2LogLevel[level]; } void Settings::SetLogTags(const std::vector& tags) @@ -328,34 +374,60 @@ void Settings::SetLogTags(const std::vector& tags) // Reset logTags. struct LogTags newLogTags; - for (auto& tag : tags) + for (const auto& tag : tags) { if (tag == "info") + { newLogTags.info = true; + } else if (tag == "ice") + { newLogTags.ice = true; + } else if (tag == "dtls") + { newLogTags.dtls = true; + } else if (tag == "rtp") + { newLogTags.rtp = true; + } else if (tag == "srtp") + { newLogTags.srtp = true; + } else if (tag == "rtcp") + { newLogTags.rtcp = true; + } else if (tag == "rtx") + { newLogTags.rtx = true; + } else if (tag == "bwe") + { newLogTags.bwe = true; + } else if (tag == "score") + { newLogTags.score = true; + } else if (tag == "simulcast") + { newLogTags.simulcast = true; + } else if (tag == "svc") + { newLogTags.svc = true; + } else if (tag == "sctp") + { newLogTags.sctp = true; + } else if (tag == "message") + { newLogTags.message = true; + } } Settings::configuration.logTags = newLogTags; diff --git a/worker/src/Utils/Crypto.cpp b/worker/src/Utils/Crypto.cpp index 0717e7f8c0..e5c696efaf 100644 --- a/worker/src/Utils/Crypto.cpp +++ b/worker/src/Utils/Crypto.cpp @@ -14,7 +14,7 @@ namespace Utils thread_local EVP_MAC_CTX* Crypto::hmacSha1Ctx{ nullptr }; thread_local uint8_t Crypto::hmacSha1Buffer[SHA_DIGEST_LENGTH]; // clang-format off - const uint32_t Crypto::crc32Table[] = + const uint32_t Crypto::Crc32Table[] = { 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, @@ -71,10 +71,14 @@ namespace Utils MS_TRACE(); if (Crypto::hmacSha1Ctx != nullptr) + { EVP_MAC_CTX_free(Crypto::hmacSha1Ctx); + } if (Crypto::mac != nullptr) + { EVP_MAC_free(Crypto::mac); + } } const uint8_t* Crypto::GetHmacSha1(const std::string& key, const uint8_t* data, size_t len) diff --git a/worker/src/Utils/File.cpp b/worker/src/Utils/File.cpp index 8ec27aed25..58155d0862 100644 --- a/worker/src/Utils/File.cpp +++ b/worker/src/Utils/File.cpp @@ -20,23 +20,31 @@ namespace Utils { MS_TRACE(); - struct stat fileStat; // NOLINT(cppcoreguidelines-pro-type-member-init) + struct stat fileStat + { + }; // NOLINT(cppcoreguidelines-pro-type-member-init) int err; // Ensure the given file exists. err = stat(file, &fileStat); if (err != 0) + { MS_THROW_ERROR("cannot read file '%s': %s", file, std::strerror(errno)); + } // Ensure it is a regular file. if (!S_ISREG(fileStat.st_mode)) + { MS_THROW_ERROR("'%s' is not a regular file", file); + } // Ensure it is readable. err = access(file, R_OK); if (err != 0) + { MS_THROW_ERROR("cannot read file '%s': %s", file, std::strerror(errno)); + } } } // namespace Utils diff --git a/worker/src/Utils/IP.cpp b/worker/src/Utils/IP.cpp index 16b9fdd626..349f141862 100644 --- a/worker/src/Utils/IP.cpp +++ b/worker/src/Utils/IP.cpp @@ -13,17 +13,25 @@ namespace Utils MS_TRACE(); if (ip.size() >= INET6_ADDRSTRLEN) + { return AF_UNSPEC; + } - auto ipPtr = ip.c_str(); + const auto* ipPtr = ip.c_str(); char ipBuffer[INET6_ADDRSTRLEN] = { 0 }; if (uv_inet_pton(AF_INET, ipPtr, ipBuffer) == 0) + { return AF_INET; + } else if (uv_inet_pton(AF_INET6, ipPtr, ipBuffer) == 0) + { return AF_INET6; + } else + { return AF_UNSPEC; + } } void IP::GetAddressInfo(const struct sockaddr* addr, int& family, std::string& ip, uint16_t& port) @@ -44,7 +52,9 @@ namespace Utils sizeof(ipBuffer)); if (err) + { MS_ABORT("uv_inet_ntop() failed: %s", uv_strerror(err)); + } port = static_cast(ntohs(reinterpret_cast(addr)->sin_port)); @@ -61,7 +71,9 @@ namespace Utils sizeof(ipBuffer)); if (err) + { MS_ABORT("uv_inet_ntop() failed: %s", uv_strerror(err)); + } port = static_cast(ntohs(reinterpret_cast(addr)->sin6_port)); @@ -79,11 +91,34 @@ namespace Utils ip.assign(ipBuffer); } + size_t IP::GetAddressLen(const struct sockaddr* addr) + { + MS_TRACE(); + + switch (addr->sa_family) + { + case AF_INET: + { + return sizeof(struct sockaddr_in); + } + + case AF_INET6: + { + return sizeof(struct sockaddr_in6); + } + + default: + { + MS_ABORT("unknown network family: %d", static_cast(addr->sa_family)); + } + } + } + void IP::NormalizeIp(std::string& ip) { MS_TRACE(); - sockaddr_storage addrStorage; + sockaddr_storage addrStorage{}; char ipBuffer[INET6_ADDRSTRLEN] = { 0 }; int err; diff --git a/worker/src/Utils/String.cpp b/worker/src/Utils/String.cpp index 0e5cdd2ede..b29042f31e 100644 --- a/worker/src/Utils/String.cpp +++ b/worker/src/Utils/String.cpp @@ -38,9 +38,13 @@ namespace Utils olen = len * 4 / 3 + 4; // 3-byte blocks to 4-byte. if (olen < len) + { MS_THROW_TYPE_ERROR("integer overflow"); + } else if (olen > BufferOutSize - 1) + { MS_THROW_TYPE_ERROR("data too big"); + } end = data + len; in = data; @@ -73,14 +77,14 @@ namespace Utils *pos++ = '='; } - return std::string(reinterpret_cast(out), pos - out); + return { reinterpret_cast(out), static_cast(pos - out) }; } std::string Utils::String::Base64Encode(const std::string& str) { MS_TRACE(); - auto* data = reinterpret_cast(str.c_str()); + const auto* data = reinterpret_cast(str.c_str()); return Base64Encode(data, str.size()); } @@ -100,7 +104,9 @@ namespace Utils // NOTE: This is not really accurate but anyway. if (len > BufferOutSize - 1) + { MS_THROW_TYPE_ERROR("data too big"); + } std::memset(dtable, 0x80, 256); @@ -114,11 +120,15 @@ namespace Utils for (i = 0; i < len; ++i) { if (dtable[data[i]] != 0x80) + { count++; + } } if (count == 0 || count % 4) + { MS_THROW_TYPE_ERROR("invalid data"); + } pos = out; count = 0; @@ -128,10 +138,14 @@ namespace Utils tmp = dtable[data[i]]; if (tmp == 0x80) + { continue; + } if (data[i] == '=') + { pad++; + } block[count] = tmp; count++; @@ -146,11 +160,17 @@ namespace Utils if (pad) { if (pad == 1) + { pos--; + } else if (pad == 2) + { pos -= 2; + } else + { MS_THROW_TYPE_ERROR("integer padding"); + } break; } @@ -166,47 +186,8 @@ namespace Utils { MS_TRACE(); - auto* data = reinterpret_cast(str.c_str()); + const auto* data = reinterpret_cast(str.c_str()); return Base64Decode(data, str.size(), outLen); } - - std::vector Utils::String::Split(const std::string& str, char separator, size_t limit) - { - std::vector items; - size_t pos = 0; - - while (pos < str.length()) - { - auto found = str.find(separator, pos); - - std::string item; - if (found != std::string::npos) - { - item = str.substr(pos, found - pos); - items.push_back(item); - } - else - { - item = str.substr(pos, str.size()); - items.push_back(item); - - break; - } - - // Escape the separator character. - pos += item.length() + 1; - - // Limit reached, add the remaining buffer to the last item. - if (limit != 0 && items.size() >= limit) - { - item = str.substr(pos, str.size()); - items.push_back(item); - - break; - } - } - - return items; - } } // namespace Utils diff --git a/worker/src/Worker.cpp b/worker/src/Worker.cpp index 659f84f8d4..2785301357 100644 --- a/worker/src/Worker.cpp +++ b/worker/src/Worker.cpp @@ -3,49 +3,57 @@ #include "Worker.hpp" #include "ChannelMessageRegistrator.hpp" +#ifdef MS_LIBURING_SUPPORTED +#include "DepLibUring.hpp" +#endif #include "DepLibUV.hpp" #include "DepUsrSCTP.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" #include "Settings.hpp" #include "Channel/ChannelNotifier.hpp" -#include "PayloadChannel/PayloadChannelNotifier.hpp" +#include "FBS/response.h" +#include "FBS/worker.h" /* Instance methods. */ -Worker::Worker(::Channel::ChannelSocket* channel, PayloadChannel::PayloadChannelSocket* payloadChannel) - : channel(channel), payloadChannel(payloadChannel) +Worker::Worker(::Channel::ChannelSocket* channel) : channel(channel) { MS_TRACE(); // Set us as Channel's listener. this->channel->SetListener(this); - // Set us as PayloadChannel's listener. - this->payloadChannel->SetListener(this); - - // Set the SignalHandler. - this->signalsHandler = new SignalsHandler(this); + // Set the SignalHandle. + this->signalHandle = new SignalHandle(this); // Set up the RTC::Shared singleton. this->shared = new RTC::Shared( /*channelMessageRegistrator*/ new ChannelMessageRegistrator(), - /*channelNotifier*/ new Channel::ChannelNotifier(this->channel), - /*payloadChannelNotifier*/ new PayloadChannel::PayloadChannelNotifier(this->payloadChannel)); + /*channelNotifier*/ new Channel::ChannelNotifier(this->channel)); #ifdef MS_EXECUTABLE { // Add signals to handle. - this->signalsHandler->AddSignal(SIGINT, "INT"); - this->signalsHandler->AddSignal(SIGTERM, "TERM"); + this->signalHandle->AddSignal(SIGINT, "INT"); + this->signalHandle->AddSignal(SIGTERM, "TERM"); } #endif // Create the Checker instance in DepUsrSCTP. DepUsrSCTP::CreateChecker(); +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + // Start polling CQEs, which will create a uv_pool_t handle. + DepLibUring::StartPollingCQEs(); + } +#endif + // Tell the Node process that we are running. - this->shared->channelNotifier->Emit(Logger::pid, "running"); + this->shared->channelNotifier->Emit( + std::to_string(Logger::Pid), FBS::Notification::Event::WORKER_RUNNING); MS_DEBUG_DEV("starting libuv loop"); DepLibUV::RunLoop(); @@ -57,7 +65,9 @@ Worker::~Worker() MS_TRACE(); if (!this->closed) + { Close(); + } } void Worker::Close() @@ -65,12 +75,14 @@ void Worker::Close() MS_TRACE(); if (this->closed) + { return; + } this->closed = true; - // Delete the SignalsHandler. - delete this->signalsHandler; + // Delete the SignalHandle. + delete this->signalHandle; // Delete all Routers. for (auto& kv : this->mapRouters) @@ -96,50 +108,70 @@ void Worker::Close() // Close the Checker instance in DepUsrSCTP. DepUsrSCTP::CloseChecker(); +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + // Stop polling CQEs, which will close the uv_pool_t handle. + DepLibUring::StopPollingCQEs(); + } +#endif + // Close the Channel. this->channel->Close(); - - // Close the PayloadChannel. - this->payloadChannel->Close(); } -void Worker::FillJson(json& jsonObject) const +flatbuffers::Offset Worker::FillBuffer( + flatbuffers::FlatBufferBuilder& builder) const { - MS_TRACE(); - - // Add pid. - jsonObject["pid"] = Logger::pid; - // Add webRtcServerIds. - jsonObject["webRtcServerIds"] = json::array(); - auto jsonWebRtcServerIdsIt = jsonObject.find("webRtcServerIds"); + std::vector> webRtcServerIds; + webRtcServerIds.reserve(this->mapWebRtcServers.size()); - for (auto& kv : this->mapWebRtcServers) + for (const auto& kv : this->mapWebRtcServers) { - auto& WebRtcServerId = kv.first; + const auto& webRtcServerId = kv.first; - jsonWebRtcServerIdsIt->emplace_back(WebRtcServerId); + webRtcServerIds.push_back(builder.CreateString(webRtcServerId)); } // Add routerIds. - jsonObject["routerIds"] = json::array(); - auto jsonRouterIdsIt = jsonObject.find("routerIds"); + std::vector> routerIds; + routerIds.reserve(this->mapRouters.size()); - for (auto& kv : this->mapRouters) + for (const auto& kv : this->mapRouters) { - auto& routerId = kv.first; + const auto& routerId = kv.first; - jsonRouterIdsIt->emplace_back(routerId); + routerIds.push_back(builder.CreateString(routerId)); } // Add channelMessageHandlers. - jsonObject["channelMessageHandlers"] = json::object(); - auto jsonChannelMessageHandlersIt = jsonObject.find("channelMessageHandlers"); + auto channelMessageHandlers = this->shared->channelMessageRegistrator->FillBuffer(builder); - this->shared->channelMessageRegistrator->FillJson(*jsonChannelMessageHandlersIt); +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + return FBS::Worker::CreateDumpResponseDirect( + builder, + Logger::Pid, + &webRtcServerIds, + &routerIds, + channelMessageHandlers, + DepLibUring::FillBuffer(builder)); + } + else + { + return FBS::Worker::CreateDumpResponseDirect( + builder, Logger::Pid, &webRtcServerIds, &routerIds, channelMessageHandlers); + } +#else + return FBS::Worker::CreateDumpResponseDirect( + builder, Logger::Pid, &webRtcServerIds, &routerIds, channelMessageHandlers); +#endif } -void Worker::FillJsonResourceUsage(json& jsonObject) const +flatbuffers::Offset Worker::FillBufferResourceUsage( + flatbuffers::FlatBufferBuilder& builder) const { MS_TRACE(); @@ -149,140 +181,114 @@ void Worker::FillJsonResourceUsage(json& jsonObject) const err = uv_getrusage(std::addressof(uvRusage)); if (err != 0) + { MS_THROW_ERROR("uv_getrusagerequest() failed: %s", uv_strerror(err)); + } - // Add ru_utime (uv_timeval_t, user CPU time used, converted to ms). - jsonObject["ru_utime"] = - (uvRusage.ru_utime.tv_sec * static_cast(1000)) + (uvRusage.ru_utime.tv_usec / 1000); - - // Add ru_stime (uv_timeval_t, system CPU time used, converted to ms). - jsonObject["ru_stime"] = - (uvRusage.ru_stime.tv_sec * static_cast(1000)) + (uvRusage.ru_stime.tv_usec / 1000); - - // Add ru_maxrss (uint64_t, maximum resident set size). - jsonObject["ru_maxrss"] = uvRusage.ru_maxrss; - - // Add ru_ixrss (uint64_t, integral shared memory size). - jsonObject["ru_ixrss"] = uvRusage.ru_ixrss; - - // Add ru_idrss (uint64_t, integral unshared data size). - jsonObject["ru_idrss"] = uvRusage.ru_idrss; - - // Add ru_isrss (uint64_t, integral unshared stack size). - jsonObject["ru_isrss"] = uvRusage.ru_isrss; - - // Add ru_minflt (uint64_t, page reclaims, soft page faults). - jsonObject["ru_minflt"] = uvRusage.ru_minflt; - - // Add ru_majflt (uint64_t, page faults, hard page faults). - jsonObject["ru_majflt"] = uvRusage.ru_majflt; - - // Add ru_nswap (uint64_t, swaps). - jsonObject["ru_nswap"] = uvRusage.ru_nswap; - - // Add ru_inblock (uint64_t, block input operations). - jsonObject["ru_inblock"] = uvRusage.ru_inblock; + return FBS::Worker::CreateResourceUsageResponse( + builder, + // Add ru_utime (uv_timeval_t, user CPU time used, converted to ms). + (uvRusage.ru_utime.tv_sec * static_cast(1000)) + (uvRusage.ru_utime.tv_usec / 1000), + // Add ru_stime (uv_timeval_t, system CPU time used, converted to ms). + (uvRusage.ru_stime.tv_sec * static_cast(1000)) + (uvRusage.ru_stime.tv_usec / 1000), + // Add ru_maxrss (uint64_t, maximum resident set size). + uvRusage.ru_maxrss, - // Add ru_oublock (uint64_t, block output operations). - jsonObject["ru_oublock"] = uvRusage.ru_oublock; + // Add ru_ixrss (uint64_t, integral shared memory size). + uvRusage.ru_ixrss, - // Add ru_msgsnd (uint64_t, IPC messages sent). - jsonObject["ru_msgsnd"] = uvRusage.ru_msgsnd; + // Add ru_idrss (uint64_t, integral unshared data size). + uvRusage.ru_idrss, - // Add ru_msgrcv (uint64_t, IPC messages received). - jsonObject["ru_msgrcv"] = uvRusage.ru_msgrcv; + // Add ru_isrss (uint64_t, integral unshared stack size). + uvRusage.ru_isrss, - // Add ru_nsignals (uint64_t, signals received). - jsonObject["ru_nsignals"] = uvRusage.ru_nsignals; + // Add ru_minflt (uint64_t, page reclaims, soft page faults). + uvRusage.ru_minflt, - // Add ru_nvcsw (uint64_t, voluntary context switches). - jsonObject["ru_nvcsw"] = uvRusage.ru_nvcsw; + // Add ru_majflt (uint64_t, page faults, hard page faults). + uvRusage.ru_majflt, - // Add ru_nivcsw (uint64_t, involuntary context switches). - jsonObject["ru_nivcsw"] = uvRusage.ru_nivcsw; -} + // Add ru_nswap (uint64_t, swaps). + uvRusage.ru_nswap, -void Worker::SetNewWebRtcServerIdFromData(json& data, std::string& webRtcServerId) const -{ - MS_TRACE(); + // Add ru_inblock (uint64_t, block input operations). + uvRusage.ru_inblock, - auto jsonWebRtcServerIdIt = data.find("webRtcServerId"); + // Add ru_oublock (uint64_t, block output operations). + uvRusage.ru_oublock, - if (jsonWebRtcServerIdIt == data.end() || !jsonWebRtcServerIdIt->is_string()) - MS_THROW_ERROR("missing webRtcServerId"); + // Add ru_msgsnd (uint64_t, IPC messages sent). + uvRusage.ru_msgsnd, - webRtcServerId.assign(jsonWebRtcServerIdIt->get()); + // Add ru_msgrcv (uint64_t, IPC messages received). + uvRusage.ru_msgrcv, - if (this->mapWebRtcServers.find(webRtcServerId) != this->mapWebRtcServers.end()) - MS_THROW_ERROR("a WebRtcServer with same webRtcServerId already exists"); + // Add ru_nsignals (uint64_t, signals received). + uvRusage.ru_nsignals, + // Add ru_nvcsw (uint64_t, voluntary context switches). + uvRusage.ru_nvcsw, + // Add ru_nivcsw (uint64_t, involuntary context switches). + uvRusage.ru_nivcsw); } -RTC::WebRtcServer* Worker::GetWebRtcServerFromData(json& data) const +RTC::WebRtcServer* Worker::GetWebRtcServer(const std::string& webRtcServerId) const { - MS_TRACE(); - - auto jsonWebRtcServerIdIt = data.find("webRtcServerId"); - - if (jsonWebRtcServerIdIt == data.end() || !jsonWebRtcServerIdIt->is_string()) - MS_THROW_ERROR("missing handlerId.webRtcServerId"); - - auto it = this->mapWebRtcServers.find(jsonWebRtcServerIdIt->get()); + auto it = this->mapWebRtcServers.find(webRtcServerId); if (it == this->mapWebRtcServers.end()) + { MS_THROW_ERROR("WebRtcServer not found"); + } - RTC::WebRtcServer* webRtcServer = it->second; - - return webRtcServer; + return it->second; } -void Worker::SetNewRouterIdFromData(json& data, std::string& routerId) const +RTC::Router* Worker::GetRouter(const std::string& routerId) const { MS_TRACE(); - auto jsonRouterIdIt = data.find("routerId"); - - if (jsonRouterIdIt == data.end() || !jsonRouterIdIt->is_string()) - MS_THROW_ERROR("missing routerId"); + auto it = this->mapRouters.find(routerId); - routerId.assign(jsonRouterIdIt->get()); + if (it == this->mapRouters.end()) + { + MS_THROW_ERROR("Router not found"); + } - if (this->mapRouters.find(routerId) != this->mapRouters.end()) - MS_THROW_ERROR("a Router with same routerId already exists"); + return it->second; } -RTC::Router* Worker::GetRouterFromData(json& data) const +void Worker::CheckNoWebRtcServer(const std::string& webRtcServerId) const { - MS_TRACE(); - - auto jsonRouterIdIt = data.find("routerId"); - - if (jsonRouterIdIt == data.end() || !jsonRouterIdIt->is_string()) - MS_THROW_ERROR("missing routerId"); - - auto it = this->mapRouters.find(jsonRouterIdIt->get()); - - if (it == this->mapRouters.end()) - MS_THROW_ERROR("Router not found"); - - RTC::Router* router = it->second; + if (this->mapWebRtcServers.find(webRtcServerId) != this->mapWebRtcServers.end()) + { + MS_THROW_ERROR("a WebRtcServer with same webRtcServerId already exists"); + } +} - return router; +void Worker::CheckNoRouter(const std::string& routerId) const +{ + if (this->mapRouters.find(routerId) != this->mapRouters.end()) + { + MS_THROW_ERROR("a Router with same routerId already exists"); + } } -inline void Worker::HandleRequest(Channel::ChannelRequest* request) +void Worker::HandleRequest(Channel::ChannelRequest* request) { MS_TRACE(); MS_DEBUG_DEV( - "Channel request received [method:%s, id:%" PRIu32 "]", request->method.c_str(), request->id); + "Channel request received [method:%s, id:%" PRIu32 "]", request->methodCStr, request->id); - switch (request->methodId) + switch (request->method) { - case Channel::ChannelRequest::MethodId::WORKER_CLOSE: + case Channel::ChannelRequest::Method::WORKER_CLOSE: { if (this->closed) + { return; + } MS_DEBUG_DEV("Worker close request, stopping"); @@ -291,44 +297,42 @@ inline void Worker::HandleRequest(Channel::ChannelRequest* request) break; } - case Channel::ChannelRequest::MethodId::WORKER_DUMP: + case Channel::ChannelRequest::Method::WORKER_DUMP: { - json data = json::object(); + auto dumpOffset = FillBuffer(request->GetBufferBuilder()); - FillJson(data); - - request->Accept(data); + request->Accept(FBS::Response::Body::Worker_DumpResponse, dumpOffset); break; } - case Channel::ChannelRequest::MethodId::WORKER_GET_RESOURCE_USAGE: + case Channel::ChannelRequest::Method::WORKER_GET_RESOURCE_USAGE: { - json data = json::object(); - - FillJsonResourceUsage(data); + auto resourceUsageOffset = FillBufferResourceUsage(request->GetBufferBuilder()); - request->Accept(data); + request->Accept(FBS::Response::Body::Worker_ResourceUsageResponse, resourceUsageOffset); break; } - case Channel::ChannelRequest::MethodId::WORKER_UPDATE_SETTINGS: + case Channel::ChannelRequest::Method::WORKER_UPDATE_SETTINGS: { Settings::HandleRequest(request); break; } - case Channel::ChannelRequest::MethodId::WORKER_CREATE_WEBRTC_SERVER: + case Channel::ChannelRequest::Method::WORKER_CREATE_WEBRTCSERVER: { try { - std::string webRtcServerId; + const auto* const body = request->data->body_as(); - SetNewWebRtcServerIdFromData(request->data, webRtcServerId); + const std::string webRtcServerId = body->webRtcServerId()->str(); - auto* webRtcServer = new RTC::WebRtcServer(this->shared, webRtcServerId, request->data); + CheckNoWebRtcServer(webRtcServerId); + + auto* webRtcServer = new RTC::WebRtcServer(this->shared, webRtcServerId, body->listenInfos()); this->mapWebRtcServers[webRtcServerId] = webRtcServer; @@ -338,27 +342,31 @@ inline void Worker::HandleRequest(Channel::ChannelRequest* request) } catch (const MediaSoupTypeError& error) { - MS_THROW_TYPE_ERROR("%s [method:%s]", error.what(), request->method.c_str()); + MS_THROW_TYPE_ERROR("%s [method:%s]", error.what(), request->methodCStr); } catch (const MediaSoupError& error) { - MS_THROW_ERROR("%s [method:%s]", error.what(), request->method.c_str()); + MS_THROW_ERROR("%s [method:%s]", error.what(), request->methodCStr); } break; } - case Channel::ChannelRequest::MethodId::WORKER_WEBRTC_SERVER_CLOSE: + case Channel::ChannelRequest::Method::WORKER_WEBRTCSERVER_CLOSE: { RTC::WebRtcServer* webRtcServer{ nullptr }; + const auto* body = request->data->body_as(); + + auto webRtcServerId = body->webRtcServerId()->str(); + try { - webRtcServer = GetWebRtcServerFromData(request->data); + webRtcServer = GetWebRtcServer(webRtcServerId); } catch (const MediaSoupError& error) { - MS_THROW_ERROR("%s [method:%s]", error.what(), request->method.c_str()); + MS_THROW_ERROR("%s [method:%s]", error.what(), request->methodCStr); } // Remove it from the map and delete it. @@ -373,17 +381,19 @@ inline void Worker::HandleRequest(Channel::ChannelRequest* request) break; } - case Channel::ChannelRequest::MethodId::WORKER_CREATE_ROUTER: + case Channel::ChannelRequest::Method::WORKER_CREATE_ROUTER: { - std::string routerId; + const auto* body = request->data->body_as(); + + auto routerId = body->routerId()->str(); try { - SetNewRouterIdFromData(request->data, routerId); + CheckNoRouter(routerId); } catch (const MediaSoupError& error) { - MS_THROW_ERROR("%s [method:%s]", error.what(), request->method.c_str()); + MS_THROW_ERROR("%s [method:%s]", error.what(), request->methodCStr); } auto* router = new RTC::Router(this->shared, routerId, this); @@ -397,17 +407,21 @@ inline void Worker::HandleRequest(Channel::ChannelRequest* request) break; } - case Channel::ChannelRequest::MethodId::WORKER_CLOSE_ROUTER: + case Channel::ChannelRequest::Method::WORKER_CLOSE_ROUTER: { RTC::Router* router{ nullptr }; + const auto* body = request->data->body_as(); + + auto routerId = body->routerId()->str(); + try { - router = GetRouterFromData(request->data); + router = GetRouter(routerId); } catch (const MediaSoupError& error) { - MS_THROW_ERROR("%s [method:%s]", error.what(), request->method.c_str()); + MS_THROW_ERROR("%s [method:%s]", error.what(), request->methodCStr); } // Remove it from the map and delete it. @@ -439,11 +453,11 @@ inline void Worker::HandleRequest(Channel::ChannelRequest* request) } catch (const MediaSoupTypeError& error) { - MS_THROW_TYPE_ERROR("%s [method:%s]", error.what(), request->method.c_str()); + MS_THROW_TYPE_ERROR("%s [method:%s]", error.what(), request->methodCStr); } catch (const MediaSoupError& error) { - MS_THROW_ERROR("%s [method:%s]", error.what(), request->method.c_str()); + MS_THROW_ERROR("%s [method:%s]", error.what(), request->methodCStr); } break; @@ -451,108 +465,66 @@ inline void Worker::HandleRequest(Channel::ChannelRequest* request) } } -inline void Worker::OnChannelClosed(Channel::ChannelSocket* /*socket*/) -{ - MS_TRACE_STD(); - - // Only needed for executable, library user can close channel earlier and it is fine. -#ifdef MS_EXECUTABLE - // If the pipe is remotely closed it may mean that mediasoup Node process - // abruptly died (SIGKILL?) so we must die. - MS_ERROR_STD("channel remotely closed, closing myself"); -#endif - - Close(); -} - -inline void Worker::HandleRequest(PayloadChannel::PayloadChannelRequest* request) +void Worker::HandleNotification(Channel::ChannelNotification* notification) { MS_TRACE(); - MS_DEBUG_DEV( - "PayloadChannel request received [method:%s, id:%" PRIu32 "]", - request->method.c_str(), - request->id); + MS_DEBUG_DEV("Channel notification received [event:%s]", notification->eventCStr); try { auto* handler = - this->shared->channelMessageRegistrator->GetPayloadChannelRequestHandler(request->handlerId); - - if (handler == nullptr) - { - MS_THROW_ERROR( - "PayloadChannel request handler with ID %s not found", request->handlerId.c_str()); - } - - handler->HandleRequest(request); - } - catch (const MediaSoupTypeError& error) - { - MS_THROW_TYPE_ERROR("%s [method:%s]", error.what(), request->method.c_str()); - } - catch (const MediaSoupError& error) - { - MS_THROW_ERROR("%s [method:%s]", error.what(), request->method.c_str()); - } -} - -inline void Worker::HandleNotification(PayloadChannel::PayloadChannelNotification* notification) -{ - MS_TRACE(); - - MS_DEBUG_DEV("PayloadChannel notification received [event:%s]", notification->event.c_str()); - - try - { - auto* handler = this->shared->channelMessageRegistrator->GetPayloadChannelNotificationHandler( - notification->handlerId); + this->shared->channelMessageRegistrator->GetChannelNotificationHandler(notification->handlerId); if (handler == nullptr) { MS_THROW_ERROR( - "PayloadChannel notification handler with ID %s not found", notification->handlerId.c_str()); + "Channel notification handler with ID %s not found", notification->handlerId.c_str()); } handler->HandleNotification(notification); } catch (const MediaSoupTypeError& error) { - MS_THROW_TYPE_ERROR("%s [event:%s]", error.what(), notification->event.c_str()); + MS_THROW_TYPE_ERROR("%s [event:%s]", error.what(), notification->eventCStr); } catch (const MediaSoupError& error) { - MS_THROW_ERROR("%s [method:%s]", error.what(), notification->event.c_str()); + MS_THROW_ERROR("%s [method:%s]", error.what(), notification->eventCStr); } } -inline void Worker::OnPayloadChannelClosed(PayloadChannel::PayloadChannelSocket* /*payloadChannel*/) +void Worker::OnChannelClosed(Channel::ChannelSocket* /*socket*/) { - MS_TRACE(); + MS_TRACE_STD(); // Only needed for executable, library user can close channel earlier and it is fine. #ifdef MS_EXECUTABLE // If the pipe is remotely closed it may mean that mediasoup Node process // abruptly died (SIGKILL?) so we must die. - MS_ERROR_STD("payloadChannel remotely closed, closing myself"); + MS_ERROR_STD("channel remotely closed, closing myself"); #endif Close(); } -inline void Worker::OnSignal(SignalsHandler* /*signalsHandler*/, int signum) +void Worker::OnSignal(SignalHandle* /*signalHandle*/, int signum) { MS_TRACE(); if (this->closed) + { return; + } switch (signum) { case SIGINT: { if (this->closed) + { return; + } MS_DEBUG_DEV("INT signal received, closing myself"); @@ -564,7 +536,9 @@ inline void Worker::OnSignal(SignalsHandler* /*signalsHandler*/, int signum) case SIGTERM: { if (this->closed) + { return; + } MS_DEBUG_DEV("TERM signal received, closing myself"); @@ -580,8 +554,7 @@ inline void Worker::OnSignal(SignalsHandler* /*signalsHandler*/, int signum) } } -inline RTC::WebRtcServer* Worker::OnRouterNeedWebRtcServer( - RTC::Router* /*router*/, std::string& webRtcServerId) +RTC::WebRtcServer* Worker::OnRouterNeedWebRtcServer(RTC::Router* /*router*/, std::string& webRtcServerId) { MS_TRACE(); diff --git a/worker/src/handles/SignalsHandler.cpp b/worker/src/handles/SignalHandle.cpp similarity index 66% rename from worker/src/handles/SignalsHandler.cpp rename to worker/src/handles/SignalHandle.cpp index 60a63c5c0c..9dd8fb3cf7 100644 --- a/worker/src/handles/SignalsHandler.cpp +++ b/worker/src/handles/SignalHandle.cpp @@ -1,7 +1,7 @@ -#define MS_CLASS "SignalsHandler" +#define MS_CLASS "SignalHandle" // #define MS_LOG_DEV_LEVEL 3 -#include "handles/SignalsHandler.hpp" +#include "handles/SignalHandle.hpp" #include "DepLibUV.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" @@ -10,53 +10,42 @@ inline static void onSignal(uv_signal_t* handle, int signum) { - static_cast(handle->data)->OnUvSignal(signum); + static_cast(handle->data)->OnUvSignal(signum); } -inline static void onClose(uv_handle_t* handle) +inline static void onCloseSignal(uv_handle_t* handle) { - delete handle; + delete reinterpret_cast(handle); } /* Instance methods. */ -SignalsHandler::SignalsHandler(Listener* listener) : listener(listener) +SignalHandle::SignalHandle(Listener* listener) : listener(listener) { MS_TRACE(); } -SignalsHandler::~SignalsHandler() +SignalHandle::~SignalHandle() { MS_TRACE(); if (!this->closed) - Close(); -} - -void SignalsHandler::Close() -{ - MS_TRACE(); - - if (this->closed) - return; - - this->closed = true; - - for (auto* uvHandle : this->uvHandles) { - uv_close(reinterpret_cast(uvHandle), static_cast(onClose)); + InternalClose(); } } -void SignalsHandler::AddSignal(int signum, const std::string& name) +void SignalHandle::AddSignal(int signum, const std::string& name) { MS_TRACE(); if (this->closed) + { MS_THROW_ERROR("closed"); + } int err; - auto uvHandle = new uv_signal_t; + auto* uvHandle = new uv_signal_t; uvHandle->data = static_cast(this); @@ -72,13 +61,32 @@ void SignalsHandler::AddSignal(int signum, const std::string& name) err = uv_signal_start(uvHandle, static_cast(onSignal), signum); if (err != 0) + { MS_THROW_ERROR("uv_signal_start() failed for signal %s: %s", name.c_str(), uv_strerror(err)); + } // Enter the UV handle into the vector. this->uvHandles.push_back(uvHandle); } -inline void SignalsHandler::OnUvSignal(int signum) +void SignalHandle::InternalClose() +{ + MS_TRACE(); + + if (this->closed) + { + return; + } + + this->closed = true; + + for (auto* uvHandle : this->uvHandles) + { + uv_close(reinterpret_cast(uvHandle), static_cast(onCloseSignal)); + } +} + +void SignalHandle::OnUvSignal(int signum) { MS_TRACE(); diff --git a/worker/src/handles/TcpConnectionHandler.cpp b/worker/src/handles/TcpConnectionHandle.cpp similarity index 68% rename from worker/src/handles/TcpConnectionHandler.cpp rename to worker/src/handles/TcpConnectionHandle.cpp index 5fd6a7f045..ea53cb8ff9 100644 --- a/worker/src/handles/TcpConnectionHandler.cpp +++ b/worker/src/handles/TcpConnectionHandle.cpp @@ -1,8 +1,11 @@ -#define MS_CLASS "TcpConnectionHandler" +#define MS_CLASS "TcpConnectionHandle" // #define MS_LOG_DEV_LEVEL 3 -#include "handles/TcpConnectionHandler.hpp" +#include "handles/TcpConnectionHandle.hpp" #include "DepLibUV.hpp" +#ifdef MS_LIBURING_SUPPORTED +#include "DepLibUring.hpp" +#endif #include "Logger.hpp" #include "MediaSoupErrors.hpp" #include "Utils.hpp" @@ -12,37 +15,45 @@ inline static void onAlloc(uv_handle_t* handle, size_t suggestedSize, uv_buf_t* buf) { - auto* connection = static_cast(handle->data); + auto* connection = static_cast(handle->data); if (connection) + { connection->OnUvReadAlloc(suggestedSize, buf); + } } inline static void onRead(uv_stream_t* handle, ssize_t nread, const uv_buf_t* buf) { - auto* connection = static_cast(handle->data); + auto* connection = static_cast(handle->data); if (connection) + { connection->OnUvRead(nread, buf); + } } inline static void onWrite(uv_write_t* req, int status) { - auto* writeData = static_cast(req->data); + auto* writeData = static_cast(req->data); auto* handle = req->handle; - auto* connection = static_cast(handle->data); - auto* cb = writeData->cb; + auto* connection = static_cast(handle->data); + const auto* cb = writeData->cb; if (connection) + { connection->OnUvWrite(status, cb); + } // Delete the UvWriteData struct and the cb. delete writeData; } -inline static void onClose(uv_handle_t* handle) +// NOTE: We have different onCloseXxx() callbacks to avoid an ASAN warning by +// ensuring that we call `delete xxx` with same type as `new xxx` before. +inline static void onCloseTcp(uv_handle_t* handle) { - delete handle; + delete reinterpret_cast(handle); } inline static void onShutdown(uv_shutdown_t* req, int /*status*/) @@ -52,90 +63,66 @@ inline static void onShutdown(uv_shutdown_t* req, int /*status*/) delete req; // Now do close the handle. - uv_close(reinterpret_cast(handle), static_cast(onClose)); + uv_close(reinterpret_cast(handle), static_cast(onCloseTcp)); } /* Instance methods. */ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init) -TcpConnectionHandler::TcpConnectionHandler(size_t bufferSize) : bufferSize(bufferSize) +TcpConnectionHandle::TcpConnectionHandle(size_t bufferSize) + : bufferSize(bufferSize), uvHandle(new uv_tcp_t) { MS_TRACE(); - this->uvHandle = new uv_tcp_t; this->uvHandle->data = static_cast(this); // NOTE: Don't allocate the buffer here. Instead wait for the first uv_alloc_cb(). } -TcpConnectionHandler::~TcpConnectionHandler() +TcpConnectionHandle::~TcpConnectionHandle() { MS_TRACE(); if (!this->closed) - Close(); + { + InternalClose(); + } delete[] this->buffer; } -void TcpConnectionHandler::Close() +void TcpConnectionHandle::TriggerClose() { MS_TRACE(); if (this->closed) + { return; + } - int err; - - this->closed = true; - - // Tell the UV handle that the TcpConnectionHandler has been closed. - this->uvHandle->data = nullptr; - - // Don't read more. - err = uv_read_stop(reinterpret_cast(this->uvHandle)); - - if (err != 0) - MS_ABORT("uv_read_stop() failed: %s", uv_strerror(err)); + InternalClose(); - // If there is no error and the peer didn't close its connection side then close gracefully. - if (!this->hasError && !this->isClosedByPeer) - { - // Use uv_shutdown() so pending data to be written will be sent to the peer - // before closing. - auto req = new uv_shutdown_t; - req->data = static_cast(this); - err = uv_shutdown( - req, reinterpret_cast(this->uvHandle), static_cast(onShutdown)); - - if (err != 0) - MS_ABORT("uv_shutdown() failed: %s", uv_strerror(err)); - } - // Otherwise directly close the socket. - else - { - uv_close(reinterpret_cast(this->uvHandle), static_cast(onClose)); - } + this->listener->OnTcpConnectionClosed(this); } -void TcpConnectionHandler::Dump() const +void TcpConnectionHandle::Dump() const { - MS_DUMP(""); - MS_DUMP(" localIp : %s", this->localIp.c_str()); - MS_DUMP(" localPort : %" PRIu16, static_cast(this->localPort)); - MS_DUMP(" remoteIp : %s", this->peerIp.c_str()); - MS_DUMP(" remotePort : %" PRIu16, static_cast(this->peerPort)); - MS_DUMP(" closed : %s", !this->closed ? "open" : "closed"); - MS_DUMP(""); + MS_DUMP(""); + MS_DUMP(" localIp: %s", this->localIp.c_str()); + MS_DUMP(" localPort: %" PRIu16, static_cast(this->localPort)); + MS_DUMP(" remoteIp: %s", this->peerIp.c_str()); + MS_DUMP(" remotePort: %" PRIu16, static_cast(this->peerPort)); + MS_DUMP(" closed: %s", this->closed ? "yes" : "no"); + MS_DUMP(""); } -void TcpConnectionHandler::Setup( +void TcpConnectionHandle::Setup( Listener* listener, struct sockaddr_storage* localAddr, const std::string& localIp, uint16_t localPort) { MS_TRACE(); // Set the UV handle. - int err = uv_tcp_init(DepLibUV::GetLoop(), this->uvHandle); + const int err = uv_tcp_init(DepLibUV::GetLoop(), this->uvHandle); if (err != 0) { @@ -154,32 +141,51 @@ void TcpConnectionHandler::Setup( this->localPort = localPort; } -void TcpConnectionHandler::Start() +void TcpConnectionHandle::Start() { MS_TRACE(); if (this->closed) + { return; + } + // NOLINTNEXTLINE(misc-const-correctness) int err = uv_read_start( reinterpret_cast(this->uvHandle), static_cast(onAlloc), static_cast(onRead)); if (err != 0) + { MS_THROW_ERROR("uv_read_start() failed: %s", uv_strerror(err)); + } // Get the peer address. if (!SetPeerAddress()) + { MS_THROW_ERROR("error setting peer IP and port"); + } + +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + err = uv_fileno(reinterpret_cast(this->uvHandle), std::addressof(this->fd)); + + if (err != 0) + { + MS_THROW_ERROR("uv_fileno() failed: %s", uv_strerror(err)); + } + } +#endif } -void TcpConnectionHandler::Write( +void TcpConnectionHandle::Write( const uint8_t* data1, size_t len1, const uint8_t* data2, size_t len2, - TcpConnectionHandler::onSendCallback* cb) + TcpConnectionHandle::onSendCallback* cb) { MS_TRACE(); @@ -205,14 +211,40 @@ void TcpConnectionHandler::Write( return; } - size_t totalLen = len1 + len2; - uv_buf_t buffers[2]; - int written{ 0 }; - int err; +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + if (!DepLibUring::IsActive()) + { + goto write_libuv; + } + + // Prepare the data to be sent. + // NOTE: If all SQEs are currently in use or no UserData entry is available we'll + // fall back to libuv. + auto prepared = DepLibUring::PrepareWrite(this->fd, data1, len1, data2, len2, cb); + + if (!prepared) + { + MS_DEBUG_DEV("cannot write via liburing, fallback to libuv"); + + goto write_libuv; + } + + return; + } + +write_libuv: +#endif // First try uv_try_write(). In case it can not directly write all the given // data then build a uv_req_t and use uv_write(). + const size_t totalLen = len1 + len2; + uv_buf_t buffers[2]; + int written{ 0 }; + int err; + buffers[0] = uv_buf_init(reinterpret_cast(const_cast(data1)), len1); buffers[1] = uv_buf_init(reinterpret_cast(const_cast(data2)), len2); written = uv_try_write(reinterpret_cast(this->uvHandle), buffers, 2); @@ -246,8 +278,8 @@ void TcpConnectionHandler::Write( written = 0; } - size_t pendingLen = totalLen - written; - auto* writeData = new UvWriteData(pendingLen); + const size_t pendingLen = totalLen - written; + auto* writeData = new UvWriteData(pendingLen); writeData->req.data = static_cast(writeData); @@ -269,7 +301,7 @@ void TcpConnectionHandler::Write( writeData->cb = cb; - uv_buf_t buffer = uv_buf_init(reinterpret_cast(writeData->store), pendingLen); + const uv_buf_t buffer = uv_buf_init(reinterpret_cast(writeData->store), pendingLen); err = uv_write( &writeData->req, @@ -283,7 +315,9 @@ void TcpConnectionHandler::Write( MS_WARN_DEV("uv_write() failed: %s", uv_strerror(err)); if (cb) + { (*cb)(false); + } // Delete the UvWriteData struct (it will delete the store and cb too). delete writeData; @@ -295,16 +329,62 @@ void TcpConnectionHandler::Write( } } -void TcpConnectionHandler::ErrorReceiving() +void TcpConnectionHandle::ErrorReceiving() { MS_TRACE(); - Close(); + InternalClose(); this->listener->OnTcpConnectionClosed(this); } -bool TcpConnectionHandler::SetPeerAddress() +void TcpConnectionHandle::InternalClose() +{ + MS_TRACE(); + + if (this->closed) + { + return; + } + + int err; + + this->closed = true; + + // Tell the UV handle that the TcpConnectionHandle has been closed. + this->uvHandle->data = nullptr; + + // Don't read more. + err = uv_read_stop(reinterpret_cast(this->uvHandle)); + + if (err != 0) + { + MS_ABORT("uv_read_stop() failed: %s", uv_strerror(err)); + } + + // If there is no error and the peer didn't close its connection side then close gracefully. + if (!this->hasError && !this->isClosedByPeer) + { + // Use uv_shutdown() so pending data to be written will be sent to the peer + // before closing. + auto* req = new uv_shutdown_t; + req->data = static_cast(this); + err = uv_shutdown( + req, reinterpret_cast(this->uvHandle), static_cast(onShutdown)); + + if (err != 0) + { + MS_ABORT("uv_shutdown() failed: %s", uv_strerror(err)); + } + } + // Otherwise directly close the socket. + else + { + uv_close(reinterpret_cast(this->uvHandle), static_cast(onCloseTcp)); + } +} + +bool TcpConnectionHandle::SetPeerAddress() { MS_TRACE(); @@ -328,13 +408,15 @@ bool TcpConnectionHandler::SetPeerAddress() return true; } -inline void TcpConnectionHandler::OnUvReadAlloc(size_t /*suggestedSize*/, uv_buf_t* buf) +inline void TcpConnectionHandle::OnUvReadAlloc(size_t /*suggestedSize*/, uv_buf_t* buf) { MS_TRACE(); // If this is the first call to onUvReadAlloc() then allocate the receiving buffer now. if (!this->buffer) + { this->buffer = new uint8_t[this->bufferSize]; + } // Tell UV to write after the last data byte in the buffer. buf->base = reinterpret_cast(this->buffer + this->bufferDataLen); @@ -352,12 +434,14 @@ inline void TcpConnectionHandler::OnUvReadAlloc(size_t /*suggestedSize*/, uv_buf } } -inline void TcpConnectionHandler::OnUvRead(ssize_t nread, const uv_buf_t* /*buf*/) +inline void TcpConnectionHandle::OnUvRead(ssize_t nread, const uv_buf_t* /*buf*/) { MS_TRACE(); if (nread == 0) + { return; + } // Data received. if (nread > 0) @@ -379,7 +463,7 @@ inline void TcpConnectionHandler::OnUvRead(ssize_t nread, const uv_buf_t* /*buf* this->isClosedByPeer = true; // Close server side of the connection. - Close(); + InternalClose(); // Notify the listener. this->listener->OnTcpConnectionClosed(this); @@ -392,14 +476,14 @@ inline void TcpConnectionHandler::OnUvRead(ssize_t nread, const uv_buf_t* /*buf* this->hasError = true; // Close server side of the connection. - Close(); + InternalClose(); // Notify the listener. this->listener->OnTcpConnectionClosed(this); } } -inline void TcpConnectionHandler::OnUvWrite(int status, TcpConnectionHandler::onSendCallback* cb) +inline void TcpConnectionHandle::OnUvWrite(int status, TcpConnectionHandle::onSendCallback* cb) { MS_TRACE(); @@ -408,19 +492,25 @@ inline void TcpConnectionHandler::OnUvWrite(int status, TcpConnectionHandler::on if (status == 0) { if (cb) + { (*cb)(true); + } } else { if (status != UV_EPIPE && status != UV_ENOTCONN) + { this->hasError = true; + } MS_WARN_DEV("write error, closing the connection: %s", uv_strerror(status)); if (cb) + { (*cb)(false); + } - Close(); + InternalClose(); this->listener->OnTcpConnectionClosed(this); } diff --git a/worker/src/handles/TcpServerHandler.cpp b/worker/src/handles/TcpServerHandle.cpp similarity index 50% rename from worker/src/handles/TcpServerHandler.cpp rename to worker/src/handles/TcpServerHandle.cpp index a3213d709e..def778a577 100644 --- a/worker/src/handles/TcpServerHandler.cpp +++ b/worker/src/handles/TcpServerHandle.cpp @@ -1,7 +1,7 @@ -#define MS_CLASS "TcpServerHandler" +#define MS_CLASS "TcpServerHandle" // #define MS_LOG_DEV_LEVEL 3 -#include "handles/TcpServerHandler.hpp" +#include "handles/TcpServerHandle.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" #include "Utils.hpp" @@ -14,21 +14,23 @@ static constexpr int ListenBacklog{ 512 }; inline static void onConnection(uv_stream_t* handle, int status) { - auto* server = static_cast(handle->data); + auto* server = static_cast(handle->data); if (server) + { server->OnUvConnection(status); + } } -inline static void onClose(uv_handle_t* handle) +inline static void onCloseTcp(uv_handle_t* handle) { - delete handle; + delete reinterpret_cast(handle); } /* Instance methods. */ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init) -TcpServerHandler::TcpServerHandler(uv_tcp_t* uvHandle) : uvHandle(uvHandle) +TcpServerHandle::TcpServerHandle(uv_tcp_t* uvHandle) : uvHandle(uvHandle) { MS_TRACE(); @@ -43,7 +45,7 @@ TcpServerHandler::TcpServerHandler(uv_tcp_t* uvHandle) : uvHandle(uvHandle) if (err != 0) { - uv_close(reinterpret_cast(this->uvHandle), static_cast(onClose)); + uv_close(reinterpret_cast(this->uvHandle), static_cast(onCloseTcp)); MS_THROW_ERROR("uv_listen() failed: %s", uv_strerror(err)); } @@ -51,59 +53,109 @@ TcpServerHandler::TcpServerHandler(uv_tcp_t* uvHandle) : uvHandle(uvHandle) // Set local address. if (!SetLocalAddress()) { - uv_close(reinterpret_cast(this->uvHandle), static_cast(onClose)); + uv_close(reinterpret_cast(this->uvHandle), static_cast(onCloseTcp)); MS_THROW_ERROR("error setting local IP and port"); } } -TcpServerHandler::~TcpServerHandler() +TcpServerHandle::~TcpServerHandle() { MS_TRACE(); if (!this->closed) - Close(); + { + InternalClose(); + } } -void TcpServerHandler::Close() +void TcpServerHandle::Dump() const +{ + MS_DUMP(""); + MS_DUMP(" localIp: %s", this->localIp.c_str()); + MS_DUMP(" localPort: %" PRIu16, static_cast(this->localPort)); + MS_DUMP(" num connections: %zu", this->connections.size()); + MS_DUMP(" closed: %s", this->closed ? "yes" : "no"); + MS_DUMP(""); +} + +uint32_t TcpServerHandle::GetSendBufferSize() const { MS_TRACE(); - if (this->closed) - return; + int size{ 0 }; + const int err = + uv_send_buffer_size(reinterpret_cast(this->uvHandle), std::addressof(size)); - this->closed = true; + if (err) + { + MS_THROW_ERROR("uv_send_buffer_size() failed: %s", uv_strerror(err)); + } - // Tell the UV handle that the TcpServerHandler has been closed. - this->uvHandle->data = nullptr; + return static_cast(size); +} - MS_DEBUG_DEV("closing %zu active connections", this->connections.size()); +void TcpServerHandle::SetSendBufferSize(uint32_t size) +{ + MS_TRACE(); - for (auto* connection : this->connections) + auto sizeInt = static_cast(size); + + if (sizeInt <= 0) { - delete connection; + MS_THROW_TYPE_ERROR("invalid size: %d", sizeInt); + } + + const int err = + uv_send_buffer_size(reinterpret_cast(this->uvHandle), std::addressof(sizeInt)); + + if (err) + { + MS_THROW_ERROR("uv_send_buffer_size() failed: %s", uv_strerror(err)); + } +} + +uint32_t TcpServerHandle::GetRecvBufferSize() const +{ + MS_TRACE(); + + int size{ 0 }; + const int err = + uv_recv_buffer_size(reinterpret_cast(this->uvHandle), std::addressof(size)); + + if (err) + { + MS_THROW_ERROR("uv_recv_buffer_size() failed: %s", uv_strerror(err)); } - uv_close(reinterpret_cast(this->uvHandle), static_cast(onClose)); + return static_cast(size); } -void TcpServerHandler::Dump() const +void TcpServerHandle::SetRecvBufferSize(uint32_t size) { - MS_DUMP(""); - MS_DUMP( - " [TCP, local:%s :%" PRIu16 ", status:%s, connections:%zu]", - this->localIp.c_str(), - static_cast(this->localPort), - (!this->closed) ? "open" : "closed", - this->connections.size()); - MS_DUMP(""); + MS_TRACE(); + + auto sizeInt = static_cast(size); + + if (sizeInt <= 0) + { + MS_THROW_TYPE_ERROR("invalid size: %d", sizeInt); + } + + const int err = + uv_recv_buffer_size(reinterpret_cast(this->uvHandle), std::addressof(sizeInt)); + + if (err) + { + MS_THROW_ERROR("uv_recv_buffer_size() failed: %s", uv_strerror(err)); + } } -void TcpServerHandler::AcceptTcpConnection(TcpConnectionHandler* connection) +void TcpServerHandle::AcceptTcpConnection(TcpConnectionHandle* connection) { MS_TRACE(); - MS_ASSERT(connection != nullptr, "TcpConnectionHandler pointer was not allocated by the user"); + MS_ASSERT(connection != nullptr, "TcpConnectionHandle pointer was not allocated by the user"); try { @@ -117,12 +169,14 @@ void TcpServerHandler::AcceptTcpConnection(TcpConnectionHandler* connection) } // Accept the connection. - int err = uv_accept( + const int err = uv_accept( reinterpret_cast(this->uvHandle), reinterpret_cast(connection->GetUvHandle())); if (err != 0) + { MS_ABORT("uv_accept() failed: %s", uv_strerror(err)); + } // Start receiving data. try @@ -141,7 +195,31 @@ void TcpServerHandler::AcceptTcpConnection(TcpConnectionHandler* connection) this->connections.insert(connection); } -bool TcpServerHandler::SetLocalAddress() +void TcpServerHandle::InternalClose() +{ + MS_TRACE(); + + if (this->closed) + { + return; + } + + this->closed = true; + + // Tell the UV handle that the TcpServerHandle has been closed. + this->uvHandle->data = nullptr; + + MS_DEBUG_DEV("closing %zu active connections", this->connections.size()); + + for (auto* connection : this->connections) + { + delete connection; + } + + uv_close(reinterpret_cast(this->uvHandle), static_cast(onCloseTcp)); +} + +bool TcpServerHandle::SetLocalAddress() { MS_TRACE(); @@ -166,12 +244,14 @@ bool TcpServerHandler::SetLocalAddress() return true; } -inline void TcpServerHandler::OnUvConnection(int status) +inline void TcpServerHandle::OnUvConnection(int status) { MS_TRACE(); if (this->closed) + { return; + } if (status != 0) { @@ -184,13 +264,13 @@ inline void TcpServerHandler::OnUvConnection(int status) UserOnTcpConnectionAlloc(); } -inline void TcpServerHandler::OnTcpConnectionClosed(TcpConnectionHandler* connection) +inline void TcpServerHandle::OnTcpConnectionClosed(TcpConnectionHandle* connection) { MS_TRACE(); MS_DEBUG_DEV("TCP connection closed"); - // Remove the TcpConnectionHandler from the set. + // Remove the TcpConnectionHandle from the set. this->connections.erase(connection); // Notify the subclass. diff --git a/worker/src/handles/Timer.cpp b/worker/src/handles/TimerHandle.cpp similarity index 66% rename from worker/src/handles/Timer.cpp rename to worker/src/handles/TimerHandle.cpp index 825382bc35..a5e4c1afd9 100644 --- a/worker/src/handles/Timer.cpp +++ b/worker/src/handles/TimerHandle.cpp @@ -1,7 +1,7 @@ -#define MS_CLASS "Timer" +#define MS_CLASS "TimerHandle" // #define MS_LOG_DEV_LEVEL 3 -#include "handles/Timer.hpp" +#include "handles/TimerHandle.hpp" #include "DepLibUV.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" @@ -10,24 +10,23 @@ inline static void onTimer(uv_timer_t* handle) { - static_cast(handle->data)->OnUvTimer(); + static_cast(handle->data)->OnUvTimer(); } -inline static void onClose(uv_handle_t* handle) +inline static void onCloseTimer(uv_handle_t* handle) { - delete handle; + delete reinterpret_cast(handle); } /* Instance methods. */ -Timer::Timer(Listener* listener) : listener(listener) +TimerHandle::TimerHandle(Listener* listener) : listener(listener), uvHandle(new uv_timer_t) { MS_TRACE(); - this->uvHandle = new uv_timer_t; this->uvHandle->data = static_cast(this); - int err = uv_timer_init(DepLibUV::GetLoop(), this->uvHandle); + const int err = uv_timer_init(DepLibUV::GetLoop(), this->uvHandle); if (err != 0) { @@ -38,96 +37,124 @@ Timer::Timer(Listener* listener) : listener(listener) } } -Timer::~Timer() +TimerHandle::~TimerHandle() { MS_TRACE(); if (!this->closed) - Close(); -} - -void Timer::Close() -{ - MS_TRACE(); - - if (this->closed) - return; - - this->closed = true; - - uv_close(reinterpret_cast(this->uvHandle), static_cast(onClose)); + { + InternalClose(); + } } -void Timer::Start(uint64_t timeout, uint64_t repeat) +void TimerHandle::Start(uint64_t timeout, uint64_t repeat) { MS_TRACE(); if (this->closed) + { MS_THROW_ERROR("closed"); + } this->timeout = timeout; this->repeat = repeat; if (uv_is_active(reinterpret_cast(this->uvHandle)) != 0) + { Stop(); + } - int err = uv_timer_start(this->uvHandle, static_cast(onTimer), timeout, repeat); + const int err = uv_timer_start(this->uvHandle, static_cast(onTimer), timeout, repeat); if (err != 0) + { MS_THROW_ERROR("uv_timer_start() failed: %s", uv_strerror(err)); + } } -void Timer::Stop() +void TimerHandle::Stop() { MS_TRACE(); if (this->closed) + { MS_THROW_ERROR("closed"); + } - int err = uv_timer_stop(this->uvHandle); + const int err = uv_timer_stop(this->uvHandle); if (err != 0) + { MS_THROW_ERROR("uv_timer_stop() failed: %s", uv_strerror(err)); + } } -void Timer::Reset() +void TimerHandle::Reset() { MS_TRACE(); if (this->closed) + { MS_THROW_ERROR("closed"); + } if (uv_is_active(reinterpret_cast(this->uvHandle)) == 0) + { return; + } if (this->repeat == 0u) + { return; + } - int err = + const int err = uv_timer_start(this->uvHandle, static_cast(onTimer), this->repeat, this->repeat); if (err != 0) + { MS_THROW_ERROR("uv_timer_start() failed: %s", uv_strerror(err)); + } } -void Timer::Restart() +void TimerHandle::Restart() { MS_TRACE(); if (this->closed) + { MS_THROW_ERROR("closed"); + } if (uv_is_active(reinterpret_cast(this->uvHandle)) != 0) + { Stop(); + } - int err = + const int err = uv_timer_start(this->uvHandle, static_cast(onTimer), this->timeout, this->repeat); if (err != 0) + { MS_THROW_ERROR("uv_timer_start() failed: %s", uv_strerror(err)); + } +} + +void TimerHandle::InternalClose() +{ + MS_TRACE(); + + if (this->closed) + { + return; + } + + this->closed = true; + + uv_close(reinterpret_cast(this->uvHandle), static_cast(onCloseTimer)); } -inline void Timer::OnUvTimer() +inline void TimerHandle::OnUvTimer() { MS_TRACE(); diff --git a/worker/src/handles/UdpSocketHandler.cpp b/worker/src/handles/UdpSocketHandle.cpp similarity index 55% rename from worker/src/handles/UdpSocketHandler.cpp rename to worker/src/handles/UdpSocketHandle.cpp index 6cd1fa8e3e..82da018c73 100644 --- a/worker/src/handles/UdpSocketHandler.cpp +++ b/worker/src/handles/UdpSocketHandle.cpp @@ -1,7 +1,10 @@ -#define MS_CLASS "UdpSocketHandler" +#define MS_CLASS "UdpSocketHandle" // #define MS_LOG_DEV_LEVEL 3 -#include "handles/UdpSocketHandler.hpp" +#include "handles/UdpSocketHandle.hpp" +#ifdef MS_LIBURING_SUPPORTED +#include "DepLibUring.hpp" +#endif #include "Logger.hpp" #include "MediaSoupErrors.hpp" #include "Utils.hpp" @@ -16,57 +19,62 @@ thread_local static uint8_t ReadBuffer[ReadBufferSize]; inline static void onAlloc(uv_handle_t* handle, size_t suggestedSize, uv_buf_t* buf) { - auto* socket = static_cast(handle->data); + auto* socket = static_cast(handle->data); if (socket) + { socket->OnUvRecvAlloc(suggestedSize, buf); + } } inline static void onRecv( uv_udp_t* handle, ssize_t nread, const uv_buf_t* buf, const struct sockaddr* addr, unsigned int flags) { - auto* socket = static_cast(handle->data); + auto* socket = static_cast(handle->data); if (socket) + { socket->OnUvRecv(nread, buf, addr, flags); + } } inline static void onSend(uv_udp_send_t* req, int status) { - auto* sendData = static_cast(req->data); + auto* sendData = static_cast(req->data); auto* handle = req->handle; - auto* socket = static_cast(handle->data); - auto* cb = sendData->cb; + auto* socket = static_cast(handle->data); + const auto* cb = sendData->cb; if (socket) + { socket->OnUvSend(status, cb); + } // Delete the UvSendData struct (it will delete the store and cb too). delete sendData; } -inline static void onClose(uv_handle_t* handle) +inline static void onCloseUdp(uv_handle_t* handle) { - delete handle; + delete reinterpret_cast(handle); } /* Instance methods. */ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init) -UdpSocketHandler::UdpSocketHandler(uv_udp_t* uvHandle) : uvHandle(uvHandle) +UdpSocketHandle::UdpSocketHandle(uv_udp_t* uvHandle) : uvHandle(uvHandle) { MS_TRACE(); - int err; - this->uvHandle->data = static_cast(this); - err = uv_udp_recv_start( + // NOLINTNEXTLINE(misc-const-correctness) + int err = uv_udp_recv_start( this->uvHandle, static_cast(onAlloc), static_cast(onRecv)); if (err != 0) { - uv_close(reinterpret_cast(this->uvHandle), static_cast(onClose)); + uv_close(reinterpret_cast(this->uvHandle), static_cast(onCloseUdp)); MS_THROW_ERROR("uv_udp_recv_start() failed: %s", uv_strerror(err)); } @@ -74,52 +82,45 @@ UdpSocketHandler::UdpSocketHandler(uv_udp_t* uvHandle) : uvHandle(uvHandle) // Set local address. if (!SetLocalAddress()) { - uv_close(reinterpret_cast(this->uvHandle), static_cast(onClose)); + uv_close(reinterpret_cast(this->uvHandle), static_cast(onCloseUdp)); MS_THROW_ERROR("error setting local IP and port"); } -} -UdpSocketHandler::~UdpSocketHandler() -{ - MS_TRACE(); +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + err = uv_fileno(reinterpret_cast(this->uvHandle), std::addressof(this->fd)); - if (!this->closed) - Close(); + if (err != 0) + { + MS_THROW_ERROR("uv_fileno() failed: %s", uv_strerror(err)); + } + } +#endif } -void UdpSocketHandler::Close() +UdpSocketHandle::~UdpSocketHandle() { MS_TRACE(); - if (this->closed) - return; - - this->closed = true; - - // Tell the UV handle that the UdpSocketHandler has been closed. - this->uvHandle->data = nullptr; - - // Don't read more. - int err = uv_udp_recv_stop(this->uvHandle); - - if (err != 0) - MS_ABORT("uv_udp_recv_stop() failed: %s", uv_strerror(err)); - - uv_close(reinterpret_cast(this->uvHandle), static_cast(onClose)); + if (!this->closed) + { + InternalClose(); + } } -void UdpSocketHandler::Dump() const +void UdpSocketHandle::Dump() const { - MS_DUMP(""); - MS_DUMP(" localIp : %s", this->localIp.c_str()); - MS_DUMP(" localPort : %" PRIu16, static_cast(this->localPort)); - MS_DUMP(" closed : %s", !this->closed ? "open" : "closed"); - MS_DUMP(""); + MS_DUMP(""); + MS_DUMP(" localIp: %s", this->localIp.c_str()); + MS_DUMP(" localPort: %" PRIu16, static_cast(this->localPort)); + MS_DUMP(" closed: %s", this->closed ? "yes" : "no"); + MS_DUMP(""); } -void UdpSocketHandler::Send( - const uint8_t* data, size_t len, const struct sockaddr* addr, UdpSocketHandler::onSendCallback* cb) +void UdpSocketHandle::Send( + const uint8_t* data, size_t len, const struct sockaddr* addr, UdpSocketHandle::onSendCallback* cb) { MS_TRACE(); @@ -145,11 +146,37 @@ void UdpSocketHandler::Send( return; } +#ifdef MS_LIBURING_SUPPORTED + if (DepLibUring::IsEnabled()) + { + if (!DepLibUring::IsActive()) + { + goto send_libuv; + } + + // Prepare the data to be sent. + // NOTE: If all SQEs are currently in use or no UserData entry is available we'll + // fall back to libuv. + auto prepared = DepLibUring::PrepareSend(this->fd, data, len, addr, cb); + + if (!prepared) + { + MS_DEBUG_DEV("cannot send via liburing, fallback to libuv"); + + goto send_libuv; + } + + return; + } + +send_libuv: +#endif + // First try uv_udp_try_send(). In case it can not directly send the datagram // then build a uv_req_t and use uv_udp_send(). uv_buf_t buffer = uv_buf_init(reinterpret_cast(const_cast(data)), len); - int sent = uv_udp_try_send(this->uvHandle, &buffer, 1, addr); + const int sent = uv_udp_try_send(this->uvHandle, &buffer, 1, addr); // Entire datagram was sent. Done. if (sent == static_cast(len)) @@ -194,7 +221,7 @@ void UdpSocketHandler::Send( buffer = uv_buf_init(reinterpret_cast(sendData->store), len); - int err = uv_udp_send( + const int err = uv_udp_send( &sendData->req, this->uvHandle, &buffer, 1, addr, static_cast(onSend)); if (err != 0) @@ -204,7 +231,9 @@ void UdpSocketHandler::Send( MS_WARN_DEV("uv_udp_send() failed: %s", uv_strerror(err)); if (cb) + { (*cb)(false); + } // Delete the UvSendData struct (it will delete the store and cb too). delete sendData; @@ -216,7 +245,104 @@ void UdpSocketHandler::Send( } } -bool UdpSocketHandler::SetLocalAddress() +uint32_t UdpSocketHandle::GetSendBufferSize() const +{ + MS_TRACE(); + + int size{ 0 }; + const int err = + uv_send_buffer_size(reinterpret_cast(this->uvHandle), std::addressof(size)); + + if (err) + { + MS_THROW_ERROR("uv_send_buffer_size() failed: %s", uv_strerror(err)); + } + + return static_cast(size); +} + +void UdpSocketHandle::SetSendBufferSize(uint32_t size) +{ + MS_TRACE(); + + auto sizeInt = static_cast(size); + + if (sizeInt <= 0) + { + MS_THROW_TYPE_ERROR("invalid size: %d", sizeInt); + } + + const int err = + uv_send_buffer_size(reinterpret_cast(this->uvHandle), std::addressof(sizeInt)); + + if (err) + { + MS_THROW_ERROR("uv_send_buffer_size() failed: %s", uv_strerror(err)); + } +} + +uint32_t UdpSocketHandle::GetRecvBufferSize() const +{ + MS_TRACE(); + + int size{ 0 }; + const int err = + uv_recv_buffer_size(reinterpret_cast(this->uvHandle), std::addressof(size)); + + if (err) + { + MS_THROW_ERROR("uv_recv_buffer_size() failed: %s", uv_strerror(err)); + } + + return static_cast(size); +} + +void UdpSocketHandle::SetRecvBufferSize(uint32_t size) +{ + MS_TRACE(); + + auto sizeInt = static_cast(size); + + if (sizeInt <= 0) + { + MS_THROW_TYPE_ERROR("invalid size: %d", sizeInt); + } + + const int err = + uv_recv_buffer_size(reinterpret_cast(this->uvHandle), std::addressof(sizeInt)); + + if (err) + { + MS_THROW_ERROR("uv_recv_buffer_size() failed: %s", uv_strerror(err)); + } +} + +void UdpSocketHandle::InternalClose() +{ + MS_TRACE(); + + if (this->closed) + { + return; + } + + this->closed = true; + + // Tell the UV handle that the UdpSocketHandle has been closed. + this->uvHandle->data = nullptr; + + // Don't read more. + const int err = uv_udp_recv_stop(this->uvHandle); + + if (err != 0) + { + MS_ABORT("uv_udp_recv_stop() failed: %s", uv_strerror(err)); + } + + uv_close(reinterpret_cast(this->uvHandle), static_cast(onCloseUdp)); +} + +bool UdpSocketHandle::SetLocalAddress() { MS_TRACE(); @@ -241,7 +367,8 @@ bool UdpSocketHandler::SetLocalAddress() return true; } -inline void UdpSocketHandler::OnUvRecvAlloc(size_t /*suggestedSize*/, uv_buf_t* buf) +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) +inline void UdpSocketHandle::OnUvRecvAlloc(size_t /*suggestedSize*/, uv_buf_t* buf) { MS_TRACE(); @@ -251,14 +378,16 @@ inline void UdpSocketHandler::OnUvRecvAlloc(size_t /*suggestedSize*/, uv_buf_t* buf->len = ReadBufferSize; } -inline void UdpSocketHandler::OnUvRecv( +inline void UdpSocketHandle::OnUvRecv( ssize_t nread, const uv_buf_t* buf, const struct sockaddr* addr, unsigned int flags) { MS_TRACE(); // NOTE: Ignore if there is nothing to read or if it was an empty datagram. if (nread == 0) + { return; + } // Check flags. if ((flags & UV_UDP_PARTIAL) != 0u) @@ -284,7 +413,8 @@ inline void UdpSocketHandler::OnUvRecv( } } -inline void UdpSocketHandler::OnUvSend(int status, UdpSocketHandler::onSendCallback* cb) +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) +inline void UdpSocketHandle::OnUvSend(int status, UdpSocketHandle::onSendCallback* cb) { MS_TRACE(); @@ -293,7 +423,9 @@ inline void UdpSocketHandler::OnUvSend(int status, UdpSocketHandler::onSendCallb if (status == 0) { if (cb) + { (*cb)(true); + } } else { @@ -302,6 +434,8 @@ inline void UdpSocketHandler::OnUvSend(int status, UdpSocketHandler::onSendCallb #endif if (cb) + { (*cb)(false); + } } } diff --git a/worker/src/handles/UnixStreamSocket.cpp b/worker/src/handles/UnixStreamSocketHandle.cpp similarity index 61% rename from worker/src/handles/UnixStreamSocket.cpp rename to worker/src/handles/UnixStreamSocketHandle.cpp index cb7c16e49c..8b34b8a2ce 100644 --- a/worker/src/handles/UnixStreamSocket.cpp +++ b/worker/src/handles/UnixStreamSocketHandle.cpp @@ -3,10 +3,10 @@ * Channel. */ -#define MS_CLASS "UnixStreamSocket" +#define MS_CLASS "UnixStreamSocketHandle" // #define MS_LOG_DEV_LEVEL 3 -#include "handles/UnixStreamSocket.hpp" +#include "handles/UnixStreamSocketHandle.hpp" #include "DepLibUV.hpp" #include "Logger.hpp" #include "MediaSoupErrors.hpp" @@ -16,37 +16,45 @@ inline static void onAlloc(uv_handle_t* handle, size_t suggestedSize, uv_buf_t* buf) { - auto* socket = static_cast(handle->data); + auto* socket = static_cast(handle->data); if (socket) + { socket->OnUvReadAlloc(suggestedSize, buf); + } } inline static void onRead(uv_stream_t* handle, ssize_t nread, const uv_buf_t* buf) { - auto* socket = static_cast(handle->data); + auto* socket = static_cast(handle->data); if (socket) + { socket->OnUvRead(nread, buf); + } } inline static void onWrite(uv_write_t* req, int status) { - auto* writeData = static_cast(req->data); + auto* writeData = static_cast(req->data); auto* handle = req->handle; - auto* socket = static_cast(handle->data); + auto* socket = static_cast(handle->data); - // Just notify the UnixStreamSocket when error. + // Just notify the UnixStreamSocketHandle when error. if (socket && status != 0) + { socket->OnUvWriteError(status); + } // Delete the UvWriteData struct. delete writeData; } -inline static void onClose(uv_handle_t* handle) +// NOTE: We have different onCloseXxx() callbacks to avoid an ASAN warning by +// ensuring that we call `delete xxx` with same type as `new xxx` before. +inline static void onClosePipe(uv_handle_t* handle) { - delete handle; + delete reinterpret_cast(handle); } inline static void onShutdown(uv_shutdown_t* req, int /*status*/) @@ -56,19 +64,19 @@ inline static void onShutdown(uv_shutdown_t* req, int /*status*/) delete req; // Now do close the handle. - uv_close(reinterpret_cast(handle), static_cast(onClose)); + uv_close(reinterpret_cast(handle), static_cast(onClosePipe)); } /* Instance methods. */ -UnixStreamSocket::UnixStreamSocket(int fd, size_t bufferSize, UnixStreamSocket::Role role) - : bufferSize(bufferSize), role(role) +UnixStreamSocketHandle::UnixStreamSocketHandle( + int fd, size_t bufferSize, UnixStreamSocketHandle::Role role) + : uvHandle(new uv_pipe_t), bufferSize(bufferSize), role(role) { MS_TRACE_STD(); int err; - this->uvHandle = new uv_pipe_t; this->uvHandle->data = static_cast(this); err = uv_pipe_init(DepLibUV::GetLoop(), this->uvHandle, 0); @@ -85,12 +93,12 @@ UnixStreamSocket::UnixStreamSocket(int fd, size_t bufferSize, UnixStreamSocket:: if (err != 0) { - uv_close(reinterpret_cast(this->uvHandle), static_cast(onClose)); + uv_close(reinterpret_cast(this->uvHandle), static_cast(onClosePipe)); MS_THROW_ERROR_STD("uv_pipe_open() failed: %s", uv_strerror(err)); } - if (this->role == UnixStreamSocket::Role::CONSUMER) + if (this->role == UnixStreamSocketHandle::Role::CONSUMER) { // Start reading. err = uv_read_start( @@ -100,7 +108,7 @@ UnixStreamSocket::UnixStreamSocket(int fd, size_t bufferSize, UnixStreamSocket:: if (err != 0) { - uv_close(reinterpret_cast(this->uvHandle), static_cast(onClose)); + uv_close(reinterpret_cast(this->uvHandle), static_cast(onClosePipe)); MS_THROW_ERROR_STD("uv_read_start() failed: %s", uv_strerror(err)); } @@ -109,67 +117,81 @@ UnixStreamSocket::UnixStreamSocket(int fd, size_t bufferSize, UnixStreamSocket:: // NOTE: Don't allocate the buffer here. Instead wait for the first uv_alloc_cb(). } -UnixStreamSocket::~UnixStreamSocket() +UnixStreamSocketHandle::~UnixStreamSocketHandle() { MS_TRACE_STD(); - delete[] this->buffer; - if (!this->closed) + { Close(); + } + + delete[] this->buffer; } -void UnixStreamSocket::Close() +// NOTE: In UnixStreamSocketHandle we need a poublic Close() method and cannot +// just rely on the destructor plus a private InternalClose() method. +void UnixStreamSocketHandle::Close() { MS_TRACE_STD(); if (this->closed) + { return; + } int err; this->closed = true; - // Tell the UV handle that the UnixStreamSocket has been closed. + // Tell the UV handle that the UnixStreamSocketHandle has been closed. this->uvHandle->data = nullptr; - if (this->role == UnixStreamSocket::Role::CONSUMER) + if (this->role == UnixStreamSocketHandle::Role::CONSUMER) { // Don't read more. err = uv_read_stop(reinterpret_cast(this->uvHandle)); if (err != 0) + { MS_ABORT("uv_read_stop() failed: %s", uv_strerror(err)); + } } // If there is no error and the peer didn't close its pipe side then close gracefully. - if (this->role == UnixStreamSocket::Role::PRODUCER && !this->hasError && !this->isClosedByPeer) + if (this->role == UnixStreamSocketHandle::Role::PRODUCER && !this->hasError && !this->isClosedByPeer) { // Use uv_shutdown() so pending data to be written will be sent to the peer before closing. - auto req = new uv_shutdown_t; + auto* req = new uv_shutdown_t; req->data = static_cast(this); err = uv_shutdown( req, reinterpret_cast(this->uvHandle), static_cast(onShutdown)); if (err != 0) + { MS_ABORT("uv_shutdown() failed: %s", uv_strerror(err)); + } } // Otherwise directly close the socket. else { - uv_close(reinterpret_cast(this->uvHandle), static_cast(onClose)); + uv_close(reinterpret_cast(this->uvHandle), static_cast(onClosePipe)); } } -void UnixStreamSocket::Write(const uint8_t* data, size_t len) +void UnixStreamSocketHandle::Write(const uint8_t* data, size_t len) { MS_TRACE_STD(); if (this->closed) + { return; + } if (len == 0) + { return; + } // First try uv_try_write(). In case it can not directly send all the given data // then build a uv_req_t and use uv_write(). @@ -197,15 +219,15 @@ void UnixStreamSocket::Write(const uint8_t* data, size_t len) written = 0; } - size_t pendingLen = len - written; - auto* writeData = new UvWriteData(pendingLen); + const size_t pendingLen = len - written; + auto* writeData = new UvWriteData(pendingLen); writeData->req.data = static_cast(writeData); std::memcpy(writeData->store, data + written, pendingLen); buffer = uv_buf_init(reinterpret_cast(writeData->store), pendingLen); - int err = uv_write( + const int err = uv_write( &writeData->req, reinterpret_cast(this->uvHandle), &buffer, @@ -221,13 +243,87 @@ void UnixStreamSocket::Write(const uint8_t* data, size_t len) } } -inline void UnixStreamSocket::OnUvReadAlloc(size_t /*suggestedSize*/, uv_buf_t* buf) +uint32_t UnixStreamSocketHandle::GetSendBufferSize() const +{ + MS_TRACE(); + + int size{ 0 }; + const int err = + uv_send_buffer_size(reinterpret_cast(this->uvHandle), std::addressof(size)); + + if (err) + { + MS_THROW_ERROR_STD("uv_send_buffer_size() failed: %s", uv_strerror(err)); + } + + return static_cast(size); +} + +void UnixStreamSocketHandle::SetSendBufferSize(uint32_t size) +{ + MS_TRACE(); + + auto sizeInt = static_cast(size); + + if (sizeInt <= 0) + { + MS_THROW_TYPE_ERROR_STD("invalid size: %d", sizeInt); + } + + const int err = + uv_send_buffer_size(reinterpret_cast(this->uvHandle), std::addressof(sizeInt)); + + if (err) + { + MS_THROW_ERROR_STD("uv_send_buffer_size() failed: %s", uv_strerror(err)); + } +} + +uint32_t UnixStreamSocketHandle::GetRecvBufferSize() const +{ + MS_TRACE(); + + int size{ 0 }; + const int err = + uv_recv_buffer_size(reinterpret_cast(this->uvHandle), std::addressof(size)); + + if (err) + { + MS_THROW_ERROR_STD("uv_recv_buffer_size() failed: %s", uv_strerror(err)); + } + + return static_cast(size); +} + +void UnixStreamSocketHandle::SetRecvBufferSize(uint32_t size) +{ + MS_TRACE(); + + auto sizeInt = static_cast(size); + + if (sizeInt <= 0) + { + MS_THROW_TYPE_ERROR_STD("invalid size: %d", sizeInt); + } + + const int err = + uv_recv_buffer_size(reinterpret_cast(this->uvHandle), std::addressof(sizeInt)); + + if (err) + { + MS_THROW_ERROR_STD("uv_recv_buffer_size() failed: %s", uv_strerror(err)); + } +} + +inline void UnixStreamSocketHandle::OnUvReadAlloc(size_t /*suggestedSize*/, uv_buf_t* buf) { MS_TRACE_STD(); // If this is the first call to onUvReadAlloc() then allocate the receiving buffer now. if (!this->buffer) + { this->buffer = new uint8_t[this->bufferSize]; + } // Tell UV to write after the last data byte in the buffer. buf->base = reinterpret_cast(this->buffer + this->bufferDataLen); @@ -245,12 +341,14 @@ inline void UnixStreamSocket::OnUvReadAlloc(size_t /*suggestedSize*/, uv_buf_t* } } -inline void UnixStreamSocket::OnUvRead(ssize_t nread, const uv_buf_t* /*buf*/) +inline void UnixStreamSocketHandle::OnUvRead(ssize_t nread, const uv_buf_t* /*buf*/) { MS_TRACE_STD(); if (nread == 0) + { return; + } // Data received. if (nread > 0) @@ -287,12 +385,14 @@ inline void UnixStreamSocket::OnUvRead(ssize_t nread, const uv_buf_t* /*buf*/) } } -inline void UnixStreamSocket::OnUvWriteError(int error) +inline void UnixStreamSocketHandle::OnUvWriteError(int error) { MS_TRACE_STD(); if (error != UV_EPIPE && error != UV_ENOTCONN) + { this->hasError = true; + } MS_ERROR_STD("write error, closing the pipe: %s", uv_strerror(error)); diff --git a/worker/src/lib.cpp b/worker/src/lib.cpp index 86246cf270..8387e5f35f 100644 --- a/worker/src/lib.cpp +++ b/worker/src/lib.cpp @@ -3,6 +3,9 @@ #include "common.hpp" #include "DepLibSRTP.hpp" +#ifdef MS_LIBURING_SUPPORTED +#include "DepLibUring.hpp" +#endif #include "DepLibUV.hpp" #include "DepLibWebRTC.hpp" #include "DepOpenSSL.hpp" @@ -13,35 +16,26 @@ #include "Utils.hpp" #include "Worker.hpp" #include "Channel/ChannelSocket.hpp" -#include "PayloadChannel/PayloadChannelSocket.hpp" #include "RTC/DtlsTransport.hpp" #include "RTC/SrtpSession.hpp" #include #include -#include -#include // sigaction() -#include // std::_Exit(), std::genenv() -#include // std::cerr, std::endl +#include // sigaction() #include void IgnoreSignals(); +// NOLINTNEXTLINE extern "C" int mediasoup_worker_run( int argc, char* argv[], const char* version, int consumerChannelFd, int producerChannelFd, - int payloadConsumeChannelFd, - int payloadProduceChannelFd, ChannelReadFn channelReadFn, ChannelReadCtx channelReadCtx, ChannelWriteFn channelWriteFn, - ChannelWriteCtx channelWriteCtx, - PayloadChannelReadFn payloadChannelReadFn, - PayloadChannelReadCtx payloadChannelReadCtx, - PayloadChannelWriteFn payloadChannelWriteFn, - PayloadChannelWriteCtx payloadChannelWriteCtx) + ChannelWriteCtx channelWriteCtx) { // Initialize libuv stuff (we need it for the Channel). DepLibUV::ClassInit(); @@ -51,11 +45,6 @@ extern "C" int mediasoup_worker_run( // deallocate its UV handles. std::unique_ptr channel{ nullptr }; - // PayloadChannel socket. If Worker instance runs properly, this socket is - // closed by it in its destructor. Otherwise it's closed here by also letting - // libuv deallocate its UV handles. - std::unique_ptr payloadChannel{ nullptr }; - try { if (channelReadFn) @@ -75,31 +64,8 @@ extern "C" int mediasoup_worker_run( DepLibUV::RunLoop(); DepLibUV::ClassDestroy(); - return 1; - } - - try - { - if (payloadChannelReadFn) - { - payloadChannel.reset(new PayloadChannel::PayloadChannelSocket( - payloadChannelReadFn, payloadChannelReadCtx, payloadChannelWriteFn, payloadChannelWriteCtx)); - } - else - { - payloadChannel.reset( - new PayloadChannel::PayloadChannelSocket(payloadConsumeChannelFd, payloadProduceChannelFd)); - } - } - catch (const MediaSoupError& error) - { - MS_ERROR_STD("error creating the PayloadChannel: %s", error.what()); - - channel->Close(); - DepLibUV::RunLoop(); - DepLibUV::ClassDestroy(); - - return 1; + // 40 is a custom exit code to notify "unknown error" to the Node library. + return 40; } // Initialize the Logger. @@ -114,7 +80,6 @@ extern "C" int mediasoup_worker_run( MS_ERROR_STD("settings error: %s", error.what()); channel->Close(); - payloadChannel->Close(); DepLibUV::RunLoop(); DepLibUV::ClassDestroy(); @@ -126,11 +91,11 @@ extern "C" int mediasoup_worker_run( MS_ERROR_STD("unexpected settings error: %s", error.what()); channel->Close(); - payloadChannel->Close(); DepLibUV::RunLoop(); DepLibUV::ClassDestroy(); - return 1; + // 40 is a custom exit code to notify "unknown error" to the Node library. + return 40; } MS_DEBUG_TAG(info, "starting mediasoup-worker process [version:%s]", version); @@ -160,6 +125,9 @@ extern "C" int mediasoup_worker_run( DepOpenSSL::ClassInit(); DepLibSRTP::ClassInit(); DepUsrSCTP::ClassInit(); +#ifdef MS_LIBURING_SUPPORTED + DepLibUring::ClassInit(); +#endif DepLibWebRTC::ClassInit(); Utils::Crypto::ClassInit(); RTC::DtlsTransport::ClassInit(); @@ -171,12 +139,15 @@ extern "C" int mediasoup_worker_run( #endif // Run the Worker. - Worker worker(channel.get(), payloadChannel.get()); + const Worker worker(channel.get()); // Free static stuff. DepLibSRTP::ClassDestroy(); Utils::Crypto::ClassDestroy(); DepLibWebRTC::ClassDestroy(); +#ifdef MS_LIBURING_SUPPORTED + DepLibUring::ClassDestroy(); +#endif RTC::DtlsTransport::ClassDestroy(); DepUsrSCTP::ClassDestroy(); DepLibUV::ClassDestroy(); @@ -193,7 +164,8 @@ extern "C" int mediasoup_worker_run( { MS_ERROR_STD("failure exit: %s", error.what()); - return 1; + // 40 is a custom exit code to notify "unknown error" to the Node library. + return 40; } } @@ -203,10 +175,12 @@ void IgnoreSignals() MS_TRACE(); int err; - struct sigaction act; // NOLINT(cppcoreguidelines-pro-type-member-init) + struct sigaction act + { + }; // NOLINT(cppcoreguidelines-pro-type-member-init) // clang-format off - absl::flat_hash_map ignoredSignals = + absl::flat_hash_map const ignoredSignals = { { "PIPE", SIGPIPE }, { "HUP", SIGHUP }, @@ -221,17 +195,21 @@ void IgnoreSignals() err = sigfillset(&act.sa_mask); if (err != 0) + { MS_THROW_ERROR("sigfillset() failed: %s", std::strerror(errno)); + } - for (auto& kv : ignoredSignals) + for (const auto& kv : ignoredSignals) { const auto& sigName = kv.first; - int sigId = kv.second; + const int sigId = kv.second; err = sigaction(sigId, &act, nullptr); if (err != 0) + { MS_THROW_ERROR("sigaction() failed for signal %s: %s", sigName.c_str(), std::strerror(errno)); + } } #endif } diff --git a/worker/src/lib.rs b/worker/src/lib.rs index 0e4f4e4fb7..f686458385 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -1,5 +1,12 @@ use std::os::raw::{c_char, c_int, c_void}; +pub use planus_codegen::*; + +mod planus_codegen { + #![allow(clippy::all)] + include!(concat!(env!("OUT_DIR"), "/fbs.rs")); +} + #[repr(transparent)] #[derive(Copy, Clone)] pub struct UvAsyncT(pub *const c_void); @@ -38,43 +45,6 @@ pub type ChannelWriteFn = unsafe extern "C" fn( unsafe impl Send for ChannelWriteCtx {} -#[repr(transparent)] -pub struct PayloadChannelReadCtx(pub *const c_void); -pub type PayloadChannelReadFreeFn = Option< - unsafe extern "C" fn( - /* message: */ *mut u8, - /* message_len: */ u32, - /* message_ctx: */ usize, - ), ->; -pub type PayloadChannelReadFn = unsafe extern "C" fn( - /* message: */ *mut *mut u8, - /* message_len: */ *mut u32, - /* message_ctx: */ *mut usize, - /* payload: */ *mut *mut u8, - /* payload_len: */ *mut u32, - /* payload_capacity: */ *mut usize, - // This is `uv_async_t` handle that can be called later with `uv_async_send()` when there is - // more data to read - /* handle */ - UvAsyncT, - /* ctx: */ PayloadChannelReadCtx, -) -> PayloadChannelReadFreeFn; - -unsafe impl Send for PayloadChannelReadCtx {} - -#[repr(transparent)] -pub struct PayloadChannelWriteCtx(pub *const c_void); -pub type PayloadChannelWriteFn = unsafe extern "C" fn( - /* message: */ *const u8, - /* message_len: */ u32, - /* payload: */ *const u8, - /* payload_len: */ u32, - /* ctx: */ PayloadChannelWriteCtx, -); - -unsafe impl Send for PayloadChannelWriteCtx {} - #[link(name = "mediasoup-worker", kind = "static")] extern "C" { /// Returns `0` on success, or an error code `< 0` on failure @@ -86,15 +56,9 @@ extern "C" { version: *const c_char, consumer_channel_fd: c_int, producer_channel_fd: c_int, - payload_consumer_channel_fd: c_int, - payload_producer_channel_fd: c_int, channel_read_fn: ChannelReadFn, channel_read_ctx: ChannelReadCtx, channel_write_fn: ChannelWriteFn, channel_write_ctx: ChannelWriteCtx, - payload_channel_read_fn: PayloadChannelReadFn, - payload_channel_read_ctx: PayloadChannelReadCtx, - payload_channel_write_fn: PayloadChannelWriteFn, - payload_channel_write_ctx: PayloadChannelWriteCtx, ) -> c_int; } diff --git a/worker/src/main.cpp b/worker/src/main.cpp index 363ae1a213..55f46c19c3 100644 --- a/worker/src/main.cpp +++ b/worker/src/main.cpp @@ -3,13 +3,11 @@ #include "MediaSoupErrors.hpp" #include "lib.hpp" -#include // std::_Exit(), std::genenv() +#include // std::_Exit() #include static constexpr int ConsumerChannelFd{ 3 }; static constexpr int ProducerChannelFd{ 4 }; -static constexpr int PayloadConsumerChannelFd{ 5 }; -static constexpr int PayloadProducerChannelFd{ 6 }; int main(int argc, char* argv[]) { @@ -18,35 +16,14 @@ int main(int argc, char* argv[]) { MS_ERROR_STD("you don't seem to be my real father!"); - std::_Exit(EXIT_FAILURE); + // 41 is a custom exit code to notify about "missing MEDIASOUP_VERSION" env. + std::_Exit(41); } - std::string version = std::getenv("MEDIASOUP_VERSION"); + const std::string version = std::getenv("MEDIASOUP_VERSION"); auto statusCode = mediasoup_worker_run( - argc, - argv, - version.c_str(), - ConsumerChannelFd, - ProducerChannelFd, - PayloadConsumerChannelFd, - PayloadProducerChannelFd, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr); + argc, argv, version.c_str(), ConsumerChannelFd, ProducerChannelFd, nullptr, nullptr, nullptr, nullptr); - switch (statusCode) - { - case 0: - std::_Exit(EXIT_SUCCESS); - case 1: - std::_Exit(EXIT_FAILURE); - case 42: - std::_Exit(42); - } + std::_Exit(statusCode); } diff --git a/worker/subprojects/.clang-tidy b/worker/subprojects/.clang-tidy new file mode 100644 index 0000000000..18b170ce1f --- /dev/null +++ b/worker/subprojects/.clang-tidy @@ -0,0 +1,7 @@ +# Bad workaround to disable clang-tidy checks in worker/subprojects folder. +# Ideally we should use a modern version so worker/.clang-tidy-ignore would be +# honored. Here we should also be able to do Checks: '-*' to disable all rules +# but if we do it it fails with "Error: no checks enable". +--- +Checks: 'modernize-*' +... diff --git a/worker/subprojects/abseil-cpp.wrap b/worker/subprojects/abseil-cpp.wrap index 72928cb6ac..9a8e07278f 100644 --- a/worker/subprojects/abseil-cpp.wrap +++ b/worker/subprojects/abseil-cpp.wrap @@ -1,23 +1,108 @@ [wrap-file] -directory = abseil-cpp-20211102.0 -source_url = https://github.com/abseil/abseil-cpp/archive/20211102.0.tar.gz -source_filename = abseil-cpp-20211102.0.tar.gz -source_hash = dcf71b9cba8dc0ca9940c4b316a0c796be8fab42b070bb6b7cab62b48f0e66c4 -patch_filename = abseil-cpp_20211102.0-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/abseil-cpp_20211102.0-2/get_patch -patch_hash = 9463930367b0db984435350c7d7614e400faa8811a7e9a2def5a63ff39fdb325 +directory = abseil-cpp-20240722.0 +source_url = https://github.com/abseil/abseil-cpp/releases/download/20240722.0/abseil-cpp-20240722.0.tar.gz +source_filename = abseil-cpp-20240722.0.tar.gz +source_hash = f50e5ac311a81382da7fa75b97310e4b9006474f9560ac46f54a9967f07d4ae3 +patch_filename = abseil-cpp_20240722.0-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/abseil-cpp_20240722.0-1/get_patch +patch_hash = 692bbbc39cacaba4dc4b0c8b2fbbe32736c9cde6377acfa0d52088797af14ded +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/abseil-cpp_20240722.0-1/abseil-cpp-20240722.0.tar.gz +wrapdb_version = 20240722.0-1 [provide] absl_base = absl_base_dep absl_container = absl_container_dep absl_debugging = absl_debugging_dep +absl_log = absl_log_dep absl_flags = absl_flags_dep absl_hash = absl_hash_dep +absl_crc = absl_crc_dep absl_numeric = absl_numeric_dep +absl_profiling = absl_profiling_dep absl_random = absl_random_dep absl_status = absl_status_dep absl_strings = absl_strings_dep absl_synchronization = absl_synchronization_dep absl_time = absl_time_dep absl_types = absl_types_dep - +absl_algorithm_container = absl_base_dep +absl_any_invocable = absl_base_dep +absl_bad_any_cast_impl = absl_types_dep +absl_bad_optional_access = absl_types_dep +absl_bad_variant_access = absl_types_dep +absl_bind_front = absl_base_dep +absl_city = absl_hash_dep +absl_civil_time = absl_time_dep +absl_cleanup = absl_base_dep +absl_cord = absl_strings_dep +absl_cord_internal = absl_strings_dep +absl_cordz_functions = absl_strings_dep +absl_cordz_handle = absl_strings_dep +absl_cordz_info = absl_strings_dep +absl_cordz_sample_token = absl_strings_dep +absl_core_headers = absl_base_dep +absl_crc32c = absl_crc_dep +absl_debugging_internal = absl_debugging_dep +absl_demangle_internal = absl_debugging_dep +absl_die_if_null = absl_log_dep +absl_examine_stack = absl_debugging_dep +absl_exponential_biased = absl_profiling_dep +absl_failure_signal_handler = absl_debugging_dep +absl_flags_commandlineflag = absl_flags_dep +absl_flags_commandlineflag_internal = absl_flags_dep +absl_flags_config = absl_flags_dep +absl_flags_internal = absl_flags_dep +absl_flags_marshalling = absl_flags_dep +absl_flags_parse = absl_flags_dep +absl_flags_private_handle_accessor = absl_flags_dep +absl_flags_program_name = absl_flags_dep +absl_flags_reflection = absl_flags_dep +absl_flags_usage = absl_flags_dep +absl_flags_usage_internal = absl_flags_dep +absl_flat_hash_map = absl_container_dep +absl_flat_hash_set = absl_container_dep +absl_function_ref = absl_base_dep +absl_graphcycles_internal = absl_synchronization_dep +absl_hashtablez_sampler = absl_container_dep +absl_inlined_vector = absl_container_dep +absl_int128 = absl_numeric_dep +absl_leak_check = absl_debugging_dep +absl_log_initialize = absl_log_dep +absl_log_internal_check_op = absl_log_dep +absl_log_internal_message = absl_log_dep +absl_log_severity = absl_base_dep +absl_low_level_hash = absl_hash_dep +absl_memory = absl_base_dep +absl_optional = absl_types_dep +absl_periodic_sampler = absl_profiling_dep +absl_random_bit_gen_ref = absl_random_dep +absl_random_distributions = absl_random_dep +absl_random_internal_distribution_test_util = absl_random_dep +absl_random_internal_platform = absl_random_dep +absl_random_internal_pool_urbg = absl_random_dep +absl_random_internal_randen = absl_random_dep +absl_random_internal_randen_hwaes = absl_random_dep +absl_random_internal_randen_hwaes_impl = absl_random_dep +absl_random_internal_randen_slow = absl_random_dep +absl_random_internal_seed_material = absl_random_dep +absl_random_random = absl_random_dep +absl_random_seed_gen_exception = absl_random_dep +absl_random_seed_sequences = absl_random_dep +absl_raw_hash_set = absl_container_dep +absl_raw_logging_internal = absl_base_dep +absl_scoped_set_env = absl_base_dep +absl_span = absl_types_dep +absl_spinlock_wait = absl_base_dep +absl_stacktrace = absl_debugging_dep +absl_statusor = absl_status_dep +absl_str_format = absl_strings_dep +absl_str_format_internal = absl_strings_dep +absl_strerror = absl_base_dep +absl_string_view = absl_strings_dep +absl_strings_internal = absl_strings_dep +absl_symbolize = absl_debugging_dep +absl_throw_delegate = absl_base_dep +absl_time_zone = absl_time_dep +absl_type_traits = absl_base_dep +absl_utility = absl_base_dep +absl_variant = absl_types_dep diff --git a/worker/subprojects/catch2.wrap b/worker/subprojects/catch2.wrap index c82b310fb0..90bcddd3bc 100644 --- a/worker/subprojects/catch2.wrap +++ b/worker/subprojects/catch2.wrap @@ -1,12 +1,11 @@ [wrap-file] -directory = Catch2-2.13.7 -source_url = https://github.com/catchorg/Catch2/archive/v2.13.7.zip -source_filename = Catch2-2.13.7.zip -source_hash = 3f3ccd90ad3a8fbb1beeb15e6db440ccdcbebe378dfd125d07a1f9a587a927e9 -patch_filename = catch2_2.13.7-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/catch2_2.13.7-1/get_patch -patch_hash = 2f7369645d747e5bd866317ac1dd4c3d04dc97d3aad4fc6b864bdf75d3b57158 +directory = Catch2-3.6.0 +source_url = https://github.com/catchorg/Catch2/archive/v3.6.0.tar.gz +source_filename = Catch2-3.6.0.tar.gz +source_hash = 485932259a75c7c6b72d4b874242c489ea5155d17efa345eb8cc72159f49f356 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/catch2_3.6.0-1/Catch2-3.6.0.tar.gz +wrapdb_version = 3.6.0-1 [provide] catch2 = catch2_dep - +catch2-with-main = catch2_with_main_dep diff --git a/worker/subprojects/flatbuffers.wrap b/worker/subprojects/flatbuffers.wrap new file mode 100644 index 0000000000..57cca8ca88 --- /dev/null +++ b/worker/subprojects/flatbuffers.wrap @@ -0,0 +1,14 @@ +[wrap-file] +directory = flatbuffers-24.3.6 +source_url = https://github.com/google/flatbuffers/archive/v24.3.6.tar.gz +source_filename = flatbuffers-24.3.6.tar.gz +source_hash = 5d8bfbf5b1b4c47f516e7673677f0e8db0efd32f262f7a14c3fd5ff67e2bd8fc +patch_filename = flatbuffers_24.3.6-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/flatbuffers_24.3.6-1/get_patch +patch_hash = bc0e1035a67ae74b1f862491fe2b0fd49b2889d989508143fff0a45508421bd7 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/flatbuffers_24.3.6-1/flatbuffers-24.3.6.tar.gz +wrapdb_version = 24.3.6-1 + +[provide] +flatbuffers = flatbuffers_dep +program_names = flatc, flathash diff --git a/worker/subprojects/libsrtp2.wrap b/worker/subprojects/libsrtp2.wrap index 89fc64cfee..d0181794b0 100644 --- a/worker/subprojects/libsrtp2.wrap +++ b/worker/subprojects/libsrtp2.wrap @@ -1,8 +1,8 @@ [wrap-file] -directory = libsrtp-2.4.2 -source_url = https://github.com/cisco/libsrtp/archive/refs/tags/v2.4.2.zip -source_filename = libsrtp-2.4.2.zip -source_hash = 35b1ae7a6256224feb058f1feb42170537a44896340f80e77b49cc59af686a82 +directory = libsrtp-3.0-alpha +source_url = https://github.com/versatica/libsrtp/archive/v3.0-alpha.zip +source_filename = libsrtp-3.0-alpha.zip +source_hash = 946a472b888ca8d51df172def7681f3f9b14768109ffd22af08fddb1be77d2c6 [provide] libsrtp2 = libsrtp2_dep diff --git a/worker/subprojects/liburing.wrap b/worker/subprojects/liburing.wrap new file mode 100644 index 0000000000..00de3a7614 --- /dev/null +++ b/worker/subprojects/liburing.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = liburing-liburing-2.5 +source_url = https://github.com/axboe/liburing/archive/refs/tags/liburing-2.5.tar.gz +source_filename = liburing-2.5.tar.gz +source_hash = 456f5f882165630f0dc7b75e8fd53bd01a955d5d4720729b4323097e6e9f2a98 +patch_filename = liburing_2.5-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/liburing_2.5-1/get_patch +patch_hash = d72f651e0edd8102535af575d682ce86c3fc2fdabb40b8faa2659d0f7d437f44 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/liburing_2.5-1/liburing-2.5.tar.gz +wrapdb_version = 2.5-1 + +[provide] +dependency_names = liburing diff --git a/worker/subprojects/libuv.wrap b/worker/subprojects/libuv.wrap index 6bd828d8b0..94f0018009 100644 --- a/worker/subprojects/libuv.wrap +++ b/worker/subprojects/libuv.wrap @@ -1,12 +1,13 @@ [wrap-file] -directory = libuv-v1.44.2 -source_url = https://dist.libuv.org/dist/v1.44.2/libuv-v1.44.2.tar.gz -source_filename = libuv-v1.44.2.tar.gz -source_hash = ccfcdc968c55673c6526d8270a9c8655a806ea92468afcbcabc2b16040f03cb4 -patch_filename = libuv_1.44.2-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/libuv_1.44.2-1/get_patch -patch_hash = c77f6104cffd53f697c3030fccbfd5cc684b59772e8f24529b01908ee27bd751 -wrapdb_version = 1.44.2-1 +directory = libuv-v1.48.0 +source_url = https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz +source_filename = libuv-v1.48.0.tar.gz +source_hash = 7f1db8ac368d89d1baf163bac1ea5fe5120697a73910c8ae6b2fffb3551d59fb +patch_filename = libuv_1.48.0-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libuv_1.48.0-1/get_patch +patch_hash = 27b18917c914a5d6dfb459073710e9bfb6b2962d69d4e0bad5bc7b1173482be7 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libuv_1.48.0-1/libuv-v1.48.0.tar.gz +wrapdb_version = 1.48.0-1 [provide] libuv = libuv_dep diff --git a/worker/subprojects/nlohmann_json.wrap b/worker/subprojects/nlohmann_json.wrap deleted file mode 100644 index 857277a676..0000000000 --- a/worker/subprojects/nlohmann_json.wrap +++ /dev/null @@ -1,10 +0,0 @@ -[wrap-file] -directory = nlohmann_json-3.10.5 -lead_directory_missing = true -source_url = https://github.com/nlohmann/json/releases/download/v3.10.5/include.zip -source_filename = nlohmann_json-3.10.5.zip -source_hash = b94997df68856753b72f0d7a3703b7d484d4745c567f3584ef97c96c25a5798e - -[provide] -nlohmann_json = nlohmann_json_dep - diff --git a/worker/subprojects/openssl.wrap b/worker/subprojects/openssl.wrap index 2f16b50451..873d55106e 100644 --- a/worker/subprojects/openssl.wrap +++ b/worker/subprojects/openssl.wrap @@ -1,12 +1,13 @@ [wrap-file] -directory = openssl-3.0.7 -source_url = https://www.openssl.org/source/openssl-3.0.7.tar.gz -source_filename = openssl-3.0.72.tar.gz -source_hash = 83049d042a260e696f62406ac5c08bf706fd84383f945cf21bd61e9ed95c396e -patch_filename = openssl_3.0.7-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/openssl_3.0.7-1/get_patch -patch_hash = 8f04d911dc22d1dddc6a192ab27d6d8275976a252bd9c73e09f95f1f927e42b5 -wrapdb_version = 3.0.7-1 +directory = openssl-3.0.8 +source_url = https://www.openssl.org/source/openssl-3.0.8.tar.gz +source_filename = openssl-3.0.8.tar.gz +source_hash = 6c13d2bf38fdf31eac3ce2a347073673f5d63263398f1f69d0df4a41253e4b3e +patch_filename = openssl_3.0.8-3_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/openssl_3.0.8-3/get_patch +patch_hash = 300da189e106942347d61a4a4295aa2edbcf06184f8d13b4cee0bed9fb936963 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/openssl_3.0.8-3/openssl-3.0.8.tar.gz +wrapdb_version = 3.0.8-3 [provide] libcrypto = libcrypto_dep diff --git a/worker/subprojects/usrsctp.wrap b/worker/subprojects/usrsctp.wrap index 67ca2c8728..469f66291b 100644 --- a/worker/subprojects/usrsctp.wrap +++ b/worker/subprojects/usrsctp.wrap @@ -1,8 +1,8 @@ [wrap-file] -directory = usrsctp-4e06feb01cadcd127d119486b98a4bd3d64aa1e7 -source_url = https://github.com/sctplab/usrsctp/archive/4e06feb01cadcd127d119486b98a4bd3d64aa1e7.zip -source_filename = 4e06feb01cadcd127d119486b98a4bd3d64aa1e7.zip -source_hash = 15f7844c4c4ca93228ae0fe844182c72edd1d809b461cb97b1bb687a804dd4fc +directory = usrsctp-d45b53f5dfa79533f5c5e7aefa5d7570405afb39 +source_url = https://github.com/sctplab/usrsctp/archive/d45b53f5dfa79533f5c5e7aefa5d7570405afb39.zip +source_filename = d45b53f5dfa79533f5c5e7aefa5d7570405afb39.zip +source_hash = da5f9adafc48fb5451f8355fee06508db52a3fca11138852c73ecb844f3b8647 [provide] usrsctp = usrsctp_dep diff --git a/worker/tasks.py b/worker/tasks.py new file mode 100644 index 0000000000..9bb71f4bb2 --- /dev/null +++ b/worker/tasks.py @@ -0,0 +1,623 @@ +# Ignore these pylint warnings, conventions and refactoring messages: +# - W0301: Unnecessary semicolon (unnecessary-semicolon) +# - W0622: Redefining built-in 'format' (redefined-builtin) +# - W0702: No exception type(s) specified (bare-except) +# - C0114: Missing module docstring (missing-module-docstring) +# - C0301: Line too long (line-too-long) +# +# pylint: disable=W0301,W0622,W0702,C0114,C0301 + +# +# This is a tasks.py file for the Python invoke package: +# https://docs.pyinvoke.org/. +# +# It's a replacement of our Makefile with same tasks. +# +# Usage: +# pip install invoke +# invoke --list +# + +import sys; +import os; +import inspect; +import shutil; +# We import this from a custom location and pylint doesn't know. +from invoke import task, call; # pylint: disable=import-error + +MEDIASOUP_BUILDTYPE = os.getenv('MEDIASOUP_BUILDTYPE') or 'Release'; +WORKER_DIR = os.path.dirname(os.path.abspath( + inspect.getframeinfo(inspect.currentframe()).filename +)); +# NOTE: MEDIASOUP_OUT_DIR is overrided by build.rs. +MEDIASOUP_OUT_DIR = os.getenv('MEDIASOUP_OUT_DIR') or f'{WORKER_DIR}/out'; +MEDIASOUP_INSTALL_DIR = os.getenv('MEDIASOUP_INSTALL_DIR') or f'{MEDIASOUP_OUT_DIR}/{MEDIASOUP_BUILDTYPE}'; +BUILD_DIR = os.getenv('BUILD_DIR') or f'{MEDIASOUP_INSTALL_DIR}/build'; +# Custom pip folder for invoke package. +# NOTE: We invoke `pip install` always with `--no-user` to make it not complain +# about "can not combine --user and --target". +PIP_INVOKE_DIR = f'{MEDIASOUP_OUT_DIR}/pip_invoke'; +# Custom pip folder for meson and ninja packages. +PIP_MESON_NINJA_DIR = f'{MEDIASOUP_OUT_DIR}/pip_meson_ninja'; +# Custom pip folder for pylint package. +# NOTE: We do this because using --target and --upgrade in `pip install` is not +# supported and a latter invokation entirely replaces the bin folder. +PIP_PYLINT_DIR = f'{MEDIASOUP_OUT_DIR}/pip_pylint'; +# If available (only on some *nix systems), os.sched_getaffinity(0) gets set of +# CPUs the calling thread is restricted to. Instead, os.cpu_count() returns the +# total number of CPUs in a system (it doesn't take into account how many of them +# the calling thread can use). +NUM_CORES = len(os.sched_getaffinity(0)) if hasattr(os, 'sched_getaffinity') else os.cpu_count(); +PYTHON = os.getenv('PYTHON') or sys.executable; +MESON = os.getenv('MESON') or f'{PIP_MESON_NINJA_DIR}/bin/meson'; +MESON_VERSION = os.getenv('MESON_VERSION') or '1.5.0'; +# MESON_ARGS can be used to provide extra configuration parameters to meson, +# such as adding defines or changing optimization options. For instance, use +# `MESON_ARGS="-Dms_log_trace=true -Dms_log_file_line=true" npm i` to compile +# worker with tracing and enabled. +# NOTE: On Windows make sure to add `--vsenv` or have MSVS environment already +# active if you override this parameter. +MESON_ARGS = os.getenv('MESON_ARGS') if os.getenv('MESON_ARGS') else '--vsenv' if os.name == 'nt' else ''; +# Let's use a specific version of ninja to avoid buggy version 1.11.1: +# https://mediasoup.discourse.group/t/partly-solved-could-not-detect-ninja-v1-8-2-or-newer/ +# https://github.com/ninja-build/ninja/issues/2211 +NINJA_VERSION = os.getenv('NINJA_VERSION') or '1.10.2.4'; +PYLINT_VERSION = os.getenv('PYLINT_VERSION') or '3.0.2'; +NPM = os.getenv('NPM') or 'npm'; +DOCKER = os.getenv('DOCKER') or 'docker'; +# pty=True in ctx.run() is not available on Windows so if stdout is not a TTY +# let's assume PTY is not supported. Related issue in invoke project: +# https://github.com/pyinvoke/invoke/issues/561 +PTY_SUPPORTED = sys.stdout.isatty(); +# Use sh (widely supported, more than bash) if not in Windows. +SHELL = '/bin/sh' if not os.name == 'nt' else None; + +# Disable `*.pyc` files creation. +os.environ['PYTHONDONTWRITEBYTECODE'] = 'true'; + +# Instruct meson where to look for ninja binary. +if os.name == 'nt': + # Windows is, of course, special. + os.environ['NINJA'] = f'{PIP_MESON_NINJA_DIR}/bin/ninja.exe'; +else: + os.environ['NINJA'] = f'{PIP_MESON_NINJA_DIR}/bin/ninja'; + +# Instruct Python where to look for modules it needs, such that meson actually +# runs from installed location. +# NOTE: On Windows we must use ; instead of : to separate paths. +PYTHONPATH = os.getenv('PYTHONPATH') or ''; +if os.name == 'nt': + os.environ['PYTHONPATH'] = f'{PIP_INVOKE_DIR};{PIP_MESON_NINJA_DIR};{PIP_PYLINT_DIR};{PYTHONPATH}'; +else: + os.environ['PYTHONPATH'] = f'{PIP_INVOKE_DIR}:{PIP_MESON_NINJA_DIR}:{PIP_PYLINT_DIR}:{PYTHONPATH}'; + + +@task +def meson_ninja(ctx): + """ + Install meson and ninja (also update Python pip and setuptools packages) + """ + if os.path.isfile(MESON): + return; + + # Updated pip and setuptools are needed for meson. + # `--system` is not present everywhere and is only needed as workaround for + # Debian-specific issue (copied from https://github.com/gluster/gstatus/pull/33), + # fallback to command without `--system` if the first one fails. + try: + ctx.run( + f'"{PYTHON}" -m pip install --system --upgrade --no-user --target "{PIP_MESON_NINJA_DIR}" pip setuptools', + echo=True, + hide=True, + shell=SHELL + ); + except: + ctx.run( + f'"{PYTHON}" -m pip install --upgrade --no-user --target "{PIP_MESON_NINJA_DIR}" pip setuptools', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + # Workaround for NixOS and Guix that don't work with pre-built binaries, see: + # https://github.com/NixOS/nixpkgs/issues/142383. + pip_build_binaries = '--no-binary :all:' if os.path.isfile('/etc/NIXOS') or os.path.isdir('/etc/guix') else ''; + + # Install meson and ninja using pip into our custom location, so we don't + # depend on system-wide installation. + ctx.run( + f'"{PYTHON}" -m pip install --upgrade --no-user --target "{PIP_MESON_NINJA_DIR}" {pip_build_binaries} meson=={MESON_VERSION} ninja=={NINJA_VERSION}', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task(pre=[meson_ninja]) +def setup(ctx, meson_args=MESON_ARGS): + """ + Run meson setup + """ + if MEDIASOUP_BUILDTYPE == 'Release': + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" setup --prefix "{MEDIASOUP_INSTALL_DIR}" --bindir "" --libdir "" --buildtype release -Db_ndebug=true {meson_args} "{BUILD_DIR}"', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + elif MEDIASOUP_BUILDTYPE == 'Debug': + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" setup --prefix "{MEDIASOUP_INSTALL_DIR}" --bindir "" --libdir "" --buildtype debug {meson_args} "{BUILD_DIR}"', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + else: + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" setup --prefix "{MEDIASOUP_INSTALL_DIR}" --bindir "" --libdir "" --buildtype {MEDIASOUP_BUILDTYPE} -Db_ndebug=if-release {meson_args} "{BUILD_DIR}"', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task +def clean(ctx): # pylint: disable=unused-argument + """ + Clean the installation directory + """ + shutil.rmtree(MEDIASOUP_INSTALL_DIR, ignore_errors=True); + + +@task +def clean_build(ctx): # pylint: disable=unused-argument + """ + Clean the build directory + """ + shutil.rmtree(BUILD_DIR, ignore_errors=True); + + +@task +def clean_pip(ctx): # pylint: disable=unused-argument + """ + Clean the local pip setup + """ + shutil.rmtree(PIP_MESON_NINJA_DIR, ignore_errors=True); + shutil.rmtree(PIP_PYLINT_DIR, ignore_errors=True); + + +@task(pre=[meson_ninja]) +def clean_subprojects(ctx): + """ + Clean meson subprojects + """ + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" subprojects purge --include-cache --confirm', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task +def clean_all(ctx): + """ + Clean meson subprojects and all installed/built artificats + """ + with ctx.cd(f'"{WORKER_DIR}"'): + try: + ctx.run( + f'"{MESON}" subprojects purge --include-cache --confirm', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + except: + pass; + + shutil.rmtree(MEDIASOUP_OUT_DIR, ignore_errors=True); + shutil.rmtree('include/FBS', ignore_errors=True); + + +@task(pre=[meson_ninja]) +def update_wrap_file(ctx, subproject): + """ + Update the wrap file of a subproject + """ + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" subprojects update --reset {subproject}', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task(pre=[setup]) +def flatc(ctx): + """ + Compile FlatBuffers FBS files + """ + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" compile -C "{BUILD_DIR}" flatbuffers-generator', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task(pre=[setup, flatc], default=True) +def mediasoup_worker(ctx): + """ + Compile mediasoup-worker binary + """ + if os.getenv('MEDIASOUP_WORKER_BIN'): + print('skipping mediasoup-worker compilation due to the existence of the MEDIASOUP_WORKER_BIN environment variable'); + return; + + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" compile -C "{BUILD_DIR}" -j {NUM_CORES} mediasoup-worker', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" install -C "{BUILD_DIR}" --no-rebuild --tags mediasoup-worker', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task(pre=[setup, flatc]) +def libmediasoup_worker(ctx): + """ + Compile libmediasoup-worker library + """ + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" compile -C "{BUILD_DIR}" -j {NUM_CORES} libmediasoup-worker', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" install -C "{BUILD_DIR}" --no-rebuild --tags libmediasoup-worker', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task(pre=[setup, flatc]) +def xcode(ctx): + """ + Setup Xcode project + """ + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" setup --buildtype {MEDIASOUP_BUILDTYPE.lower()} --backend xcode "{MEDIASOUP_OUT_DIR}/xcode"', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task +def lint(ctx): + """ + Lint source code + """ + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{NPM}" run lint --prefix scripts/', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + if not os.path.isdir(PIP_PYLINT_DIR): + # Install pylint using pip into our custom location. + ctx.run( + f'"{PYTHON}" -m pip install --upgrade --no-user --target="{PIP_PYLINT_DIR}" pylint=={PYLINT_VERSION}', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{PYTHON}" -m pylint tasks.py', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task +def format(ctx): + """ + Format source code according to lint rules + """ + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{NPM}" run format --prefix scripts/', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task(pre=[setup, flatc]) +def test(ctx): + """ + Run worker tests + """ + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" compile -C "{BUILD_DIR}" -j {NUM_CORES} mediasoup-worker-test', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" install -C "{BUILD_DIR}" --no-rebuild --tags mediasoup-worker-test', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + mediasoup_worker_test = 'mediasoup-worker-test.exe' if os.name == 'nt' else 'mediasoup-worker-test'; + mediasoup_test_tags = os.getenv('MEDIASOUP_TEST_TAGS') or ''; + + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{BUILD_DIR}/{mediasoup_worker_test}" --invisibles --colour-mode=ansi {mediasoup_test_tags}', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task(pre=[call(setup, meson_args=MESON_ARGS + ' -Db_sanitize=address -Db_lundef=false'), flatc]) +def test_asan_address(ctx): + """ + Run worker test with Address Sanitizer with '-fsanitize=address' + """ + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" compile -C "{BUILD_DIR}" -j {NUM_CORES} mediasoup-worker-test-asan-address', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" install -C "{BUILD_DIR}" --no-rebuild --tags mediasoup-worker-test-asan-address', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + mediasoup_test_tags = os.getenv('MEDIASOUP_TEST_TAGS') or ''; + + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'ASAN_OPTIONS=detect_leaks=1 "{BUILD_DIR}/mediasoup-worker-test-asan-address" --invisibles {mediasoup_test_tags}', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task(pre=[call(setup, meson_args=MESON_ARGS + ' -Db_sanitize=undefined -Db_lundef=false'), flatc]) +def test_asan_undefined(ctx): + """ + Run worker test with undefined Sanitizer with -fsanitize=undefined + """ + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" compile -C "{BUILD_DIR}" -j {NUM_CORES} mediasoup-worker-test-asan-undefined', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" install -C "{BUILD_DIR}" --no-rebuild --tags mediasoup-worker-test-asan-undefined', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + mediasoup_test_tags = os.getenv('MEDIASOUP_TEST_TAGS') or ''; + + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'ASAN_OPTIONS=detect_leaks=1 "{BUILD_DIR}/mediasoup-worker-test-asan-undefined" --invisibles {mediasoup_test_tags}', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task(pre=[call(setup, meson_args=MESON_ARGS + ' -Db_sanitize=thread -Db_lundef=false'), flatc]) +def test_asan_thread(ctx): + """ + Run worker test with thread Sanitizer with -fsanitize=thread + """ + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" compile -C "{BUILD_DIR}" -j {NUM_CORES} mediasoup-worker-test-asan-thread', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" install -C "{BUILD_DIR}" --no-rebuild --tags mediasoup-worker-test-asan-thread', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + mediasoup_test_tags = os.getenv('MEDIASOUP_TEST_TAGS') or ''; + + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'ASAN_OPTIONS=detect_leaks=1 "{BUILD_DIR}/mediasoup-worker-test-asan-thread" --invisibles {mediasoup_test_tags}', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task +def tidy(ctx): + """ + Perform C++ checks with clang-tidy + """ + mediasoup_tidy_checks = os.getenv('MEDIASOUP_TIDY_CHECKS') or ''; + mediasoup_tidy_files = os.getenv('MEDIASOUP_TIDY_FILES') or ''; + mediasoup_clang_tidy_dir = os.getenv('MEDIASOUP_CLANG_TIDY_DIR'); + + # MEDIASOUP_CLANG_TIDY_DIR env variable is mandatory. + # NOTE: sys.exit(text) exists the program with status code 1. + if not mediasoup_clang_tidy_dir: + sys.exit('missing MEDIASOUP_CLANG_TIDY_DIR env variable'); + + if mediasoup_tidy_checks: + mediasoup_tidy_checks = '-*,' + mediasoup_tidy_checks; + + if not mediasoup_tidy_files: + mediasoup_tidy_files = 'src/*.cpp src/**/*.cpp src/**/**.cpp'; + + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{PYTHON}" "{mediasoup_clang_tidy_dir}/run-clang-tidy" -clang-tidy-binary="{mediasoup_clang_tidy_dir}/clang-tidy" -clang-apply-replacements-binary="{mediasoup_clang_tidy_dir}/clang-apply-replacements" -p="{BUILD_DIR}" -j={NUM_CORES} -fix -checks={mediasoup_tidy_checks} {mediasoup_tidy_files}', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task(pre=[call(setup, meson_args=MESON_ARGS + ' -Db_sanitize=address -Db_lundef=false'), flatc]) +def fuzzer(ctx): + """ + Build the mediasoup-worker-fuzzer binary (which uses libFuzzer) + """ + + # NOTE: We need to pass '-Db_sanitize=address' to enable fuzzer in all Meson + # subprojects, so we pass it to the setup() task. + + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" compile -C "{BUILD_DIR}" -j {NUM_CORES} mediasoup-worker-fuzzer', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{MESON}" install -C "{BUILD_DIR}" --no-rebuild --tags mediasoup-worker-fuzzer', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task +def fuzzer_run_all(ctx): + """ + Run all fuzzer cases + """ + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'LSAN_OPTIONS=verbosity=1:log_threads=1 "{BUILD_DIR}/mediasoup-worker-fuzzer" -artifact_prefix=fuzzer/reports/ -max_len=1400 fuzzer/new-corpus deps/webrtc-fuzzer-corpora/corpora/stun-corpus deps/webrtc-fuzzer-corpora/corpora/rtp-corpus deps/webrtc-fuzzer-corpora/corpora/rtcp-corpus', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task +def docker(ctx): + """ + Build a Linux Ubuntu Docker image with fuzzer capable clang++ + """ + if os.getenv('DOCKER_NO_CACHE') == 'true': + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{DOCKER}" build -f Dockerfile --no-cache --tag mediasoup/docker:latest .', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + else: + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{DOCKER}" build -f Dockerfile --tag mediasoup/docker:latest .', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task +def docker_run(ctx): + """ + Run a container of the Ubuntu Docker image created in the docker task + """ + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{DOCKER}" run --name=mediasoupDocker -it --rm --privileged --cap-add SYS_PTRACE -v "{WORKER_DIR}/../:/mediasoup" mediasoup/docker:latest', + echo=True, + pty=True, # NOTE: Needed to enter the terminal of the Docker image. + shell=SHELL + ); + + +@task +def docker_alpine(ctx): + """ + Build a Linux Alpine Docker image + """ + if os.getenv('DOCKER_NO_CACHE') == 'true': + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{DOCKER}" build -f Dockerfile.alpine --no-cache --tag mediasoup/docker-alpine:latest .', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + else: + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{DOCKER}" build -f Dockerfile.alpine --tag mediasoup/docker-alpine:latest .', + echo=True, + pty=PTY_SUPPORTED, + shell=SHELL + ); + + +@task +def docker_alpine_run(ctx): + """ + Run a container of the Alpine Docker image created in the docker_alpine task + """ + with ctx.cd(f'"{WORKER_DIR}"'): + ctx.run( + f'"{DOCKER}" run --name=mediasoupDockerAlpine -it --rm --privileged --cap-add SYS_PTRACE -v "{WORKER_DIR}/../:/mediasoup" mediasoup/docker-alpine:latest', + echo=True, + pty=True, # NOTE: Needed to enter the terminal of the Docker image. + shell=SHELL + ); diff --git a/worker/test/include/helpers.hpp b/worker/test/include/helpers.hpp index a416052a40..c95f323a2e 100644 --- a/worker/test/include/helpers.hpp +++ b/worker/test/include/helpers.hpp @@ -18,7 +18,9 @@ namespace helpers std::ifstream in(filePath, std::ios::ate | std::ios::binary); if (!in) + { return false; + } *len = static_cast(in.tellg()) - 1; in.seekg(0, std::ios::beg); @@ -31,9 +33,12 @@ namespace helpers inline bool addToBuffer(uint8_t* buf, int* size, uint8_t* data, size_t len) { if (*size + len > BUFFER_SIZE) + { return false; + } int i = 0; + if (len == 1) { buf[*size] = *data; @@ -59,12 +64,15 @@ namespace helpers std::ifstream in(filePath, std::ios::ate | std::ios::binary); if (!in) + { return false; + } in.seekg(pos, std::ios::beg); in.read(reinterpret_cast(payload), bytes); in.close(); + return true; } @@ -93,52 +101,78 @@ namespace helpers // write the RTP header if (!addToBuffer(buf, &packet_size, buffer, 16)) + { return false; + } // write ID and length of frame marking extension // if first layer then length should be 0, else 1 oneByte = oneByte | 1 << 4; + if (sid != -1) { oneByte = oneByte | 0x01; } if (!addToBuffer(buf, &packet_size, &oneByte, 1)) + { return false; + } // write SEIDB TID bits oneByte = 0; + if (firstSliceId == 1) + { oneByte = oneByte | 1 << 7; + } if (lastSliceId == 1) + { oneByte = oneByte | 1 << 6; + } if (isIdr == 1) + { oneByte = oneByte | 1 << 5; + } if (tid != -1) + { oneByte = oneByte | tid; + } if (!addToBuffer(buf, &packet_size, &oneByte, 1)) + { return false; + } // write DID QID bits oneByte = 0; + if (sid != -1) + { oneByte = oneByte | sid << 6; + } if (!addToBuffer(buf, &packet_size, &oneByte, 1)) + { return false; + } // write TL0PICIDX oneByte = 0; + if (!addToBuffer(buf, &packet_size, &oneByte, 1)) + { return false; + } // write payload if (!addToBuffer(buf, &packet_size, payload, nalLength)) + { return false; + } *len = packet_size; diff --git a/worker/test/src/PayloadChannel/TestPayloadChannelNotification.cpp b/worker/test/src/PayloadChannel/TestPayloadChannelNotification.cpp index 0fb6a865cc..2a929f7745 100644 --- a/worker/test/src/PayloadChannel/TestPayloadChannelNotification.cpp +++ b/worker/test/src/PayloadChannel/TestPayloadChannelNotification.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "PayloadChannel/PayloadChannelNotification.hpp" -#include +#include SCENARIO("PayloadChannelNotification", "[channel][notification]") { diff --git a/worker/test/src/PayloadChannel/TestPayloadChannelRequest.cpp b/worker/test/src/PayloadChannel/TestPayloadChannelRequest.cpp index 5b1299fba1..302c9d6378 100644 --- a/worker/test/src/PayloadChannel/TestPayloadChannelRequest.cpp +++ b/worker/test/src/PayloadChannel/TestPayloadChannelRequest.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "PayloadChannel/PayloadChannelRequest.hpp" -#include +#include SCENARIO("PayloadChannelRequest", "[channel][request]") { diff --git a/worker/test/src/RTC/Codecs/TestH264.cpp b/worker/test/src/RTC/Codecs/TestH264.cpp index 4f50fb6689..47132e96cc 100644 --- a/worker/test/src/RTC/Codecs/TestH264.cpp +++ b/worker/test/src/RTC/Codecs/TestH264.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/Codecs/H264.hpp" -#include +#include #include // std::memcmp() using namespace RTC; @@ -21,10 +21,9 @@ SCENARIO("parse H264 payload descriptor", "[codecs][h264]") std::memcpy(buffer, originalBuffer, sizeof(buffer)); - const auto* payloadDescriptor = Codecs::H264::Parse(buffer, sizeof(buffer)); + std::unique_ptr payloadDescriptor{ Codecs::H264::Parse( + buffer, sizeof(buffer)) }; REQUIRE(payloadDescriptor); - - delete payloadDescriptor; } } diff --git a/worker/test/src/RTC/Codecs/TestH264_SVC.cpp b/worker/test/src/RTC/Codecs/TestH264_SVC.cpp index 9af9b71c62..dc7380959d 100644 --- a/worker/test/src/RTC/Codecs/TestH264_SVC.cpp +++ b/worker/test/src/RTC/Codecs/TestH264_SVC.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/Codecs/H264_SVC.hpp" -#include +#include #include // std::memcmp() using namespace RTC; @@ -21,7 +21,9 @@ SCENARIO("parse H264_SVC payload descriptor", "[codecs][h264_svc]") std::memcpy(buffer, originalBuffer, sizeof(buffer)); - const auto* payloadDescriptor = Codecs::H264_SVC::Parse(buffer, sizeof(buffer)); + std::unique_ptr payloadDescriptor{ + Codecs::H264_SVC::Parse(buffer, sizeof(buffer)) + }; REQUIRE(payloadDescriptor); @@ -31,8 +33,6 @@ SCENARIO("parse H264_SVC payload descriptor", "[codecs][h264_svc]") REQUIRE(payloadDescriptor->isKeyFrame == true); REQUIRE(payloadDescriptor->hasTlIndex == false); REQUIRE(payloadDescriptor->hasSlIndex == false); - - delete payloadDescriptor; } SECTION("parse payload descriptor for NALU 8") @@ -49,7 +49,9 @@ SCENARIO("parse H264_SVC payload descriptor", "[codecs][h264_svc]") std::memcpy(buffer, originalBuffer, sizeof(buffer)); - const auto* payloadDescriptor = Codecs::H264_SVC::Parse(buffer, sizeof(buffer)); + std::unique_ptr payloadDescriptor{ + Codecs::H264_SVC::Parse(buffer, sizeof(buffer)) + }; REQUIRE(payloadDescriptor); @@ -59,8 +61,6 @@ SCENARIO("parse H264_SVC payload descriptor", "[codecs][h264_svc]") REQUIRE(payloadDescriptor->isKeyFrame == false); REQUIRE(payloadDescriptor->hasTlIndex == false); REQUIRE(payloadDescriptor->hasSlIndex == false); - - delete payloadDescriptor; } SECTION("parse payload descriptor for NALU 1") @@ -77,7 +77,9 @@ SCENARIO("parse H264_SVC payload descriptor", "[codecs][h264_svc]") std::memcpy(buffer, originalBuffer, sizeof(buffer)); - const auto* payloadDescriptor = Codecs::H264_SVC::Parse(buffer, sizeof(buffer)); + std::unique_ptr payloadDescriptor{ + Codecs::H264_SVC::Parse(buffer, sizeof(buffer)) + }; REQUIRE(payloadDescriptor); @@ -87,8 +89,6 @@ SCENARIO("parse H264_SVC payload descriptor", "[codecs][h264_svc]") REQUIRE(payloadDescriptor->isKeyFrame == false); REQUIRE(payloadDescriptor->hasTlIndex == false); REQUIRE(payloadDescriptor->hasSlIndex == false); - - delete payloadDescriptor; } SECTION("parse payload descriptor for NALU 5") @@ -105,7 +105,9 @@ SCENARIO("parse H264_SVC payload descriptor", "[codecs][h264_svc]") std::memcpy(buffer, originalBuffer, sizeof(buffer)); - const auto* payloadDescriptor = Codecs::H264_SVC::Parse(buffer, sizeof(buffer)); + std::unique_ptr payloadDescriptor{ + Codecs::H264_SVC::Parse(buffer, sizeof(buffer)) + }; REQUIRE(payloadDescriptor); @@ -115,8 +117,6 @@ SCENARIO("parse H264_SVC payload descriptor", "[codecs][h264_svc]") REQUIRE(payloadDescriptor->isKeyFrame == true); REQUIRE(payloadDescriptor->hasTlIndex == false); REQUIRE(payloadDescriptor->hasSlIndex == false); - - delete payloadDescriptor; } SECTION("parse payload descriptor for NALU 14") @@ -133,7 +133,9 @@ SCENARIO("parse H264_SVC payload descriptor", "[codecs][h264_svc]") std::memcpy(buffer, originalBuffer, sizeof(buffer)); - const auto* payloadDescriptor = Codecs::H264_SVC::Parse(buffer, sizeof(buffer)); + std::unique_ptr payloadDescriptor{ + Codecs::H264_SVC::Parse(buffer, sizeof(buffer)) + }; REQUIRE(payloadDescriptor); @@ -145,8 +147,6 @@ SCENARIO("parse H264_SVC payload descriptor", "[codecs][h264_svc]") REQUIRE(payloadDescriptor->isKeyFrame == false); REQUIRE(payloadDescriptor->hasTlIndex == true); REQUIRE(payloadDescriptor->hasSlIndex == true); - - delete payloadDescriptor; } SECTION("parse payload descriptor for NALU 20") @@ -163,7 +163,9 @@ SCENARIO("parse H264_SVC payload descriptor", "[codecs][h264_svc]") std::memcpy(buffer, originalBuffer, sizeof(buffer)); - const auto* payloadDescriptor = Codecs::H264_SVC::Parse(buffer, sizeof(buffer)); + std::unique_ptr payloadDescriptor{ + Codecs::H264_SVC::Parse(buffer, sizeof(buffer)) + }; REQUIRE(payloadDescriptor); @@ -175,7 +177,5 @@ SCENARIO("parse H264_SVC payload descriptor", "[codecs][h264_svc]") REQUIRE(payloadDescriptor->isKeyFrame == false); REQUIRE(payloadDescriptor->hasTlIndex == true); REQUIRE(payloadDescriptor->hasSlIndex == true); - - delete payloadDescriptor; } } diff --git a/worker/test/src/RTC/Codecs/TestVP8.cpp b/worker/test/src/RTC/Codecs/TestVP8.cpp index 676ab514d4..8b4a418847 100644 --- a/worker/test/src/RTC/Codecs/TestVP8.cpp +++ b/worker/test/src/RTC/Codecs/TestVP8.cpp @@ -1,7 +1,7 @@ #include "common.hpp" #include "RTC/Codecs/VP8.hpp" -#include -#include // std::memcmp() +#include +#include // std::memcmp(), std::memcpy() using namespace RTC; @@ -38,7 +38,8 @@ SCENARIO("parse VP8 payload descriptor", "[codecs][vp8]") std::memcpy(buffer, originalBuffer, sizeof(buffer)); - const auto* payloadDescriptor = Codecs::VP8::Parse(buffer, sizeof(buffer)); + std::unique_ptr payloadDescriptor{ Codecs::VP8::Parse( + buffer, sizeof(buffer)) }; REQUIRE(payloadDescriptor); @@ -77,8 +78,6 @@ SCENARIO("parse VP8 payload descriptor", "[codecs][vp8]") REQUIRE(std::memcmp(buffer, originalBuffer, sizeof(buffer)) == 0); } } - - delete payloadDescriptor; } SECTION("parse payload descriptor 2") @@ -113,7 +112,8 @@ SCENARIO("parse VP8 payload descriptor", "[codecs][vp8]") std::memcpy(buffer, originalBuffer, sizeof(buffer)); // Parse the buffer. - const auto* payloadDescriptor = Codecs::VP8::Parse(buffer, sizeof(buffer)); + std::unique_ptr payloadDescriptor{ Codecs::VP8::Parse( + buffer, sizeof(buffer)) }; REQUIRE(payloadDescriptor); @@ -152,8 +152,6 @@ SCENARIO("parse VP8 payload descriptor", "[codecs][vp8]") REQUIRE(std::memcmp(buffer, originalBuffer, sizeof(buffer)) == 0); } } - - delete payloadDescriptor; }; SECTION("parse payload descriptor. I flag set but no space for pictureId") @@ -228,7 +226,9 @@ Codecs::VP8::PayloadDescriptor* CreatePacket( buffer[5] = tlIndex << 6; if (layerSync) + { buffer[5] |= 0x20; // y bit + } auto* payloadDescriptor = Codecs::VP8::Parse(buffer, bufferLen); diff --git a/worker/test/src/RTC/Codecs/TestVP9.cpp b/worker/test/src/RTC/Codecs/TestVP9.cpp index 0a94ac1988..77dec6365c 100644 --- a/worker/test/src/RTC/Codecs/TestVP9.cpp +++ b/worker/test/src/RTC/Codecs/TestVP9.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/Codecs/VP9.hpp" -#include +#include #include // std::memcmp() using namespace RTC; diff --git a/worker/test/src/RTC/RTCP/TestBye.cpp b/worker/test/src/RTC/RTCP/TestBye.cpp index a7825a01c5..2085e0791c 100644 --- a/worker/test/src/RTC/RTCP/TestBye.cpp +++ b/worker/test/src/RTC/RTCP/TestBye.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/Bye.hpp" -#include +#include #include // std::memcmp() #include @@ -47,11 +47,11 @@ SCENARIO("RTCP BYE parsing", "[parser][rtcp][bye]") { SECTION("parse BYE packet") { - ByePacket* packet = ByePacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ ByePacket::Parse(buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -64,8 +64,6 @@ SCENARIO("RTCP BYE parsing", "[parser][rtcp][bye]") REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } SECTION("create ByePacket") diff --git a/worker/test/src/RTC/RTCP/TestFeedbackPsAfb.cpp b/worker/test/src/RTC/RTCP/TestFeedbackPsAfb.cpp index e6112b70cf..3d7f8227a1 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackPsAfb.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackPsAfb.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/FeedbackPsAfb.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -39,11 +39,11 @@ SCENARIO("RTCP Feedback PS AFB parsing", "[parser][rtcp][feedback-ps][afb]") { using namespace TestFeedbackPsAfb; - FeedbackPsAfbPacket* packet = FeedbackPsAfbPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackPsAfbPacket::Parse(buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -56,7 +56,5 @@ SCENARIO("RTCP Feedback PS AFB parsing", "[parser][rtcp][feedback-ps][afb]") REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } } diff --git a/worker/test/src/RTC/RTCP/TestFeedbackPsFir.cpp b/worker/test/src/RTC/RTCP/TestFeedbackPsFir.cpp index 1a182e2958..b5781802e9 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackPsFir.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackPsFir.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/FeedbackPsFir.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -44,11 +44,11 @@ SCENARIO("RTCP Feedback PS FIR parsing", "[parser][rtcp][feedback-ps][fir]") SECTION("parse FeedbackPsFirPacket") { - FeedbackPsFirPacket* packet = FeedbackPsFirPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackPsFirPacket::Parse(buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -61,8 +61,6 @@ SCENARIO("RTCP Feedback PS FIR parsing", "[parser][rtcp][feedback-ps][fir]") REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } SECTION("create FeedbackPsFirPacket") diff --git a/worker/test/src/RTC/RTCP/TestFeedbackPsLei.cpp b/worker/test/src/RTC/RTCP/TestFeedbackPsLei.cpp index 4d092ffb5e..485a6382cc 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackPsLei.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackPsLei.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/FeedbackPsLei.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -41,11 +41,11 @@ SCENARIO("RTCP Feedback PS LEI parsing", "[parser][rtcp][feedback-ps][lei]") SECTION("parse FeedbackPsLeiPacket") { - FeedbackPsLeiPacket* packet = FeedbackPsLeiPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackPsLeiPacket::Parse(buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -58,8 +58,6 @@ SCENARIO("RTCP Feedback PS LEI parsing", "[parser][rtcp][feedback-ps][lei]") REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } SECTION("create FeedbackPsLeiPacket") diff --git a/worker/test/src/RTC/RTCP/TestFeedbackPsPli.cpp b/worker/test/src/RTC/RTCP/TestFeedbackPsPli.cpp index 1d3d83e25f..762f9e5b72 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackPsPli.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackPsPli.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/FeedbackPsPli.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -35,11 +35,11 @@ SCENARIO("RTCP Feeback RTP PLI parsing", "[parser][rtcp][feedback-ps][pli]") SECTION("parse FeedbackPsPliPacket") { - FeedbackPsPliPacket* packet = FeedbackPsPliPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackPsPliPacket::Parse(buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -52,8 +52,6 @@ SCENARIO("RTCP Feeback RTP PLI parsing", "[parser][rtcp][feedback-ps][pli]") REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } SECTION("create FeedbackPsPliPacket") diff --git a/worker/test/src/RTC/RTCP/TestFeedbackPsRemb.cpp b/worker/test/src/RTC/RTCP/TestFeedbackPsRemb.cpp index cbea39771e..e71eb05f95 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackPsRemb.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackPsRemb.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/FeedbackPsRemb.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -43,11 +43,11 @@ SCENARIO("RTCP Feedback PS parsing", "[parser][rtcp][feedback-ps][remb]") SECTION("parse FeedbackPsRembPacket") { - FeedbackPsRembPacket* packet = FeedbackPsRembPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackPsRembPacket::Parse(buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -60,8 +60,6 @@ SCENARIO("RTCP Feedback PS parsing", "[parser][rtcp][feedback-ps][remb]") REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } SECTION("create FeedbackPsRembPacket") diff --git a/worker/test/src/RTC/RTCP/TestFeedbackPsRpsi.cpp b/worker/test/src/RTC/RTCP/TestFeedbackPsRpsi.cpp index 8447af6f53..2b5fe999af 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackPsRpsi.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackPsRpsi.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/FeedbackPsRpsi.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -49,11 +49,11 @@ SCENARIO("RTCP Feedback PS RPSI parsing", "[parser][rtcp][feedback-ps][rpsi]") SECTION("parse FeedbackPsRpsiPacket") { - FeedbackPsRpsiPacket* packet = FeedbackPsRpsiPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackPsRpsiPacket::Parse(buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -66,7 +66,5 @@ SCENARIO("RTCP Feedback PS RPSI parsing", "[parser][rtcp][feedback-ps][rpsi]") REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } } diff --git a/worker/test/src/RTC/RTCP/TestFeedbackPsSli.cpp b/worker/test/src/RTC/RTCP/TestFeedbackPsSli.cpp index 56eba471bc..d3def79324 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackPsSli.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackPsSli.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/FeedbackPsSli.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -46,11 +46,11 @@ SCENARIO("RTCP Feedback PS SLI parsing", "[parser][rtcp][feedback-ps][sli]") SECTION("parse FeedbackPsSliPacket") { - FeedbackPsSliPacket* packet = FeedbackPsSliPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackPsSliPacket::Parse(buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -63,7 +63,5 @@ SCENARIO("RTCP Feedback PS SLI parsing", "[parser][rtcp][feedback-ps][sli]") REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } } diff --git a/worker/test/src/RTC/RTCP/TestFeedbackPsTst.cpp b/worker/test/src/RTC/RTCP/TestFeedbackPsTst.cpp index 859c7564dc..99427598ee 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackPsTst.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackPsTst.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/FeedbackPsTst.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -45,11 +45,11 @@ SCENARIO("RTCP Feedback PS TSTN parsing", "[parser][rtcp][feedback-ps][tstn]") SECTION("parse FeedbackPsTstPacket") { - FeedbackPsTstnPacket* packet = FeedbackPsTstnPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackPsTstnPacket::Parse(buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -62,8 +62,6 @@ SCENARIO("RTCP Feedback PS TSTN parsing", "[parser][rtcp][feedback-ps][tstn]") REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } SECTION("create FeedbackPsTstPacket") diff --git a/worker/test/src/RTC/RTCP/TestFeedbackPsVbcm.cpp b/worker/test/src/RTC/RTCP/TestFeedbackPsVbcm.cpp index d8f8c3a585..9743091158 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackPsVbcm.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackPsVbcm.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/FeedbackPsVbcm.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -55,11 +55,11 @@ SCENARIO("RTCP Feedback PS VBCM parsing", "[parser][rtcp][feedback-ps][vbcm]") SECTION("parse FeedbackPsVbcmPacket") { - FeedbackPsVbcmPacket* packet = FeedbackPsVbcmPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackPsVbcmPacket::Parse(buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -72,7 +72,5 @@ SCENARIO("RTCP Feedback PS VBCM parsing", "[parser][rtcp][feedback-ps][vbcm]") REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } } diff --git a/worker/test/src/RTC/RTCP/TestFeedbackRtpEcn.cpp b/worker/test/src/RTC/RTCP/TestFeedbackRtpEcn.cpp index 3cad5af92a..1536de5130 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackRtpEcn.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackRtpEcn.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/FeedbackRtpEcn.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -61,11 +61,11 @@ SCENARIO("RTCP Feeback RTP ECN parsing", "[parser][rtcp][feedback-rtp][ecn]") { using namespace TestFeedbackRtpEcn; - FeedbackRtpEcnPacket* packet = FeedbackRtpEcnPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackRtpEcnPacket::Parse(buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -78,7 +78,5 @@ SCENARIO("RTCP Feeback RTP ECN parsing", "[parser][rtcp][feedback-rtp][ecn]") REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } } diff --git a/worker/test/src/RTC/RTCP/TestFeedbackRtpNack.cpp b/worker/test/src/RTC/RTCP/TestFeedbackRtpNack.cpp index 7c48a03b4d..2d62caa2bd 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackRtpNack.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackRtpNack.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/FeedbackRtpNack.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -45,11 +45,12 @@ SCENARIO("RTCP Feeback RTP NACK parsing", "[parser][rtcp][feedback-rtp][nack]") SECTION("parse FeedbackRtpNackItem") { - FeedbackRtpNackPacket* packet = FeedbackRtpNackPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackRtpNackPacket::Parse( + buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -62,8 +63,6 @@ SCENARIO("RTCP Feeback RTP NACK parsing", "[parser][rtcp][feedback-rtp][nack]") REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } SECTION("create FeedbackRtpNackPacket") diff --git a/worker/test/src/RTC/RTCP/TestFeedbackRtpSrReq.cpp b/worker/test/src/RTC/RTCP/TestFeedbackRtpSrReq.cpp index 0608823c6b..1604dfabc4 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackRtpSrReq.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackRtpSrReq.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/FeedbackRtpSrReq.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -35,11 +35,12 @@ SCENARIO("RTCP Feeback RTP SR-REQ parsing", "[parser][rtcp][feedback-rtp][sr-req SECTION("parse FeedbackRtpSrReqPacket") { - FeedbackRtpSrReqPacket* packet = FeedbackRtpSrReqPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackRtpSrReqPacket::Parse( + buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -52,8 +53,6 @@ SCENARIO("RTCP Feeback RTP SR-REQ parsing", "[parser][rtcp][feedback-rtp][sr-req REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } SECTION("create FeedbackRtpSrReqPacket") diff --git a/worker/test/src/RTC/RTCP/TestFeedbackRtpTllei.cpp b/worker/test/src/RTC/RTCP/TestFeedbackRtpTllei.cpp index 08e3c97d8f..f39ff48e7a 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackRtpTllei.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackRtpTllei.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/FeedbackRtpTllei.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -45,11 +45,12 @@ SCENARIO("RTCP Feeback RTP TLLEI parsing", "[parser][rtcp][feedback-rtp][tllei]" { using namespace TestFeedbackRtpTllei; - FeedbackRtpTlleiPacket* packet = FeedbackRtpTlleiPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackRtpTlleiPacket::Parse( + buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -62,7 +63,5 @@ SCENARIO("RTCP Feeback RTP TLLEI parsing", "[parser][rtcp][feedback-rtp][tllei]" REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } } diff --git a/worker/test/src/RTC/RTCP/TestFeedbackRtpTmmb.cpp b/worker/test/src/RTC/RTCP/TestFeedbackRtpTmmb.cpp index c5ae5e5478..b099aa448e 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackRtpTmmb.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackRtpTmmb.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/FeedbackRtpTmmb.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -48,11 +48,12 @@ SCENARIO("RTCP Feeback RTP TMMBR parsing", "[parser][rtcp][feedback-rtp][tmmb]") SECTION("parse FeedbackRtpTmmbrPacket") { - FeedbackRtpTmmbrPacket* packet = FeedbackRtpTmmbrPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackRtpTmmbrPacket::Parse( + buffer, sizeof(buffer)) }; REQUIRE(packet); - verify(packet); + verify(packet.get()); SECTION("serialize packet instance") { @@ -64,15 +65,12 @@ SCENARIO("RTCP Feeback RTP TMMBR parsing", "[parser][rtcp][feedback-rtp][tmmb]") // represent the same content. SECTION("create a packet out of the serialized buffer") { - FeedbackRtpTmmbrPacket* packet = FeedbackRtpTmmbrPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ FeedbackRtpTmmbrPacket::Parse( + buffer, sizeof(buffer)) }; - verify(packet); - - delete packet; + verify(packet.get()); } } - - delete packet; } SECTION("create FeedbackRtpTmmbrPacket") diff --git a/worker/test/src/RTC/RTCP/TestFeedbackRtpTransport.cpp b/worker/test/src/RTC/RTCP/TestFeedbackRtpTransport.cpp index 9cc7b65f59..68fc15d258 100644 --- a/worker/test/src/RTC/RTCP/TestFeedbackRtpTransport.cpp +++ b/worker/test/src/RTC/RTCP/TestFeedbackRtpTransport.cpp @@ -1,7 +1,7 @@ #include "common.hpp" #include "Logger.hpp" #include "RTC/RTCP/FeedbackRtpTransport.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -69,7 +69,7 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" SECTION( "create FeedbackRtpTransportPacket, small delta run length chunk and single large delta status packet") { - auto* packet = new FeedbackRtpTransportPacket(senderSsrc, mediaSsrc); + auto packet = std::make_unique(senderSsrc, mediaSsrc); REQUIRE(packet); @@ -97,7 +97,16 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" packet->SetFeedbackPacketCount(1); for (auto& input : inputs) - packet->AddPacket(input.sequenceNumber, input.timestamp, input.maxPacketSize); + { + if (std::addressof(input) == std::addressof(inputs.front())) + { + packet->SetBase(input.sequenceNumber + 1, input.timestamp); + } + else + { + packet->AddPacket(input.sequenceNumber, input.timestamp, input.maxPacketSize); + } + } REQUIRE(packet->GetLatestSequenceNumber() == 1013); REQUIRE(packet->GetLatestTimestamp() == 1000000013); @@ -132,7 +141,8 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" SECTION("parse serialized buffer") { - auto* packet2 = FeedbackRtpTransportPacket::Parse(buffer, len); + std::unique_ptr packet2{ FeedbackRtpTransportPacket::Parse( + buffer, len) }; REQUIRE(packet2); REQUIRE(packet2->GetBaseSequenceNumber() == 1000); @@ -146,17 +156,13 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" REQUIRE(len == len2); REQUIRE(std::memcmp(buffer, buffer2, len) == 0); REQUIRE(packet2->GetSize() == len2); - - delete packet2; } } - - delete packet; } SECTION("create FeedbackRtpTransportPacket, run length chunk (2)") { - auto* packet = new FeedbackRtpTransportPacket(senderSsrc, mediaSsrc); + auto packet = std::make_unique(senderSsrc, mediaSsrc); /* clang-format off */ std::vector inputs = @@ -170,7 +176,16 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" packet->SetFeedbackPacketCount(10); for (auto& input : inputs) - packet->AddPacket(input.sequenceNumber, input.timestamp, input.maxPacketSize); + { + if (std::addressof(input) == std::addressof(inputs.front())) + { + packet->SetBase(input.sequenceNumber + 1, input.timestamp); + } + else + { + packet->AddPacket(input.sequenceNumber, input.timestamp, input.maxPacketSize); + } + } packet->Finish(); validate(inputs, packet->GetPacketResults()); @@ -191,7 +206,8 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" SECTION("parse serialized buffer") { - auto* packet2 = FeedbackRtpTransportPacket::Parse(buffer, len); + std::unique_ptr packet2{ FeedbackRtpTransportPacket::Parse( + buffer, len) }; REQUIRE(packet2); REQUIRE(packet2->GetBaseSequenceNumber() == 1000); @@ -205,12 +221,8 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" REQUIRE(len == len2); REQUIRE(std::memcmp(buffer, buffer2, len) == 0); REQUIRE(packet2->GetSize() == len2); - - delete packet2; } } - - delete packet; } SECTION("create FeedbackRtpTransportPacket, mixed chunks") @@ -228,12 +240,21 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" }; /* clang-format on */ - auto* packet = new FeedbackRtpTransportPacket(senderSsrc, mediaSsrc); + auto packet = std::make_unique(senderSsrc, mediaSsrc); packet->SetFeedbackPacketCount(1); for (auto& input : inputs) - packet->AddPacket(input.sequenceNumber, input.timestamp, input.maxPacketSize); + { + if (std::addressof(input) == std::addressof(inputs.front())) + { + packet->SetBase(input.sequenceNumber + 1, input.timestamp); + } + else + { + packet->AddPacket(input.sequenceNumber, input.timestamp, input.maxPacketSize); + } + } packet->Finish(); validate(inputs, packet->GetPacketResults()); @@ -254,7 +275,8 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" SECTION("parse serialized buffer") { - auto* packet2 = FeedbackRtpTransportPacket::Parse(buffer, len); + std::unique_ptr packet2{ FeedbackRtpTransportPacket::Parse( + buffer, len) }; REQUIRE(packet2); REQUIRE(packet2->GetBaseSequenceNumber() == 1000); @@ -268,12 +290,8 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" REQUIRE(len == len2); REQUIRE(std::memcmp(buffer, buffer2, len) == 0); REQUIRE(packet2->GetSize() == len2); - - delete packet2; } } - - delete packet; } SECTION("create FeedbackRtpTransportPacket, incomplete two bit vector chunk") @@ -284,12 +302,21 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" { 1001, 1000000700, RtcpMtu }, }; - auto* packet = new FeedbackRtpTransportPacket(senderSsrc, mediaSsrc); + auto packet = std::make_unique(senderSsrc, mediaSsrc); packet->SetFeedbackPacketCount(1); for (auto& input : inputs) - packet->AddPacket(input.sequenceNumber, input.timestamp, input.maxPacketSize); + { + if (std::addressof(input) == std::addressof(inputs.front())) + { + packet->SetBase(input.sequenceNumber + 1, input.timestamp); + } + else + { + packet->AddPacket(input.sequenceNumber, input.timestamp, input.maxPacketSize); + } + } packet->Finish(); validate(inputs, packet->GetPacketResults()); @@ -310,7 +337,8 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" SECTION("parse serialized buffer") { - auto* packet2 = FeedbackRtpTransportPacket::Parse(buffer, len); + std::unique_ptr packet2{ FeedbackRtpTransportPacket::Parse( + buffer, len) }; REQUIRE(packet2); REQUIRE(packet2->GetBaseSequenceNumber() == 1000); @@ -324,12 +352,8 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" REQUIRE(len == len2); REQUIRE(std::memcmp(buffer, buffer2, len) == 0); REQUIRE(packet2->GetSize() == len2); - - delete packet2; } } - - delete packet; } SECTION("create two sequential FeedbackRtpTransportPackets") @@ -349,12 +373,21 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" }; /* clang-format on */ - auto* packet = new FeedbackRtpTransportPacket(senderSsrc, mediaSsrc); + auto packet = std::make_unique(senderSsrc, mediaSsrc); packet->SetFeedbackPacketCount(1); for (auto& input : inputs) - packet->AddPacket(input.sequenceNumber, input.timestamp, input.maxPacketSize); + { + if (std::addressof(input) == std::addressof(inputs.front())) + { + packet->SetBase(input.sequenceNumber + 1, input.timestamp); + } + else + { + packet->AddPacket(input.sequenceNumber, input.timestamp, input.maxPacketSize); + } + } packet->Finish(); validate(inputs, packet->GetPacketResults()); @@ -373,7 +406,8 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" SECTION("parse serialized buffer") { - auto* packet2 = FeedbackRtpTransportPacket::Parse(buffer, len); + std::unique_ptr packet2{ FeedbackRtpTransportPacket::Parse( + buffer, len) }; REQUIRE(packet2); REQUIRE(packet2->GetBaseSequenceNumber() == 1000); @@ -387,8 +421,6 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" REQUIRE(len == len2); REQUIRE(std::memcmp(buffer, buffer2, len) == 0); REQUIRE(packet2->GetSize() == len2); - - delete packet2; } auto latestWideSeqNumber = packet->GetLatestSequenceNumber(); @@ -408,12 +440,21 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" }; /* clang-format on */ - auto* packet2 = new FeedbackRtpTransportPacket(senderSsrc, mediaSsrc); + auto packet2 = std::make_unique(senderSsrc, mediaSsrc); packet2->SetFeedbackPacketCount(2); for (auto& input : inputs2) - packet2->AddPacket(input.sequenceNumber, input.timestamp, input.maxPacketSize); + { + if (std::addressof(input) == std::addressof(inputs2.front())) + { + packet2->SetBase(input.sequenceNumber + 1, input.timestamp); + } + else + { + packet2->AddPacket(input.sequenceNumber, input.timestamp, input.maxPacketSize); + } + } packet2->Finish(); validate(inputs2, packet2->GetPacketResults()); @@ -431,7 +472,8 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" SECTION("parse serialized buffer") { - auto* packet3 = FeedbackRtpTransportPacket::Parse(buffer, len); + std::unique_ptr packet3{ FeedbackRtpTransportPacket::Parse( + buffer, len) }; REQUIRE(packet3); REQUIRE(packet3->GetBaseSequenceNumber() == 1008); @@ -445,12 +487,7 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" REQUIRE(len == len2); REQUIRE(std::memcmp(buffer, buffer2, len) == 0); REQUIRE(packet3->GetSize() == len2); - - delete packet3; } - - delete packet2; - delete packet; } SECTION("parse FeedbackRtpTransportPacket, one bit vector chunk") @@ -469,7 +506,8 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" }; // clang-format on - auto* packet = FeedbackRtpTransportPacket::Parse(data, sizeof(data)); + std::unique_ptr packet{ FeedbackRtpTransportPacket::Parse( + data, sizeof(data)) }; REQUIRE(packet); REQUIRE(packet->GetSize() == sizeof(data)); @@ -490,8 +528,6 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" REQUIRE(len == sizeof(data)); REQUIRE(std::memcmp(data, buffer, len) == 0); } - - delete packet; } SECTION("parse FeedbackRtpTransportPacket with negative reference time") @@ -507,17 +543,18 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" }; // clang-format on - auto* packet = FeedbackRtpTransportPacket::Parse(data, sizeof(data)); + std::unique_ptr packet{ FeedbackRtpTransportPacket::Parse( + data, sizeof(data)) }; REQUIRE(packet); REQUIRE(packet->GetSize() == sizeof(data)); REQUIRE(packet->GetBaseSequenceNumber() == 39); REQUIRE(packet->GetPacketStatusCount() == 0); - REQUIRE(packet->GetReferenceTime() == 16777214); // 0xFFFFFE (unsigned 24 bits) + REQUIRE(packet->GetReferenceTime() == -2); // 0xFFFFFE = -2 (signed 24 bits) REQUIRE( packet->GetReferenceTimestamp() == FeedbackRtpTransportPacket::TimeWrapPeriod + - static_cast(16777214) * FeedbackRtpTransportPacket::BaseTimeTick); + static_cast(-2) * FeedbackRtpTransportPacket::BaseTimeTick); REQUIRE(packet->GetFeedbackPacketCount() == 1); SECTION("serialize packet") @@ -528,8 +565,6 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" REQUIRE(len == sizeof(data)); REQUIRE(std::memcmp(data, buffer, len) == 0); } - - delete packet; } SECTION("parse FeedbackRtpTransportPacket generated by Chrome") @@ -546,17 +581,18 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" }; // clang-format on - auto* packet = FeedbackRtpTransportPacket::Parse(data, sizeof(data)); + std::unique_ptr packet{ FeedbackRtpTransportPacket::Parse( + data, sizeof(data)) }; REQUIRE(packet); REQUIRE(packet->GetSize() == sizeof(data)); REQUIRE(packet->GetBaseSequenceNumber() == 1); REQUIRE(packet->GetPacketStatusCount() == 2); - REQUIRE(packet->GetReferenceTime() == 12408746); + REQUIRE(packet->GetReferenceTime() == -4368470); REQUIRE( packet->GetReferenceTimestamp() == FeedbackRtpTransportPacket::TimeWrapPeriod + - static_cast(12408746) * FeedbackRtpTransportPacket::BaseTimeTick); + static_cast(-4368470) * FeedbackRtpTransportPacket::BaseTimeTick); REQUIRE(packet->GetFeedbackPacketCount() == 0); @@ -568,16 +604,14 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" REQUIRE(len == sizeof(data)); REQUIRE(std::memcmp(data, buffer, len) == 0); } - - delete packet; } SECTION("parse FeedbackRtpTransportPacket generated by Chrome with libwebrtc as a reference") { using FeedbackPacketsMeta = struct { - uint32_t baseTimeRaw; - uint64_t baseTimeMs; + int32_t baseTimeRaw; + int64_t baseTimeMs; uint16_t baseSequence; size_t packetStatusCount; std::vector deltas; @@ -593,8 +627,8 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" .packetStatusCount = 1, .deltas = std::vector{ 57 }, .buffer = std::vector{ 0xaf, 0xcd, 0x00, 0x05, 0xfa, 0x17, 0xfa, 0x17, - 0x00, 0x00, 0x04, 0xd2, 0x00, 0x0d, 0x00, 0x01, - 0x00, 0x8A, 0xB0, 0x00, 0x20, 0x01, 0xE4, 0x01 } }, + 0x00, 0x00, 0x04, 0xd2, 0x00, 0x0d, 0x00, 0x01, + 0x00, 0x8A, 0xB0, 0x00, 0x20, 0x01, 0xE4, 0x01 } }, { .baseTimeRaw = 35504, .baseTimeMs = 1076014080, .baseSequence = 14, @@ -624,14 +658,14 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" 0x00, 0x00, 0x10, 0x00, 0x04, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x08, 0x00, 0x04, 0x00, 0x10 } }, - { .baseTimeRaw = 12408746, - .baseTimeMs = 1867901568, + { .baseTimeRaw = -4368470, + .baseTimeMs = 794159744, .baseSequence = 1, .packetStatusCount = 2, .deltas = std::vector{ 35, 17 }, .buffer = std::vector{ 0x8F, 0xCD, 0x00, 0x05, 0xFA, 0x17, 0xFA, 0x17, - 0x39, 0xE9, 0x42, 0x38, 0x00, 0x01, 0x00, 0x02, - 0xBD, 0x57, 0xAA, 0x00, 0x20, 0x02, 0x8C, 0x44 } }, + 0x39, 0xE9, 0x42, 0x38, 0x00, 0x01, 0x00, 0x02, + 0xBD, 0x57, 0xAA, 0x00, 0x20, 0x02, 0x8C, 0x44 } }, { .baseTimeRaw = 818995, .baseTimeMs = 1126157504, @@ -690,16 +724,16 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" .packetStatusCount = 2, .deltas = std::vector{ 44, 18 }, .buffer = std::vector{ 0x8F, 0xCD, 0x00, 0x05, 0xFA, 0x17, 0xFA, 0x17, - 0x08, 0xEB, 0x06, 0xD7, 0x00, 0x11, 0x00, 0x02, - 0x0C, 0x89, 0x14, 0x01, 0x20, 0x02, 0xB0, 0x48 } }, + 0x08, 0xEB, 0x06, 0xD7, 0x00, 0x11, 0x00, 0x02, + 0x0C, 0x89, 0x14, 0x01, 0x20, 0x02, 0xB0, 0x48 } }, { .baseTimeRaw = 821524, .baseTimeMs = 1126319360, .baseSequence = 17, .packetStatusCount = 1, .deltas = std::vector{ 62 }, .buffer = std::vector{ 0xAF, 0xCD, 0x00, 0x05, 0xFA, 0x17, 0xFA, 0x17, - 0x20, 0x92, 0x5E, 0xB7, 0x00, 0x11, 0x00, 0x01, - 0x0C, 0x89, 0x14, 0x00, 0x20, 0x01, 0xF8, 0x01 } }, + 0x20, 0x92, 0x5E, 0xB7, 0x00, 0x11, 0x00, 0x01, + 0x0C, 0x89, 0x14, 0x00, 0x20, 0x01, 0xF8, 0x01 } }, { .baseTimeRaw = 821526, .baseTimeMs = 1126319488, .baseSequence = 19, @@ -712,8 +746,10 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" for (const auto& packetMeta : feedbackPacketsMeta) { - auto buffer = packetMeta.buffer; - auto* feedback = FeedbackRtpTransportPacket::Parse(buffer.data(), buffer.size()); + auto buffer = packetMeta.buffer; + + std::unique_ptr feedback{ FeedbackRtpTransportPacket::Parse( + buffer.data(), buffer.size()) }; REQUIRE(feedback->GetReferenceTime() == packetMeta.baseTimeRaw); REQUIRE(feedback->GetReferenceTimestamp() == packetMeta.baseTimeMs); @@ -729,7 +765,6 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" REQUIRE(static_cast(resultDelta / 4) == delta); deltasIt++; } - delete feedback; } } @@ -737,9 +772,9 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" { auto MaxBaseTime = FeedbackRtpTransportPacket::TimeWrapPeriod - FeedbackRtpTransportPacket::BaseTimeTick; - auto* packet1 = new FeedbackRtpTransportPacket(senderSsrc, mediaSsrc); - auto* packet2 = new FeedbackRtpTransportPacket(senderSsrc, mediaSsrc); - auto* packet3 = new FeedbackRtpTransportPacket(senderSsrc, mediaSsrc); + auto packet1 = std::make_unique(senderSsrc, mediaSsrc); + auto packet2 = std::make_unique(senderSsrc, mediaSsrc); + auto packet3 = std::make_unique(senderSsrc, mediaSsrc); packet1->SetReferenceTime(MaxBaseTime); packet2->SetReferenceTime(MaxBaseTime + FeedbackRtpTransportPacket::BaseTimeTick); @@ -759,9 +794,5 @@ SCENARIO("RTCP Feeback RTP transport", "[parser][rtcp][feedback-rtp][transport]" REQUIRE(packet2->GetBaseDelta(packet1->GetReferenceTimestamp()) == 64); REQUIRE(packet3->GetBaseDelta(packet2->GetReferenceTimestamp()) == 64); REQUIRE(packet3->GetBaseDelta(packet1->GetReferenceTimestamp()) == 128); - - delete packet1; - delete packet2; - delete packet3; } } diff --git a/worker/test/src/RTC/RTCP/TestPacket.cpp b/worker/test/src/RTC/RTCP/TestPacket.cpp index 63ceddb861..48218dbb12 100644 --- a/worker/test/src/RTC/RTCP/TestPacket.cpp +++ b/worker/test/src/RTC/RTCP/TestPacket.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/Packet.hpp" -#include +#include using namespace RTC::RTCP; @@ -18,11 +18,9 @@ SCENARIO("RTCP parsing", "[parser][rtcp][packet]") SECTION("a RTCP packet may only contain the RTCP common header") { - Packet* packet = Packet::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ Packet::Parse(buffer, sizeof(buffer)) }; REQUIRE(packet); - - delete packet; } SECTION("a too small RTCP packet should fail") @@ -30,11 +28,9 @@ SCENARIO("RTCP parsing", "[parser][rtcp][packet]") // Provide a wrong packet length. size_t length = sizeof(buffer) - 1; - Packet* packet = Packet::Parse(buffer, length); + std::unique_ptr packet{ Packet::Parse(buffer, length) }; REQUIRE_FALSE(packet); - - delete packet; } SECTION("a RTCP packet with incorrect version should fail") @@ -42,11 +38,9 @@ SCENARIO("RTCP parsing", "[parser][rtcp][packet]") // Set an incorrect version value (0). buffer[0] &= 0b00111111; - Packet* packet = Packet::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ Packet::Parse(buffer, sizeof(buffer)) }; REQUIRE_FALSE(packet); - - delete packet; } SECTION("a RTCP packet with incorrect length should fail") @@ -54,11 +48,9 @@ SCENARIO("RTCP parsing", "[parser][rtcp][packet]") // Set the packet length to zero. buffer[3] = 1; - Packet* packet = Packet::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ Packet::Parse(buffer, sizeof(buffer)) }; REQUIRE_FALSE(packet); - - delete packet; } SECTION("a RTCP packet with unknown type should fail") @@ -66,10 +58,8 @@ SCENARIO("RTCP parsing", "[parser][rtcp][packet]") // Set and unknown packet type (0). buffer[1] = 0; - Packet* packet = Packet::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ Packet::Parse(buffer, sizeof(buffer)) }; REQUIRE_FALSE(packet); - - delete packet; } } diff --git a/worker/test/src/RTC/RTCP/TestReceiverReport.cpp b/worker/test/src/RTC/RTCP/TestReceiverReport.cpp index 879ab0c080..914d4c1de8 100644 --- a/worker/test/src/RTC/RTCP/TestReceiverReport.cpp +++ b/worker/test/src/RTC/RTCP/TestReceiverReport.cpp @@ -1,7 +1,7 @@ #include "common.hpp" #include "RTC/RTCP/ReceiverReport.hpp" #include "RTC/RTCP/SenderReport.hpp" // sizeof(SenderReport::Header) -#include +#include using namespace RTC::RTCP; @@ -53,7 +53,7 @@ SCENARIO("RTCP RR parsing", "[parser][rtcp][rr]") { SECTION("parse RR packet with a single report") { - ReceiverReportPacket* packet = ReceiverReportPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ ReceiverReportPacket::Parse(buffer, sizeof(buffer)) }; REQUIRE(packet->GetCount() == 1); @@ -67,7 +67,8 @@ SCENARIO("RTCP RR parsing", "[parser][rtcp][rr]") packet->Serialize(serialized); - ReceiverReportPacket* packet2 = ReceiverReportPacket::Parse(serialized, sizeof(buffer)); + std::unique_ptr packet2{ ReceiverReportPacket::Parse( + serialized, sizeof(buffer)) }; REQUIRE(packet2->GetType() == Type::RR); REQUIRE(packet2->GetCount() == 1); @@ -81,26 +82,21 @@ SCENARIO("RTCP RR parsing", "[parser][rtcp][rr]") verify(report); - delete packet2; - SECTION("compare serialized packet with original buffer") { REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } SECTION("parse RR") { - ReceiverReport* report = ReceiverReport::Parse(rrBuffer, ReceiverReport::HeaderSize); + std::unique_ptr report{ ReceiverReport::Parse( + rrBuffer, ReceiverReport::HeaderSize) }; REQUIRE(report); - verify(report); - - delete report; + verify(report.get()); } SECTION("create RR packet with more than 31 reports") @@ -132,7 +128,8 @@ SCENARIO("RTCP RR parsing", "[parser][rtcp][rr]") // Serialization must contain 2 RR packets since report count exceeds 31. packet.Serialize(buffer); - auto* packet2 = static_cast(Packet::Parse(buffer, sizeof(buffer))); + std::unique_ptr packet2{ static_cast( + Packet::Parse(buffer, sizeof(buffer))) }; REQUIRE(packet2 != nullptr); REQUIRE(packet2->GetCount() == 31); @@ -172,7 +169,6 @@ SCENARIO("RTCP RR parsing", "[parser][rtcp][rr]") REQUIRE(report->GetDelaySinceLastSenderReport() == 31 + i); } - delete packet2; delete packet3; } diff --git a/worker/test/src/RTC/RTCP/TestSdes.cpp b/worker/test/src/RTC/RTCP/TestSdes.cpp index 46bf9ae77c..b03df3c855 100644 --- a/worker/test/src/RTC/RTCP/TestSdes.cpp +++ b/worker/test/src/RTC/RTCP/TestSdes.cpp @@ -1,6 +1,7 @@ #include "common.hpp" +#include "RTC/RTCP/Packet.hpp" #include "RTC/RTCP/Sdes.hpp" -#include +#include #include // std::memcmp() #include @@ -11,66 +12,433 @@ namespace TestSdes // RTCP Sdes Packet. // clang-format off - uint8_t buffer[] = + uint8_t buffer1[] = { 0x81, 0xca, 0x00, 0x06, // Type: 202 (SDES), Count: 1, Length: 6 0x9f, 0x65, 0xe7, 0x42, // SSRC: 0x9f65e742 + // Chunk 1 0x01, 0x10, 0x74, 0x37, // Item Type: 1 (CNAME), Length: 16, Value: t7mkYnCm46OcINy/ 0x6d, 0x6b, 0x59, 0x6e, 0x43, 0x6d, 0x34, 0x36, 0x4f, 0x63, 0x49, 0x4e, - 0x79, 0x2f, 0x00, 0x00 + 0x79, 0x2f, 0x00, 0x00 // 2 null octets }; // clang-format on - uint8_t* chunkBuffer = buffer + Packet::CommonHeaderSize; + // First chunk (chunk 1). + uint32_t ssrc1{ 0x9f65e742 }; + // First item (item 1). + SdesItem::Type item1Type{ SdesItem::Type::CNAME }; + std::string item1Value{ "t7mkYnCm46OcINy/" }; + size_t item1Length{ 16u }; - // SDES values. - uint32_t ssrc{ 0x9f65e742 }; - SdesItem::Type type{ SdesItem::Type::CNAME }; - std::string value{ "t7mkYnCm46OcINy/" }; - size_t length = 16; - - void verify(SdesChunk* chunk) + // clang-format off + uint8_t buffer2[] = { - REQUIRE(chunk->GetSsrc() == ssrc); + 0xa2, 0xca, 0x00, 0x0d, // Padding, Type: 202 (SDES), Count: 2, Length: 13 + // Chunk 2 + 0x00, 0x00, 0x04, 0xd2, // SSRC: 1234 + 0x01, 0x06, 0x71, 0x77, // Item Type: 1 (CNAME), Length: 6, Text: "qwerty" + 0x65, 0x72, 0x74, 0x79, + 0x06, 0x06, 0x69, 0xc3, // Item Type: 6 (TOOL), Length: 6, Text: "iÃąaki" + 0xb1, 0x61, 0x6b, 0x69, + 0x00, 0x00, 0x00, 0x00, // 4 null octets + // Chunk 3 + 0x00, 0x00, 0x16, 0x2e, // SSRC: 5678 + 0x05, 0x11, 0x73, 0x6f, // Item Type: 5 (LOC), Length: 17, Text: "somewhere œÃĻâ‚Ŧ" + 0x6d, 0x65, 0x77, 0x68, + 0x65, 0x72, 0x65, 0x20, + 0xc5, 0x93, 0xc3, 0xa6, + 0xe2, 0x82, 0xac, 0x00, // 1 null octet + 0x00, 0x00, 0x00, 0x00 // Pading (4 bytes) + }; + // clang-format on - SdesItem* item = *(chunk->Begin()); + // First chunk (chunk 2). + uint32_t ssrc2{ 1234 }; + // First item (item 2). + SdesItem::Type item2Type{ SdesItem::Type::CNAME }; + std::string item2Value{ "qwerty" }; + size_t item2Length{ 6u }; + // First item (item 3). + SdesItem::Type item3Type{ SdesItem::Type::TOOL }; + std::string item3Value{ "iÃąaki" }; + size_t item3Length{ 6u }; + + // Second chunk (chunk 3). + uint32_t ssrc3{ 5678 }; + // First item (item 4). + SdesItem::Type item4Type{ SdesItem::Type::LOC }; + std::string item4Value{ "somewhere œÃĻâ‚Ŧ" }; + size_t item4Length{ 17u }; - REQUIRE(item->GetType() == type); - REQUIRE(item->GetLength() == length); - REQUIRE(std::string(item->GetValue(), length) == value); - } + // clang-format off + uint8_t buffer3[] = + { + 0x81, 0xca, 0x00, 0x03, // Type: 202 (SDES), Count: 1, Length: 3 + // Chunk + 0x11, 0x22, 0x33, 0x44, // SSRC: 0x11223344 + 0x05, 0x02, 0x61, 0x62, // Item Type: 5 (LOC), Length: 2, Text: "ab" + 0x00, 0x00, 0x00, 0x00 // 4 null octets + }; + // clang-format on + + // First chunk (chunk 4). + uint32_t ssrc4{ 0x11223344 }; + // First item (item 5). + SdesItem::Type item5Type{ SdesItem::Type::LOC }; + std::string item5Value{ "ab" }; + size_t item5Length{ 2u }; } // namespace TestSdes using namespace TestSdes; SCENARIO("RTCP SDES parsing", "[parser][rtcp][sdes]") { - SECTION("parse packet") + SECTION("parse packet 1") { - SdesPacket* packet = SdesPacket::Parse(buffer, sizeof(buffer)); - SdesChunk* chunk = *(packet->Begin()); - - auto* header = reinterpret_cast(buffer); + std::unique_ptr packet{ SdesPacket::Parse(buffer1, sizeof(buffer1)) }; + auto* header = reinterpret_cast(buffer1); + REQUIRE(packet); REQUIRE(ntohs(header->length) == 6); - REQUIRE(chunk); + REQUIRE(packet->GetSize() == 28); + REQUIRE(packet->GetCount() == 1); + + size_t chunkIdx{ 0u }; - verify(chunk); + for (auto it = packet->Begin(); it != packet->End(); ++it, ++chunkIdx) + { + auto* chunk = *it; + + switch (chunkIdx) + { + /* First chunk (chunk 1). */ + case 0: + { + // Chunk size must be 24 bytes (including 4 null octets). + REQUIRE(chunk->GetSize() == 24); + REQUIRE(chunk->GetSsrc() == ssrc1); + + size_t itemIdx{ 0u }; + + for (auto it2 = chunk->Begin(); it2 != chunk->End(); ++it2, ++itemIdx) + { + auto* item = *it2; + + switch (itemIdx) + { + /* First item (item 1). */ + case 0: + { + REQUIRE(item->GetType() == item1Type); + REQUIRE(item->GetLength() == item1Length); + REQUIRE(std::string(item->GetValue(), item1Length) == item1Value); + + break; + } + } + } + + // There is 1 item. + REQUIRE(itemIdx == 1); + + break; + } + } + } + + // There is 1 chunk. + REQUIRE(chunkIdx == 1); SECTION("serialize SdesChunk instance") { - uint8_t serialized[sizeof(buffer) - Packet::CommonHeaderSize] = { 0 }; + auto it = packet->Begin(); + auto* chunk1 = *it; + uint8_t* chunk1Buffer = buffer1 + Packet::CommonHeaderSize; + + // NOTE: Length of first chunk (including null octets) is 24. + uint8_t serialized1[24] = { 0 }; + + chunk1->Serialize(serialized1); + + REQUIRE(std::memcmp(chunk1Buffer, serialized1, 24) == 0); + } + } + + SECTION("parse packet 2") + { + std::unique_ptr packet{ SdesPacket::Parse(buffer2, sizeof(buffer2)) }; + auto* header = reinterpret_cast(buffer2); + + REQUIRE(packet); + REQUIRE(ntohs(header->length) == 13); + // Despite total buffer size is 56 bytes, our GetSize() method doesn't not + // consider RTCP padding (4 bytes in this case). + REQUIRE(packet->GetSize() == 52); + REQUIRE(packet->GetCount() == 2); + + size_t chunkIdx{ 0u }; + + for (auto it = packet->Begin(); it != packet->End(); ++it, ++chunkIdx) + { + auto* chunk = *it; + + switch (chunkIdx) + { + /* First chunk (chunk 2). */ + case 0: + { + // Chunk size must be 24 bytes (including 4 null octets). + REQUIRE(chunk->GetSize() == 24); + REQUIRE(chunk->GetSsrc() == ssrc2); + + size_t itemIdx{ 0u }; + + for (auto it2 = chunk->Begin(); it2 != chunk->End(); ++it2, ++itemIdx) + { + auto* item = *it2; + + switch (itemIdx) + { + /* First item (item 2). */ + case 0: + { + REQUIRE(item->GetType() == item2Type); + REQUIRE(item->GetLength() == item2Length); + REQUIRE(std::string(item->GetValue(), item2Length) == item2Value); + + break; + } + + /* Second item (item 3). */ + case 1: + { + REQUIRE(item->GetType() == item3Type); + REQUIRE(item->GetLength() == item3Length); + REQUIRE(std::string(item->GetValue(), item3Length) == item3Value); + + break; + } + } + } + + // There are 2 items. + REQUIRE(itemIdx == 2); + + break; + } + + /* Second chunk (chunk 3). */ + case 1: + { + // Chunk size must be 24 bytes (including 1 null octet). + REQUIRE(chunk->GetSize() == 24); + REQUIRE(chunk->GetSsrc() == ssrc3); + + size_t itemIdx{ 0u }; + + for (auto it2 = chunk->Begin(); it2 != chunk->End(); ++it2, ++itemIdx) + { + auto* item = *it2; + + switch (itemIdx) + { + /* First item (item 4). */ + case 0: + { + REQUIRE(item->GetType() == item4Type); + REQUIRE(item->GetLength() == item4Length); + REQUIRE(std::string(item->GetValue(), item4Length) == item4Value); + + break; + } + } + } + + // There is 1 item. + REQUIRE(itemIdx == 1); + + break; + } + } + } + + // There are 2 chunks. + REQUIRE(chunkIdx == 2); + + SECTION("serialize SdesChunk instances") + { + auto it = packet->Begin(); + auto* chunk1 = *it; + uint8_t* chunk1Buffer = buffer2 + Packet::CommonHeaderSize; + + // NOTE: Length of first chunk (including null octets) is 24. + uint8_t serialized1[24] = { 0 }; + + chunk1->Serialize(serialized1); + + REQUIRE(std::memcmp(chunk1Buffer, serialized1, 24) == 0); + + auto* chunk2 = *(++it); + uint8_t* chunk2Buffer = buffer2 + Packet::CommonHeaderSize + 24; - chunk->Serialize(serialized); + // NOTE: Length of second chunk (including null octets) is 24. + uint8_t serialized2[24] = { 0 }; - SECTION("compare serialized SdesChunk with original buffer") + chunk2->Serialize(serialized2); + + REQUIRE(std::memcmp(chunk2Buffer, serialized2, 24) == 0); + } + } + + SECTION("parse packet 3") + { + std::unique_ptr packet{ SdesPacket::Parse(buffer3, sizeof(buffer3)) }; + auto* header = reinterpret_cast(buffer3); + + REQUIRE(packet); + REQUIRE(ntohs(header->length) == 3); + REQUIRE(packet->GetSize() == 16); + REQUIRE(packet->GetCount() == 1); + + size_t chunkIdx{ 0u }; + + for (auto it = packet->Begin(); it != packet->End(); ++it, ++chunkIdx) + { + auto* chunk = *it; + + switch (chunkIdx) { - REQUIRE(std::memcmp(chunkBuffer, serialized, sizeof(buffer) - Packet::CommonHeaderSize) == 0); + /* First chunk (chunk 4). */ + case 0: + { + REQUIRE(chunk->GetSize() == 12); + REQUIRE(chunk->GetSsrc() == ssrc4); + + size_t itemIdx{ 0u }; + + for (auto it2 = chunk->Begin(); it2 != chunk->End(); ++it2, ++itemIdx) + { + auto* item = *it2; + + switch (itemIdx) + { + /* First item (item 5). */ + case 0: + { + REQUIRE(item->GetType() == item5Type); + REQUIRE(item->GetLength() == item5Length); + REQUIRE(std::string(item->GetValue(), item5Length) == item5Value); + + break; + } + } + } + + // There is 1 item. + REQUIRE(itemIdx == 1); + + break; + } } } - delete packet; + + // There is 1 chunk. + REQUIRE(chunkIdx == 1); + + SECTION("serialize SdesChunk instance") + { + auto it = packet->Begin(); + auto* chunk1 = *it; + uint8_t* chunk1Buffer = buffer3 + Packet::CommonHeaderSize; + + // NOTE: Length of first chunk (including null octets) is 12. + uint8_t serialized1[12] = { 0 }; + + chunk1->Serialize(serialized1); + + REQUIRE(std::memcmp(chunk1Buffer, serialized1, 12) == 0); + } + } + + SECTION("parsing a packet with missing null octects fails") + { + // clang-format off + uint8_t buffer[] = + { + 0x81, 0xca, 0x00, 0x02, // Type: 202 (SDES), Count: 1, Length: 2 + // Chunk + 0x11, 0x22, 0x33, 0x44, // SSRC: 0x11223344 + 0x08, 0x02, 0x61, 0x62 // Item Type: 8 (PRIV), Length: 2, Text: "ab" + }; + + SdesPacket* packet = SdesPacket::Parse(buffer, sizeof(buffer)); + + REQUIRE(!packet); + } + + SECTION("create SDES packet with 31 chunks") + { + const size_t count = 31; + + SdesPacket packet; + // Create a chunk and an item to obtain their size. + auto chunk = std::make_unique(1234 /*ssrc*/); + auto* item1 = + new RTC::RTCP::SdesItem(SdesItem::Type::CNAME, item1Value.size(), item1Value.c_str()); + + chunk->AddItem(item1); + + auto chunkSize = chunk->GetSize(); + + for (auto i{ 1 }; i <= count; ++i) + { + // Create chunk and add to packet. + SdesChunk* chunk = new SdesChunk(i /*ssrc*/); + + auto* item1 = + new RTC::RTCP::SdesItem(SdesItem::Type::CNAME, item1Value.size(), item1Value.c_str()); + + chunk->AddItem(item1); + + packet.AddChunk(chunk); + } + + REQUIRE(packet.GetCount() == count); + REQUIRE(packet.GetSize() == Packet::CommonHeaderSize + (count * chunkSize)); + + uint8_t buffer1[1500] = { 0 }; + + // Serialization must contain 1 SDES packet since report count doesn't + // exceed 31. + packet.Serialize(buffer1); + + std::unique_ptr packet2{static_cast(Packet::Parse(buffer1, sizeof(buffer1)))}; + + REQUIRE(packet2 != nullptr); + REQUIRE(packet2->GetCount() == count); + REQUIRE(packet2->GetSize() == Packet::CommonHeaderSize + (count * chunkSize)); + + auto reportIt = packet2->Begin(); + + for (auto i{ 1 }; i <= 31; ++i, reportIt++) + { + auto* chunk = *reportIt; + + REQUIRE(chunk->GetSsrc() == i); + + auto* item = *(chunk->Begin()); + + REQUIRE(item->GetType() == SdesItem::Type::CNAME); + REQUIRE(item->GetSize() == 2 + item1Value.size()); + REQUIRE(std::string(item->GetValue()) == item1Value); + } + + std::unique_ptr packet3{static_cast(packet2->GetNext())}; + + REQUIRE(packet3 == nullptr); + } SECTION("create SDES packet with more than 31 chunks") @@ -78,34 +446,45 @@ SCENARIO("RTCP SDES parsing", "[parser][rtcp][sdes]") const size_t count = 33; SdesPacket packet; + // Create a chunk and an item to obtain their size. + auto chunk = std::make_unique(1234 /*ssrc*/); + auto* item1 = + new RTC::RTCP::SdesItem(SdesItem::Type::CNAME, item1Value.size(), item1Value.c_str()); + + chunk->AddItem(item1); - for (auto i = 1; i <= count; i++) + auto chunkSize = chunk->GetSize(); + + for (auto i{ 1 }; i <= count; ++i) { // Create chunk and add to packet. SdesChunk* chunk = new SdesChunk(i /*ssrc*/); - auto* item = new RTC::RTCP::SdesItem(SdesItem::Type::CNAME, value.size(), value.c_str()); + auto* item1 = + new RTC::RTCP::SdesItem(SdesItem::Type::CNAME, item1Value.size(), item1Value.c_str()); - chunk->AddItem(item); + chunk->AddItem(item1); packet.AddChunk(chunk); } REQUIRE(packet.GetCount() == count); + REQUIRE(packet.GetSize() == Packet::CommonHeaderSize + (31 * chunkSize) + Packet::CommonHeaderSize + ((count - 31) * chunkSize)); - uint8_t buffer[1500] = { 0 }; + uint8_t buffer1[1500] = { 0 }; - // Serialization must contain 2 RR packets since report count exceeds 31. - packet.Serialize(buffer); + // Serialization must contain 2 SDES packets since report count exceeds 31. + packet.Serialize(buffer1); - auto* packet2 = static_cast(Packet::Parse(buffer, sizeof(buffer))); + std::unique_ptr packet2 {static_cast(Packet::Parse(buffer1, sizeof(buffer1)))}; REQUIRE(packet2 != nullptr); REQUIRE(packet2->GetCount() == 31); + REQUIRE(packet2->GetSize() == Packet::CommonHeaderSize + (31 * chunkSize)); auto reportIt = packet2->Begin(); - for (auto i = 1; i <= 31; i++, reportIt++) + for (auto i{ 1 }; i <= 31; ++i, reportIt++) { auto* chunk = *reportIt; @@ -114,18 +493,19 @@ SCENARIO("RTCP SDES parsing", "[parser][rtcp][sdes]") auto* item = *(chunk->Begin()); REQUIRE(item->GetType() == SdesItem::Type::CNAME); - REQUIRE(item->GetSize() == 2 + value.size()); - REQUIRE(std::string(item->GetValue()) == value); + REQUIRE(item->GetSize() == 2 + item1Value.size()); + REQUIRE(std::string(item->GetValue()) == item1Value); } SdesPacket* packet3 = static_cast(packet2->GetNext()); REQUIRE(packet3 != nullptr); - REQUIRE(packet3->GetCount() == 2); + REQUIRE(packet3->GetCount() == count - 31); + REQUIRE(packet3->GetSize() == Packet::CommonHeaderSize + ((count - 31) * chunkSize)); reportIt = packet3->Begin(); - for (auto i = 1; i <= 2; i++, reportIt++) + for (auto i{ 1 }; i <= 2; ++i, reportIt++) { auto* chunk = *reportIt; @@ -134,23 +514,28 @@ SCENARIO("RTCP SDES parsing", "[parser][rtcp][sdes]") auto* item = *(chunk->Begin()); REQUIRE(item->GetType() == SdesItem::Type::CNAME); - REQUIRE(item->GetSize() == 2 + value.size()); - REQUIRE(std::string(item->GetValue()) == value); + REQUIRE(item->GetSize() == 2 + item1Value.size()); + REQUIRE(std::string(item->GetValue()) == item1Value); } - delete packet2; delete packet3; } SECTION("create SdesChunk") { - auto* item = new SdesItem(type, length, value.c_str()); + auto* item = new SdesItem(item1Type, item1Length, item1Value.c_str()); // Create sdes chunk. - SdesChunk chunk(ssrc); + SdesChunk chunk(ssrc1); chunk.AddItem(item); - verify(&chunk); + REQUIRE(chunk.GetSsrc() == ssrc1); + + SdesItem* item1 = *(chunk.Begin()); + + REQUIRE(item1->GetType() == item1Type); + REQUIRE(item1->GetLength() == item1Length); + REQUIRE(std::string(item1->GetValue(), item1Length) == item1Value); } } diff --git a/worker/test/src/RTC/RTCP/TestSenderReport.cpp b/worker/test/src/RTC/RTCP/TestSenderReport.cpp index 48862861a0..e874ba92a1 100644 --- a/worker/test/src/RTC/RTCP/TestSenderReport.cpp +++ b/worker/test/src/RTC/RTCP/TestSenderReport.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/RTCP/SenderReport.hpp" -#include +#include #include // std::memcmp() using namespace RTC::RTCP; @@ -50,7 +50,7 @@ SCENARIO("RTCP SR parsing", "[parser][rtcp][sr]") { SECTION("parse SR packet") { - SenderReportPacket* packet = SenderReportPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ SenderReportPacket::Parse(buffer, sizeof(buffer)) }; auto* report = *(packet->Begin()); @@ -67,17 +67,15 @@ SCENARIO("RTCP SR parsing", "[parser][rtcp][sr]") REQUIRE(std::memcmp(buffer, serialized, sizeof(buffer)) == 0); } } - - delete packet; } SECTION("parse SR") { - SenderReport* report = SenderReport::Parse(srBuffer, SenderReport::HeaderSize); + std::unique_ptr report{ SenderReport::Parse(srBuffer, SenderReport::HeaderSize) }; REQUIRE(report); - verify(report); + verify(report.get()); SECTION("serialize SenderReport instance") { @@ -90,8 +88,6 @@ SCENARIO("RTCP SR parsing", "[parser][rtcp][sr]") REQUIRE(std::memcmp(srBuffer, serialized, SenderReport::HeaderSize) == 0); } } - - delete report; } SECTION("create SR packet multiple reports") @@ -122,7 +118,8 @@ SCENARIO("RTCP SR parsing", "[parser][rtcp][sr]") SenderReport* reports[count]{ nullptr }; - auto* packet2 = static_cast(Packet::Parse(buffer, sizeof(buffer))); + std::unique_ptr packet2{ static_cast( + Packet::Parse(buffer, sizeof(buffer))) }; REQUIRE(packet2 != nullptr); @@ -154,7 +151,6 @@ SCENARIO("RTCP SR parsing", "[parser][rtcp][sr]") REQUIRE(report->GetOctetCount() == i); } - delete packet2; delete packet3; delete packet4; } diff --git a/worker/test/src/RTC/RTCP/TestXr.cpp b/worker/test/src/RTC/RTCP/TestXr.cpp index 09db0d0862..b1569bddc8 100644 --- a/worker/test/src/RTC/RTCP/TestXr.cpp +++ b/worker/test/src/RTC/RTCP/TestXr.cpp @@ -1,16 +1,137 @@ #include "common.hpp" +#include "RTC/RTCP/XR.hpp" #include "RTC/RTCP/XrDelaySinceLastRr.hpp" #include "RTC/RTCP/XrReceiverReferenceTime.hpp" -#include -#include // std::memcmp +#include +#include // std::memcmp(), std::memcpy() using namespace RTC::RTCP; +SCENARIO("RTCP XR parsing", "[parser][rtcp][xr]") +{ + // clang-format off + uint8_t buffer[] = + { + 0xa0, 0xcf, 0x00, 0x09, // Padding, Type: 207 (XR), Length: 9 + 0x5d, 0x93, 0x15, 0x34, // Sender SSRC: 0x5d931534 + // Extended Report DLRR + 0x05, 0x00, 0x00, 0x06, // BT: 5 (DLRR), Block Length: 6 + 0x11, 0x12, 0x13, 0x14, // SSRC 1 + 0x00, 0x11, 0x00, 0x11, // LRR 1 + 0x11, 0x00, 0x11, 0x00, // DLRR 1 + 0x21, 0x22, 0x23, 0x24, // SSRC 2 + 0x00, 0x22, 0x00, 0x22, // LRR 2 + 0x22, 0x00, 0x22, 0x00, // DLRR 2 + 0x00, 0x00, 0x00, 0x04 // Padding (4 bytes) + }; + // clang-format on + + SECTION("parse XR packet") + { + std::unique_ptr packet(ExtendedReportPacket::Parse(buffer, sizeof(buffer))); + + REQUIRE(packet); + // Despite total buffer size is 40 bytes, our GetSize() method doesn't not + // consider RTCP padding (4 bytes in this case). + // https://github.com/versatica/mediasoup/issues/1233 + REQUIRE(packet->GetSize() == 36); + REQUIRE(packet->GetCount() == 0); + REQUIRE(packet->GetSsrc() == 0x5d931534); + + size_t blockIdx{ 0u }; + + for (auto it = packet->Begin(); it != packet->End(); ++it, ++blockIdx) + { + auto* block = *it; + + switch (blockIdx) + { + case 0: + { + REQUIRE(block->GetSize() == 28); + + size_t ssrcInfoIdx{ 0u }; + auto* dlrrBlock = reinterpret_cast(block); + + for (auto it2 = dlrrBlock->Begin(); it2 != dlrrBlock->End(); ++it2, ++ssrcInfoIdx) + { + auto* ssrcInfo = *it2; + + switch (ssrcInfoIdx) + { + case 0: + { + REQUIRE(ssrcInfo->GetSsrc() == 0x11121314); + REQUIRE(ssrcInfo->GetLastReceiverReport() == 0x00110011); + REQUIRE(ssrcInfo->GetDelaySinceLastReceiverReport() == 0x11001100); + + break; + } + + case 1: + { + REQUIRE(ssrcInfo->GetSsrc() == 0x21222324); + REQUIRE(ssrcInfo->GetLastReceiverReport() == 0x00220022); + REQUIRE(ssrcInfo->GetDelaySinceLastReceiverReport() == 0x22002200); + + break; + } + } + } + + // There are 2 SSRC infos. + REQUIRE(ssrcInfoIdx == 2); + + break; + } + } + } + + // There are 1 block (the DLRR block). + REQUIRE(blockIdx == 1); + + SECTION("serialize packet instance") + { + // NOTE: Padding in RTCP is removed (if not needed) when serializing the + // packet, so we must mangle the buffer content (padding bit) and the + // buffer length before comparing the serialized packet with and original + // buffer. + + const size_t paddingBytes{ 4 }; + const size_t serializedBufferLength = sizeof(buffer) - paddingBytes; + uint8_t serialized[serializedBufferLength] = { 0 }; + + // Clone the original buffer into a new buffer without padding. + uint8_t clonedBuffer[serializedBufferLength] = { 0 }; + std::memcpy(clonedBuffer, buffer, serializedBufferLength); + + // Remove the padding bit in the first byte of the cloned buffer. + clonedBuffer[0] = 0x80; + + // Change RTCP length field in the cloned buffer. + clonedBuffer[3] = clonedBuffer[3] - 1; + + packet->Serialize(serialized); + + std::unique_ptr packet2( + ExtendedReportPacket::Parse(serialized, serializedBufferLength)); + + REQUIRE(packet2->GetType() == Type::XR); + REQUIRE(packet2->GetCount() == 0); + REQUIRE(packet2->GetSize() == 36); + + REQUIRE(std::memcmp(clonedBuffer, serialized, serializedBufferLength) == 0); + } + } +} + SCENARIO("RTCP XrDelaySinceLastRt parsing", "[parser][rtcp][xr-dlrr]") { SECTION("create RRT") { // Create local report and check content. + // NOTE: We cannot use unique_ptr here since the instance lifecycle will be + // managed by the packet. auto* report1 = new ReceiverReferenceTime(); report1->SetNtpSec(11111111); @@ -26,6 +147,8 @@ SCENARIO("RTCP XrDelaySinceLastRt parsing", "[parser][rtcp][xr-dlrr]") report1->Serialize(bufferReport1); // Create a new report out of the external buffer. + // NOTE: We cannot use unique_ptr here since the instance lifecycle will be + // managed by the packet. auto report2 = ReceiverReferenceTime::Parse(bufferReport1, report1->GetSize()); REQUIRE(report1->GetType() == report2->GetType()); @@ -57,7 +180,8 @@ SCENARIO("RTCP XrDelaySinceLastRt parsing", "[parser][rtcp][xr-dlrr]") packet1->Serialize(bufferPacket1); // Create a new packet out of the external buffer. - auto packet2 = ExtendedReportPacket::Parse(bufferPacket1, packet1->GetSize()); + std::unique_ptr packet2( + ExtendedReportPacket::Parse(bufferPacket1, packet1->GetSize())); REQUIRE(packet2->GetType() == packet1->GetType()); REQUIRE(packet2->GetCount() == packet1->GetCount()); @@ -72,7 +196,11 @@ SCENARIO("RTCP XrDelaySinceLastRt parsing", "[parser][rtcp][xr-dlrr]") SECTION("create DLRR") { // Create local report and check content. - auto* report1 = new DelaySinceLastRr(); + // NOTE: We cannot use unique_ptr here since the instance lifecycle will be + // managed by the packet. + auto* report1 = new DelaySinceLastRr(); + // NOTE: We cannot use unique_ptr here since the instance lifecycle will be + // managed by the report. auto* ssrcInfo1 = new DelaySinceLastRr::SsrcInfo(); ssrcInfo1->SetSsrc(1234); @@ -92,6 +220,8 @@ SCENARIO("RTCP XrDelaySinceLastRt parsing", "[parser][rtcp][xr-dlrr]") report1->Serialize(bufferReport1); // Create a new report out of the external buffer. + // NOTE: We cannot use unique_ptr here since the instance lifecycle will be + // managed by the packet. auto report2 = DelaySinceLastRr::Parse(bufferReport1, report1->GetSize()); REQUIRE(report1->GetType() == report2->GetType()); @@ -130,7 +260,8 @@ SCENARIO("RTCP XrDelaySinceLastRt parsing", "[parser][rtcp][xr-dlrr]") packet1->Serialize(bufferPacket1); // Create a new packet out of the external buffer. - auto packet2 = ExtendedReportPacket::Parse(bufferPacket1, packet1->GetSize()); + std::unique_ptr packet2( + ExtendedReportPacket::Parse(bufferPacket1, packet1->GetSize())); REQUIRE(packet2->GetType() == packet1->GetType()); REQUIRE(packet2->GetCount() == packet1->GetCount()); diff --git a/worker/test/src/RTC/TestKeyFrameRequestManager.cpp b/worker/test/src/RTC/TestKeyFrameRequestManager.cpp index 0fdc8449a3..69d459037a 100644 --- a/worker/test/src/RTC/TestKeyFrameRequestManager.cpp +++ b/worker/test/src/RTC/TestKeyFrameRequestManager.cpp @@ -1,7 +1,7 @@ #include "common.hpp" #include "DepLibUV.hpp" #include "RTC/KeyFrameRequestManager.hpp" -#include +#include using namespace RTC; diff --git a/worker/test/src/RTC/TestNackGenerator.cpp b/worker/test/src/RTC/TestNackGenerator.cpp index e1c6b090eb..caaefdbe4a 100644 --- a/worker/test/src/RTC/TestNackGenerator.cpp +++ b/worker/test/src/RTC/TestNackGenerator.cpp @@ -3,7 +3,7 @@ #include "RTC/Codecs/PayloadDescriptorHandler.hpp" #include "RTC/NackGenerator.hpp" #include "RTC/RtpPacket.hpp" -#include +#include #include using namespace RTC; @@ -118,7 +118,7 @@ uint8_t rtpBuffer[] = // clang-format on // [pt:123, seq:21006, timestamp:1533790901] -RtpPacket* packet = RtpPacket::Parse(rtpBuffer, sizeof(rtpBuffer)); +std::unique_ptr packet(RtpPacket::Parse(rtpBuffer, sizeof(rtpBuffer))); void validate(std::vector& inputs) { @@ -133,7 +133,7 @@ void validate(std::vector& inputs) packet->SetPayloadDescriptorHandler(tpdh); packet->SetSequenceNumber(input.seq); - nackGenerator.ReceivePacket(packet, /*isRecovered*/ false); + nackGenerator.ReceivePacket(packet.get(), /*isRecovered*/ false); listener.Check(nackGenerator); } diff --git a/worker/test/src/RTC/TestRateCalculator.cpp b/worker/test/src/RTC/TestRateCalculator.cpp index ae566fe53e..6a27abaf06 100644 --- a/worker/test/src/RTC/TestRateCalculator.cpp +++ b/worker/test/src/RTC/TestRateCalculator.cpp @@ -1,7 +1,7 @@ #include "common.hpp" #include "DepLibUV.hpp" #include "RTC/RateCalculator.hpp" -#include +#include #include using namespace RTC; diff --git a/worker/test/src/RTC/TestRtpEncodingParameters.cpp b/worker/test/src/RTC/TestRtpEncodingParameters.cpp index 98eb329a63..2de9089972 100644 --- a/worker/test/src/RTC/TestRtpEncodingParameters.cpp +++ b/worker/test/src/RTC/TestRtpEncodingParameters.cpp @@ -1,5 +1,5 @@ #include "common.hpp" -#include +#include #include static const std::regex ScalabilityModeRegex( diff --git a/worker/test/src/RTC/TestRtpPacket.cpp b/worker/test/src/RTC/TestRtpPacket.cpp index 327f9f3fac..0afd766d9e 100644 --- a/worker/test/src/RTC/TestRtpPacket.cpp +++ b/worker/test/src/RTC/TestRtpPacket.cpp @@ -1,7 +1,7 @@ #include "common.hpp" #include "helpers.hpp" #include "RTC/RtpPacket.hpp" -#include +#include #include // std::memset() #include #include @@ -20,12 +20,16 @@ SCENARIO("parse RTP packets", "[parser][rtp]") std::string rid; if (!helpers::readBinaryFile("data/packet1.raw", buffer, &len)) + { FAIL("cannot open file"); + } - RtpPacket* packet = RtpPacket::Parse(buffer, len); + std::unique_ptr packet{ RtpPacket::Parse(buffer, len) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->HasHeaderExtension() == true); @@ -46,8 +50,6 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(extenValue == nullptr); REQUIRE(packet->ReadRid(rid) == false); REQUIRE(rid == ""); - - delete packet; } SECTION("parse packet2.raw") @@ -55,12 +57,16 @@ SCENARIO("parse RTP packets", "[parser][rtp]") size_t len; if (!helpers::readBinaryFile("data/packet2.raw", buffer, &len)) + { FAIL("cannot open file"); + } - RtpPacket* packet = RtpPacket::Parse(buffer, len); + std::unique_ptr packet{ RtpPacket::Parse(buffer, len) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->HasHeaderExtension() == false); @@ -72,8 +78,6 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->GetHeaderExtensionLength() == 0); REQUIRE(packet->HasOneByteExtensions() == false); REQUIRE(packet->HasTwoBytesExtensions() == false); - - delete packet; } SECTION("parse packet3.raw") @@ -86,12 +90,16 @@ SCENARIO("parse RTP packets", "[parser][rtp]") uint32_t absSendTime; if (!helpers::readBinaryFile("data/packet3.raw", buffer, &len)) + { FAIL("cannot open file"); + } - RtpPacket* packet = RtpPacket::Parse(buffer, len); + std::unique_ptr packet{ RtpPacket::Parse(buffer, len) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->HasHeaderExtension() == true); @@ -127,7 +135,7 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->ReadAbsSendTime(absSendTime) == true); REQUIRE(absSendTime == 0x65341e); - auto* clonedPacket = packet->Clone(); + std::unique_ptr clonedPacket{ packet->Clone() }; std::memset(buffer, '0', sizeof(buffer)); @@ -162,9 +170,6 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(extenValue[2] == 0x1e); REQUIRE(clonedPacket->ReadAbsSendTime(absSendTime) == true); REQUIRE(absSendTime == 0x65341e); - - delete packet; - delete clonedPacket; } SECTION("create RtpPacket without header extension") @@ -178,10 +183,12 @@ SCENARIO("parse RTP packets", "[parser][rtp]") }; // clang-format on - RtpPacket* packet = RtpPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ RtpPacket::Parse(buffer, sizeof(buffer)) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->HasHeaderExtension() == false); @@ -191,8 +198,6 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->HasOneByteExtensions() == false); REQUIRE(packet->HasTwoBytesExtensions() == false); REQUIRE(packet->GetSsrc() == 5); - - delete packet; } SECTION("create RtpPacket with One-Byte header extension") @@ -210,10 +215,12 @@ SCENARIO("parse RTP packets", "[parser][rtp]") }; // clang-format on - RtpPacket* packet = RtpPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ RtpPacket::Parse(buffer, sizeof(buffer)) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->HasHeaderExtension() == true); @@ -232,8 +239,6 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->GetPayloadLength() == 1000); REQUIRE(packet->GetSize() == 1028); - - delete packet; } SECTION("create RtpPacket with Two-Bytes header extension") @@ -255,10 +260,12 @@ SCENARIO("parse RTP packets", "[parser][rtp]") uint8_t extenLen; uint8_t* extenValue; - RtpPacket* packet = RtpPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ RtpPacket::Parse(buffer, sizeof(buffer)) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->HasHeaderExtension() == true); @@ -298,8 +305,6 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->HasExtension(5) == false); REQUIRE(extenValue == nullptr); REQUIRE(extenLen == 0); - - delete packet; } SECTION("rtx encryption-decryption") @@ -322,10 +327,12 @@ SCENARIO("parse RTP packets", "[parser][rtp]") uint32_t rtxSsrc{ 6 }; uint16_t rtxSeq{ 80 }; - RtpPacket* packet = RtpPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ RtpPacket::Parse(buffer, sizeof(buffer)) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->HasHeaderExtension() == true); @@ -338,9 +345,7 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->HasOneByteExtensions() == false); REQUIRE(packet->HasTwoBytesExtensions()); - auto rtxPacket = packet->Clone(); - - delete packet; + std::unique_ptr rtxPacket{ packet->Clone() }; std::memset(buffer, '0', sizeof(buffer)); @@ -369,8 +374,6 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(rtxPacket->GetHeaderExtensionLength() == 12); REQUIRE(rtxPacket->HasOneByteExtensions() == false); REQUIRE(rtxPacket->HasTwoBytesExtensions()); - - delete rtxPacket; } SECTION("create RtpPacket and apply payload shift to it") @@ -395,11 +398,13 @@ SCENARIO("parse RTP packets", "[parser][rtp]") }; // clang-format on - size_t len = 40; - RtpPacket* packet = RtpPacket::Parse(buffer, len); + size_t len = 40; + std::unique_ptr packet{ RtpPacket::Parse(buffer, len) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->GetPayloadType() == 1); @@ -413,6 +418,7 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->HasTwoBytesExtensions() == false); REQUIRE(packet->GetPayloadLength() == 8); REQUIRE(packet->GetPayloadPadding() == 4); + REQUIRE(packet->GetPayload()[packet->GetPayloadLength() + packet->GetPayloadPadding() - 1] == 4); REQUIRE(packet->GetSize() == 40); auto* payload = packet->GetPayload(); @@ -426,11 +432,12 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(payload[6] == 0x06); REQUIRE(payload[7] == 0x07); + // NOTE: This will remove padding. packet->ShiftPayload(0, 2, true); REQUIRE(packet->GetPayloadLength() == 10); - REQUIRE(packet->GetPayloadPadding() == 4); - REQUIRE(packet->GetSize() == 42); + REQUIRE(packet->GetPayloadPadding() == 0); + REQUIRE(packet->GetSize() == 38); REQUIRE(payload[2] == 0x00); REQUIRE(payload[3] == 0x01); REQUIRE(payload[4] == 0x02); @@ -443,8 +450,8 @@ SCENARIO("parse RTP packets", "[parser][rtp]") packet->ShiftPayload(0, 2, false); REQUIRE(packet->GetPayloadLength() == 8); - REQUIRE(packet->GetPayloadPadding() == 4); - REQUIRE(packet->GetSize() == 40); + REQUIRE(packet->GetPayloadPadding() == 0); + REQUIRE(packet->GetSize() == 36); REQUIRE(payload[0] == 0x00); REQUIRE(payload[1] == 0x01); REQUIRE(payload[2] == 0x02); @@ -454,18 +461,18 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(payload[6] == 0x06); REQUIRE(payload[7] == 0x07); - // NOTE: This will require padding to 4 bytes. + // NOTE: This will remove padding. packet->SetPayloadLength(14); - REQUIRE(packet->GetPayloadLength() == 16); + REQUIRE(packet->GetPayloadLength() == 14); REQUIRE(packet->GetPayloadPadding() == 0); - REQUIRE(packet->GetSize() == 44); + REQUIRE(packet->GetSize() == 42); packet->ShiftPayload(4, 4, true); - REQUIRE(packet->GetPayloadLength() == 20); + REQUIRE(packet->GetPayloadLength() == 18); REQUIRE(packet->GetPayloadPadding() == 0); - REQUIRE(packet->GetSize() == 48); + REQUIRE(packet->GetSize() == 46); REQUIRE(payload[0] == 0x00); REQUIRE(payload[1] == 0x01); REQUIRE(payload[2] == 0x02); @@ -480,8 +487,6 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->GetPayloadLength() == 1000); REQUIRE(packet->GetPayloadPadding() == 0); REQUIRE(packet->GetSize() == 1028); - - delete packet; } SECTION("set One-Byte header extensions") @@ -502,16 +507,19 @@ SCENARIO("parse RTP packets", "[parser][rtp]") 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 }; // clang-format on - RtpPacket* packet = RtpPacket::Parse(buffer, 28); + std::unique_ptr packet{ RtpPacket::Parse(buffer, 28) }; std::vector extensions; uint8_t extenLen; uint8_t* extenValue; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->GetSize() == 28); REQUIRE(packet->HasHeaderExtension() == false); @@ -521,6 +529,7 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->HasTwoBytesExtensions() == false); REQUIRE(packet->GetPayloadLength() == 12); REQUIRE(packet->GetPayloadPadding() == 4); + REQUIRE(packet->GetPayload()[packet->GetPayloadLength() + packet->GetPayloadPadding() - 1] == 4); REQUIRE(packet->GetPayload()[0] == 0x11); REQUIRE(packet->GetPayload()[packet->GetPayloadLength() - 1] == 0xCC); @@ -536,6 +545,7 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->HasTwoBytesExtensions() == false); REQUIRE(packet->GetPayloadLength() == 12); REQUIRE(packet->GetPayloadPadding() == 4); + REQUIRE(packet->GetPayload()[packet->GetPayloadLength() + packet->GetPayloadPadding() - 1] == 4); REQUIRE(packet->GetPayload()[0] == 0x11); REQUIRE(packet->GetPayload()[packet->GetPayloadLength() - 1] == 0xCC); @@ -588,6 +598,7 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->HasTwoBytesExtensions() == false); REQUIRE(packet->GetPayloadLength() == 12); REQUIRE(packet->GetPayloadPadding() == 4); + REQUIRE(packet->GetPayload()[packet->GetPayloadLength() + packet->GetPayloadPadding() - 1] == 4); REQUIRE(packet->GetPayload()[0] == 0x11); REQUIRE(packet->GetPayload()[packet->GetPayloadLength() - 1] == 0xCC); REQUIRE(packet->GetExtension(0, extenLen) == nullptr); @@ -623,6 +634,7 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->HasTwoBytesExtensions() == false); REQUIRE(packet->GetPayloadLength() == 12); REQUIRE(packet->GetPayloadPadding() == 4); + REQUIRE(packet->GetPayload()[packet->GetPayloadLength() + packet->GetPayloadPadding() - 1] == 4); REQUIRE(packet->GetPayload()[0] == 0x11); REQUIRE(packet->GetPayload()[packet->GetPayloadLength() - 1] == 0xCC); REQUIRE(packet->GetExtension(1, extenLen) == nullptr); @@ -644,8 +656,6 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(extenValue[1] == 0x02); REQUIRE(extenValue[2] == 0x03); REQUIRE(extenValue[3] == 0x00); - - delete packet; } SECTION("set Two-Bytes header extensions") @@ -665,16 +675,22 @@ SCENARIO("parse RTP packets", "[parser][rtp]") 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 }; // clang-format on - RtpPacket* packet = RtpPacket::Parse(buffer, 28); + std::unique_ptr packet{ RtpPacket::Parse(buffer, 28) }; std::vector extensions; uint8_t extenLen; uint8_t* extenValue; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->GetSize() == 28); REQUIRE(packet->HasHeaderExtension() == false); @@ -684,6 +700,7 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->HasTwoBytesExtensions() == false); REQUIRE(packet->GetPayloadLength() == 12); REQUIRE(packet->GetPayloadPadding() == 4); + REQUIRE(packet->GetPayload()[packet->GetPayloadLength() + packet->GetPayloadPadding() - 1] == 4); REQUIRE(packet->GetPayload()[0] == 0x11); REQUIRE(packet->GetPayload()[packet->GetPayloadLength() - 1] == 0xCC); @@ -699,6 +716,7 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->HasTwoBytesExtensions() == true); REQUIRE(packet->GetPayloadLength() == 12); REQUIRE(packet->GetPayloadPadding() == 4); + REQUIRE(packet->GetPayload()[packet->GetPayloadLength() + packet->GetPayloadPadding() - 1] == 4); REQUIRE(packet->GetPayload()[0] == 0x11); REQUIRE(packet->GetPayload()[packet->GetPayloadLength() - 1] == 0xCC); @@ -737,6 +755,7 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->HasTwoBytesExtensions() == true); REQUIRE(packet->GetPayloadLength() == 12); REQUIRE(packet->GetPayloadPadding() == 4); + REQUIRE(packet->GetPayload()[packet->GetPayloadLength() + packet->GetPayloadPadding() - 1] == 4); REQUIRE(packet->GetPayload()[0] == 0x11); REQUIRE(packet->GetPayload()[packet->GetPayloadLength() - 1] == 0xCC); REQUIRE(packet->GetExtension(0, extenLen) == nullptr); @@ -780,6 +799,7 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->HasTwoBytesExtensions() == true); REQUIRE(packet->GetPayloadLength() == 12); REQUIRE(packet->GetPayloadPadding() == 4); + REQUIRE(packet->GetPayload()[packet->GetPayloadLength() + packet->GetPayloadPadding() - 1] == 4); REQUIRE(packet->GetPayload()[0] == 0x11); REQUIRE(packet->GetPayload()[packet->GetPayloadLength() - 1] == 0xCC); REQUIRE(packet->GetExtension(1, extenLen) == nullptr); @@ -789,8 +809,6 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(packet->GetExtension(24, extenLen)); REQUIRE(packet->HasExtension(24) == true); REQUIRE(extenLen == 4); - - delete packet; } SECTION("read frame-marking extension") @@ -807,10 +825,12 @@ SCENARIO("parse RTP packets", "[parser][rtp]") }; // clang-format on - RtpPacket* packet = RtpPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ RtpPacket::Parse(buffer, sizeof(buffer)) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->HasHeaderExtension() == true); @@ -839,7 +859,5 @@ SCENARIO("parse RTP packets", "[parser][rtp]") REQUIRE(frameMarking->tid == 3); REQUIRE(frameMarking->lid == 1); REQUIRE(frameMarking->tl0picidx == 5); - - delete packet; } } diff --git a/worker/test/src/RTC/TestRtpPacketH264Svc.cpp b/worker/test/src/RTC/TestRtpPacketH264Svc.cpp index 022bee4a13..844bfe3853 100644 --- a/worker/test/src/RTC/TestRtpPacketH264Svc.cpp +++ b/worker/test/src/RTC/TestRtpPacketH264Svc.cpp @@ -2,8 +2,9 @@ #include "helpers.hpp" #include "RTC/Codecs/H264_SVC.hpp" #include "RTC/RtpPacket.hpp" -#include +#include #include // std::memset() +#include #include #include @@ -21,12 +22,16 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") uint8_t* extenValue; if (!helpers::readBinaryFile("data/H264_SVC/I0-7.bin", buffer, &len)) + { FAIL("cannot open file"); + } - RtpPacket* packet = RtpPacket::Parse(buffer, len); + std::unique_ptr packet{ RtpPacket::Parse(buffer, len) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->HasHeaderExtension() == true); @@ -54,8 +59,9 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") // Read frame-marking. packet->ReadFrameMarking(&frameMarking, frameMarkingLen); - const auto* payloadDescriptor = - Codecs::H264_SVC::Parse(payload, sizeof(payload), frameMarking, frameMarkingLen); + std::unique_ptr payloadDescriptor{ + Codecs::H264_SVC::Parse(payload, sizeof(payload), frameMarking, frameMarkingLen) + }; REQUIRE(payloadDescriptor); @@ -68,10 +74,6 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") REQUIRE(payloadDescriptor->tlIndex == 0); REQUIRE(payloadDescriptor->hasSlIndex == false); REQUIRE(payloadDescriptor->isKeyFrame == true); - - delete payloadDescriptor; - - delete packet; } SECTION("parse I0-8.bin") @@ -81,12 +83,16 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") uint8_t* extenValue; if (!helpers::readBinaryFile("data/H264_SVC/I0-8.bin", buffer, &len)) + { FAIL("cannot open file"); + } - RtpPacket* packet = RtpPacket::Parse(buffer, len); + std::unique_ptr packet{ RtpPacket::Parse(buffer, len) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->HasHeaderExtension() == true); @@ -114,8 +120,9 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") // Read frame-marking. packet->ReadFrameMarking(&frameMarking, frameMarkingLen); - const auto* payloadDescriptor = - Codecs::H264_SVC::Parse(payload, sizeof(payload), frameMarking, frameMarkingLen); + std::unique_ptr payloadDescriptor{ + Codecs::H264_SVC::Parse(payload, sizeof(payload), frameMarking, frameMarkingLen) + }; REQUIRE(payloadDescriptor); @@ -128,10 +135,6 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") REQUIRE(payloadDescriptor->tlIndex == 0); REQUIRE(payloadDescriptor->hasSlIndex == false); REQUIRE(payloadDescriptor->isKeyFrame == false); - - delete payloadDescriptor; - - delete packet; } SECTION("parse I0-5.bin") @@ -141,12 +144,16 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") uint8_t* extenValue; if (!helpers::readBinaryFile("data/H264_SVC/I0-5.bin", buffer, &len)) + { FAIL("cannot open file"); + } - RtpPacket* packet = RtpPacket::Parse(buffer, len); + std::unique_ptr packet{ RtpPacket::Parse(buffer, len) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->HasHeaderExtension() == true); @@ -174,8 +181,9 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") // Read frame-marking. packet->ReadFrameMarking(&frameMarking, frameMarkingLen); - const auto* payloadDescriptor = - Codecs::H264_SVC::Parse(payload, sizeof(payload), frameMarking, frameMarkingLen); + std::unique_ptr payloadDescriptor{ + Codecs::H264_SVC::Parse(payload, sizeof(payload), frameMarking, frameMarkingLen) + }; REQUIRE(payloadDescriptor); @@ -187,10 +195,6 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") REQUIRE(payloadDescriptor->isKeyFrame == true); REQUIRE(payloadDescriptor->hasSlIndex == false); REQUIRE(payloadDescriptor->hasTlIndex == false); - - delete payloadDescriptor; - - delete packet; } SECTION("parse I1-15.bin") @@ -200,12 +204,16 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") uint8_t* extenValue; if (!helpers::readBinaryFile("data/H264_SVC/I1-15.bin", buffer, &len)) + { FAIL("cannot open file"); + } - RtpPacket* packet = RtpPacket::Parse(buffer, len); + std::unique_ptr packet{ RtpPacket::Parse(buffer, len) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->HasHeaderExtension() == true); @@ -233,8 +241,9 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") // Read frame-marking. packet->ReadFrameMarking(&frameMarking, frameMarkingLen); - const auto* payloadDescriptor = - Codecs::H264_SVC::Parse(payload, sizeof(payload), frameMarking, frameMarkingLen); + std::unique_ptr payloadDescriptor{ + Codecs::H264_SVC::Parse(payload, sizeof(payload), frameMarking, frameMarkingLen) + }; REQUIRE(payloadDescriptor); @@ -247,10 +256,6 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") REQUIRE(payloadDescriptor->tlIndex == 0); REQUIRE(payloadDescriptor->hasSlIndex == false); REQUIRE(payloadDescriptor->isKeyFrame == false); - - delete payloadDescriptor; - - delete packet; } SECTION("parse I0-14.bin") @@ -260,12 +265,16 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") uint8_t* extenValue; if (!helpers::readBinaryFile("data/H264_SVC/I0-14.bin", buffer, &len)) + { FAIL("cannot open file"); + } - RtpPacket* packet = RtpPacket::Parse(buffer, len); + std::unique_ptr packet{ RtpPacket::Parse(buffer, len) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->HasHeaderExtension() == true); @@ -293,8 +302,9 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") // Read frame-marking. packet->ReadFrameMarking(&frameMarking, frameMarkingLen); - const auto* payloadDescriptor = - Codecs::H264_SVC::Parse(payload, sizeof(payload), frameMarking, frameMarkingLen); + std::unique_ptr payloadDescriptor{ + Codecs::H264_SVC::Parse(payload, sizeof(payload), frameMarking, frameMarkingLen) + }; REQUIRE(payloadDescriptor); @@ -307,10 +317,6 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") REQUIRE(payloadDescriptor->tlIndex == 0); REQUIRE(payloadDescriptor->hasSlIndex == false); REQUIRE(payloadDescriptor->isKeyFrame == true); - - delete payloadDescriptor; - - delete packet; } SECTION("parse 2SL-I14.bin") @@ -320,12 +326,16 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") uint8_t* extenValue; if (!helpers::readBinaryFile("data/H264_SVC/2SL-I14.bin", buffer, &len)) + { FAIL("cannot open file"); + } - RtpPacket* packet = RtpPacket::Parse(buffer, len); + std::unique_ptr packet{ RtpPacket::Parse(buffer, len) }; if (!packet) + { FAIL("not a RTP packet"); + } REQUIRE(packet->HasMarker() == false); REQUIRE(packet->HasHeaderExtension() == true); @@ -353,8 +363,9 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") // Read frame-marking. packet->ReadFrameMarking(&frameMarking, frameMarkingLen); - const auto* payloadDescriptor = - Codecs::H264_SVC::Parse(payload, sizeof(payload), frameMarking, frameMarkingLen); + std::unique_ptr payloadDescriptor{ + Codecs::H264_SVC::Parse(payload, sizeof(payload), frameMarking, frameMarkingLen) + }; REQUIRE(payloadDescriptor); @@ -368,10 +379,6 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") REQUIRE(payloadDescriptor->hasSlIndex); REQUIRE(payloadDescriptor->slIndex == 0); REQUIRE(payloadDescriptor->isKeyFrame == true); - - delete payloadDescriptor; - - delete packet; } SECTION("create and test RTP files") @@ -390,10 +397,12 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") std::fstream nf; nf.open("data/H264_SVC/naluInfo/naluInfo.csv", std::ios::in); + if (nf.is_open()) { std::string line, word; getline(nf, line); // omit the header in the CSV + while (getline(nf, line)) { std::stringstream s(line); @@ -412,19 +421,26 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") if (!helpers::readPayloadData( "data/H264_SVC/naluInfo/naluInfo.264", pos + 4, bytes - 4, buffer)) + { FAIL("Failed to read payload data!\n"); + } // TODO: One additional byte is written as last value is omitted in the // test bench std::string strFile1 = "rtp-" + std::to_string(rows) + ".bin"; + if (!helpers::writeRtpPacket( strFile1.c_str(), type, bytes - 4, sid, tid, isIdr, start, end, buffer, buffer2, &len)) + { FAIL("Failed to write RTP packet!\n"); + } - RtpPacket* packet = RtpPacket::Parse(buffer2, len); + std::unique_ptr packet{ RtpPacket::Parse(buffer2, len) }; if (!packet) + { FAIL("not a RTP packet"); + } packet->SetFrameMarkingExtensionId(1); @@ -435,8 +451,9 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") // Read frame-marking. packet->ReadFrameMarking(&frameMarking, frameMarkingLen); - const auto* payloadDescriptor = Codecs::H264_SVC::Parse( - payload, packet->GetPayloadLength(), frameMarking, frameMarkingLen); + std::unique_ptr payloadDescriptor{ + Codecs::H264_SVC::Parse(payload, packet->GetPayloadLength(), frameMarking, frameMarkingLen) + }; REQUIRE(payloadDescriptor); @@ -444,9 +461,6 @@ SCENARIO("parse RTP packets with H264 SVC", "[parser][rtp]") pos += bytes; rows++; - - delete payloadDescriptor; - delete packet; } nf.close(); diff --git a/worker/test/src/RTC/TestRtpRetransmissionBuffer.cpp b/worker/test/src/RTC/TestRtpRetransmissionBuffer.cpp new file mode 100644 index 0000000000..7158509ed2 --- /dev/null +++ b/worker/test/src/RTC/TestRtpRetransmissionBuffer.cpp @@ -0,0 +1,317 @@ +#include "common.hpp" +#include "RTC/RtpPacket.hpp" +#include "RTC/RtpRetransmissionBuffer.hpp" +#include +#include + +using namespace RTC; + +// Class inheriting from RtpRetransmissionBuffer so we can access its protected +// buffer member. +class RtpMyRetransmissionBuffer : public RtpRetransmissionBuffer +{ +public: + struct VerificationItem + { + bool isPresent; + uint16_t sequenceNumber; + uint32_t timestamp; + }; + +public: + RtpMyRetransmissionBuffer(uint16_t maxItems, uint32_t maxRetransmissionDelayMs, uint32_t clockRate) + : RtpRetransmissionBuffer(maxItems, maxRetransmissionDelayMs, clockRate) + { + } + +public: + void Insert(uint16_t seq, uint32_t timestamp) + { + // clang-format off + uint8_t rtpBuffer[] = + { + 0b10000000, 0b01111011, 0b01010010, 0b00001110, + 0b01011011, 0b01101011, 0b11001010, 0b10110101, + 0, 0, 0, 2 + }; + // clang-format on + + std::unique_ptr packet{ RtpPacket::Parse(rtpBuffer, sizeof(rtpBuffer)) }; + + packet->SetSequenceNumber(seq); + packet->SetTimestamp(timestamp); + + std::shared_ptr sharedPacket; + + RtpRetransmissionBuffer::Insert(packet.get(), sharedPacket); + } + + void AssertBuffer(std::vector verificationBuffer) + { + REQUIRE(verificationBuffer.size() == this->buffer.size()); + + for (size_t idx{ 0u }; idx < verificationBuffer.size(); ++idx) + { + auto& verificationItem = verificationBuffer.at(idx); + auto* item = this->buffer.at(idx); + + REQUIRE(verificationItem.isPresent == !!item); + + if (item) + { + REQUIRE(verificationItem.sequenceNumber == item->sequenceNumber); + REQUIRE(verificationItem.timestamp == item->timestamp); + } + } + } +}; + +SCENARIO("RtpRetransmissionBuffer", "[rtp][rtx]") +{ + SECTION("proper packets received in order") + { + uint16_t maxItems{ 4 }; + uint32_t maxRetransmissionDelayMs{ 2000u }; + uint32_t clockRate{ 90000 }; + + RtpMyRetransmissionBuffer myRetransmissionBuffer(maxItems, maxRetransmissionDelayMs, clockRate); + + myRetransmissionBuffer.Insert(10001, 1000000000); + myRetransmissionBuffer.Insert(10002, 1000000000); + myRetransmissionBuffer.Insert(10003, 1000000200); + myRetransmissionBuffer.Insert(10004, 1000000200); + + // clang-format off + myRetransmissionBuffer.AssertBuffer( + { + { true, 10001, 1000000000 }, + { true, 10002, 1000000000 }, + { true, 10003, 1000000200 }, + { true, 10004, 1000000200 } + } + ); + // clang-format on + } + + SECTION("proper packets received out of order") + { + uint16_t maxItems{ 4 }; + uint32_t maxRetransmissionDelayMs{ 2000u }; + uint32_t clockRate{ 90000 }; + + RtpMyRetransmissionBuffer myRetransmissionBuffer(maxItems, maxRetransmissionDelayMs, clockRate); + + myRetransmissionBuffer.Insert(20004, 2000000200); + myRetransmissionBuffer.Insert(20001, 2000000000); + myRetransmissionBuffer.Insert(20003, 2000000200); + myRetransmissionBuffer.Insert(20002, 2000000000); + + // clang-format off + myRetransmissionBuffer.AssertBuffer( + { + { true, 20001, 2000000000 }, + { true, 20002, 2000000000 }, + { true, 20003, 2000000200 }, + { true, 20004, 2000000200 } + } + ); + // clang-format on + } + + SECTION("packet with too new sequence number produces buffer emptying") + { + uint16_t maxItems{ 4 }; + uint32_t maxRetransmissionDelayMs{ 2000u }; + uint32_t clockRate{ 90000 }; + + RtpMyRetransmissionBuffer myRetransmissionBuffer(maxItems, maxRetransmissionDelayMs, clockRate); + + myRetransmissionBuffer.Insert(30001, 3000000000); + myRetransmissionBuffer.Insert(30002, 3000000000); + myRetransmissionBuffer.Insert(30003, 3000000200); + myRetransmissionBuffer.Insert(40000, 3000003000); + + // clang-format off + myRetransmissionBuffer.AssertBuffer( + { + { true, 40000, 3000003000 } + } + ); + // clang-format on + } + + SECTION("blank slots are properly created") + { + uint16_t maxItems{ 10 }; + uint32_t maxRetransmissionDelayMs{ 2000u }; + uint32_t clockRate{ 90000 }; + + RtpMyRetransmissionBuffer myRetransmissionBuffer(maxItems, maxRetransmissionDelayMs, clockRate); + + myRetransmissionBuffer.Insert(40002, 4000000002); + // Packet must be discarded since its timestamp is lower than in seq 40002. + myRetransmissionBuffer.Insert(40003, 4000000001); + // Must produce 1 blank slot. + myRetransmissionBuffer.Insert(40004, 4000000004); + // Discarded (duplicated). + myRetransmissionBuffer.Insert(40002, 4000000002); + // Must produce 4 blank slot. + myRetransmissionBuffer.Insert(40008, 4000000008); + myRetransmissionBuffer.Insert(40006, 4000000006); + // Must produce 1 blank slot at the front. + myRetransmissionBuffer.Insert(40000, 4000000000); + + // clang-format off + myRetransmissionBuffer.AssertBuffer( + { + { true, 40000, 4000000000 }, + { false, 0, 0 }, + { true, 40002, 4000000002 }, + { false, 0, 0 }, + { true, 40004, 4000000004 }, + { false, 0, 0 }, + { true, 40006, 4000000006 }, + { false, 0, 0 }, + { true, 40008, 4000000008 } + } + ); + // clang-format on + } + + SECTION("packet with too old sequence number is discarded") + { + uint16_t maxItems{ 4 }; + uint32_t maxRetransmissionDelayMs{ 2000u }; + uint32_t clockRate{ 90000 }; + + RtpMyRetransmissionBuffer myRetransmissionBuffer(maxItems, maxRetransmissionDelayMs, clockRate); + + myRetransmissionBuffer.Insert(10001, 1000000001); + myRetransmissionBuffer.Insert(10002, 1000000002); + myRetransmissionBuffer.Insert(10003, 1000000003); + // Too old seq. + myRetransmissionBuffer.Insert(40000, 1000000000); + + // clang-format off + myRetransmissionBuffer.AssertBuffer( + { + { true, 10001, 1000000001 }, + { true, 10002, 1000000002 }, + { true, 10003, 1000000003 } + } + ); + // clang-format on + } + + SECTION("packet with too old timestamp is discarded") + { + uint16_t maxItems{ 4 }; + uint32_t maxRetransmissionDelayMs{ 2000u }; + uint32_t clockRate{ 90000 }; + + RtpMyRetransmissionBuffer myRetransmissionBuffer(maxItems, maxRetransmissionDelayMs, clockRate); + + auto maxDiffTs = static_cast(maxRetransmissionDelayMs * clockRate / 1000); + + myRetransmissionBuffer.Insert(10001, 1000000001); + myRetransmissionBuffer.Insert(10002, 1000000002); + myRetransmissionBuffer.Insert(10003, 1000000003); + // Too old timestamp (subtract 100 to avoid math issues). + myRetransmissionBuffer.Insert(10000, 1000000003 - maxDiffTs - 100); + + // clang-format off + myRetransmissionBuffer.AssertBuffer( + { + { true, 10001, 1000000001 }, + { true, 10002, 1000000002 }, + { true, 10003, 1000000003 } + } + ); + // clang-format on + } + + SECTION("packet with very newest timestamp is inserted as newest item despite its seq is old") + { + uint16_t maxItems{ 4 }; + uint32_t maxRetransmissionDelayMs{ 2000u }; + uint32_t clockRate{ 90000 }; + + RtpMyRetransmissionBuffer myRetransmissionBuffer(maxItems, maxRetransmissionDelayMs, clockRate); + + // Scenario based on https://github.com/versatica/mediasoup/issues/1037. + + myRetransmissionBuffer.Insert(24816, 1024930187); + myRetransmissionBuffer.Insert(24980, 1025106407); + myRetransmissionBuffer.Insert(18365, 1026593387); + + // clang-format off + myRetransmissionBuffer.AssertBuffer( + { + { true, 18365, 1026593387 } + } + ); + // clang-format on + } + + SECTION( + "packet with lower seq than newest packet in the buffer and higher timestamp forces buffer emptying") + { + uint16_t maxItems{ 4 }; + uint32_t maxRetransmissionDelayMs{ 2000u }; + uint32_t clockRate{ 90000 }; + + RtpMyRetransmissionBuffer myRetransmissionBuffer(maxItems, maxRetransmissionDelayMs, clockRate); + + myRetransmissionBuffer.Insert(33331, 1000000001); + myRetransmissionBuffer.Insert(33332, 1000000002); + myRetransmissionBuffer.Insert(33330, 1000000003); + + // clang-format off + myRetransmissionBuffer.AssertBuffer( + { + { true, 33330, 1000000003 } + } + ); + // clang-format on + } + + SECTION("fuzzer generated packets") + { + uint16_t maxItems{ 2500u }; + uint32_t maxRetransmissionDelayMs{ 2000u }; + uint32_t clockRate{ 90000 }; + + RtpMyRetransmissionBuffer myRetransmissionBuffer(maxItems, maxRetransmissionDelayMs, clockRate); + + // These packets reproduce an already fixed crash reported here: + // https://github.com/versatica/mediasoup/issues/1027#issuecomment-1478464584 + // I've commented first packets and just left those that produce the crash. + + // myRetransmissionBuffer.Insert(14906, 976891962); + // myRetransmissionBuffer.Insert(14906, 976891962); + // myRetransmissionBuffer.Insert(14906, 976892730); + // myRetransmissionBuffer.Insert(13157, 862283031); + // myRetransmissionBuffer.Insert(13114, 859453491); + // myRetransmissionBuffer.Insert(14906, 976892264); + // myRetransmissionBuffer.Insert(14906, 976897098); + // myRetransmissionBuffer.Insert(13114, 859464290); + // myRetransmissionBuffer.Insert(14906, 976889088); + // myRetransmissionBuffer.Insert(13056, 855638184); + // myRetransmissionBuffer.Insert(14906, 976891950); + // myRetransmissionBuffer.Insert(17722, 1161443894); + // myRetransmissionBuffer.Insert(12846, 841888049); + // myRetransmissionBuffer.Insert(14906, 976905830); + // myRetransmissionBuffer.Insert(15677, 1027420485); + // myRetransmissionBuffer.Insert(33742, 2211317269); + // myRetransmissionBuffer.Insert(14906, 976892672); + // myRetransmissionBuffer.Insert(13102, 858665774); + // myRetransmissionBuffer.Insert(12850, 842150702); + // myRetransmissionBuffer.Insert(14906, 976891941); + // myRetransmissionBuffer.Insert(15677, 1027423549); + // myRetransmissionBuffer.Insert(12346, 809120580); + // myRetransmissionBuffer.Insert(12645, 828715313); + myRetransmissionBuffer.Insert(12645, 828702743); + myRetransmissionBuffer.Insert(33998, 2228092928); + myRetransmissionBuffer.Insert(33998, 2228092928); + } +} diff --git a/worker/test/src/RTC/TestRtpStreamRecv.cpp b/worker/test/src/RTC/TestRtpStreamRecv.cpp index 417759523f..388513fc71 100644 --- a/worker/test/src/RTC/TestRtpStreamRecv.cpp +++ b/worker/test/src/RTC/TestRtpStreamRecv.cpp @@ -3,7 +3,7 @@ #include "RTC/RtpPacket.hpp" #include "RTC/RtpStream.hpp" #include "RTC/RtpStreamRecv.hpp" -#include +#include #include using namespace RTC; @@ -11,6 +11,7 @@ using namespace RTC; // 17: 16 bit mask + the initial sequence number. static constexpr size_t MaxRequestedPackets{ 17 }; static constexpr unsigned int SendNackDelay{ 0u }; // In ms. +static const bool UseRtpInactivityCheck{ false }; SCENARIO("receive RTP packets and trigger NACK", "[rtp][rtpstream]") { @@ -85,7 +86,9 @@ SCENARIO("receive RTP packets and trigger NACK", "[rtp][rtpstream]") for (size_t i{ 1 }; i < MaxRequestedPackets; ++i) { if ((bitmask & 1) != 0) + { this->nackedSeqNumbers.push_back(firstSeq + i); + } bitmask >>= 1; } @@ -125,10 +128,12 @@ SCENARIO("receive RTP packets and trigger NACK", "[rtp][rtpstream]") }; // clang-format on - RtpPacket* packet = RtpPacket::Parse(buffer, sizeof(buffer)); + std::unique_ptr packet{ RtpPacket::Parse(buffer, sizeof(buffer)) }; if (!packet) + { FAIL("not a RTP packet"); + } RtpStream::Params params; @@ -141,28 +146,28 @@ SCENARIO("receive RTP packets and trigger NACK", "[rtp][rtpstream]") SECTION("NACK one packet") { RtpStreamRecvListener listener; - RtpStreamRecv rtpStream(&listener, params, SendNackDelay); + RtpStreamRecv rtpStream(&listener, params, SendNackDelay, UseRtpInactivityCheck); packet->SetSequenceNumber(1); - rtpStream.ReceivePacket(packet); + rtpStream.ReceivePacket(packet.get()); packet->SetSequenceNumber(3); listener.shouldTriggerNack = true; listener.shouldTriggerPLI = false; listener.shouldTriggerFIR = false; - rtpStream.ReceivePacket(packet); + rtpStream.ReceivePacket(packet.get()); REQUIRE(listener.nackedSeqNumbers.size() == 1); REQUIRE(listener.nackedSeqNumbers[0] == 2); listener.nackedSeqNumbers.clear(); packet->SetSequenceNumber(2); - rtpStream.ReceivePacket(packet); + rtpStream.ReceivePacket(packet.get()); REQUIRE(listener.nackedSeqNumbers.size() == 0); packet->SetSequenceNumber(4); - rtpStream.ReceivePacket(packet); + rtpStream.ReceivePacket(packet.get()); REQUIRE(listener.nackedSeqNumbers.size() == 0); } @@ -170,16 +175,16 @@ SCENARIO("receive RTP packets and trigger NACK", "[rtp][rtpstream]") SECTION("wrapping sequence numbers") { RtpStreamRecvListener listener; - RtpStreamRecv rtpStream(&listener, params, SendNackDelay); + RtpStreamRecv rtpStream(&listener, params, SendNackDelay, UseRtpInactivityCheck); packet->SetSequenceNumber(0xfffe); - rtpStream.ReceivePacket(packet); + rtpStream.ReceivePacket(packet.get()); packet->SetSequenceNumber(1); listener.shouldTriggerNack = true; listener.shouldTriggerPLI = false; listener.shouldTriggerFIR = false; - rtpStream.ReceivePacket(packet); + rtpStream.ReceivePacket(packet.get()); REQUIRE(listener.nackedSeqNumbers.size() == 2); REQUIRE(listener.nackedSeqNumbers[0] == 0xffff); @@ -190,21 +195,19 @@ SCENARIO("receive RTP packets and trigger NACK", "[rtp][rtpstream]") SECTION("require key frame") { RtpStreamRecvListener listener; - RtpStreamRecv rtpStream(&listener, params, SendNackDelay); + RtpStreamRecv rtpStream(&listener, params, SendNackDelay, UseRtpInactivityCheck); packet->SetSequenceNumber(1); - rtpStream.ReceivePacket(packet); + rtpStream.ReceivePacket(packet.get()); // Seq different is bigger than MaxNackPackets in NackGenerator, so it // triggers a key frame. packet->SetSequenceNumber(1003); listener.shouldTriggerPLI = true; listener.shouldTriggerFIR = false; - rtpStream.ReceivePacket(packet); + rtpStream.ReceivePacket(packet.get()); } // Must run the loop to wait for UV timers and close them. DepLibUV::RunLoop(); - - delete packet; } diff --git a/worker/test/src/RTC/TestRtpStreamSend.cpp b/worker/test/src/RTC/TestRtpStreamSend.cpp index b981730cc6..de52e87f83 100644 --- a/worker/test/src/RTC/TestRtpStreamSend.cpp +++ b/worker/test/src/RTC/TestRtpStreamSend.cpp @@ -3,21 +3,22 @@ #include "RTC/RtpPacket.hpp" #include "RTC/RtpStream.hpp" #include "RTC/RtpStreamSend.hpp" -#include +#include #include // #define PERFORMANCE_TEST 1 using namespace RTC; -static RtpPacket* CreateRtpPacket(uint8_t* buffer, uint16_t seq, uint32_t timestamp) +static std::unique_ptr CreateRtpPacket( + uint8_t* buffer, size_t len, uint16_t seq, uint32_t timestamp) { - auto* packet = RtpPacket::Parse(buffer, 1500); + auto* packet = RtpPacket::Parse(buffer, len); packet->SetSequenceNumber(seq); packet->SetTimestamp(timestamp); - return packet; + return std::unique_ptr(packet); } static void SendRtpPacket(std::vector> streams, RtpPacket* packet) @@ -78,15 +79,15 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") SECTION("receive NACK and get retransmitted packets") { // packet1 [pt:123, seq:21006, timestamp:1533790901] - auto packet1 = CreateRtpPacket(rtpBuffer1, 21006, 1533790901); + auto packet1(CreateRtpPacket(rtpBuffer1, sizeof(rtpBuffer1), 21006, 1533790901)); // packet2 [pt:123, seq:21007, timestamp:1533790901] - auto packet2 = CreateRtpPacket(rtpBuffer2, 21007, 1533790901); + auto packet2(CreateRtpPacket(rtpBuffer2, sizeof(rtpBuffer2), 21007, 1533790901)); // packet3 [pt:123, seq:21008, timestamp:1533793871] - auto packet3 = CreateRtpPacket(rtpBuffer3, 21008, 1533793871); + auto packet3(CreateRtpPacket(rtpBuffer3, sizeof(rtpBuffer3), 21008, 1533793871)); // packet4 [pt:123, seq:21009, timestamp:1533793871] - auto packet4 = CreateRtpPacket(rtpBuffer4, 21009, 1533793871); + auto packet4(CreateRtpPacket(rtpBuffer4, sizeof(rtpBuffer4), 21009, 1533793871)); // packet5 [pt:123, seq:21010, timestamp:1533796931] - auto packet5 = CreateRtpPacket(rtpBuffer5, 21010, 1533796931); + auto packet5(CreateRtpPacket(rtpBuffer5, sizeof(rtpBuffer5), 21010, 1533796931)); // Create a RtpStreamSend instance. TestRtpStreamListener testRtpStreamListener; @@ -99,16 +100,16 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") params.mimeType.type = RTC::RtpCodecMimeType::Type::VIDEO; std::string mid; - auto* stream = new RtpStreamSend(&testRtpStreamListener, params, mid); + auto stream = std::make_unique(&testRtpStreamListener, params, mid); // Receive all the packets (some of them not in order and/or duplicated). - SendRtpPacket({ { stream, params.ssrc } }, packet1); - SendRtpPacket({ { stream, params.ssrc } }, packet3); - SendRtpPacket({ { stream, params.ssrc } }, packet2); - SendRtpPacket({ { stream, params.ssrc } }, packet3); - SendRtpPacket({ { stream, params.ssrc } }, packet4); - SendRtpPacket({ { stream, params.ssrc } }, packet5); - SendRtpPacket({ { stream, params.ssrc } }, packet5); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet1.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet3.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet2.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet3.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet4.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet5.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet5.get()); // Create a NACK item that request for all the packets. RTCP::FeedbackRtpNackPacket nackPacket(0, params.ssrc); @@ -123,11 +124,11 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") REQUIRE(testRtpStreamListener.retransmittedPackets.size() == 5); - auto rtxPacket1 = testRtpStreamListener.retransmittedPackets[0]; - auto rtxPacket2 = testRtpStreamListener.retransmittedPackets[1]; - auto rtxPacket3 = testRtpStreamListener.retransmittedPackets[2]; - auto rtxPacket4 = testRtpStreamListener.retransmittedPackets[3]; - auto rtxPacket5 = testRtpStreamListener.retransmittedPackets[4]; + auto* rtxPacket1 = testRtpStreamListener.retransmittedPackets[0]; + auto* rtxPacket2 = testRtpStreamListener.retransmittedPackets[1]; + auto* rtxPacket3 = testRtpStreamListener.retransmittedPackets[2]; + auto* rtxPacket4 = testRtpStreamListener.retransmittedPackets[3]; + auto* rtxPacket5 = testRtpStreamListener.retransmittedPackets[4]; testRtpStreamListener.retransmittedPackets.clear(); @@ -136,22 +137,20 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") CheckRtxPacket(rtxPacket3, packet3->GetSequenceNumber(), packet3->GetTimestamp()); CheckRtxPacket(rtxPacket4, packet4->GetSequenceNumber(), packet4->GetTimestamp()); CheckRtxPacket(rtxPacket5, packet5->GetSequenceNumber(), packet5->GetTimestamp()); - - delete stream; } SECTION("receive NACK and get zero retransmitted packets if useNack is not set") { // packet1 [pt:123, seq:21006, timestamp:1533790901] - auto packet1 = CreateRtpPacket(rtpBuffer1, 21006, 1533790901); + auto packet1(CreateRtpPacket(rtpBuffer1, sizeof(rtpBuffer1), 21006, 1533790901)); // packet2 [pt:123, seq:21007, timestamp:1533790901] - auto packet2 = CreateRtpPacket(rtpBuffer2, 21007, 1533790901); + auto packet2(CreateRtpPacket(rtpBuffer2, sizeof(rtpBuffer2), 21007, 1533790901)); // packet3 [pt:123, seq:21008, timestamp:1533793871] - auto packet3 = CreateRtpPacket(rtpBuffer3, 21008, 1533793871); + auto packet3(CreateRtpPacket(rtpBuffer3, sizeof(rtpBuffer3), 21008, 1533793871)); // packet4 [pt:123, seq:21009, timestamp:1533793871] - auto packet4 = CreateRtpPacket(rtpBuffer4, 21009, 1533793871); + auto packet4(CreateRtpPacket(rtpBuffer4, sizeof(rtpBuffer4), 21009, 1533793871)); // packet5 [pt:123, seq:21010, timestamp:1533796931] - auto packet5 = CreateRtpPacket(rtpBuffer5, 21010, 1533796931); + auto packet5(CreateRtpPacket(rtpBuffer5, sizeof(rtpBuffer5), 21010, 1533796931)); // Create a RtpStreamSend instance. TestRtpStreamListener testRtpStreamListener; @@ -164,16 +163,16 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") params.mimeType.type = RTC::RtpCodecMimeType::Type::VIDEO; std::string mid; - auto* stream = new RtpStreamSend(&testRtpStreamListener, params, mid); + auto stream = std::make_unique(&testRtpStreamListener, params, mid); // Receive all the packets (some of them not in order and/or duplicated). - SendRtpPacket({ { stream, params.ssrc } }, packet1); - SendRtpPacket({ { stream, params.ssrc } }, packet3); - SendRtpPacket({ { stream, params.ssrc } }, packet2); - SendRtpPacket({ { stream, params.ssrc } }, packet3); - SendRtpPacket({ { stream, params.ssrc } }, packet4); - SendRtpPacket({ { stream, params.ssrc } }, packet5); - SendRtpPacket({ { stream, params.ssrc } }, packet5); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet1.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet3.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet2.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet3.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet4.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet5.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet5.get()); // Create a NACK item that request for all the packets. RTCP::FeedbackRtpNackPacket nackPacket(0, params.ssrc); @@ -189,22 +188,20 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") REQUIRE(testRtpStreamListener.retransmittedPackets.size() == 0); testRtpStreamListener.retransmittedPackets.clear(); - - delete stream; } SECTION("receive NACK and get zero retransmitted packets for audio") { // packet1 [pt:123, seq:21006, timestamp:1533790901] - auto packet1 = CreateRtpPacket(rtpBuffer1, 21006, 1533790901); + auto packet1(CreateRtpPacket(rtpBuffer1, sizeof(rtpBuffer1), 21006, 1533790901)); // packet2 [pt:123, seq:21007, timestamp:1533790901] - auto packet2 = CreateRtpPacket(rtpBuffer2, 21007, 1533790901); + auto packet2(CreateRtpPacket(rtpBuffer2, sizeof(rtpBuffer2), 21007, 1533790901)); // packet3 [pt:123, seq:21008, timestamp:1533793871] - auto packet3 = CreateRtpPacket(rtpBuffer3, 21008, 1533793871); + auto packet3(CreateRtpPacket(rtpBuffer3, sizeof(rtpBuffer3), 21008, 1533793871)); // packet4 [pt:123, seq:21009, timestamp:1533793871] - auto packet4 = CreateRtpPacket(rtpBuffer4, 21009, 1533793871); + auto packet4(CreateRtpPacket(rtpBuffer4, sizeof(rtpBuffer4), 21009, 1533793871)); // packet5 [pt:123, seq:21010, timestamp:1533796931] - auto packet5 = CreateRtpPacket(rtpBuffer5, 21010, 1533796931); + auto packet5(CreateRtpPacket(rtpBuffer5, sizeof(rtpBuffer5), 21010, 1533796931)); // Create a RtpStreamSend instance. TestRtpStreamListener testRtpStreamListener; @@ -217,16 +214,16 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") params.mimeType.type = RTC::RtpCodecMimeType::Type::AUDIO; std::string mid; - auto* stream = new RtpStreamSend(&testRtpStreamListener, params, mid); + auto stream = std::make_unique(&testRtpStreamListener, params, mid); // Receive all the packets (some of them not in order and/or duplicated). - SendRtpPacket({ { stream, params.ssrc } }, packet1); - SendRtpPacket({ { stream, params.ssrc } }, packet3); - SendRtpPacket({ { stream, params.ssrc } }, packet2); - SendRtpPacket({ { stream, params.ssrc } }, packet3); - SendRtpPacket({ { stream, params.ssrc } }, packet4); - SendRtpPacket({ { stream, params.ssrc } }, packet5); - SendRtpPacket({ { stream, params.ssrc } }, packet5); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet1.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet3.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet2.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet3.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet4.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet5.get()); + SendRtpPacket({ { stream.get(), params.ssrc } }, packet5.get()); // Create a NACK item that request for all the packets. RTCP::FeedbackRtpNackPacket nackPacket(0, params.ssrc); @@ -242,16 +239,14 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") REQUIRE(testRtpStreamListener.retransmittedPackets.size() == 0); testRtpStreamListener.retransmittedPackets.clear(); - - delete stream; } SECTION("receive NACK in different RtpStreamSend instances and get retransmitted packets") { // packet1 [pt:123, seq:21006, timestamp:1533790901] - auto packet1 = CreateRtpPacket(rtpBuffer1, 21006, 1533790901); + auto packet1(CreateRtpPacket(rtpBuffer1, sizeof(rtpBuffer1), 21006, 1533790901)); // packet2 [pt:123, seq:21007, timestamp:1533790901] - auto packet2 = CreateRtpPacket(rtpBuffer2, 21007, 1533790901); + auto packet2(CreateRtpPacket(rtpBuffer2, sizeof(rtpBuffer2), 21007, 1533790901)); // Create two RtpStreamSend instances. TestRtpStreamListener testRtpStreamListener1; @@ -265,7 +260,7 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") params1.mimeType.type = RTC::RtpCodecMimeType::Type::VIDEO; std::string mid; - auto* stream1 = new RtpStreamSend(&testRtpStreamListener1, params1, mid); + std::unique_ptr stream1(new RtpStreamSend(&testRtpStreamListener1, params1, mid)); RtpStream::Params params2; @@ -274,11 +269,11 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") params2.useNack = true; params2.mimeType.type = RTC::RtpCodecMimeType::Type::VIDEO; - auto* stream2 = new RtpStreamSend(&testRtpStreamListener2, params2, mid); + std::unique_ptr stream2(new RtpStreamSend(&testRtpStreamListener2, params2, mid)); // Receive all the packets in both streams. - SendRtpPacket({ { stream1, params1.ssrc }, { stream2, params2.ssrc } }, packet1); - SendRtpPacket({ { stream1, params1.ssrc }, { stream2, params2.ssrc } }, packet2); + SendRtpPacket({ { stream1.get(), params1.ssrc }, { stream2.get(), params2.ssrc } }, packet1.get()); + SendRtpPacket({ { stream1.get(), params1.ssrc }, { stream2.get(), params2.ssrc } }, packet2.get()); // Create a NACK item that request for all the packets. RTCP::FeedbackRtpNackPacket nackPacket(0, params1.ssrc); @@ -294,8 +289,8 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") REQUIRE(testRtpStreamListener1.retransmittedPackets.size() == 2); - auto rtxPacket1 = testRtpStreamListener1.retransmittedPackets[0]; - auto rtxPacket2 = testRtpStreamListener1.retransmittedPackets[1]; + auto* rtxPacket1 = testRtpStreamListener1.retransmittedPackets[0]; + auto* rtxPacket2 = testRtpStreamListener1.retransmittedPackets[1]; testRtpStreamListener1.retransmittedPackets.clear(); @@ -314,23 +309,20 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") CheckRtxPacket(rtxPacket1, packet1->GetSequenceNumber(), packet1->GetTimestamp()); CheckRtxPacket(rtxPacket2, packet2->GetSequenceNumber(), packet2->GetTimestamp()); - - delete stream1; - delete stream2; } - SECTION("packets get retransmitted as long as they don't exceed MaxRetransmissionDelay") + SECTION("packets get retransmitted as long as they don't exceed MaxRetransmissionDelayForVideoMs") { uint32_t clockRate = 90000; uint32_t firstTs = 1533790901; - uint32_t diffTs = RtpStreamSend::MaxRetransmissionDelay * clockRate / 1000; + uint32_t diffTs = RtpStreamSend::MaxRetransmissionDelayForVideoMs * clockRate / 1000; uint32_t secondTs = firstTs + diffTs; - auto packet1 = CreateRtpPacket(rtpBuffer1, 21006, firstTs); - auto packet2 = CreateRtpPacket(rtpBuffer2, 21007, secondTs - 1); + auto packet1(CreateRtpPacket(rtpBuffer1, sizeof(rtpBuffer1), 21006, firstTs)); + auto packet2(CreateRtpPacket(rtpBuffer2, sizeof(rtpBuffer2), 21007, secondTs - 1)); // Create a RtpStreamSend instance. - TestRtpStreamListener testRtpStreamListener1; + TestRtpStreamListener testRtpStreamListener; RtpStream::Params params1; @@ -340,11 +332,11 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") params1.mimeType.type = RTC::RtpCodecMimeType::Type::VIDEO; std::string mid; - auto* stream = new RtpStreamSend(&testRtpStreamListener1, params1, mid); + auto stream = std::make_unique(&testRtpStreamListener, params1, mid); // Receive all the packets. - SendRtpPacket({ { stream, params1.ssrc } }, packet1); - SendRtpPacket({ { stream, params1.ssrc } }, packet2); + SendRtpPacket({ { stream.get(), params1.ssrc } }, packet1.get()); + SendRtpPacket({ { stream.get(), params1.ssrc } }, packet2.get()); // Create a NACK item that request for all the packets. RTCP::FeedbackRtpNackPacket nackPacket(0, params1.ssrc); @@ -358,31 +350,33 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") // Process the NACK packet on stream1. stream->ReceiveNack(&nackPacket); - REQUIRE(testRtpStreamListener1.retransmittedPackets.size() == 2); + REQUIRE(testRtpStreamListener.retransmittedPackets.size() == 2); - auto rtxPacket1 = testRtpStreamListener1.retransmittedPackets[0]; - auto rtxPacket2 = testRtpStreamListener1.retransmittedPackets[1]; + auto* rtxPacket1 = testRtpStreamListener.retransmittedPackets[0]; + auto* rtxPacket2 = testRtpStreamListener.retransmittedPackets[1]; - testRtpStreamListener1.retransmittedPackets.clear(); + testRtpStreamListener.retransmittedPackets.clear(); CheckRtxPacket(rtxPacket1, packet1->GetSequenceNumber(), packet1->GetTimestamp()); CheckRtxPacket(rtxPacket2, packet2->GetSequenceNumber(), packet2->GetTimestamp()); - - delete stream; } - SECTION("packets don't get retransmitted if MaxRetransmissionDelay is exceeded") + SECTION("packets don't get retransmitted if MaxRetransmissionDelayForVideoMs is exceeded") { uint32_t clockRate = 90000; uint32_t firstTs = 1533790901; - uint32_t diffTs = RtpStreamSend::MaxRetransmissionDelay * clockRate / 1000; - uint32_t secondTs = firstTs + diffTs; + uint32_t diffTs = RtpStreamSend::MaxRetransmissionDelayForVideoMs * clockRate / 1000; + // Make second packet arrive more than MaxRetransmissionDelayForVideoMs later. + uint32_t secondTs = firstTs + diffTs + 100; + // Send a third packet so it will clean old packets from the buffer. + uint32_t thirdTs = firstTs + (2 * diffTs); - auto packet1 = CreateRtpPacket(rtpBuffer1, 21006, firstTs); - auto packet2 = CreateRtpPacket(rtpBuffer2, 21007, secondTs); + auto packet1(CreateRtpPacket(rtpBuffer1, sizeof(rtpBuffer1), 21006, firstTs)); + auto packet2(CreateRtpPacket(rtpBuffer2, sizeof(rtpBuffer2), 21007, secondTs)); + auto packet3(CreateRtpPacket(rtpBuffer3, sizeof(rtpBuffer3), 21008, thirdTs)); // Create a RtpStreamSend instance. - TestRtpStreamListener testRtpStreamListener1; + TestRtpStreamListener testRtpStreamListener; RtpStream::Params params1; @@ -392,13 +386,14 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") params1.mimeType.type = RTC::RtpCodecMimeType::Type::VIDEO; std::string mid; - auto* stream = new RtpStreamSend(&testRtpStreamListener1, params1, mid); + auto stream = std::make_unique(&testRtpStreamListener, params1, mid); // Receive all the packets. - SendRtpPacket({ { stream, params1.ssrc } }, packet1); - SendRtpPacket({ { stream, params1.ssrc } }, packet2); + SendRtpPacket({ { stream.get(), params1.ssrc } }, packet1.get()); + SendRtpPacket({ { stream.get(), params1.ssrc } }, packet2.get()); + SendRtpPacket({ { stream.get(), params1.ssrc } }, packet3.get()); - // Create a NACK item that request for all the packets. + // Create a NACK item that requests for all packets. RTCP::FeedbackRtpNackPacket nackPacket(0, params1.ssrc); auto* nackItem = new RTCP::FeedbackRtpNackItem(21006, 0b0000000000000001); @@ -410,15 +405,56 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") // Process the NACK packet on stream1. stream->ReceiveNack(&nackPacket); - REQUIRE(testRtpStreamListener1.retransmittedPackets.size() == 1); + REQUIRE(testRtpStreamListener.retransmittedPackets.size() == 1); - auto rtxPacket2 = testRtpStreamListener1.retransmittedPackets[0]; + auto* rtxPacket2 = testRtpStreamListener.retransmittedPackets[0]; - testRtpStreamListener1.retransmittedPackets.clear(); + testRtpStreamListener.retransmittedPackets.clear(); CheckRtxPacket(rtxPacket2, packet2->GetSequenceNumber(), packet2->GetTimestamp()); + } - delete stream; + SECTION("packets get removed from the retransmission buffer if seq number of the stream is reset") + { + // This scenario reproduce the "too bad sequence number" and "bad sequence + // number" scenarios in RtpStream::UpdateSeq(). + auto packet1(CreateRtpPacket(rtpBuffer1, sizeof(rtpBuffer1), 50001, 1000001)); + auto packet2(CreateRtpPacket(rtpBuffer2, sizeof(rtpBuffer2), 50002, 1000002)); + // Third packet has bad sequence number (its seq is more than MaxDropout=3000 + // older than current max seq) and will be dropped. + auto packet3(CreateRtpPacket(rtpBuffer3, sizeof(rtpBuffer3), 40003, 1000003)); + // Forth packet has seq=badSeq+1 so will be accepted and will trigger a + // stream reset. + auto packet4(CreateRtpPacket(rtpBuffer4, sizeof(rtpBuffer4), 40004, 1000004)); + + // Create a RtpStreamSend instance. + TestRtpStreamListener testRtpStreamListener; + + RtpStream::Params params1; + + params1.ssrc = 1111; + params1.clockRate = 90000; + params1.useNack = true; + params1.mimeType.type = RTC::RtpCodecMimeType::Type::VIDEO; + + std::string mid; + auto stream = std::make_unique(&testRtpStreamListener, params1, mid); + + SendRtpPacket({ { stream.get(), params1.ssrc } }, packet1.get()); + SendRtpPacket({ { stream.get(), params1.ssrc } }, packet2.get()); + SendRtpPacket({ { stream.get(), params1.ssrc } }, packet3.get()); + SendRtpPacket({ { stream.get(), params1.ssrc } }, packet4.get()); + + // Create a NACK item that requests for packets 1 and 2. + RTCP::FeedbackRtpNackPacket nackPacket2(0, params1.ssrc); + auto* nackItem2 = new RTCP::FeedbackRtpNackItem(50001, 0b0000000000000001); + + nackPacket2.AddItem(nackItem2); + + // Process the NACK packet on stream1. + stream->ReceiveNack(&nackPacket2); + + REQUIRE(testRtpStreamListener.retransmittedPackets.size() == 0); } #ifdef PERFORMANCE_TEST @@ -435,7 +471,7 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") params.mimeType.type = RTC::RtpCodecMimeType::Type::VIDEO; std::string mid; - auto* stream = new RtpStreamSend(&testRtpStreamListener, params, mid); + std::unique_ptr stream1(new RtpStreamSend(&testRtpStreamListener, params, mid)); size_t iterations = 10000000; @@ -449,16 +485,14 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") std::shared_ptr sharedPacket(packet); - stream->ReceivePacket(packet, sharedPacket); + stream1->ReceivePacket(packet, sharedPacket); } std::chrono::duration dur = std::chrono::system_clock::now() - start; std::cout << "nullptr && initialized shared_ptr: \t" << dur.count() << " seconds" << std::endl; - delete stream; - params.mimeType.type = RTC::RtpCodecMimeType::Type::AUDIO; - stream = new RtpStreamSend(&testRtpStreamListener, params, mid); + std::unique_ptr stream2(new RtpStreamSend(&testRtpStreamListener, params, mid)); start = std::chrono::system_clock::now(); @@ -470,13 +504,11 @@ SCENARIO("NACK and RTP packets retransmission", "[rtp][rtcp][nack]") auto* packet = RtpPacket::Parse(rtpBuffer1, 1500); packet->SetSsrc(1111); - stream->ReceivePacket(packet, sharedPacket); + stream2->ReceivePacket(packet, sharedPacket); } dur = std::chrono::system_clock::now() - start; std::cout << "raw && empty shared_ptr duration: \t" << dur.count() << " seconds" << std::endl; - - delete stream; } #endif } diff --git a/worker/test/src/RTC/TestSeqManager.cpp b/worker/test/src/RTC/TestSeqManager.cpp index 5c4624fe6d..9ae42b7ddd 100644 --- a/worker/test/src/RTC/TestSeqManager.cpp +++ b/worker/test/src/RTC/TestSeqManager.cpp @@ -1,19 +1,18 @@ #include "common.hpp" #include "RTC/SeqManager.hpp" -#include +#include #include #include using namespace RTC; -constexpr uint16_t kMaxNumberFor15Bits = (1 << 15) - 1; +constexpr uint16_t MaxNumberFor15Bits = (1 << 15) - 1; template struct TestSeqManagerInput { - TestSeqManagerInput( - T input, T output, bool sync = false, bool drop = false, T offset = 0, int64_t maxInput = -1) - : input(input), output(output), sync(sync), drop(drop), offset(offset), maxInput(maxInput) + TestSeqManagerInput(T input, T output, bool sync = false, bool drop = false, int64_t maxInput = -1) + : input(input), output(output), sync(sync), drop(drop), maxInput(maxInput) { } @@ -21,7 +20,6 @@ struct TestSeqManagerInput T output{ 0 }; bool sync{ false }; bool drop{ false }; - T offset{ 0 }; int64_t maxInput{ -1 }; }; @@ -31,10 +29,9 @@ void validate(SeqManager& seqManager, std::vector>& for (auto& element : inputs) { if (element.sync) + { seqManager.Sync(element.input - 1); - - if (element.offset) - seqManager.Offset(element.offset); + } if (element.drop) { @@ -48,6 +45,7 @@ void validate(SeqManager& seqManager, std::vector>& // Covert to string because otherwise Catch will print uint8_t as char. REQUIRE(std::to_string(output) == std::to_string(element.output)); + if (element.maxInput != -1) { REQUIRE(std::to_string(element.maxInput) == std::to_string(seqManager.GetMaxInput())); @@ -56,7 +54,7 @@ void validate(SeqManager& seqManager, std::vector>& } } -SCENARIO("SeqManager", "[rtc]") +SCENARIO("SeqManager", "[rtc][SeqMananger]") { SECTION("0 is greater than 65000") { @@ -178,6 +176,21 @@ SCENARIO("SeqManager", "[rtc]") validate(seqManager2, inputs); } + SECTION("receive out of order numbers with a big jump") + { + // clang-format off + std::vector> inputs = + { + { 4, 4, false, false }, + { 3, 3, false, false }, + { 65535, 65535, false, false }, + }; + // clang-format on + + SeqManager seqManager; + validate(seqManager, inputs); + } + SECTION("receive mixed numbers with a big jump, drop before jump") { // clang-format off @@ -299,14 +312,14 @@ SCENARIO("SeqManager", "[rtc]") // clang-format off std::vector> inputs = { - { 0, 0, false, false }, - { 1, 1, false, false }, - { 2, 2, false, false }, - { 80, 23, true, false, 20 }, - { 81, 24, false, false }, - { 82, 25, false, false }, - { 83, 26, false, false }, - { 84, 27, false, false } + { 0, 0, false, false }, + { 1, 1, false, false }, + { 2, 2, false, false }, + { 80, 3, true, false }, + { 81, 4, false, false }, + { 82, 5, false, false }, + { 83, 6, false, false }, + { 84, 7, false, false } }; // clang-format on @@ -553,16 +566,16 @@ SCENARIO("SeqManager", "[rtc]") // clang-format off std::vector> inputs = { - { 32762, 1, true, false, 0, 32762 }, - { 32763, 2, false, false, 0, 32763 }, - { 32764, 3, false, false, 0, 32764 }, - { 32765, 0, false, true, 0, 32765 }, - { 32766, 0, false, true, 0, 32766 }, - { 32767, 4, false, false, 0, 32767 }, - { 0, 5, false, false, 0, 0 }, - { 1, 6, false, false, 0, 1 }, - { 2, 7, false, false, 0, 2 }, - { 3, 8, false, false, 0, 3 } + { 32762, 1, true, false, 32762 }, + { 32763, 2, false, false, 32763 }, + { 32764, 3, false, false, 32764 }, + { 32765, 0, false, true, 32765 }, + { 32766, 0, false, true, 32766 }, + { 32767, 4, false, false, 32767 }, + { 0, 5, false, false, 0 }, + { 1, 6, false, false, 1 }, + { 2, 7, false, false, 2 }, + { 3, 8, false, false, 3 } }; // clang-format on @@ -575,12 +588,12 @@ SCENARIO("SeqManager", "[rtc]") // clang-format off std::vector> inputs = { - { 0, 1, true, false, 0, 0 }, + { 0, 1, true, false, 0 }, }; - for (uint16_t j = 0; j < 100; ++j) { + for (uint16_t j = 0; j < 3; ++j) { for (uint16_t i = 1; i < std::numeric_limits::max(); ++i) { - uint16_t output = i + 1; - inputs.push_back({ i, output, false, false, 0, i }); + const uint16_t output = i + 1; + inputs.emplace_back( i, output, false, false, i ); } } // clang-format on @@ -594,12 +607,12 @@ SCENARIO("SeqManager", "[rtc]") // clang-format off std::vector> inputs = { - { 0, 1, true, false, 0, 0 }, + { 0, 1, true, false, 0, }, }; - for (uint16_t j = 0; j < 100; ++j) { - for (uint16_t i = 1; i < kMaxNumberFor15Bits; ++i) { - uint16_t output = i + 1; - inputs.push_back({ i, output, false, false, 0, i }); + for (uint16_t j = 0; j < 3; ++j) { + for (uint16_t i = 1; i < MaxNumberFor15Bits; ++i) { + const uint16_t output = i + 1; + inputs.emplace_back( i, output, false, false, i ); } } // clang-format on @@ -607,4 +620,814 @@ SCENARIO("SeqManager", "[rtc]") SeqManager seqManager; validate(seqManager, inputs); } + + SECTION("should produce same output for same old input before drop (15 bits range)") + { + // clang-format off + std::vector> inputs = + { + { 10, 1, true, false }, // sync. + { 11, 2, false, false }, + { 12, 3, false, false }, + { 13, 4, false, false }, + { 14, 0, false, true }, // drop. + { 15, 5, false, false }, + { 12, 3, false, false } + }; + // clang-format on + + SeqManager seqManager; + validate(seqManager, inputs); + } + + SECTION("should properly clean previous cycle drops") + { + // clang-format off + std::vector> inputs = + { + { 1, 1, false, false }, + { 2, 0, false, true }, // Drop. + { 3, 2, false, false }, + { 4, 3, false, false }, + { 5, 4, false, false }, + { 6, 5, false, false }, + { 7, 6, false, false }, + { 0, 7, false, false }, + { 1, 0, false, false }, + { 2, 1, false, false }, + { 3, 2, false, false } + }; + // clang-format on + + SeqManager seqManager; + validate(seqManager, inputs); + } + + SECTION("dropped inputs to be removed going out of range, 1.") + { + // clang-format off + std::vector> inputs = + { + { 36964, 36964, false, false }, + { 25923, 0, false, true }, // Drop. + { 25701, 25701, false, false }, + { 17170, 0, false, true }, // Drop. + { 25923, 25923, false, false }, + { 4728, 0, false, true }, // Drop. + { 17170, 17170, false, false }, + { 30738, 0, false, true }, // Drop. + { 4728, 4728, false, false }, + { 4806, 0, false, true }, // Drop. + { 30738, 30738, false, false }, + { 50886, 0, false, true }, // Drop. + { 4806, 4805, false, false }, // Previously dropped. + { 50774, 0, false, true }, // Drop. + { 50886, 4805, false, false }, // Previously dropped. + { 22136, 0, false, true }, // Drop. + { 50774, 50773, false, false }, + { 30910, 0, false, true }, // Drop. + { 22136, 50773, false, false }, // Previously dropped. + { 48862, 0, false, true }, // Drop. + { 30910, 30909, false, false }, + { 56832, 0, false, true }, // Drop. + { 48862, 48861, false, false }, + { 2, 0, false, true }, // Drop. + { 56832, 48861, false, false }, // Previously dropped. + { 530, 0, false, true }, // Drop. + { 2, 48861, false, false }, // Previously dropped. + }; + // clang-format on + + SeqManager seqManager; + validate(seqManager, inputs); + } + + SECTION("dropped inputs to be removed go out of range, 2.") + { + // clang-format off + std::vector> inputs = + { + { 36960, 36960, false, false }, + { 3328, 0, false, true }, // Drop. + { 24589, 24588, false, false }, + { 120, 0, false, true }, // Drop. + { 3328, 24588, false, false }, // Previously dropped. + { 30848, 0, false, true }, // Drop. + { 120, 120, false, false }, + }; + // clang-format on + + SeqManager seqManager; + validate(seqManager, inputs); + } + + SECTION("dropped inputs to be removed go out of range, 3.") + { + // clang-format off + std::vector> inputs = + { + { 36964, 36964, false, false }, + { 65396 , 0, false, true }, // Drop. + { 25855, 25854, false, false }, + { 29793 , 0, false, true }, // Drop. + { 65396, 25854, false, false }, // Previously dropped. + { 25087, 0, false, true }, // Drop. + { 29793, 25854, false, false }, // Previously dropped. + { 65535 , 0, false, true }, // Drop. + { 25087, 25086, false, false }, + }; + // clang-format on + + SeqManager seqManager; + validate(seqManager, inputs); + } + + SECTION("receive ordered numbers, no sync, no drop (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 0, 1000, false, false }, + { 1, 1001, false, false }, + { 2, 1002, false, false }, + { 3, 1003, false, false }, + { 4, 1004, false, false }, + { 5, 1005, false, false }, + { 6, 1006, false, false }, + { 7, 1007, false, false }, + { 8, 1008, false, false }, + { 9, 1009, false, false }, + { 10, 1010, false, false }, + { 11, 1011, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 1000u); + SeqManager seqManager2(/*initialOutput*/ 1000u); + validate(seqManager, inputs); + validate(seqManager2, inputs); + } + + SECTION("receive ordered numbers, sync, no drop (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 0, 2000, false, false }, + { 1, 2001, false, false }, + { 2, 2002, false, false }, + { 80, 2003, true, false }, + { 81, 2004, false, false }, + { 82, 2005, false, false }, + { 83, 2006, false, false }, + { 84, 2007, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 2000u); + SeqManager seqManager2(/*initialOutput*/ 2000u); + validate(seqManager, inputs); + validate(seqManager2, inputs); + } + + SECTION("receive ordered numbers, sync, drop (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 0, 3000, false, false }, + { 1, 3001, false, false }, + { 2, 3002, false, false }, + { 3, 3003, false, false }, + { 4, 3004, true, false }, // sync. + { 5, 3005, false, false }, + { 6, 3006, false, false }, + { 7, 3007, true, false }, // sync. + { 8, 3000, false, true }, // drop. + { 9, 3008, false, false }, + { 11, 3000, false, true }, // drop. + { 10, 3009, false, false }, + { 12, 3010, false, false }, + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 3000u); + SeqManager seqManager2(/*initialOutput*/ 3000u); + validate(seqManager, inputs); + validate(seqManager2, inputs); + } + + SECTION("receive ordered wrapped numbers (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 65533, 997, false, false }, + { 65534, 998, false, false }, + { 65535, 999, false, false }, + { 0, 1000, false, false }, + { 1, 1001, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 1000u); + validate(seqManager, inputs); + } + + SECTION("receive sequence numbers with a big jump (with initial output)") + { + // clang-format off + std::vector> inputs1 = + { + { 0, 32000, false, false }, + { 1, 32001, false, false }, + { 1000, 33000, false, false }, + { 1001, 33001, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 32000u); + validate(seqManager, inputs1); + + // clang-format off + std::vector> inputs2 = + { + { 0, 32000, false, false }, + { 1, 32001, false, false }, + { 1000, 232, false, false }, + { 1001, 233, false, false } + }; + // clang-format on + + SeqManager seqManager2(/*initialOutput*/ 32000u); + validate(seqManager2, inputs2); + } + + SECTION("receive out of order numbers with a big jump (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 4, 1004, false, false }, + { 3, 1003, false, false }, + { 65535, 999, false, false }, + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 1000u); + validate(seqManager, inputs); + } + + SECTION("receive mixed numbers with a big jump, drop before jump (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 0, 1000, false, false }, + { 1, 1000, false, true }, // drop. + { 100, 1099, false, false }, + { 100, 1099, false, false }, + { 103, 1000, false, true }, // drop. + { 101, 1100, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 1000); + SeqManager seqManager2(/*initialOutput*/ 1000); + validate(seqManager, inputs); + validate(seqManager2, inputs); + } + + SECTION("receive mixed numbers with a big jump, drop after jump (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 0, 2000, false, false }, + { 1, 2001, false, false }, + { 100, 2000, false, true }, // drop. + { 103, 2000, false, true }, // drop. + { 101, 2100, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 2000); + SeqManager seqManager2(/*initialOutput*/ 2000); + validate(seqManager, inputs); + validate(seqManager2, inputs); + } + + SECTION("drop, receive numbers newer and older than the one dropped (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 0, 2000, false, false }, + { 2, 2000, false, true }, // drop. + { 3, 2002, false, false }, + { 4, 2003, false, false }, + { 1, 2001, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 2000); + SeqManager seqManager2(/*initialOutput*/ 2000); + validate(seqManager, inputs); + validate(seqManager2, inputs); + } + + SECTION("receive mixed numbers, sync, drop (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 0, 10000, false, false }, + { 1, 10001, false, false }, + { 2, 10002, false, false }, + { 3, 10003, false, false }, + { 7, 10007, false, false }, + { 6, 10000, false, true }, // drop. + { 8, 10008, false, false }, + { 10, 10010, false, false }, + { 9, 10009, false, false }, + { 11, 10011, false, false }, + { 0, 10012, true, false }, // sync. + { 2, 10014, false, false }, + { 3, 10015, false, false }, + { 4, 10016, false, false }, + { 5, 10017, false, false }, + { 6, 10018, false, false }, + { 7, 10019, false, false }, + { 8, 10020, false, false }, + { 9, 10021, false, false }, + { 10, 10022, false, false }, + { 9, 10000, false, true }, // drop. + { 61, 10023, true, false }, // sync. + { 62, 10024, false, false }, + { 63, 10025, false, false }, + { 64, 10026, false, false }, + { 65, 10027, false, false }, + { 11, 10028, true, false }, // sync. + { 12, 10029, false, false }, + { 13, 10030, false, false }, + { 14, 10031, false, false }, + { 15, 10032, false, false }, + { 1, 10033, true, false }, // sync. + { 2, 10034, false, false }, + { 3, 10035, false, false }, + { 4, 10036, false, false }, + { 5, 10037, false, false }, + { 65533, 10038, true, false }, // sync. + { 65534, 10039, false, false }, + { 65535, 10040, false, false }, + { 0, 10041, true, false }, // sync. + { 1, 10042, false, false }, + { 3, 10000, false, true }, // drop. + { 4, 10044, false, false }, + { 5, 10045, false, false }, + { 6, 10046, false, false }, + { 7, 10047, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 10000); + validate(seqManager, inputs); + } + + SECTION("receive ordered numbers, sync, no drop, increase input (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 0, 1, false, false }, + { 1, 2, false, false }, + { 2, 3, false, false }, + { 80, 4, true, false }, + { 81, 5, false, false }, + { 82, 6, false, false }, + { 83, 7, false, false }, + { 84, 8, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 1); + SeqManager seqManager2(/*initialOutput*/ 1); + validate(seqManager, inputs); + validate(seqManager2, inputs); + } + + SECTION("drop many inputs at the beginning (using uint16_t) (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 1, 1001, false, false }, + { 2, 1000, false, true }, // drop. + { 3, 1000, false, true }, // drop. + { 4, 1000, false, true }, // drop. + { 5, 1000, false, true }, // drop. + { 6, 1000, false, true }, // drop. + { 7, 1000, false, true }, // drop. + { 8, 1000, false, true }, // drop. + { 9, 1000, false, true }, // drop. + { 120, 1112, false, false }, + { 121, 1113, false, false }, + { 122, 1114, false, false }, + { 123, 1115, false, false }, + { 124, 1116, false, false }, + { 125, 1117, false, false }, + { 126, 1118, false, false }, + { 127, 1119, false, false }, + { 128, 1120, false, false }, + { 129, 1121, false, false }, + { 130, 1122, false, false }, + { 131, 1123, false, false }, + { 132, 1124, false, false }, + { 133, 1125, false, false }, + { 134, 1126, false, false }, + { 135, 1127, false, false }, + { 136, 1128, false, false }, + { 137, 1129, false, false }, + { 138, 1130, false, false }, + { 139, 1131, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 1000); + SeqManager seqManager2(/*initialOutput*/ 1000); + validate(seqManager, inputs); + validate(seqManager2, inputs); + } + + SECTION("drop many inputs at the beginning (using uint8_t) (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 1, 201, false, false }, + { 2, 200, false, true }, // drop. + { 3, 200, false, true }, // drop. + { 4, 200, false, true }, // drop. + { 5, 200, false, true }, // drop. + { 6, 200, false, true }, // drop. + { 7, 200, false, true }, // drop. + { 8, 200, false, true }, // drop. + { 9, 200, false, true }, // drop. + { 120, 56, false, false }, + { 121, 57, false, false }, + { 122, 58, false, false }, + { 123, 59, false, false }, + { 124, 60, false, false }, + { 125, 61, false, false }, + { 126, 62, false, false }, + { 127, 63, false, false }, + { 128, 64, false, false }, + { 129, 65, false, false }, + { 130, 66, false, false }, + { 131, 67, false, false }, + { 132, 68, false, false }, + { 133, 69, false, false }, + { 134, 70, false, false }, + { 135, 71, false, false }, + { 136, 72, false, false }, + { 137, 73, false, false }, + { 138, 74, false, false }, + { 139, 75, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 200); + validate(seqManager, inputs); + } + + SECTION("receive mixed numbers, sync, drop in range 15 (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 0, 100, false, false }, + { 1, 101, false, false }, + { 2, 102, false, false }, + { 3, 103, false, false }, + { 7, 107, false, false }, + { 6, 100, false, true }, // drop. + { 8, 108, false, false }, + { 10, 110, false, false }, + { 9, 109, false, false }, + { 11, 111, false, false }, + { 0, 112, true, false }, // sync. + { 2, 114, false, false }, + { 3, 115, false, false }, + { 4, 116, false, false }, + { 5, 117, false, false }, + { 6, 118, false, false }, + { 7, 119, false, false }, + { 8, 120, false, false }, + { 9, 121, false, false }, + { 10, 122, false, false }, + { 9, 100, false, true }, // drop. + { 61, 123, true, false }, // sync. + { 62, 124, false, false }, + { 63, 125, false, false }, + { 64, 126, false, false }, + { 65, 127, false, false }, + { 11, 128, true, false }, // sync. + { 12, 129, false, false }, + { 13, 130, false, false }, + { 14, 131, false, false }, + { 15, 132, false, false }, + { 1, 133, true, false }, // sync. + { 2, 134, false, false }, + { 3, 135, false, false }, + { 4, 136, false, false }, + { 5, 137, false, false }, + { 32767, 138, true, false }, // sync. + { 32768, 139, false, false }, + { 32769, 140, false, false }, + { 0, 141, true, false }, // sync. + { 1, 142, false, false }, + { 3, 100, false, true }, // drop. + { 4, 144, false, false }, + { 5, 145, false, false }, + { 6, 146, false, false }, + { 7, 147, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 100); + validate(seqManager, inputs); + } + + SECTION("drop many inputs at the beginning (using uint16_t with high values) (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 1, 201, false, false }, + { 2, 200, false, true }, // drop. + { 3, 200, false, true }, // drop. + { 4, 200, false, true }, // drop. + { 5, 200, false, true }, // drop. + { 6, 200, false, true }, // drop. + { 7, 200, false, true }, // drop. + { 8, 200, false, true }, // drop. + { 9, 200, false, true }, // drop. + { 32768, 32960, false, false }, + { 32769, 32961, false, false }, + { 32770, 32962, false, false }, + { 32771, 32963, false, false }, + { 32772, 32964, false, false }, + { 32773, 32965, false, false }, + { 32774, 32966, false, false }, + { 32775, 32967, false, false }, + { 32776, 32968, false, false }, + { 32777, 32969, false, false }, + { 32778, 32970, false, false }, + { 32779, 32971, false, false }, + { 32780, 32972, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 200); + validate(seqManager, inputs); + } + + SECTION("sync and drop some input near max-value (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 65530, 201, true, false }, + { 65531, 202, false, false }, + { 65532, 203, false, false }, + { 65533, 200, false, true }, + { 65534, 200, false, true }, + { 65535, 204, false, false }, + { 0, 205, false, false }, + { 1, 206, false, false }, + { 2, 207, false, false }, + { 3, 208, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 200); + validate(seqManager, inputs); + } + + SECTION( + "drop many inputs at the beginning (using uint16_t range 15 with high values) (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 1, 101, false, false }, + { 2, 100, false, true }, // drop. + { 3, 100, false, true }, // drop. + { 4, 100, false, true }, // drop. + { 5, 100, false, true }, // drop. + { 6, 100, false, true }, // drop. + { 7, 100, false, true }, // drop. + { 8, 100, false, true }, // drop. + { 9, 100, false, true }, // drop. + { 16384, 16476, false, false }, + { 16385, 16477, false, false }, + { 16386, 16478, false, false }, + { 16387, 16479, false, false }, + { 16388, 16480, false, false }, + { 16389, 16481, false, false }, + { 16390, 16482, false, false }, + { 16391, 16483, false, false }, + { 16392, 16484, false, false }, + { 16393, 16485, false, false }, + { 16394, 16486, false, false }, + { 16395, 16487, false, false }, + { 16396, 16488, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 100); + validate(seqManager, inputs); + } + + SECTION("sync and drop some input near max-value in a 15bit range (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 32762, 101, true, false, 32762 }, + { 32763, 102, false, false, 32763 }, + { 32764, 103, false, false, 32764 }, + { 32765, 100, false, true, 32765 }, + { 32766, 100, false, true, 32766 }, + { 32767, 104, false, false, 32767 }, + { 0, 105, false, false, 0 }, + { 1, 106, false, false, 1 }, + { 2, 107, false, false, 2 }, + { 3, 108, false, false, 3 } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 100); + validate(seqManager, inputs); + } + + SECTION("should update all values during multiple roll overs (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 0, 101, true, false, 0 }, + }; + for (uint16_t j = 0; j < 3; ++j) { + for (uint16_t i = 1; i < std::numeric_limits::max(); ++i) { + const uint16_t output = (i + 1 + 100) & std::numeric_limits::max(); + inputs.emplace_back( i, output, false, false, i ); + } + } + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 100); + validate(seqManager, inputs); + } + + SECTION("should update all values during multiple roll overs (15 bits range) (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 0, 101, true, false, 0, }, + }; + for (uint16_t j = 0; j < 3; ++j) { + for (uint16_t i = 1; i < MaxNumberFor15Bits; ++i) { + const uint16_t output = (i + 1 + 100) & MaxNumberFor15Bits; + inputs.emplace_back( i, output, false, false, i ); + } + } + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 100); + validate(seqManager, inputs); + } + + SECTION( + "should produce same output for same old input before drop (15 bits range) (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 10, 10001, true, false }, // sync. + { 11, 10002, false, false }, + { 12, 10003, false, false }, + { 13, 10004, false, false }, + { 14, 10000, false, true }, // drop. + { 15, 10005, false, false }, + { 12, 10003, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 10000); + validate(seqManager, inputs); + } + + SECTION("should properly clean previous cycle drops (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 1, 3, false, false }, + { 2, 2, false, true }, // Drop. + { 3, 4, false, false }, + { 4, 5, false, false }, + { 5, 6, false, false }, + { 6, 7, false, false }, + { 7, 0, false, false }, + { 0, 1, false, false }, + { 1, 2, false, false }, + { 2, 3, false, false }, + { 3, 4, false, false } + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 2); + validate(seqManager, inputs); + } + + SECTION("dropped inputs to be removed going out of range, 1. (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 36964, 46964, false, false }, + { 25923, 10000, false, true }, // Drop. + { 25701, 35701, false, false }, + { 17170, 10000, false, true }, // Drop. + { 25923, 35923, false, false }, + { 4728, 10000, false, true }, // Drop. + { 17170, 27170, false, false }, + { 30738, 10000, false, true }, // Drop. + { 4728, 14728, false, false }, + { 4806, 10000, false, true }, // Drop. + { 30738, 40738, false, false }, + { 50886, 10000, false, true }, // Drop. + { 4806, 14805, false, false }, // Previously dropped. + { 50774, 10000, false, true }, // Drop. + { 50886, 14805, false, false }, // Previously dropped. + { 22136, 10000, false, true }, // Drop. + { 50774, 60773, false, false }, + { 30910, 10000, false, true }, // Drop. + { 22136, 60773, false, false }, // Previously dropped. + { 48862, 10000, false, true }, // Drop. + { 30910, 40909, false, false }, + { 56832, 10000, false, true }, // Drop. + { 48862, 58861, false, false }, + { 2, 10000, false, true }, // Drop. + { 56832, 58861, false, false }, // Previously dropped. + { 530, 10000, false, true }, // Drop. + { 2, 58861, false, false }, // Previously dropped. + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 10000); + validate(seqManager, inputs); + } + + SECTION("dropped inputs to be removed go out of range, 2. (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 36960, 37060, false, false }, + { 3328, 100, false, true }, // Drop. + { 24589, 24688, false, false }, + { 120, 100, false, true }, // Drop. + { 3328, 24688, false, false }, // Previously dropped. + { 30848, 100, false, true }, // Drop. + { 120, 220, false, false }, + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 100); + validate(seqManager, inputs); + } + + SECTION("dropped inputs to be removed go out of range, 3. (with initial output)") + { + // clang-format off + std::vector> inputs = + { + { 36964, 37964, false, false }, + { 65396 , 1000, false, true }, // Drop. + { 25855, 26854, false, false }, + { 29793 , 1000, false, true }, // Drop. + { 65396, 26854, false, false }, // Previously dropped. + { 25087, 1000, false, true }, // Drop. + { 29793, 26854, false, false }, // Previously dropped. + { 65535 , 1000, false, true }, // Drop. + { 25087, 26086, false, false }, + }; + // clang-format on + + SeqManager seqManager(/*initialOutput*/ 1000); + validate(seqManager, inputs); + } } diff --git a/worker/test/src/RTC/TestTransportCongestionControlServer.cpp b/worker/test/src/RTC/TestTransportCongestionControlServer.cpp new file mode 100644 index 0000000000..3a02b433ba --- /dev/null +++ b/worker/test/src/RTC/TestTransportCongestionControlServer.cpp @@ -0,0 +1,254 @@ +#include "common.hpp" +#include "DepLibUV.hpp" +#include "RTC/TransportCongestionControlServer.hpp" +#include + +using namespace RTC; + +struct TestTransportCongestionControlServerInput +{ + uint16_t wideSeqNumber; + uint64_t nowMs; +}; + +struct TestTransportCongestionControlServerResult +{ + uint16_t wideSeqNumber; + bool received; + uint64_t timestamp; +}; + +using TestResults = std::deque>; + +class TestTransportCongestionControlServerListener : public TransportCongestionControlServer::Listener +{ +public: + virtual void OnTransportCongestionControlServerSendRtcpPacket( + RTC::TransportCongestionControlServer* tccServer, RTC::RTCP::Packet* packet) override + { + auto* tccPacket = dynamic_cast(packet); + + if (!tccPacket) + { + return; + } + + auto packetResults = tccPacket->GetPacketResults(); + + REQUIRE(!this->results.empty()); + + auto testResults = this->results.front(); + this->results.pop_front(); + + REQUIRE(testResults.size() == packetResults.size()); + + auto packetResultIt = packetResults.begin(); + auto testResultIt = testResults.begin(); + + for (; packetResultIt != packetResults.end() && testResultIt != testResults.end(); + ++packetResultIt, ++testResultIt) + { + REQUIRE(packetResultIt->sequenceNumber == testResultIt->wideSeqNumber); + REQUIRE(packetResultIt->received == testResultIt->received); + + if (packetResultIt->received) + { + REQUIRE(packetResultIt->receivedAtMs == testResultIt->timestamp); + } + } + } + +public: + void SetResults(TestResults& results) + { + this->results = results; + } + + void Check() + { + REQUIRE(this->results.empty()); + } + +private: + TestResults results; +}; + +// clang-format off +uint8_t buffer[] = +{ + 0x90, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x05, + 0xbe, 0xde, 0x00, 0x01, // Header Extensions + 0x51, 0x60, 0xee, 0x00 // TCC Feedback +}; +// clang-format on + +void validate(std::vector& inputs, TestResults& results) +{ + TestTransportCongestionControlServerListener listener; + auto tccServer = + TransportCongestionControlServer(&listener, RTC::BweType::TRANSPORT_CC, RTC::MtuSize); + + tccServer.SetMaxIncomingBitrate(150000); + tccServer.TransportConnected(); + + std::unique_ptr packet{ RtpPacket::Parse(buffer, sizeof(buffer)) }; + + packet->SetTransportWideCc01ExtensionId(5); + packet->SetSequenceNumber(1); + + // Save results. + listener.SetResults(results); + + uint64_t startTs = inputs[0].nowMs; + uint64_t TransportCcFeedbackSendInterval{ 100u }; // In ms. + + for (auto input : inputs) + { + // Periodic sending TCC packets. + uint64_t diffTs = input.nowMs - startTs; + + if (diffTs >= TransportCcFeedbackSendInterval) + { + tccServer.FillAndSendTransportCcFeedback(); + startTs = input.nowMs; + } + + packet->UpdateTransportWideCc01(input.wideSeqNumber); + tccServer.IncomingPacket(input.nowMs, packet.get()); + } + + tccServer.FillAndSendTransportCcFeedback(); + listener.Check(); +}; + +SCENARIO("TransportCongestionControlServer", "[rtp]") +{ + SECTION("normal time and sequence") + { + // clang-format off + std::vector inputs + { + { 1u, 1000u }, + { 2u, 1050u }, + { 3u, 1100u }, + { 4u, 1150u }, + { 5u, 1200u }, + }; + + TestResults results + { + { + { 1u, true, 1000u }, + { 2u, true, 1050u }, + }, + { + { 3u, true, 1100u }, + { 4u, true, 1150u }, + }, + { + { 5u, true, 1200u }, + }, + }; + // clang-format on + + validate(inputs, results); + } + + SECTION("lost packets") + { + // clang-format off + std::vector inputs + { + { 1u, 1000u }, + { 3u, 1050u }, + { 5u, 1100u }, + { 6u, 1150u }, + }; + + TestResults results + { + { + { 1u, true, 1000u }, + { 2u, false, 0u }, + { 3u, true, 1050u }, + }, + { + { 4u, false, 0u }, + { 5u, true, 1100u }, + { 6u, true, 1150u }, + }, + }; + // clang-format on + + validate(inputs, results); + } + + SECTION("duplicate packets") + { + // clang-format off + std::vector inputs + { + { 1u, 1000u }, + { 1u, 1050u }, + { 2u, 1100u }, + { 3u, 1150u }, + { 3u, 1200u }, + { 4u, 1250u }, + }; + + TestResults results + { + { + { 1u, true, 1000u }, + }, + { + { 2u, true, 1100u }, + { 3u, true, 1150u }, + }, + { + { 4u, true, 1250u }, + }, + }; + // clang-format on + + validate(inputs, results); + } + + SECTION("packets arrive out of order") + { + // clang-format off + std::vector inputs + { + { 1u, 1000u }, + { 2u, 1050u }, + { 4u, 1100u }, + { 5u, 1150u }, + { 3u, 1200u }, // Out of order + { 6u, 1250u }, + }; + + TestResults results + { + { + { 1u, true, 1000u }, + { 2u, true, 1050u }, + }, + { + { 3u, false, 0u }, + { 4u, true, 1100u }, + { 5u, true, 1150u }, + }, + { + { 3u, true, 1200u }, + { 4u, true, 1100u }, + { 5u, true, 1150u }, + { 6u, true, 1250u }, + }, + }; + // clang-format on + + validate(inputs, results); + } +} diff --git a/worker/test/src/RTC/TestTrendCalculator.cpp b/worker/test/src/RTC/TestTrendCalculator.cpp index f4dcf09591..bd466e63a7 100644 --- a/worker/test/src/RTC/TestTrendCalculator.cpp +++ b/worker/test/src/RTC/TestTrendCalculator.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "RTC/TrendCalculator.hpp" -#include +#include using namespace RTC; diff --git a/worker/test/src/Utils/TestBits.cpp b/worker/test/src/Utils/TestBits.cpp index e2c95c0450..f388e4cb63 100644 --- a/worker/test/src/Utils/TestBits.cpp +++ b/worker/test/src/Utils/TestBits.cpp @@ -1,8 +1,6 @@ #include "common.hpp" #include "Utils.hpp" -#include - -using namespace Utils; +#include SCENARIO("Utils::Bits::CountSetBits()") { diff --git a/worker/test/src/Utils/TestByte.cpp b/worker/test/src/Utils/TestByte.cpp new file mode 100644 index 0000000000..43cc193ed5 --- /dev/null +++ b/worker/test/src/Utils/TestByte.cpp @@ -0,0 +1,61 @@ +#include "common.hpp" +#include "Utils.hpp" +#include + +SCENARIO("Utils::Byte") +{ + // NOTE: The setup and teardown are implicit in how Catch2 works, meaning that + // this buffer is initialized before each SECTION below. + // Docs: https://github.com/catchorg/Catch2/blob/devel/docs/tutorial.md#test-cases-and-sections + + // clang-format off + uint8_t buffer[] = + { + 0b00000000, 0b00000001, 0b00000010, 0b00000011, + 0b10000000, 0b01000000, 0b00100000, 0b00010000, + 0b01111111, 0b11111111, 0b11111111, 0b00000000, + 0b11111111, 0b11111111, 0b11111111, 0b00000000, + 0b10000000, 0b00000000, 0b00000000, 0b00000000 + }; + // clang-format on + + SECTION("Utils::Byte::Get3Bytes()") + { + // Bytes 4,5 and 6 in the array are number 8405024. + REQUIRE(Utils::Byte::Get3Bytes(buffer, 4) == 8405024); + } + + SECTION("Utils::Byte::Set3Bytes()") + { + Utils::Byte::Set3Bytes(buffer, 4, 5666777); + REQUIRE(Utils::Byte::Get3Bytes(buffer, 4) == 5666777); + } + + SECTION("Utils::Byte::Get3BytesSigned()") + { + // Bytes 8, 9 and 10 in the array are number 8388607 since first bit is 0 and + // all other bits are 1, so it must be maximum positive 24 bits signed integer, + // which is Math.pow(2, 23) - 1 = 8388607. + REQUIRE(Utils::Byte::Get3BytesSigned(buffer, 8) == 8388607); + + // Bytes 12, 13 and 14 in the array are number -1. + REQUIRE(Utils::Byte::Get3BytesSigned(buffer, 12) == -1); + + // Bytes 16, 17 and 18 in the array are number -8388608 since first bit is 1 + // and all other bits are 0, so it must be minimum negative 24 bits signed + // integer, which is -1 * Math.pow(2, 23) = -8388608. + REQUIRE(Utils::Byte::Get3BytesSigned(buffer, 16) == -8388608); + } + + SECTION("Utils::Byte::Set3BytesSigned()") + { + Utils::Byte::Set3BytesSigned(buffer, 0, 8388607); + REQUIRE(Utils::Byte::Get3BytesSigned(buffer, 0) == 8388607); + + Utils::Byte::Set3BytesSigned(buffer, 0, -1); + REQUIRE(Utils::Byte::Get3BytesSigned(buffer, 0) == -1); + + Utils::Byte::Set3BytesSigned(buffer, 0, -8388608); + REQUIRE(Utils::Byte::Get3BytesSigned(buffer, 0) == -8388608); + } +} diff --git a/worker/test/src/Utils/TestIP.cpp b/worker/test/src/Utils/TestIP.cpp index b97424dafa..3b6731c443 100644 --- a/worker/test/src/Utils/TestIP.cpp +++ b/worker/test/src/Utils/TestIP.cpp @@ -1,7 +1,7 @@ #include "common.hpp" #include "MediaSoupErrors.hpp" #include "Utils.hpp" -#include +#include #include // std::memset() #ifdef _WIN32 #include @@ -10,6 +10,7 @@ #include // sockaddr_in, sockaddr_in6 #include // struct sockaddr, struct sockaddr_storage, AF_INET, AF_INET6 #endif + using namespace Utils; SCENARIO("Utils::IP::GetFamily()") @@ -126,7 +127,7 @@ SCENARIO("Utils::IP::GetAddressInfo()") sin.sin_port = htons(10251); sin.sin_addr.s_addr = inet_addr("82.99.219.114"); - auto* addr = reinterpret_cast(&sin); + const auto* addr = reinterpret_cast(&sin); int family; std::string ip; uint16_t port; diff --git a/worker/test/src/Utils/TestJson.cpp b/worker/test/src/Utils/TestJson.cpp deleted file mode 100644 index 03ab8800e1..0000000000 --- a/worker/test/src/Utils/TestJson.cpp +++ /dev/null @@ -1,105 +0,0 @@ -#include "common.hpp" -#include "Utils.hpp" -#include -#include - -using namespace Utils; -using json = nlohmann::json; - -SCENARIO("Json::IsPositiveInteger()") -{ - json jsonValue; - int intValue; - int8_t int8Value; - int32_t int32Value; - unsigned int uintValue; - uint8_t uint8Value; - uint32_t uint32Value; - float floatValue; - json jsonArray{ json::array() }; - json jsonObject{ json::object() }; - - jsonValue = 0; - REQUIRE(Json::IsPositiveInteger(jsonValue)); - - jsonValue = 1; - REQUIRE(Json::IsPositiveInteger(jsonValue)); - - jsonValue = 0u; - REQUIRE(Json::IsPositiveInteger(jsonValue)); - - jsonValue = 1u; - REQUIRE(Json::IsPositiveInteger(jsonValue)); - - jsonValue = -1; - REQUIRE(!Json::IsPositiveInteger(jsonValue)); - - intValue = 0; - REQUIRE(Json::IsPositiveInteger(intValue)); - - intValue = 1; - REQUIRE(Json::IsPositiveInteger(intValue)); - - intValue = -1; - REQUIRE(!Json::IsPositiveInteger(intValue)); - - int8Value = 0; - REQUIRE(Json::IsPositiveInteger(int8Value)); - - int8Value = 1; - REQUIRE(Json::IsPositiveInteger(int8Value)); - - int8Value = -1; - REQUIRE(!Json::IsPositiveInteger(int8Value)); - - int32Value = 0; - REQUIRE(Json::IsPositiveInteger(int32Value)); - - int32Value = 1; - REQUIRE(Json::IsPositiveInteger(int32Value)); - - int32Value = -1; - REQUIRE(!Json::IsPositiveInteger(int32Value)); - - uintValue = 0; - REQUIRE(Json::IsPositiveInteger(uintValue)); - - uintValue = 1; - REQUIRE(Json::IsPositiveInteger(uintValue)); - - uint8Value = 0; - REQUIRE(Json::IsPositiveInteger(uint8Value)); - - uint8Value = 1; - REQUIRE(Json::IsPositiveInteger(uint8Value)); - - uint32Value = 0; - REQUIRE(Json::IsPositiveInteger(uint32Value)); - - uint32Value = 1; - REQUIRE(Json::IsPositiveInteger(uint32Value)); - - floatValue = 0; - REQUIRE(!Json::IsPositiveInteger(floatValue)); - - floatValue = 0.0; - REQUIRE(!Json::IsPositiveInteger(floatValue)); - - floatValue = 1; - REQUIRE(!Json::IsPositiveInteger(floatValue)); - - floatValue = 1.1; - REQUIRE(!Json::IsPositiveInteger(floatValue)); - - floatValue = -1; - REQUIRE(!Json::IsPositiveInteger(floatValue)); - - floatValue = -1.1; - REQUIRE(!Json::IsPositiveInteger(floatValue)); - - REQUIRE(!Json::IsPositiveInteger(jsonArray)); - REQUIRE(!Json::IsPositiveInteger(jsonObject)); - REQUIRE(!Json::IsPositiveInteger(nullptr)); - REQUIRE(!Json::IsPositiveInteger(true)); - REQUIRE(!Json::IsPositiveInteger(false)); -} diff --git a/worker/test/src/Utils/TestString.cpp b/worker/test/src/Utils/TestString.cpp index 3b4dcf016d..2c02ca3323 100644 --- a/worker/test/src/Utils/TestString.cpp +++ b/worker/test/src/Utils/TestString.cpp @@ -1,6 +1,6 @@ #include "common.hpp" #include "Utils.hpp" -#include +#include #include // std::memcmp() using namespace Utils; @@ -76,40 +76,3 @@ SCENARIO("String::Base64Encode() and String::Base64Decode()") REQUIRE(outLen == sizeof(rtpPacket)); REQUIRE(std::memcmp(decodedPtr, rtpPacket, outLen) == 0); } - -SCENARIO("String::Split()", "[utils][string]") -{ - SECTION("empty string") - { - std::string foo{}; - - auto v = String::Split(foo, ','); - - REQUIRE(v.size() == 0); - } - - SECTION("character separated string with no limit") - { - std::string foo{ "a,b,c,d" }; - - auto v = String::Split(foo, ','); - - REQUIRE(v.size() == 4); - REQUIRE(v[0] == "a"); - REQUIRE(v[1] == "b"); - REQUIRE(v[2] == "c"); - REQUIRE(v[3] == "d"); - } - - SECTION("character separated string with limit") - { - std::string foo{ "a,b,c,d" }; - - auto v = String::Split(foo, ',', 2); - - REQUIRE(v.size() == 3); - REQUIRE(v[0] == "a"); - REQUIRE(v[1] == "b"); - REQUIRE(v[2] == "c,d"); - } -} diff --git a/worker/test/src/Utils/TestTime.cpp b/worker/test/src/Utils/TestTime.cpp index 25112ffd27..7fcf0d04ff 100644 --- a/worker/test/src/Utils/TestTime.cpp +++ b/worker/test/src/Utils/TestTime.cpp @@ -1,7 +1,7 @@ #include "common.hpp" #include "DepLibUV.hpp" #include "Utils.hpp" -#include +#include using namespace Utils; diff --git a/worker/test/src/tests.cpp b/worker/test/src/tests.cpp index 17241b8f7d..b42ef7ad44 100644 --- a/worker/test/src/tests.cpp +++ b/worker/test/src/tests.cpp @@ -1,5 +1,3 @@ -#define CATCH_CONFIG_RUNNER - #include "DepLibSRTP.hpp" #include "DepLibUV.hpp" #include "DepLibWebRTC.hpp" @@ -8,7 +6,7 @@ #include "LogLevel.hpp" #include "Settings.hpp" #include "Utils.hpp" -#include +#include #include // std::getenv() int main(int argc, char* argv[]) @@ -19,15 +17,32 @@ int main(int argc, char* argv[]) if (std::getenv("MS_TEST_LOG_LEVEL")) { if (std::string(std::getenv("MS_TEST_LOG_LEVEL")) == "debug") + { logLevel = LogLevel::LOG_DEBUG; + } else if (std::string(std::getenv("MS_TEST_LOG_LEVEL")) == "warn") + { logLevel = LogLevel::LOG_WARN; + } else if (std::string(std::getenv("MS_TEST_LOG_LEVEL")) == "error") + { logLevel = LogLevel::LOG_ERROR; + } } Settings::configuration.logLevel = logLevel; + // Fill logTags based by reading ENVs. + // TODO: Add more on demand. + if (std::getenv("MS_TEST_LOG_TAG_RTP")) + { + Settings::configuration.logTags.rtp = true; + } + if (std::getenv("MS_TEST_LOG_TAG_RTCP")) + { + Settings::configuration.logTags.rtcp = true; + } + // Initialize static stuff. DepLibUV::ClassInit(); DepOpenSSL::ClassInit(); @@ -36,7 +51,9 @@ int main(int argc, char* argv[]) DepLibWebRTC::ClassInit(); Utils::Crypto::ClassInit(); - int status = Catch::Session().run(argc, argv); + Catch::Session session; + + int status = session.run(argc, argv); // Free static stuff. DepLibSRTP::ClassDestroy();

$func_code$count_code
$name$count