├── .changeset ├── README.md ├── breezy-jars-boil.md ├── config.json └── lemon-ducks-smoke.md ├── .eslintrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ ├── config.yml │ └── feature_request.yaml ├── banner_dark.png ├── banner_light.png └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .npmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── LICENSES └── Apache-2.0.txt ├── NOTICE ├── README.md ├── REUSE.toml ├── examples ├── agent-dispatch │ ├── CHANGELOG.md │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── data-streams │ ├── .env.example │ ├── README.md │ ├── assets │ │ └── maybemexico.jpg │ ├── index.ts │ ├── package.json │ └── temp │ │ └── .gitignore ├── publish-wav │ ├── .env.example │ ├── .gitattributes │ ├── index.ts │ ├── package.json │ ├── speex.wav │ └── tsconfig.json ├── receive-audio │ ├── .env.example │ ├── README.md │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── rpc │ ├── .env.example │ ├── README.md │ ├── index.ts │ └── package.json ├── webhooks-http │ ├── README.md │ ├── package.json │ └── webhook.js └── webhooks-nextjs │ ├── .eslintrc │ ├── .gitignore │ ├── README.md │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── api │ │ └── webhook.ts │ └── index.tsx │ ├── public │ ├── favicon.ico │ └── vercel.svg │ ├── styles │ ├── Home.module.css │ └── globals.css │ └── tsconfig.json ├── flake.lock ├── flake.nix ├── package.json ├── packages ├── livekit-rtc │ ├── .cargo │ │ └── config.toml │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── generate_proto.sh │ ├── npm │ │ ├── darwin-arm64 │ │ │ ├── CHANGELOG.md │ │ │ ├── README.md │ │ │ └── package.json │ │ ├── darwin-x64 │ │ │ ├── CHANGELOG.md │ │ │ ├── README.md │ │ │ └── package.json │ │ ├── linux-arm64-gnu │ │ │ ├── CHANGELOG.md │ │ │ ├── README.md │ │ │ └── package.json │ │ ├── linux-x64-gnu │ │ │ ├── CHANGELOG.md │ │ │ ├── README.md │ │ │ └── package.json │ │ └── win32-x64-msvc │ │ │ ├── CHANGELOG.md │ │ │ ├── README.md │ │ │ └── package.json │ ├── package.json │ ├── src │ │ ├── audio_filter.ts │ │ ├── audio_frame.ts │ │ ├── audio_resampler.ts │ │ ├── audio_source.ts │ │ ├── audio_stream.ts │ │ ├── data_streams │ │ │ ├── index.ts │ │ │ ├── stream_reader.ts │ │ │ ├── stream_writer.ts │ │ │ └── types.ts │ │ ├── e2ee.ts │ │ ├── ffi_client.ts │ │ ├── index.ts │ │ ├── lib.rs │ │ ├── log.ts │ │ ├── napi │ │ │ ├── native.cjs │ │ │ ├── native.d.ts │ │ │ └── native.js │ │ ├── nodejs.rs │ │ ├── participant.ts │ │ ├── proto │ │ │ ├── audio_frame_pb.ts │ │ │ ├── e2ee_pb.ts │ │ │ ├── ffi_pb.ts │ │ │ ├── handle_pb.ts │ │ │ ├── participant_pb.ts │ │ │ ├── room_pb.ts │ │ │ ├── rpc_pb.ts │ │ │ ├── stats_pb.ts │ │ │ ├── track_pb.ts │ │ │ ├── track_publication_pb.ts │ │ │ └── video_frame_pb.ts │ │ ├── room.ts │ │ ├── rpc.ts │ │ ├── track.ts │ │ ├── track_publication.ts │ │ ├── transcription.ts │ │ ├── types.ts │ │ ├── utils.test.ts │ │ ├── utils.ts │ │ ├── video_frame.ts │ │ ├── video_source.ts │ │ └── video_stream.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vite.config.js └── livekit-server-sdk │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── AccessToken.test.ts │ ├── AccessToken.ts │ ├── AgentDispatchClient.ts │ ├── EgressClient.ts │ ├── IngressClient.ts │ ├── RoomServiceClient.ts │ ├── ServiceBase.ts │ ├── SipClient.ts │ ├── TwirpRPC.ts │ ├── WebhookReceiver.test.ts │ ├── WebhookReceiver.ts │ ├── crypto │ │ ├── digest.ts │ │ └── uuid.ts │ ├── grants.test.ts │ ├── grants.ts │ └── index.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── renovate.json ├── tsconfig.eslint.json ├── tsconfig.json ├── tsup.config.ts └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/breezy-jars-boil.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@livekit/rtc-node': patch 3 | --- 4 | 5 | Bump '@types/node' pkg version and remove all explicit `ReadableStream` imports 6 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json", 3 | "changelog": [ 4 | "@livekit/changesets-changelog-github", 5 | { 6 | "repo": "livekit/node-sdks" 7 | } 8 | ], 9 | "commit": false, 10 | "fixed": [["@livekit/rtc-node", "@livekit/rtc-node-*"]], 11 | "ignore": ["example-*"], 12 | "linked": [], 13 | "access": "public", 14 | "baseBranch": "main", 15 | "updateInternalDependencies": "patch" 16 | } 17 | -------------------------------------------------------------------------------- /.changeset/lemon-ducks-smoke.md: -------------------------------------------------------------------------------- 1 | --- 2 | "agent-dispatch": patch 3 | "livekit-server-sdk": patch 4 | --- 5 | 6 | Add support for destination_country in outbound trunk 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@typescript-eslint", "eslint-plugin-tsdoc"], 3 | "extends": [ 4 | "plugin:@typescript-eslint/recommended", 5 | "turbo", 6 | "prettier" 7 | ], 8 | "env": { 9 | "browser": true, 10 | "node": true 11 | }, 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2022, 15 | "ecmaFeatures": { 16 | "jsx": true 17 | } 18 | }, 19 | 20 | "rules": { 21 | "tsdoc/syntax": "warn", 22 | "space-before-function-parens": 0, 23 | "@typescript-eslint/no-unused-vars": "error", 24 | "import/export": 0, 25 | "@typescript-eslint/ban-ts-comment": "warn", 26 | "@typescript-eslint/no-empty-interface": "warn", 27 | "@typescript-eslint/consistent-type-imports": "warn", 28 | "@typescript-eslint/no-explicit-any": "warn" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | **/*.jpg filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41E Bug report" 2 | description: Report an issue with LiveKit node SDKs 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thanks for taking the time to fill out this bug report! 8 | Please report security issues by email to security@livekit.io 9 | **Before you start, make sure you have the latest versions of the affected LiveKit npm package installed** 10 | - type: dropdown 11 | id: packages 12 | attributes: 13 | label: Select which package(s) are affected 14 | multiple: true 15 | options: 16 | - \@livekit/rtc-node 17 | - livekit-server-sdk 18 | - examples 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: bug-description 23 | attributes: 24 | label: Describe the bug 25 | description: Describe what you are expecting vs. what happens instead. 26 | placeholder: | 27 | ### What I'm expecting 28 | ### What happens instead 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: reproduction 33 | attributes: 34 | label: Reproduction 35 | description: | 36 | A detailed step-by-step guide on how to reproduce the issue or (preferably) a link to a repository that reproduces the issue. 37 | Reproductions must be [short, self-contained and correct](http://sscce.org/) and must not contain files or code that aren't relevant to the issue. 38 | We will prioritize issues that include a working minimal reproduction repository. 39 | placeholder: Reproduction 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: logs 44 | attributes: 45 | label: Logs 46 | description: | 47 | "Please include logs around the time this bug occurred. 48 | Please try not to insert an image but copy paste the log text." 49 | render: shell 50 | - type: textarea 51 | id: system-info 52 | attributes: 53 | label: System Info 54 | description: | 55 | Please mention the OS (incl. Version) and exact node version on which you are seeing this issue. 56 | For ease of use you can run `npx envinfo --system --binaries --npmPackages "{livekit-*,@livekit/*}"` from within your livekit project, to give us all the needed info about your current environment` 57 | render: shell 58 | placeholder: System, Binaries, Browsers 59 | validations: 60 | required: true 61 | - type: dropdown 62 | id: livekit-version 63 | attributes: 64 | label: LiveKit server version 65 | options: 66 | - LiveKit cloud 67 | - latest self hosted version 68 | validations: 69 | required: true 70 | - type: dropdown 71 | id: severity 72 | attributes: 73 | label: Severity 74 | options: 75 | - annoyance 76 | - serious, but I can work around it 77 | - blocking an upgrade 78 | - blocking all usage of LiveKit 79 | validations: 80 | required: true 81 | - type: textarea 82 | id: additional-context 83 | attributes: 84 | label: Additional Information 85 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Slack Community Chat 4 | url: https://livekit.io/join-slack 5 | about: Ask questions and discuss with other LiveKit users in real time. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: 'Feature Request' 2 | description: Suggest an idea for this project 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thanks for taking the time to request this feature! 8 | - type: textarea 9 | id: problem 10 | attributes: 11 | label: Describe the problem 12 | description: Please provide a clear and concise description the problem this feature would solve. The more information you can provide here, the better. 13 | placeholder: I would like to be able to ... in order to solve ... 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: solution 18 | attributes: 19 | label: Describe the proposed solution 20 | description: Please provide a clear and concise description of what you would like to happen. 21 | placeholder: I would like to see ... 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: alternatives 26 | attributes: 27 | label: Alternatives considered 28 | description: "Please provide a clear and concise description of any alternative solutions or features you've considered." 29 | - type: dropdown 30 | id: importance 31 | attributes: 32 | label: Importance 33 | description: How important is this feature to you? 34 | options: 35 | - nice to have 36 | - would make my life easier 37 | - I cannot use LiveKit without it 38 | validations: 39 | required: true 40 | - type: textarea 41 | id: additional-context 42 | attributes: 43 | label: Additional Information 44 | description: Add any other context or screenshots about the feature request here. 45 | -------------------------------------------------------------------------------- /.github/banner_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit/node-sdks/23792753e9fa63113e52caa1c51f1db83fb42b78/.github/banner_dark.png -------------------------------------------------------------------------------- /.github/banner_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit/node-sdks/23792753e9fa63113e52caa1c51f1db83fb42b78/.github/banner_light.png -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | env: 3 | APP_NAME: rtc-node 4 | MACOSX_DEPLOYMENT_TARGET: '10.13' 5 | CARGO_TERM_COLOR: always 6 | 7 | on: 8 | workflow_dispatch: 9 | push: 10 | branches: 11 | - main 12 | pull_request: 13 | paths: 14 | - 'packages/**' 15 | - 'examples/**' 16 | - 'package.json' 17 | - 'pnpm-lock.yaml' 18 | - 'REUSE.toml' 19 | - '.github/workflows' 20 | branches: 21 | - main 22 | 23 | jobs: 24 | check-changes: 25 | name: Check for changes 26 | runs-on: ubuntu-latest 27 | outputs: 28 | rtc_build: ${{ steps.changes.outputs.rtc_build }} 29 | server_sdk_build: ${{ steps.changes.outputs.server_sdk_build }} 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: dorny/paths-filter@v3 33 | id: paths 34 | with: 35 | filters: | 36 | livekit-rtc: 37 | - 'packages/livekit-rtc/**' 38 | server-sdk: 39 | - 'packages/livekit-server-sdk/**' 40 | - name: Store change outputs 41 | id: changes 42 | run: | 43 | echo "rtc_build=${{ steps.paths.outputs.livekit-rtc == 'true' || github.ref == 'refs/heads/main' }}" >> $GITHUB_OUTPUT 44 | echo "server_sdk_build=${{ steps.paths.outputs.server-sdk == 'true' || github.ref == 'refs/heads/main' }}" >> $GITHUB_OUTPUT 45 | 46 | lint: 47 | name: Formatting 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v4 51 | - uses: pnpm/action-setup@v4 52 | - name: Setup Node.js 53 | uses: actions/setup-node@v4 54 | with: 55 | node-version: 20 56 | cache: pnpm 57 | - name: Install dependencies 58 | run: pnpm install 59 | - name: Lint 60 | run: pnpm lint 61 | - name: Prettier 62 | run: pnpm format:check 63 | 64 | reuse: 65 | name: REUSE-3.2 66 | runs-on: ubuntu-latest 67 | steps: 68 | - uses: actions/checkout@v4 69 | - uses: fsfe/reuse-action@v5 70 | 71 | test: 72 | name: Test 73 | strategy: 74 | matrix: 75 | node-version: [18, 20, 22, latest] 76 | runs-on: ubuntu-latest 77 | steps: 78 | - uses: actions/checkout@v4 79 | - uses: pnpm/action-setup@v4 80 | - name: Setup Node.js 81 | uses: actions/setup-node@v4 82 | with: 83 | node-version: ${{ matrix.node-version }} 84 | cache: pnpm 85 | - name: Install dependencies 86 | run: pnpm install 87 | - name: Test livekit-rtc 88 | run: pnpm --filter="livekit-rtc" test 89 | - name: Test livekit-server-sdk (Node) 90 | run: pnpm --filter="livekit-server-sdk" test 91 | - name: Test livekit-server-sdk (Browser) 92 | run: pnpm --filter="livekit-server-sdk" test:browser 93 | - name: Test env livekit-server-sdk (Edge Runtime) 94 | run: pnpm --filter="livekit-server-sdk" test:edge 95 | 96 | build: 97 | if: ${{ needs.check-changes.outputs.rtc_build == 'true' }} 98 | strategy: 99 | fail-fast: false 100 | matrix: 101 | include: 102 | - os: macos-15-large 103 | platform: macos 104 | target: x86_64-apple-darwin 105 | macosx_deployment_target: '10.15' 106 | - os: macos-15 107 | platform: macos 108 | target: aarch64-apple-darwin 109 | macosx_deployment_target: '11.0' 110 | - os: windows-latest 111 | platform: windows 112 | target: x86_64-pc-windows-msvc 113 | - os: ubuntu-latest 114 | platform: linux 115 | target: x86_64-unknown-linux-gnu 116 | build_image: quay.io/pypa/manylinux_2_28_x86_64 117 | - os: ubuntu-24.04-arm 118 | platform: linux 119 | target: aarch64-unknown-linux-gnu 120 | build_image: quay.io/pypa/manylinux_2_28_aarch64 121 | 122 | name: stable - ${{ matrix.target }} - node@20 123 | runs-on: ${{ matrix.os }} 124 | env: 125 | RUST_BACKTRACE: full 126 | needs: check-changes 127 | steps: 128 | - uses: actions/checkout@v4 129 | with: 130 | submodules: recursive 131 | 132 | - uses: pnpm/action-setup@v4 133 | 134 | - name: Setup node 135 | uses: actions/setup-node@v4 136 | if: ${{ !matrix.docker }} 137 | with: 138 | node-version: 20 139 | cache: pnpm 140 | 141 | - uses: dtolnay/rust-toolchain@stable 142 | with: 143 | toolchain: stable 144 | targets: ${{ matrix.target }} 145 | 146 | - name: Cache cargo 147 | uses: actions/cache@v4 148 | with: 149 | path: | 150 | ~/.cargo/registry/index/ 151 | ~/.cargo/registry/cache/ 152 | ~/.cargo/git/db/ 153 | packages/livekit-rtc/.cargo-cache 154 | packages/livekit-rtc/target/ 155 | key: ${{ matrix.target }}-cargo-${{ matrix.os }} 156 | 157 | - name: Install dependencies 158 | run: pnpm install 159 | 160 | - name: Build (Linux) 161 | if: ${{ matrix.platform == 'linux' }} 162 | run: | 163 | docker run --rm -v $PWD:/workspace -w /workspace ${{ matrix.build_image }} bash -c "\ 164 | uname -a; \ 165 | export PATH=/root/.cargo/bin:\$PATH; \ 166 | export RUST_BACKTRACE=full; \ 167 | yum install openssl-devel libX11-devel mesa-libGL-devel libXext-devel clang-devel -y; \ 168 | curl --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y; \ 169 | curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -; \ 170 | yum install -y nodejs --setopt=nodesource-nodejs.module_hotfixes=1; \ 171 | npm install --global pnpm && pnpm install; \ 172 | cd packages/livekit-rtc && pnpm build --target ${{ matrix.target }}" 173 | 174 | - name: Build (macOS) 175 | if: ${{ matrix.platform == 'macos' }} 176 | env: 177 | MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} 178 | run: cd packages/livekit-rtc && pnpm build --target ${{ matrix.target }} 179 | 180 | - name: Build (Windows) 181 | if: ${{ matrix.platform == 'windows' }} 182 | run: cd packages/livekit-rtc && pnpm build --target ${{ matrix.target }} 183 | 184 | - name: Upload artifact 185 | uses: actions/upload-artifact@v4 186 | if: github.event_name != 'pull-request' 187 | with: 188 | name: bindings-${{ matrix.target }} 189 | path: packages/livekit-rtc/src/napi/${{ env.APP_NAME }}.*.node 190 | if-no-files-found: error 191 | 192 | release: 193 | needs: build 194 | if: github.ref == 'refs/heads/main' 195 | name: Release 196 | runs-on: ubuntu-latest 197 | steps: 198 | - uses: actions/checkout@v4 199 | 200 | - uses: pnpm/action-setup@v4 201 | 202 | - name: Setup node 203 | uses: actions/setup-node@v4 204 | with: 205 | node-version: 20 206 | cache: pnpm 207 | 208 | - name: Install dependencies 209 | run: pnpm install 210 | 211 | - name: Build server SDK 212 | run: pnpm --filter=livekit-server-sdk build 213 | 214 | - name: Download all artifacts 215 | uses: actions/download-artifact@v4 216 | with: 217 | path: packages/livekit-rtc/artifacts 218 | 219 | - name: Move artifacts 220 | run: pnpm artifacts 221 | working-directory: packages/livekit-rtc 222 | 223 | - name: List packages 224 | run: ls -R ./packages/livekit-rtc/npm 225 | shell: bash 226 | 227 | - name: Create Release Pull Request or Publish to npm 228 | id: changesets 229 | uses: changesets/action@v1 230 | with: 231 | publish: pnpm ci:publish 232 | env: 233 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 234 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 235 | 236 | - name: Build Server SDK Docs 237 | if: steps.changesets.outputs.published == 'true' 238 | run: pnpm --filter=livekit-server-sdk build-docs 239 | 240 | - name: S3 Upload Server SDK Docs 241 | if: steps.changesets.outputs.published == 'true' 242 | run: aws s3 cp packages/livekit-server-sdk/docs/ s3://livekit-docs/server-sdk-js --recursive 243 | env: 244 | AWS_ACCESS_KEY_ID: ${{ secrets.DOCS_DEPLOY_AWS_ACCESS_KEY }} 245 | AWS_SECRET_ACCESS_KEY: ${{ secrets.DOCS_DEPLOY_AWS_API_SECRET }} 246 | AWS_DEFAULT_REGION: 'us-east-1' 247 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # macOS 76 | .DS_Store 77 | 78 | # dotenv environment variable files 79 | .env 80 | .env.development.local 81 | .env.test.local 82 | .env.production.local 83 | .env.local 84 | 85 | # parcel-bundler cache (https://parceljs.org/) 86 | .cache 87 | .parcel-cache 88 | 89 | # Next.js build output 90 | .next 91 | out 92 | 93 | # Nuxt.js build / generate output 94 | .nuxt 95 | dist 96 | 97 | # Gatsby files 98 | .cache/ 99 | # Comment in the public line in if your project uses Gatsby and not Next.js 100 | # https://nextjs.org/blog/next-9-1#public-directory-support 101 | # public 102 | 103 | # vuepress build output 104 | .vuepress/dist 105 | 106 | # vuepress v2.x temp and cache directory 107 | .temp 108 | .cache 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | # turbo 136 | .turbo 137 | 138 | # ts-docs 139 | packages/*/docs 140 | 141 | # generated version file 142 | packages/livekit-rtc/src/version.ts 143 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "livekit-rtc/rust-sdks"] 2 | path = packages/livekit-rtc/rust-sdks 3 | url = https://github.com/livekit/rust-sdks 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/dist/ 2 | .turbo 3 | .changeset 4 | .eslintrc 5 | .reuse/dep5 6 | node_modules 7 | packages/livekit-rtc/src/napi 8 | packages/livekit-rtc/src/proto 9 | packages/livekit-rtc/target 10 | packages/livekit-rtc/src/version.ts -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "semi": true, 5 | "tabWidth": 2, 6 | "printWidth": 100, 7 | "importOrder": ["", "^[./]"], 8 | "importOrderSeparation": false, 9 | "importOrderSortSpecifiers": true, 10 | "importOrderParserPlugins": ["typescript"], 11 | "plugins": ["@trivago/prettier-plugin-sort-imports"] 12 | } 13 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2022 LiveKit, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | The LiveKit icon, the name of the repository and some sample code in the background. 13 | 14 | 15 | 16 | 17 |

18 | LiveKit Node SDKs 19 |

20 | 21 | 22 | Use this SDK to add realtime video, audio and data features to your Node app. By connecting to LiveKit Cloud or a self-hosted server, you can quickly build applications such as multi-modal AI, live streaming, or video calls with just a few lines of code. 23 | 24 | 25 | ### Warning 26 | 27 | Avoid running this with hot reloads (ex. [bun's hot reload](https://bun.sh/guides/http/hot)). This is known to cause issues with WebRTC connectivity. 28 | 29 | ## Monorepo Navigation 30 | 31 | - **Packages**: 32 | - [Server SDK](/packages/livekit-server-sdk) - to interact with server APIs. 33 | - [Node realtime SDK](/packages/livekit-rtc) - to connect to LiveKit as a server-side participant, and to publish and subscribe to audio, video, and data. 34 | - **Examples** 35 | - [Webhooks HTTP (server SDK)](/examples/webhooks-http/README.md) 36 | - [Webhooks NextJS (server SDK)](/examples/webhooks-nextjs/README.md) 37 | - [Publishing to a room (realtime SDK)](/examples/publish-wav/) 38 | 39 |
40 |
41 | 42 | ## Development Setup 43 | 44 | If you are interested in contributing to the project or running the examples that are part of this mono-repository, then you must first set up your development environment. 45 | 46 | ### Setup Monorepo 47 | 48 | This repo consists of multiple packages that partly build on top of each other. 49 | It relies on pnpm workspaces and [Turborepo](https://turbo.build/repo/docs) (which gets installed automatically). 50 | 51 | Clone the repo and run `pnpm install` the root level: 52 | 53 | ```shell 54 | pnpm install 55 | ``` 56 | 57 | In order to link up initial dependencies and check whether everything has installed correctly run 58 | 59 | ```shell 60 | pnpm build 61 | ``` 62 | 63 | This will build all the packages in `/packages` and the examples in `/examples` once. 64 | 65 | After that you can use a more granular command to only rebuild the packages you are working on. 66 | 67 | ### Setup Submodules 68 | 69 | Run the following command to install the submodules. 70 | 71 | ```shell 72 | git submodule update --init --recursive 73 | ``` 74 | 75 | Then run `pnpm build` to make sure everything is up to date. 76 | 77 | 78 |
79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
LiveKit Ecosystem
LiveKit SDKsBrowser · iOS/macOS/visionOS · Android · Flutter · React Native · Rust · Node.js · Python · Unity · Unity (WebGL)
Server APIsNode.js · Golang · Ruby · Java/Kotlin · Python · Rust · PHP (community) · .NET (community)
UI ComponentsReact · Android Compose · SwiftUI
Agents FrameworksPython · Node.js · Playground
ServicesLiveKit server · Egress · Ingress · SIP
ResourcesDocs · Example apps · Cloud · Self-hosting · CLI
89 | 90 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | version = 1 6 | SPDX-PackageName = "node-sdks" 7 | SPDX-PackageSupplier = "LiveKit, Inc. " 8 | SPDX-PackageDownloadLocation = "https://github.com/livekit/node-sdks" 9 | 10 | # trivial files 11 | [[annotations]] 12 | path = [".gitignore", ".gitmodules", ".gitattributes", "flake.lock", ".github/**", "packages/livekit-rtc/.gitignore", ".changeset/**", "**/CHANGELOG.md", "NOTICE"] 13 | SPDX-FileCopyrightText = "2024 LiveKit, Inc." 14 | SPDX-License-Identifier = "Apache-2.0" 15 | 16 | # pnpm files 17 | [[annotations]] 18 | path = ["pnpm-workspace.yaml", "pnpm-lock.yaml"] 19 | SPDX-FileCopyrightText = "2024 LiveKit, Inc." 20 | SPDX-License-Identifier = "Apache-2.0" 21 | 22 | # project configuration files 23 | [[annotations]] 24 | path = ["packages/livekit-rtc/.npmignore", ".prettierrc", ".prettierignore", ".eslintrc", "**.json", ".npmrc", "**/tsup.config.ts"] 25 | SPDX-FileCopyrightText = "2024 LiveKit, Inc." 26 | SPDX-License-Identifier = "Apache-2.0" 27 | 28 | # Rust NAPI files 29 | [[annotations]] 30 | path = ["packages/livekit-rtc/Cargo.toml", "packages/livekit-rtc/Cargo.lock", "packages/livekit-rtc/.cargo/**"] 31 | SPDX-FileCopyrightText = "2024 LiveKit, Inc." 32 | SPDX-License-Identifier = "Apache-2.0" 33 | 34 | # FFI protocol files 35 | [[annotations]] 36 | path = ["packages/livekit-rtc/src/proto/**", "packages/livekit-rtc/npm/**", "packages/livekit-rtc/src/napi/native.cjs", "packages/livekit-rtc/src/napi/native.d.ts"] 37 | SPDX-FileCopyrightText = "2024 LiveKit, Inc." 38 | SPDX-License-Identifier = "Apache-2.0" 39 | 40 | # examples 41 | [[annotations]] 42 | path = "examples/**" 43 | SPDX-FileCopyrightText = "2024 LiveKit, Inc." 44 | SPDX-License-Identifier = "Apache-2.0" 45 | -------------------------------------------------------------------------------- /examples/agent-dispatch/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # agent-dispatch 2 | 3 | ## 0.0.17 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`d842f5d77840625133911447adc90dfe12f57b0f`](https://github.com/livekit/node-sdks/commit/d842f5d77840625133911447adc90dfe12f57b0f)]: 8 | - livekit-server-sdk@2.13.0 9 | 10 | ## 0.0.16 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`276546307060bc5b9e71aa9379756f2753aad224`](https://github.com/livekit/node-sdks/commit/276546307060bc5b9e71aa9379756f2753aad224)]: 15 | - livekit-server-sdk@2.12.0 16 | 17 | ## 0.0.15 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`a56c1745ab5d1c55565a7c03b0a2738a08e192f3`](https://github.com/livekit/node-sdks/commit/a56c1745ab5d1c55565a7c03b0a2738a08e192f3), [`3619521aff237c988fd452e79496147929beb673`](https://github.com/livekit/node-sdks/commit/3619521aff237c988fd452e79496147929beb673)]: 22 | - livekit-server-sdk@2.11.0 23 | 24 | ## 0.0.14 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`7661ca041f129057c2a65dc03282dbc6e620c521`](https://github.com/livekit/node-sdks/commit/7661ca041f129057c2a65dc03282dbc6e620c521)]: 29 | - livekit-server-sdk@2.10.2 30 | 31 | ## 0.0.13 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies [[`47a3ae9ede5f731fdca99b1f81cd04e962aee405`](https://github.com/livekit/node-sdks/commit/47a3ae9ede5f731fdca99b1f81cd04e962aee405)]: 36 | - livekit-server-sdk@2.10.1 37 | 38 | ## 0.0.12 39 | 40 | ### Patch Changes 41 | 42 | - Updated dependencies [[`473c46b840db85c2174934c9d47ba7059172664d`](https://github.com/livekit/node-sdks/commit/473c46b840db85c2174934c9d47ba7059172664d), [`a8702d42bbdd796d342bde9b85907f46e6ad6480`](https://github.com/livekit/node-sdks/commit/a8702d42bbdd796d342bde9b85907f46e6ad6480)]: 43 | - livekit-server-sdk@2.10.0 44 | 45 | ## 0.0.11 46 | 47 | ### Patch Changes 48 | 49 | - Updated dependencies [[`81b2d3c20adda56b4a78e9024173c53b0979c074`](https://github.com/livekit/node-sdks/commit/81b2d3c20adda56b4a78e9024173c53b0979c074)]: 50 | - livekit-server-sdk@2.9.7 51 | 52 | ## 0.0.10 53 | 54 | ### Patch Changes 55 | 56 | - Updated dependencies [[`0b54ed594b48bee6fe376a849edc46aab53470f7`](https://github.com/livekit/node-sdks/commit/0b54ed594b48bee6fe376a849edc46aab53470f7), [`430cf4acc11ead41f71ea6aed23eef1cecc842a2`](https://github.com/livekit/node-sdks/commit/430cf4acc11ead41f71ea6aed23eef1cecc842a2)]: 57 | - livekit-server-sdk@2.9.6 58 | 59 | ## 0.0.9 60 | 61 | ### Patch Changes 62 | 63 | - Updated dependencies [[`1b89326fc8076964534b4c4bb6ad879dd17cdf12`](https://github.com/livekit/node-sdks/commit/1b89326fc8076964534b4c4bb6ad879dd17cdf12)]: 64 | - livekit-server-sdk@2.9.5 65 | 66 | ## 0.0.8 67 | 68 | ### Patch Changes 69 | 70 | - Updated dependencies [[`dd5e40fb9000a335ba06a98deb251d4108fdfc55`](https://github.com/livekit/node-sdks/commit/dd5e40fb9000a335ba06a98deb251d4108fdfc55), [`628b9fd4c2250a13f4ad0205af68df1af34bdf28`](https://github.com/livekit/node-sdks/commit/628b9fd4c2250a13f4ad0205af68df1af34bdf28)]: 71 | - livekit-server-sdk@2.9.4 72 | 73 | ## 0.0.7 74 | 75 | ### Patch Changes 76 | 77 | - Updated dependencies [[`e06bd4892b840f6c42fdf421652f2077c44885da`](https://github.com/livekit/node-sdks/commit/e06bd4892b840f6c42fdf421652f2077c44885da)]: 78 | - livekit-server-sdk@2.9.3 79 | 80 | ## 0.0.6 81 | 82 | ### Patch Changes 83 | 84 | - Updated dependencies [[`64fbd7af91f5363c1a73288cb5e9d1e2cbafccc6`](https://github.com/livekit/node-sdks/commit/64fbd7af91f5363c1a73288cb5e9d1e2cbafccc6)]: 85 | - livekit-server-sdk@2.9.2 86 | 87 | ## 0.0.5 88 | 89 | ### Patch Changes 90 | 91 | - Updated dependencies [[`16a2a105404e555699e3812d85664baeedd3151d`](https://github.com/livekit/node-sdks/commit/16a2a105404e555699e3812d85664baeedd3151d), [`93c2f2edb07fe5f6b98096d8689a5c1c70694d47`](https://github.com/livekit/node-sdks/commit/93c2f2edb07fe5f6b98096d8689a5c1c70694d47), [`902f49f8ce8ddad4a7c5538559118663d8e849be`](https://github.com/livekit/node-sdks/commit/902f49f8ce8ddad4a7c5538559118663d8e849be)]: 92 | - livekit-server-sdk@2.9.1 93 | 94 | ## 0.0.4 95 | 96 | ### Patch Changes 97 | 98 | - Updated dependencies [[`5a5d298cd9e617cd297a39b5c64a38797b5ca2bf`](https://github.com/livekit/node-sdks/commit/5a5d298cd9e617cd297a39b5c64a38797b5ca2bf), [`2aa5d0927868189a7c6d6e324ca40aaf37d06115`](https://github.com/livekit/node-sdks/commit/2aa5d0927868189a7c6d6e324ca40aaf37d06115), [`1e372ed2d015d9ac36a589f231b668d0f7a62bd6`](https://github.com/livekit/node-sdks/commit/1e372ed2d015d9ac36a589f231b668d0f7a62bd6)]: 99 | - livekit-server-sdk@2.9.0 100 | 101 | ## 0.0.3 102 | 103 | ### Patch Changes 104 | 105 | - Updated dependencies [[`7a66015eea5b06c241a5df03534e11c3d9c40a8a`](https://github.com/livekit/node-sdks/commit/7a66015eea5b06c241a5df03534e11c3d9c40a8a)]: 106 | - livekit-server-sdk@2.8.1 107 | 108 | ## 0.0.2 109 | 110 | ### Patch Changes 111 | 112 | - Updated dependencies [[`e389115217261a7b1f58a7cf249f8e6605f35870`](https://github.com/livekit/node-sdks/commit/e389115217261a7b1f58a7cf249f8e6605f35870)]: 113 | - livekit-server-sdk@2.8.0 114 | -------------------------------------------------------------------------------- /examples/agent-dispatch/index.ts: -------------------------------------------------------------------------------- 1 | import { RoomAgentDispatch, RoomConfiguration } from '@livekit/protocol'; 2 | import { AccessToken, AgentDispatchClient } from 'livekit-server-sdk'; 3 | 4 | const roomName = 'my-room'; 5 | const agentName = 'test-agent'; 6 | 7 | /** 8 | * This example demonstrates how to have an agent join a room without using 9 | * the automatic dispatch. 10 | * In order to use this feature, you must have an agent running with `agentName` set 11 | * when defining your WorkerOptions. 12 | * 13 | * A dispatch requests the agent to enter a specific room with optional metadata. 14 | */ 15 | async function createExplicitDispatch() { 16 | const agentDispatchClient = new AgentDispatchClient(process.env.LIVEKIT_URL); 17 | 18 | // this will create invoke an agent with agentName: test-agent to join `my-room` 19 | const dispatch = await agentDispatchClient.createDispatch(roomName, agentName, { 20 | metadata: '{"mydata": "myvalue"}', 21 | }); 22 | console.log('created dispatch', dispatch); 23 | 24 | const dispatches = await agentDispatchClient.listDispatch(roomName); 25 | console.log(`there are ${dispatches.length} dispatches in ${roomName}`); 26 | } 27 | 28 | /** 29 | * When agent name is set, the agent will no longer be automatically dispatched 30 | * to new rooms. If you want that agent to be dispatched to a new room as soon as 31 | * the participant connects, you can set the roomConfig with the agent 32 | * definition in the access token. 33 | */ 34 | async function createTokenWithAgentDispatch(): Promise { 35 | const at = new AccessToken(); 36 | at.identity = 'my-participant'; 37 | at.addGrant({ roomJoin: true, room: roomName }); 38 | at.roomConfig = new RoomConfiguration({ 39 | agents: [ 40 | new RoomAgentDispatch({ 41 | agentName: agentName, 42 | metadata: '{"mydata": "myvalue"}', 43 | }), 44 | ], 45 | }); 46 | return await at.toJwt(); 47 | } 48 | 49 | createTokenWithAgentDispatch().then((token) => { 50 | console.log('created participant token', token); 51 | }); 52 | 53 | console.log('creating explicit dispatch'); 54 | createExplicitDispatch(); 55 | -------------------------------------------------------------------------------- /examples/agent-dispatch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agent-dispatch", 3 | "version": "0.0.17", 4 | "description": "An example demonstrating dispatching agents with an API call", 5 | "private": "true", 6 | "author": "LiveKit", 7 | "license": "Apache-2.0", 8 | "type": "module", 9 | "main": "index.ts", 10 | "scripts": { 11 | "build": "tsc --incremental", 12 | "lint": "eslint -f unix \"**/*.ts\"" 13 | }, 14 | "dependencies": { 15 | "dotenv": "^16.4.5", 16 | "livekit-server-sdk": "workspace:*", 17 | "@livekit/protocol": "^1.39.0" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^20.10.4", 21 | "tsx": "^4.7.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/agent-dispatch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2022", 4 | "esModuleInterop": true, 5 | "target": "es2022", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist" 9 | }, 10 | "lib": ["es2022"] 11 | } 12 | -------------------------------------------------------------------------------- /examples/data-streams/.env.example: -------------------------------------------------------------------------------- 1 | # 1. Copy this file and rename it to .env 2 | # 2. Update the enviroment variables below. 3 | 4 | LIVEKIT_API_KEY=mykey 5 | LIVEKIT_API_SECRET=mysecret 6 | LIVEKIT_URL=wss://myproject.livekit.cloud 7 | -------------------------------------------------------------------------------- /examples/data-streams/README.md: -------------------------------------------------------------------------------- 1 | # Data Streams Example 2 | 3 | This example demonstrates how to use DataStreams to stream and receive both text and files from other LiveKit participants. 4 | 5 | ## Prerequisites 6 | 7 | Before running this example, make sure you have: 8 | 9 | 1. Node.js installed on your machine. 10 | 2. A LiveKit server running (either locally or remotely). 11 | 3. LiveKit API key and secret. 12 | 13 | ## Setup 14 | 15 | 1. Install dependencies: 16 | 17 | ``` 18 | pnpm install 19 | ``` 20 | 21 | 2. Create a `.env.local` file in the example directory with your LiveKit credentials: 22 | ``` 23 | LIVEKIT_API_KEY=your_api_key 24 | LIVEKIT_API_SECRET=your_api_secret 25 | LIVEKIT_URL=your_livekit_url 26 | ``` 27 | 28 | ## Running the Example 29 | 30 | To run the example, use the following command: 31 | 32 | ``` 33 | pnpm run start 34 | ``` 35 | 36 | The example will log to your terminal. 37 | -------------------------------------------------------------------------------- /examples/data-streams/assets/maybemexico.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1be6795707be15de1b8601f7fea17527f7136d21428a4d0365d3c265703fda63 3 | size 1723564 4 | -------------------------------------------------------------------------------- /examples/data-streams/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ByteStreamReader, 3 | type RemoteParticipant, 4 | Room, 5 | RoomEvent, 6 | type TextStreamReader, 7 | } from '@livekit/rtc-node'; 8 | import { config } from 'dotenv'; 9 | import fs from 'fs'; 10 | import { AccessToken } from 'livekit-server-sdk'; 11 | 12 | config({ path: '.env.local', override: false }); 13 | const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY; 14 | const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET; 15 | const LIVEKIT_URL = process.env.LIVEKIT_URL; 16 | if (!LIVEKIT_API_KEY || !LIVEKIT_API_SECRET || !LIVEKIT_URL) { 17 | throw new Error('Missing required environment variables. Please check your .env.local file.'); 18 | } 19 | 20 | const greetParticipant = async (room: Room, recipient: RemoteParticipant) => { 21 | const greeting = 'Hi this is just a text sample'; 22 | const streamWriter = await room.localParticipant?.streamText({ 23 | destinationIdentities: [recipient.identity], 24 | topic: 'chat', 25 | }); 26 | 27 | for (const c of greeting) { 28 | await streamWriter?.write(c); 29 | } 30 | 31 | streamWriter?.close(); 32 | }; 33 | 34 | const sendFile = async (room: Room, recipient: RemoteParticipant) => { 35 | console.log('sending file'); 36 | await room.localParticipant?.sendFile('./assets/maybemexico.jpg', { 37 | destinationIdentities: [recipient.identity], 38 | name: 'mex.jpg', 39 | topic: 'files', 40 | mimeType: 'image/jpg', 41 | }); 42 | console.log('done sending file'); 43 | }; 44 | 45 | const main = async () => { 46 | const roomName = `dev`; 47 | const identity = 'tester'; 48 | const token = await createToken(identity, roomName); 49 | 50 | const room = new Room(); 51 | 52 | const finishedPromise = new Promise((resolve) => { 53 | room.on(RoomEvent.ParticipantDisconnected, resolve); 54 | }); 55 | 56 | room.registerTextStreamHandler('chat', async (reader: TextStreamReader, { identity }) => { 57 | console.log(`chat message from ${identity}: ${await reader.readAll()}`); 58 | // for await (const { collected } of reader) { 59 | // console.log(collected); 60 | // } 61 | }); 62 | 63 | room.registerByteStreamHandler('files', async (reader: ByteStreamReader, { identity }) => { 64 | console.log(`welcome image received from ${identity}: ${reader.info.name}`); 65 | 66 | // create write stream and write received file to disk, make sure ./temp folder exists 67 | const writer = fs.createWriteStream(`./temp/${reader.info.name}`, {}); 68 | 69 | for await (const chunk of reader) { 70 | writer.write(chunk); 71 | } 72 | writer.close(); 73 | }); 74 | 75 | room.on(RoomEvent.ParticipantConnected, async (participant) => { 76 | await sendFile(room, participant); 77 | await greetParticipant(room, participant); 78 | }); 79 | 80 | await room.connect(LIVEKIT_URL, token); 81 | 82 | for (const [, p] of room.remoteParticipants) { 83 | await sendFile(room, p); 84 | await greetParticipant(room, p); 85 | } 86 | 87 | await finishedPromise; 88 | }; 89 | 90 | const createToken = async (identity: string, roomName: string) => { 91 | const token = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, { 92 | identity, 93 | }); 94 | token.addGrant({ 95 | room: roomName, 96 | roomJoin: true, 97 | canPublish: true, 98 | canSubscribe: true, 99 | }); 100 | return await token.toJwt(); 101 | }; 102 | 103 | main(); 104 | -------------------------------------------------------------------------------- /examples/data-streams/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-data-streams", 3 | "author": "LiveKit", 4 | "private": "true", 5 | "description": "Example of using data streams in LiveKit", 6 | "type": "module", 7 | "main": "index.ts", 8 | "scripts": { 9 | "lint": "eslint -f unix \"**/*.ts\"", 10 | "start": "tsx index.ts" 11 | }, 12 | "keywords": [], 13 | "license": "Apache-2.0", 14 | "dependencies": { 15 | "@livekit/rtc-node": "workspace:*", 16 | "dotenv": "^16.4.5", 17 | "livekit-server-sdk": "workspace:*" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^20.10.4", 21 | "tsx": "^4.7.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/data-streams/temp/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /examples/publish-wav/.env.example: -------------------------------------------------------------------------------- 1 | # 1. Copy this file and rename it to .env 2 | # 2. Update the enviroment variables below. 3 | 4 | LIVEKIT_API_KEY=devkey 5 | LIVEKIT_API_SECRET=secret 6 | LIVEKIT_URL=ws://localhost:7880 7 | -------------------------------------------------------------------------------- /examples/publish-wav/.gitattributes: -------------------------------------------------------------------------------- 1 | speex.wav filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /examples/publish-wav/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AudioFrame, 3 | AudioSource, 4 | LocalAudioTrack, 5 | Room, 6 | TrackPublishOptions, 7 | TrackSource, 8 | dispose, 9 | } from '@livekit/rtc-node'; 10 | import { config } from 'dotenv'; 11 | import { AccessToken } from 'livekit-server-sdk'; 12 | import { readFileSync } from 'node:fs'; 13 | import { join } from 'node:path'; 14 | 15 | config(); 16 | 17 | // create access token from API credentials 18 | const token = new AccessToken(process.env.LIVEKIT_API_KEY, process.env.LIVEKIT_API_SECRET, { 19 | identity: 'example-participant', 20 | }); 21 | token.addGrant({ 22 | room: 'example-room', 23 | roomJoin: true, 24 | roomCreate: true, 25 | canPublish: true, 26 | }); 27 | const jwt = await token.toJwt(); 28 | 29 | // set up room 30 | const room = new Room(); 31 | await room.connect(process.env.LIVEKIT_URL, jwt, { autoSubscribe: true, dynacast: true }); 32 | console.log('connected to room', room); 33 | 34 | // read relevant metadata from wav file 35 | // this example assumes valid encoding little-endian 36 | const sample = readFileSync(join(process.cwd(), './speex.wav')); 37 | const channels = sample.readUInt16LE(22); 38 | const sampleRate = sample.readUInt32LE(24); 39 | const dataSize = sample.readUInt32LE(40) / 2; 40 | 41 | // set up audio track 42 | const source = new AudioSource(sampleRate, channels); 43 | const track = LocalAudioTrack.createAudioTrack('audio', source); 44 | const options = new TrackPublishOptions(); 45 | const buffer = new Int16Array(sample.buffer); 46 | options.source = TrackSource.SOURCE_MICROPHONE; 47 | await room.localParticipant.publishTrack(track, options).then((pub) => pub.waitForSubscription()); 48 | 49 | let written = 44; // start of WAVE data stream 50 | const FRAME_DURATION = 1; // write 1s of audio at a time 51 | const numSamples = sampleRate * FRAME_DURATION; 52 | while (written < dataSize) { 53 | const available = dataSize - written; 54 | const frameSize = Math.min(numSamples, available); 55 | 56 | const frame = new AudioFrame( 57 | buffer.slice(written, written + frameSize), 58 | sampleRate, 59 | channels, 60 | Math.trunc(frameSize / channels), 61 | ); 62 | await source.captureFrame(frame); 63 | written += frameSize; 64 | } 65 | await source.waitForPlayout(); 66 | // release resources allocated for audio publishing 67 | await source.close(); 68 | 69 | await room.disconnect(); 70 | 71 | // disposes all resources, only use if no more sessions are expected 72 | await dispose(); 73 | -------------------------------------------------------------------------------- /examples/publish-wav/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-publish-wav", 3 | "author": "LiveKit", 4 | "private": "true", 5 | "description": "", 6 | "type": "module", 7 | "main": "index.ts", 8 | "scripts": { 9 | "build": "tsc --incremental", 10 | "lint": "eslint -f unix \"**/*.ts\"" 11 | }, 12 | "keywords": [], 13 | "license": "Apache-2.0", 14 | "dependencies": { 15 | "@livekit/rtc-node": "workspace:*", 16 | "dotenv": "^16.4.5", 17 | "livekit-server-sdk": "workspace:*" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^20.10.4", 21 | "tsx": "^4.7.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/publish-wav/speex.wav: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:86622a1571594490f66ba6eccd66524c5267b496e16830c3eea5bc4798cf405e 3 | size 212992 4 | -------------------------------------------------------------------------------- /examples/publish-wav/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2022", 4 | "esModuleInterop": true, 5 | "target": "es2022", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist" 9 | }, 10 | "lib": ["es2022"] 11 | } 12 | -------------------------------------------------------------------------------- /examples/receive-audio/.env.example: -------------------------------------------------------------------------------- 1 | # 1. Copy this file and rename it to .env 2 | # 2. Update the enviroment variables below. 3 | 4 | LIVEKIT_API_KEY=mykey 5 | LIVEKIT_API_SECRET=mysecret 6 | LIVEKIT_URL=wss://myproject.livekit.cloud 7 | -------------------------------------------------------------------------------- /examples/receive-audio/README.md: -------------------------------------------------------------------------------- 1 | # Receive audio example 2 | 3 | This example demonstrates receiving the first audio track published in a room and writing that audio data to a wav file. 4 | 5 | To run the example: 6 | 7 | - Copy .env.example to .env and fill in the values 8 | - Run `pnpm install` in the root folder of this repo 9 | - Run `tsx index.ts` in this folder 10 | - From another client, join the room `test-room` and publish an audio track 11 | -------------------------------------------------------------------------------- /examples/receive-audio/index.ts: -------------------------------------------------------------------------------- 1 | import { AudioStream, Room, RoomEvent, TrackKind } from '@livekit/rtc-node'; 2 | import type { AudioFrame } from '@livekit/rtc-node/src'; 3 | import { Buffer } from 'buffer'; 4 | import { config } from 'dotenv'; 5 | import * as fs from 'fs'; 6 | import { AccessToken } from 'livekit-server-sdk'; 7 | 8 | config(); 9 | 10 | // Constants for WAV file 11 | const BITS_PER_SAMPLE = 16; 12 | const WAV_FILE = 'output.wav'; 13 | 14 | function writeWavHeader(writer: fs.WriteStream, frame: AudioFrame) { 15 | const header = Buffer.alloc(44); 16 | const byteRate = (frame.sampleRate * frame.channels * BITS_PER_SAMPLE) / 8; 17 | const blockAlign = (frame.channels * BITS_PER_SAMPLE) / 8; 18 | 19 | writer = fs.createWriteStream(WAV_FILE); 20 | // Write the RIFF header 21 | header.write('RIFF', 0); // ChunkID 22 | header.writeUInt32LE(0, 4); // ChunkSize placeholder 23 | header.write('WAVE', 8); // Format 24 | 25 | // Write the fmt subchunk 26 | header.write('fmt ', 12); // Subchunk1ID 27 | header.writeUInt32LE(16, 16); // Subchunk1Size (PCM) 28 | header.writeUInt16LE(1, 20); // AudioFormat (PCM = 1) 29 | header.writeUInt16LE(frame.channels, 22); // NumChannels 30 | header.writeUInt32LE(frame.sampleRate, 24); // SampleRate 31 | header.writeUInt32LE(byteRate, 28); // ByteRate 32 | header.writeUInt16LE(blockAlign, 32); // BlockAlign 33 | header.writeUInt16LE(16, 34); // BitsPerSample 34 | 35 | // Write the data subchunk 36 | header.write('data', 36); // Subchunk2ID 37 | header.writeUInt32LE(0, 40); // Subchunk2Size placeholder 38 | 39 | // Write the header to the stream 40 | writer.write(header); 41 | } 42 | 43 | function updateWavHeader(path: string) { 44 | // Update the size of the audio data in the header 45 | const stats = fs.statSync(path); 46 | const fileSize = stats.size; 47 | 48 | const chunkSize = fileSize - 8; 49 | const subchunk2Size = fileSize - 44; 50 | const header = Buffer.alloc(8); 51 | header.writeUInt32LE(chunkSize, 0); 52 | header.writeUInt32LE(subchunk2Size, 4); 53 | 54 | // Reopen the file for updating the header 55 | const fd = fs.openSync(path, 'r+'); 56 | fs.writeSync(fd, header, 0, 4, 4); // Update ChunkSize 57 | fs.writeSync(fd, header, 4, 4, 40); // Update Subchunk2Size 58 | fs.closeSync(fd); 59 | } 60 | 61 | // create access token from API credentials 62 | const token = new AccessToken(process.env.LIVEKIT_API_KEY, process.env.LIVEKIT_API_SECRET, { 63 | identity: 'example-participant', 64 | }); 65 | token.addGrant({ 66 | room: 'test-room', 67 | roomJoin: true, 68 | roomCreate: true, 69 | canPublish: true, 70 | canPublishData: true, 71 | }); 72 | const jwt = await token.toJwt(); 73 | 74 | // set up room 75 | const room = new Room(); 76 | 77 | let trackToProcess: string | null = null; 78 | let writer: fs.WriteStream | null = null; 79 | 80 | room.on(RoomEvent.TrackSubscribed, async (track, publication, participant) => { 81 | console.log('subscribed to track', track.sid, publication, participant.identity); 82 | if (track.kind === TrackKind.KIND_AUDIO) { 83 | const stream = new AudioStream(track); 84 | trackToProcess = track.sid; 85 | 86 | for await (const frame of stream) { 87 | if (!trackToProcess) { 88 | return; 89 | } 90 | 91 | if (writer == null) { 92 | // create file on first frame 93 | // also guard when track is unsubscribed 94 | writer = fs.createWriteStream('output.wav'); 95 | writeWavHeader(writer, frame); 96 | } 97 | 98 | if (writer) { 99 | const buf = Buffer.from(frame.data.buffer); 100 | writer.write(buf); 101 | } 102 | } 103 | } 104 | }); 105 | 106 | const finishedPromise = new Promise((resolve) => { 107 | room.on(RoomEvent.TrackUnsubscribed, (_, publication, participant) => { 108 | console.log('unsubscribed from track', publication.sid, participant.identity); 109 | if (publication.sid === trackToProcess) { 110 | trackToProcess = null; 111 | if (writer) { 112 | writer.close(); 113 | // update header 114 | updateWavHeader(WAV_FILE); 115 | } 116 | resolve(); 117 | } 118 | }); 119 | }); 120 | 121 | await room.connect(process.env.LIVEKIT_URL, jwt, { autoSubscribe: true, dynacast: true }); 122 | console.log('connected to room', room); 123 | 124 | // stay in the room until publisher leaves 125 | await finishedPromise; 126 | -------------------------------------------------------------------------------- /examples/receive-audio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-receive-audio", 3 | "author": "LiveKit", 4 | "private": "true", 5 | "description": "", 6 | "type": "module", 7 | "main": "index.ts", 8 | "scripts": { 9 | "build": "tsc --incremental", 10 | "lint": "eslint -f unix \"**/*.ts\"" 11 | }, 12 | "keywords": [], 13 | "license": "Apache-2.0", 14 | "dependencies": { 15 | "@livekit/rtc-node": "workspace:*", 16 | "dotenv": "^16.4.5", 17 | "livekit-server-sdk": "workspace:*" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^20.10.4", 21 | "tsx": "^4.7.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/receive-audio/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2022", 4 | "esModuleInterop": true, 5 | "target": "es2022", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist" 9 | }, 10 | "lib": ["es2022"] 11 | } 12 | -------------------------------------------------------------------------------- /examples/rpc/.env.example: -------------------------------------------------------------------------------- 1 | # 1. Copy this file and rename it to .env 2 | # 2. Update the enviroment variables below. 3 | 4 | LIVEKIT_API_KEY=mykey 5 | LIVEKIT_API_SECRET=mysecret 6 | LIVEKIT_URL=wss://myproject.livekit.cloud 7 | -------------------------------------------------------------------------------- /examples/rpc/README.md: -------------------------------------------------------------------------------- 1 | # RPC Example 2 | 3 | This example demonstrates how to use RPC between two participants with LiveKit. 4 | 5 | The example includes two scenarios: 6 | 1. A simple greeting exchange. 7 | 2. A contrived function-calling service with JSON data payloads and multiple method types. 8 | 9 | ## Prerequisites 10 | 11 | Before running this example, make sure you have: 12 | 13 | 1. Node.js installed on your machine. 14 | 2. A LiveKit server running (either locally or remotely). 15 | 3. LiveKit API key and secret. 16 | 17 | ## Setup 18 | 19 | 1. Install dependencies: 20 | ``` 21 | pnpm install 22 | ``` 23 | 24 | 2. Create a `.env.local` file in the example directory with your LiveKit credentials: 25 | ``` 26 | LIVEKIT_API_KEY=your_api_key 27 | LIVEKIT_API_SECRET=your_api_secret 28 | LIVEKIT_URL=your_livekit_url 29 | ``` 30 | 31 | ## Running the Example 32 | 33 | To run the example, use the following command: 34 | 35 | ``` 36 | pnpm run start 37 | ``` 38 | 39 | The example will log to your terminal. 40 | -------------------------------------------------------------------------------- /examples/rpc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-rpc", 3 | "author": "LiveKit", 4 | "private": "true", 5 | "description": "Example of using RPC in LiveKit", 6 | "type": "module", 7 | "main": "index.ts", 8 | "scripts": { 9 | "lint": "eslint -f unix \"**/*.ts\"", 10 | "start": "tsx index.ts" 11 | }, 12 | "keywords": [], 13 | "license": "Apache-2.0", 14 | "dependencies": { 15 | "@livekit/rtc-node": "workspace:*", 16 | "dotenv": "^16.4.5", 17 | "livekit-server-sdk": "workspace:*" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^20.10.4", 21 | "tsx": "^4.7.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/webhooks-http/README.md: -------------------------------------------------------------------------------- 1 | # Using Webhooks with a Node.JS app 2 | 3 | A simple example showing how LiveKit's Webhooks work. 4 | Follow these steps to see this demo in action: 5 | 6 | - Ensure livekit-server has webhooks configured: 7 | 8 | ``` 9 | webhook: 10 | urls: 11 | - http://localhost:3000/ 12 | api_key: 13 | ``` 14 | 15 | - Start livekit-server locally 16 | - Set environment variables LIVEKIT_API_KEY and LIVEKIT_API_SECRET with your API key and secret 17 | - Run this example with `node webhook.js` 18 | - Connect a client to livekit-server 19 | - Observe the following in your Next.js app logs: 20 | 21 | ``` 22 | received webhook event { 23 | event: 'participant_joined', 24 | ... 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/webhooks-http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-webhooks-http", 3 | "description": "example using livekit-server-sdk to handle webhooks with Node HTTP", 4 | "main": "webhook.js", 5 | "author": "David Zhao", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "dependencies": { 9 | "livekit-server-sdk": "workspace:*" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/webhooks-http/webhook.js: -------------------------------------------------------------------------------- 1 | const lksdk = require('livekit-server-sdk'); 2 | 3 | const http = require('http'); 4 | const host = 'localhost'; 5 | const port = 3000; 6 | 7 | const receiver = new lksdk.WebhookReceiver( 8 | process.env.LIVEKIT_API_KEY, process.env.LIVEKIT_API_SECRET 9 | ) 10 | 11 | const webhookHandler = (req, res) => { 12 | let data = ''; 13 | req.on('data', chunk => { 14 | data += chunk; 15 | }); 16 | 17 | req.on('end', () => { 18 | const event = receiver.receive(data, req.headers.authorization); 19 | 20 | console.log('received webhook event', event); 21 | 22 | res.writeHead(200); 23 | res.end(); 24 | }); 25 | } 26 | 27 | const server = http.createServer(webhookHandler); 28 | server.listen(port, host, () => { 29 | console.log(`Webhook example running on http://${host}:${port}`); 30 | }); 31 | -------------------------------------------------------------------------------- /examples/webhooks-nextjs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "../../.eslintrc", 4 | "plugin:@next/next/recommended" 5 | ], 6 | "parserOptions": { 7 | "project": ["./tsconfig.json"] 8 | }, 9 | "settings": { 10 | "react": { 11 | "version": "18" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /examples/webhooks-nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/webhooks-nextjs/README.md: -------------------------------------------------------------------------------- 1 | # Using Webhooks with a Next.JS app 2 | 3 | This example was generated using [create-next-app](https://nextjs.org/docs/api-reference/create-next-app). 4 | 5 | We've made the following modifications to the generated project: 6 | 7 | - `pnpm add livekit-server-sdk` 8 | - [webhook.ts](pages/api/webhook.ts) 9 | - added API key and secret to [next.config.js](next.config.js) 10 | 11 | Follow these steps to see this demo in action: 12 | 13 | - Ensure livekit-server has webhooks configured: 14 | 15 | ``` 16 | webhook: 17 | urls: 18 | - http://localhost:3000/api/webhook 19 | api_key: 20 | ``` 21 | 22 | - Start livekit-server locally 23 | - Open next.config.js and fill in your API key and secret pair 24 | - Run this example with `pnpm dev` 25 | - Connect a client to livekit-server 26 | - Observe the following in your Next.js app logs: 27 | 28 | ``` 29 | received webhook event { 30 | event: 'participant_joined', 31 | ... 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /examples/webhooks-nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | serverRuntimeConfig: { 6 | livekitApiKey: 'your-api-key', 7 | livekitApiSecret: 'your-api-secret', 8 | }, 9 | }; 10 | 11 | module.exports = nextConfig; 12 | -------------------------------------------------------------------------------- /examples/webhooks-nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-webhooks-nextjs", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "livekit-server-sdk": "workspace:*", 12 | "next": "12.3.0", 13 | "react": "18.2.0", 14 | "react-dom": "18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@next/eslint-plugin-next": "^14.2.4", 18 | "@types/node": "18.7.18", 19 | "@types/react": "18.0.20", 20 | "@types/react-dom": "18.0.6" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/webhooks-nextjs/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import * as React from "react"; 3 | import type { AppProps } from 'next/app' 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return 7 | } 8 | 9 | export default MyApp 10 | -------------------------------------------------------------------------------- /examples/webhooks-nextjs/pages/api/webhook.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import { WebhookReceiver } from 'livekit-server-sdk'; 3 | import type { NextApiRequest, NextApiResponse } from 'next'; 4 | import getConfig from 'next/config'; 5 | 6 | const { serverRuntimeConfig } = getConfig(); 7 | 8 | const receiver = new WebhookReceiver( 9 | serverRuntimeConfig.livekitApiKey, 10 | serverRuntimeConfig.livekitApiSecret, 11 | ); 12 | 13 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 14 | const event = receiver.receive(req.body as string, req.headers.authorization); 15 | console.log('received webhook event', event); 16 | res.status(200).end(); 17 | } 18 | -------------------------------------------------------------------------------- /examples/webhooks-nextjs/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { NextPage } from 'next' 3 | import Head from 'next/head' 4 | import Image from 'next/image' 5 | import styles from '../styles/Home.module.css' 6 | 7 | const Home: NextPage = () => { 8 | return ( 9 | 70 | ) 71 | } 72 | 73 | export default Home 74 | -------------------------------------------------------------------------------- /examples/webhooks-nextjs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livekit/node-sdks/23792753e9fa63113e52caa1c51f1db83fb42b78/examples/webhooks-nextjs/public/favicon.ico -------------------------------------------------------------------------------- /examples/webhooks-nextjs/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/webhooks-nextjs/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | 118 | @media (prefers-color-scheme: dark) { 119 | .card, 120 | .footer { 121 | border-color: #222; 122 | } 123 | .code { 124 | background: #111; 125 | } 126 | .logo img { 127 | filter: invert(1); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /examples/webhooks-nextjs/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/webhooks-nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1711624657, 6 | "narHash": "sha256-IViG6BKCJY/I6oRNfAANf/QitYylepQSCzgam0TF+bs=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "72c6ed328aa4e5d9151b1a512f6ad83aca7529fa", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs", 22 | "utils": "utils" 23 | } 24 | }, 25 | "systems": { 26 | "locked": { 27 | "lastModified": 1681028828, 28 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 29 | "owner": "nix-systems", 30 | "repo": "default", 31 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 32 | "type": "github" 33 | }, 34 | "original": { 35 | "owner": "nix-systems", 36 | "repo": "default", 37 | "type": "github" 38 | } 39 | }, 40 | "utils": { 41 | "inputs": { 42 | "systems": "systems" 43 | }, 44 | "locked": { 45 | "lastModified": 1710146030, 46 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 47 | "owner": "numtide", 48 | "repo": "flake-utils", 49 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "numtide", 54 | "repo": "flake-utils", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | { 6 | inputs = { 7 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 8 | utils.url = "github:numtide/flake-utils"; 9 | }; 10 | 11 | outputs = { self, utils, nixpkgs }: 12 | utils.lib.eachDefaultSystem (system: 13 | let pkgs = (import nixpkgs) { 14 | inherit system; 15 | }; 16 | 17 | in { 18 | devShell = pkgs.mkShell { 19 | nativeBuildInputs = with pkgs; [ 20 | nodejs 21 | corepack 22 | cargo 23 | rustc 24 | xorg.libXext 25 | xorg.libX11 26 | libGL 27 | clang 28 | reuse 29 | git-lfs 30 | ]; 31 | LIBCLANG_PATH = "${pkgs.libclang.lib}/lib"; 32 | }; 33 | } 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@livekit/node-sdk-monorepo", 3 | "author": "LiveKit", 4 | "private": true, 5 | "type": "module", 6 | "version": "0.0.0", 7 | "scripts": { 8 | "build": "turbo run build", 9 | "build:livekit-rtc": "turbo run build --filter=@livekit/rtc-node...", 10 | "build:artifacts": "turbo run artifacts --filter=@livekit/rtc-node...", 11 | "ci:publish": "changeset publish", 12 | "api:check": "turbo run api:check", 13 | "api:update": "turbo run api:update", 14 | "format:check": "prettier --check \"**/src/**/*.{ts,tsx,md,json}\"", 15 | "format:write": "prettier --write \"**/src/**/*.{ts,tsx,md,json}\"", 16 | "lint": "turbo lint", 17 | "lint:fix": "turbo lint -- --fix", 18 | "test": "vitest run", 19 | "test:watch": "vitest" 20 | }, 21 | "devDependencies": { 22 | "@changesets/cli": "^2.27.1", 23 | "@livekit/changesets-changelog-github": "^0.0.4", 24 | "@rushstack/heft": "^0.68.0", 25 | "@trivago/prettier-plugin-sort-imports": "^5.0.0", 26 | "@typescript-eslint/eslint-plugin": "^8.0.0", 27 | "@typescript-eslint/parser": "^8.0.0", 28 | "eslint": "^8.56.0", 29 | "eslint-config-next": "^15.0.0", 30 | "eslint-config-prettier": "^10.0.0", 31 | "eslint-config-standard": "^17.1.0", 32 | "eslint-config-turbo": "^2.0.0", 33 | "eslint-plugin-import": "^2.29.1", 34 | "eslint-plugin-n": "^17.9.0", 35 | "eslint-plugin-prettier": "^5.1.3", 36 | "eslint-plugin-promise": "^7.0.0", 37 | "eslint-plugin-react": "^7.34.2", 38 | "eslint-plugin-tsdoc": "^0.4.0", 39 | "prettier": "^3.2.5", 40 | "turbo": "^2.0.0", 41 | "typescript": "^5.4.5", 42 | "vitest": "^3.0.0" 43 | }, 44 | "packageManager": "pnpm@9.15.7" 45 | } 46 | -------------------------------------------------------------------------------- /packages/livekit-rtc/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | rustflags = ["-C", "target-feature=+crt-static"] 3 | 4 | [target.aarch64-pc-windows-msvc] 5 | rustflags = ["-C", "target-feature=+crt-static"] 6 | 7 | [target.x86_64-apple-darwin] 8 | rustflags = ["-C", "link-args=-ObjC"] 9 | 10 | [target.aarch64-apple-darwin] 11 | rustflags = ["-C", "link-args=-ObjC"] 12 | 13 | [target.aarch64-apple-ios] 14 | rustflags = ["-C", "link-args=-ObjC"] 15 | 16 | [target.aarch64-apple-ios-sim] 17 | rustflags = ["-C", "link-args=-ObjC"] -------------------------------------------------------------------------------- /packages/livekit-rtc/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | lib 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | 84 | # Next.js build output 85 | .next 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and not Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | 112 | # Stores VSCode versions used for testing VSCode extensions 113 | .vscode-test 114 | 115 | # End of https://www.toptal.com/developers/gitignore/api/node 116 | 117 | # Created by https://www.toptal.com/developers/gitignore/api/macos 118 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos 119 | 120 | ### macOS ### 121 | # General 122 | .DS_Store 123 | .AppleDouble 124 | .LSOverride 125 | 126 | # Icon must end with two 127 | Icon 128 | 129 | 130 | # Thumbnails 131 | ._* 132 | 133 | # Files that might appear in the root of a volume 134 | .DocumentRevisions-V100 135 | .fseventsd 136 | .Spotlight-V100 137 | .TemporaryItems 138 | .Trashes 139 | .VolumeIcon.icns 140 | .com.apple.timemachine.donotpresent 141 | 142 | # Directories potentially created on remote AFP share 143 | .AppleDB 144 | .AppleDesktop 145 | Network Trash Folder 146 | Temporary Items 147 | .apdisk 148 | 149 | ### macOS Patch ### 150 | # iCloud generated files 151 | *.icloud 152 | 153 | # End of https://www.toptal.com/developers/gitignore/api/macos 154 | 155 | # Created by https://www.toptal.com/developers/gitignore/api/windows 156 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows 157 | 158 | ### Windows ### 159 | # Windows thumbnail cache files 160 | Thumbs.db 161 | Thumbs.db:encryptable 162 | ehthumbs.db 163 | ehthumbs_vista.db 164 | 165 | # Dump file 166 | *.stackdump 167 | 168 | # Folder config file 169 | [Dd]esktop.ini 170 | 171 | # Recycle Bin used on file shares 172 | $RECYCLE.BIN/ 173 | 174 | # Windows Installer files 175 | *.cab 176 | *.msi 177 | *.msix 178 | *.msm 179 | *.msp 180 | 181 | # Windows shortcuts 182 | *.lnk 183 | 184 | # End of https://www.toptal.com/developers/gitignore/api/windows 185 | 186 | #Added by cargo 187 | 188 | /target 189 | Cargo.lock 190 | 191 | .pnp.* 192 | .yarn/* 193 | !.yarn/patches 194 | !.yarn/plugins 195 | !.yarn/releases 196 | !.yarn/sdks 197 | !.yarn/versions 198 | 199 | *.node 200 | -------------------------------------------------------------------------------- /packages/livekit-rtc/.npmignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .cargo 4 | .github 5 | npm 6 | .eslintrc 7 | .prettierignore 8 | rustfmt.toml 9 | yarn.lock 10 | *.node 11 | .yarn 12 | __test__ 13 | renovate.json 14 | -------------------------------------------------------------------------------- /packages/livekit-rtc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "rtc-node" 4 | version = "0.0.1" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | napi = { version = "2.12.2", default-features = false, features = ["async", "napi6"] } 11 | napi-derive = "2.12.2" 12 | livekit-ffi = { path = "./rust-sdks/livekit-ffi" } 13 | prost = "0.12" 14 | prost-types = "0.12" 15 | log = "0.4.20" 16 | tokio = { version = "1.37.0", features = ["full"] } 17 | 18 | [build-dependencies] 19 | napi-build = "2.0.1" 20 | 21 | [profile.release] 22 | lto = true 23 | -------------------------------------------------------------------------------- /packages/livekit-rtc/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # 📹🎙️Node.js realtime SDK for LiveKit 8 | 9 | [![npm](https://img.shields.io/npm/v/%40livekit%2Frtc-node.svg)](https://npmjs.com/package/@livekit/rtc-node) 10 | [![livekit-rtc CI](https://github.com/livekit/node-sdks/actions/workflows/rtc-node.yml/badge.svg?branch=main)](https://github.com/livekit/node-sdks/actions/workflows/rtc-node.yml) 11 | 12 | Use this SDK to add realtime video, audio and data features to your Node app. By connecting to a self- or cloud-hosted LiveKit server, you can quickly build applications like interactive live streaming or video calls with just a few lines of code. 13 | 14 | 15 | > This SDK is currently in Developer Preview mode and not ready for production use. There will be bugs and APIs may change during this period. 16 | > 17 | > We welcome and appreciate any feedback or contributions. You can create issues here or chat live with us in the #dev channel within the [LiveKit Community Slack](https://livekit.io/join-slack). 18 | 19 | ### Warning 20 | 21 | Avoid running this with hot reloads (ex. [bun's hot reload](https://bun.sh/guides/http/hot)). This is known to cause issues with WebRTC connectivity. 22 | 23 | ## Using realtime SDK 24 | 25 | ### Connecting to a room 26 | 27 | ```typescript 28 | import { 29 | RemoteParticipant, 30 | RemoteTrack, 31 | RemoteTrackPublication, 32 | Room, 33 | RoomEvent, 34 | dispose, 35 | } from '@livekit/rtc-node'; 36 | 37 | const room = new Room(); 38 | await room.connect(url, token, { autoSubscribe: true, dynacast: true }); 39 | console.log('connected to room', room); 40 | 41 | // add event listeners 42 | room 43 | .on(RoomEvent.TrackSubscribed, handleTrackSubscribed) 44 | .on(RoomEvent.Disconnected, handleDisconnected) 45 | .on(RoomEvent.LocalTrackPublished, handleLocalTrackPublished); 46 | 47 | process.on('SIGINT', () => { 48 | await room.disconnect(); 49 | await dispose(); 50 | }); 51 | ``` 52 | 53 | ### Publishing a track 54 | 55 | ```typescript 56 | import { 57 | AudioFrame, 58 | AudioSource, 59 | LocalAudioTrack, 60 | TrackPublishOptions, 61 | TrackSource, 62 | } from '@livekit/rtc-node'; 63 | import { readFileSync } from 'node:fs'; 64 | 65 | // set up audio track 66 | const source = new AudioSource(16000, 1); 67 | const track = LocalAudioTrack.createAudioTrack('audio', source); 68 | const options = new TrackPublishOptions(); 69 | options.source = TrackSource.SOURCE_MICROPHONE; 70 | 71 | // note: if converting from Uint8Array to Int16Array, *do not* use buffer.slice! 72 | // it is marked unstable by Node and can cause undefined behaviour, such as massive chunks of 73 | // noise being added to the end. 74 | // it is recommended to use buffer.subarray instead. 75 | const sample = readFileSync(pathToFile); 76 | var buffer = new Int16Array(sample.buffer); 77 | 78 | await room.localParticipant.publishTrack(track, options); 79 | await source.captureFrame(new AudioFrame(buffer, 16000, 1, buffer.byteLength / 2)); 80 | ``` 81 | 82 | ### RPC 83 | 84 | Perform your own predefined method calls from one participant to another. 85 | 86 | This feature is especially powerful when used with [Agents](https://docs.livekit.io/agents), for instance to forward LLM function calls to your client application. 87 | 88 | #### Registering an RPC method 89 | 90 | The participant who implements the method and will receive its calls must first register support: 91 | 92 | ```typescript 93 | room.localParticipant?.registerRpcMethod( 94 | // method name - can be any string that makes sense for your application 95 | 'greet', 96 | 97 | // method handler - will be called when the method is invoked by a RemoteParticipant 98 | async (data: RpcInvocationData) => { 99 | console.log(`Received greeting from ${data.callerIdentity}: ${data.payload}`); 100 | return `Hello, ${data.callerIdentity}!`; 101 | } 102 | ); 103 | ``` 104 | 105 | In addition to the payload, your handler will also receive `responseTimeout`, which informs you the maximum time available to return a response. If you are unable to respond in time, the call will result in an error on the caller's side. 106 | 107 | #### Performing an RPC request 108 | 109 | The caller may then initiate an RPC call like so: 110 | 111 | ```typescript 112 | try { 113 | const response = await room.localParticipant!.performRpc({ 114 | destinationIdentity: 'recipient-identity', 115 | method: 'greet', 116 | payload: 'Hello from RPC!', 117 | }); 118 | console.log('RPC response:', response); 119 | } catch (error) { 120 | console.error('RPC call failed:', error); 121 | } 122 | ``` 123 | 124 | You may find it useful to adjust the `responseTimeout` parameter, which indicates the amount of time you will wait for a response. We recommend keeping this value as low as possible while still satisfying the constraints of your application. 125 | 126 | #### Errors 127 | 128 | LiveKit is a dynamic realtime environment and calls can fail for various reasons. 129 | 130 | You may throw errors of the type `RpcError` with a string `message` in an RPC method handler and they will be received on the caller's side with the message intact. Other errors will not be transmitted and will instead arrive to the caller as `1500` ("Application Error"). Other built-in errors are detailed in `RpcError`. 131 | 132 | ## Examples 133 | 134 | - [`publish-wav`](https://github.com/livekit/node-sdks/tree/main/examples/publish-wav): connect to a room and publish a .wave file 135 | - [`rpc`](https://github.com/livekit/node-sdks/tree/main/examples/rpc): simple back-and-forth RPC interaction 136 | 137 | 138 | ## Getting help / Contributing 139 | 140 | Please join us on [Slack](https://livekit.io/join-slack) to get help from our devs & community. We welcome your contributions and details can be discussed there. 141 | -------------------------------------------------------------------------------- /packages/livekit-rtc/build.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | extern crate napi_build; 6 | 7 | fn main() { 8 | napi_build::setup(); 9 | } 10 | -------------------------------------------------------------------------------- /packages/livekit-rtc/generate_proto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # SPDX-FileCopyrightText: 2024 LiveKit, Inc. 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | 7 | # This script requires protobuf-compiler and https://www.npmjs.com/package/@bufbuild/protoc-gen-es 8 | # `brew install protobuf-c && npm install -g @bufbuild/protoc-gen-es@2.2.0` 9 | 10 | FFI_PROTOCOL=./rust-sdks/livekit-ffi/protocol 11 | FFI_OUT_NODE=./src/proto 12 | 13 | # ffi 14 | PATH=$PATH:$(pwd)/node_modules/.bin \ 15 | protoc \ 16 | -I=$FFI_PROTOCOL \ 17 | --es_out $FFI_OUT_NODE \ 18 | --es_opt target=ts \ 19 | --es_opt import_extension=.js \ 20 | $FFI_PROTOCOL/audio_frame.proto \ 21 | $FFI_PROTOCOL/ffi.proto \ 22 | $FFI_PROTOCOL/handle.proto \ 23 | $FFI_PROTOCOL/participant.proto \ 24 | $FFI_PROTOCOL/room.proto \ 25 | $FFI_PROTOCOL/track.proto \ 26 | $FFI_PROTOCOL/track_publication.proto \ 27 | $FFI_PROTOCOL/video_frame.proto \ 28 | $FFI_PROTOCOL/e2ee.proto \ 29 | $FFI_PROTOCOL/stats.proto \ 30 | $FFI_PROTOCOL/rpc.proto \ 31 | $FFI_PROTOCOL/track_publication.proto 32 | -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/darwin-arm64/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @livekit/rtc-node-darwin-arm64 2 | 3 | ## 0.13.14 4 | 5 | ## 0.13.13 6 | 7 | ## 0.13.12 8 | 9 | ## 0.13.11 10 | 11 | ## 0.13.10 12 | 13 | ## 0.13.9 14 | 15 | ## 0.13.8 16 | 17 | ## 0.13.7 18 | 19 | ## 0.13.6 20 | 21 | ## 0.13.5 22 | 23 | ## 0.13.4 24 | 25 | ## 0.13.3 26 | 27 | ## 0.13.2 28 | 29 | ## 0.13.1 30 | 31 | ## 0.13.0 32 | 33 | ## 0.12.2 34 | 35 | ## 0.12.1 36 | 37 | ## 0.12.0 38 | 39 | ## 0.11.1 40 | 41 | ## 0.11.0 42 | 43 | ## 0.10.4 44 | 45 | ## 0.10.3 46 | 47 | ## 0.10.2 48 | 49 | ## 0.10.1 50 | 51 | ## 0.10.0 52 | 53 | ## 0.9.2 54 | 55 | ## 0.9.1 56 | 57 | ## 0.9.0 58 | 59 | ## 0.8.1 60 | 61 | ## 0.8.0 62 | 63 | ## 0.7.0 64 | 65 | ## 0.6.2 66 | 67 | ## 0.6.1 68 | 69 | ## 0.6.0 70 | 71 | ## 0.5.1 72 | 73 | ## 0.5.0 74 | 75 | ## 0.4.4 76 | 77 | ## 0.4.3 78 | 79 | ## 0.4.2 80 | 81 | ## 0.4.1 82 | 83 | ## 0.4.0 84 | -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/darwin-arm64/README.md: -------------------------------------------------------------------------------- 1 | # `@livekit/rtc-node-darwin-arm64` 2 | 3 | This is the **aarch64-apple-darwin** binary for `@livekit/rtc-node` 4 | -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/darwin-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@livekit/rtc-node-darwin-arm64", 3 | "version": "0.13.14", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/livekit/node-sdks.git", 7 | "directory": "packages/livekit-rtc/darwin-arm64" 8 | }, 9 | "os": [ 10 | "darwin" 11 | ], 12 | "cpu": [ 13 | "arm64" 14 | ], 15 | "main": "rtc-node.darwin-arm64.node", 16 | "files": [ 17 | "rtc-node.darwin-arm64.node" 18 | ], 19 | "license": "Apache-2.0", 20 | "engines": { 21 | "node": ">= 10" 22 | } 23 | } -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/darwin-x64/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @livekit/rtc-node-darwin-x64 2 | 3 | ## 0.13.14 4 | 5 | ## 0.13.13 6 | 7 | ## 0.13.12 8 | 9 | ## 0.13.11 10 | 11 | ## 0.13.10 12 | 13 | ## 0.13.9 14 | 15 | ## 0.13.8 16 | 17 | ## 0.13.7 18 | 19 | ## 0.13.6 20 | 21 | ## 0.13.5 22 | 23 | ## 0.13.4 24 | 25 | ## 0.13.3 26 | 27 | ## 0.13.2 28 | 29 | ## 0.13.1 30 | 31 | ## 0.13.0 32 | 33 | ## 0.12.2 34 | 35 | ## 0.12.1 36 | 37 | ## 0.12.0 38 | 39 | ## 0.11.1 40 | 41 | ## 0.11.0 42 | 43 | ## 0.10.4 44 | 45 | ## 0.10.3 46 | 47 | ## 0.10.2 48 | 49 | ## 0.10.1 50 | 51 | ## 0.10.0 52 | 53 | ## 0.9.2 54 | 55 | ## 0.9.1 56 | 57 | ## 0.9.0 58 | 59 | ## 0.8.1 60 | 61 | ## 0.8.0 62 | 63 | ## 0.7.0 64 | 65 | ## 0.6.2 66 | 67 | ## 0.6.1 68 | 69 | ## 0.6.0 70 | 71 | ## 0.5.1 72 | 73 | ## 0.5.0 74 | 75 | ## 0.4.4 76 | 77 | ## 0.4.3 78 | 79 | ## 0.4.2 80 | 81 | ## 0.4.1 82 | 83 | ## 0.4.0 84 | -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/darwin-x64/README.md: -------------------------------------------------------------------------------- 1 | # `@livekit/rtc-node-darwin-x64` 2 | 3 | This is the **x86_64-apple-darwin** binary for `@livekit/rtc-node` 4 | -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/darwin-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@livekit/rtc-node-darwin-x64", 3 | "author": "LiveKit", 4 | "version": "0.13.14", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/livekit/node-sdks.git", 8 | "directory": "packages/livekit-rtc/darwin-x64" 9 | }, 10 | "os": [ 11 | "darwin" 12 | ], 13 | "cpu": [ 14 | "x64" 15 | ], 16 | "main": "rtc-node.darwin-x64.node", 17 | "files": [ 18 | "rtc-node.darwin-x64.node" 19 | ], 20 | "license": "Apache-2.0", 21 | "engines": { 22 | "node": ">= 10" 23 | } 24 | } -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/linux-arm64-gnu/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @livekit/rtc-node-linux-arm64-gnu 2 | 3 | ## 0.13.14 4 | 5 | ## 0.13.13 6 | 7 | ## 0.13.12 8 | 9 | ## 0.13.11 10 | 11 | ## 0.13.10 12 | 13 | ## 0.13.9 14 | 15 | ## 0.13.8 16 | 17 | ## 0.13.7 18 | 19 | ## 0.13.6 20 | 21 | ## 0.13.5 22 | 23 | ## 0.13.4 24 | 25 | ## 0.13.3 26 | 27 | ## 0.13.2 28 | 29 | ## 0.13.1 30 | 31 | ## 0.13.0 32 | 33 | ## 0.12.2 34 | 35 | ## 0.12.1 36 | 37 | ## 0.12.0 38 | 39 | ## 0.11.1 40 | 41 | ## 0.11.0 42 | 43 | ## 0.10.4 44 | 45 | ## 0.10.3 46 | 47 | ## 0.10.2 48 | 49 | ## 0.10.1 50 | 51 | ## 0.10.0 52 | 53 | ## 0.9.2 54 | 55 | ## 0.9.1 56 | 57 | ## 0.9.0 58 | 59 | ## 0.8.1 60 | 61 | ## 0.8.0 62 | 63 | ## 0.7.0 64 | 65 | ## 0.6.2 66 | 67 | ## 0.6.1 68 | 69 | ## 0.6.0 70 | 71 | ## 0.5.1 72 | 73 | ## 0.5.0 74 | 75 | ## 0.4.4 76 | 77 | ## 0.4.3 78 | 79 | ## 0.4.2 80 | 81 | ## 0.4.1 82 | 83 | ## 0.4.0 84 | -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/linux-arm64-gnu/README.md: -------------------------------------------------------------------------------- 1 | # `@livekit/rtc-node-linux-arm64-gnu` 2 | 3 | This is the **aarch64-unknown-linux-gnu** binary for `@livekit/rtc-node` 4 | -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/linux-arm64-gnu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@livekit/rtc-node-linux-arm64-gnu", 3 | "author": "LiveKit", 4 | "version": "0.13.14", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/livekit/node-sdks.git", 8 | "directory": "packages/livekit-rtc/linux-arm64-gnu" 9 | }, 10 | "os": [ 11 | "linux" 12 | ], 13 | "cpu": [ 14 | "arm64" 15 | ], 16 | "main": "rtc-node.linux-arm64-gnu.node", 17 | "files": [ 18 | "rtc-node.linux-arm64-gnu.node" 19 | ], 20 | "license": "Apache-2.0", 21 | "engines": { 22 | "node": ">= 10" 23 | }, 24 | "libc": [ 25 | "glibc" 26 | ] 27 | } -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/linux-x64-gnu/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @livekit/rtc-node-linux-x64-gnu 2 | 3 | ## 0.13.14 4 | 5 | ## 0.13.13 6 | 7 | ## 0.13.12 8 | 9 | ## 0.13.11 10 | 11 | ## 0.13.10 12 | 13 | ## 0.13.9 14 | 15 | ## 0.13.8 16 | 17 | ## 0.13.7 18 | 19 | ## 0.13.6 20 | 21 | ## 0.13.5 22 | 23 | ## 0.13.4 24 | 25 | ## 0.13.3 26 | 27 | ## 0.13.2 28 | 29 | ## 0.13.1 30 | 31 | ## 0.13.0 32 | 33 | ## 0.12.2 34 | 35 | ## 0.12.1 36 | 37 | ## 0.12.0 38 | 39 | ## 0.11.1 40 | 41 | ## 0.11.0 42 | 43 | ## 0.10.4 44 | 45 | ## 0.10.3 46 | 47 | ## 0.10.2 48 | 49 | ## 0.10.1 50 | 51 | ## 0.10.0 52 | 53 | ## 0.9.2 54 | 55 | ## 0.9.1 56 | 57 | ## 0.9.0 58 | 59 | ## 0.8.1 60 | 61 | ## 0.8.0 62 | 63 | ## 0.7.0 64 | 65 | ## 0.6.2 66 | 67 | ## 0.6.1 68 | 69 | ## 0.6.0 70 | 71 | ## 0.5.1 72 | 73 | ## 0.5.0 74 | 75 | ## 0.4.4 76 | 77 | ## 0.4.3 78 | 79 | ## 0.4.2 80 | 81 | ## 0.4.1 82 | 83 | ## 0.4.0 84 | -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/linux-x64-gnu/README.md: -------------------------------------------------------------------------------- 1 | # `@livekit/rtc-node-linux-x64-gnu` 2 | 3 | This is the **x86_64-unknown-linux-gnu** binary for `@livekit/rtc-node` 4 | -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/linux-x64-gnu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@livekit/rtc-node-linux-x64-gnu", 3 | "author": "LiveKit", 4 | "version": "0.13.14", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/livekit/node-sdks.git", 8 | "directory": "packages/livekit-rtc/linux-x64-gnu" 9 | }, 10 | "os": [ 11 | "linux" 12 | ], 13 | "cpu": [ 14 | "x64" 15 | ], 16 | "main": "rtc-node.linux-x64-gnu.node", 17 | "files": [ 18 | "rtc-node.linux-x64-gnu.node" 19 | ], 20 | "license": "Apache-2.0", 21 | "engines": { 22 | "node": ">= 10" 23 | }, 24 | "libc": [ 25 | "glibc" 26 | ] 27 | } -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/win32-x64-msvc/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @livekit/rtc-node-win32-x64-msvc 2 | 3 | ## 0.13.14 4 | 5 | ## 0.13.13 6 | 7 | ## 0.13.12 8 | 9 | ## 0.13.11 10 | 11 | ## 0.13.10 12 | 13 | ## 0.13.9 14 | 15 | ## 0.13.8 16 | 17 | ## 0.13.7 18 | 19 | ## 0.13.6 20 | 21 | ## 0.13.5 22 | 23 | ## 0.13.4 24 | 25 | ## 0.13.3 26 | 27 | ## 0.13.2 28 | 29 | ## 0.13.1 30 | 31 | ## 0.13.0 32 | 33 | ## 0.12.2 34 | 35 | ## 0.12.1 36 | 37 | ## 0.12.0 38 | 39 | ## 0.11.1 40 | 41 | ## 0.11.0 42 | 43 | ## 0.10.4 44 | 45 | ## 0.10.3 46 | 47 | ## 0.10.2 48 | 49 | ## 0.10.1 50 | 51 | ## 0.10.0 52 | 53 | ## 0.9.2 54 | 55 | ## 0.9.1 56 | 57 | ## 0.9.0 58 | 59 | ## 0.8.1 60 | 61 | ## 0.8.0 62 | 63 | ## 0.7.0 64 | 65 | ## 0.6.2 66 | 67 | ## 0.6.1 68 | 69 | ## 0.6.0 70 | 71 | ## 0.5.1 72 | 73 | ## 0.5.0 74 | 75 | ## 0.4.4 76 | 77 | ## 0.4.3 78 | 79 | ## 0.4.2 80 | 81 | ## 0.4.1 82 | 83 | ## 0.4.0 84 | -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/win32-x64-msvc/README.md: -------------------------------------------------------------------------------- 1 | # `@livekit/rtc-node-win32-x64-msvc` 2 | 3 | This is the **x86_64-pc-windows-msvc** binary for `@livekit/rtc-node` 4 | -------------------------------------------------------------------------------- /packages/livekit-rtc/npm/win32-x64-msvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@livekit/rtc-node-win32-x64-msvc", 3 | "author": "LiveKit", 4 | "version": "0.13.14", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/livekit/node-sdks.git", 8 | "directory": "packages/livekit-rtc/win32-x64-msvc" 9 | }, 10 | "os": [ 11 | "win32" 12 | ], 13 | "cpu": [ 14 | "x64" 15 | ], 16 | "main": "rtc-node.win32-x64-msvc.node", 17 | "files": [ 18 | "rtc-node.win32-x64-msvc.node" 19 | ], 20 | "license": "Apache-2.0", 21 | "engines": { 22 | "node": ">= 10" 23 | } 24 | } -------------------------------------------------------------------------------- /packages/livekit-rtc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@livekit/rtc-node", 3 | "description": "LiveKit RTC Node", 4 | "license": "Apache-2.0", 5 | "author": "LiveKit", 6 | "version": "0.13.14", 7 | "main": "dist/index.js", 8 | "require": "dist/index.cjs", 9 | "types": "dist/index.d.ts", 10 | "exports": { 11 | ".": { 12 | "import": { 13 | "types": "./dist/index.d.ts", 14 | "default": "./dist/index.js" 15 | }, 16 | "require": { 17 | "types": "./dist/index.d.cts", 18 | "default": "./dist/index.cjs" 19 | } 20 | } 21 | }, 22 | "type": "module", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/livekit/node-sdks.git", 26 | "directory": "packages/livekit-rtc" 27 | }, 28 | "files": [ 29 | "dist", 30 | "src", 31 | "build.rs", 32 | "Cargo.toml", 33 | "Cargo.lock" 34 | ], 35 | "napi": { 36 | "name": "rtc-node", 37 | "triples": { 38 | "defaults": false, 39 | "additional": [ 40 | "aarch64-apple-darwin", 41 | "x86_64-apple-darwin", 42 | "aarch64-unknown-linux-gnu", 43 | "x86_64-unknown-linux-gnu", 44 | "x86_64-pc-windows-msvc" 45 | ] 46 | } 47 | }, 48 | "dependencies": { 49 | "@bufbuild/protobuf": "^1.10.0", 50 | "@livekit/mutex": "^1.0.0", 51 | "@livekit/typed-emitter": "^3.0.0", 52 | "pino": "^9.0.0", 53 | "pino-pretty": "^13.0.0" 54 | }, 55 | "devDependencies": { 56 | "@napi-rs/cli": "^2.18.0", 57 | "@types/node": "^22.13.10", 58 | "prettier": "^3.0.3", 59 | "tsup": "^8.3.5", 60 | "typescript": "^5.2.2", 61 | "@bufbuild/protoc-gen-es": "^1.10.0" 62 | }, 63 | "optionalDependencies": { 64 | "@livekit/rtc-node-darwin-arm64": "workspace:*", 65 | "@livekit/rtc-node-darwin-x64": "workspace:*", 66 | "@livekit/rtc-node-linux-arm64-gnu": "workspace:*", 67 | "@livekit/rtc-node-linux-x64-gnu": "workspace:*", 68 | "@livekit/rtc-node-win32-x64-msvc": "workspace:*" 69 | }, 70 | "engines": { 71 | "node": ">= 18" 72 | }, 73 | "scripts": { 74 | "prebuild": "node -p \"'export const SDK_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts", 75 | "build:ts": "pnpm prebuild && tsup --onSuccess \"tsc --declaration --emitDeclarationOnly\" && cp -r src/napi dist/ && cp -r src/napi/* dist/", 76 | "build": "pnpm build:ts && napi build --platform --release --dts native.d.ts --js native.cjs --pipe \"prettier -w\" src/napi", 77 | "artifacts": "pnpm build:ts && napi artifacts", 78 | "build:debug": "napi build --platform", 79 | "lint": "eslint -f unix \"src/**/*.ts\" --ignore-pattern \"src/proto/*\"", 80 | "universal": "napi universal", 81 | "version": "napi version" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/audio_filter.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import { FfiClient } from './ffi_client.js'; 5 | import type { LoadAudioFilterPluginResponse } from './proto/audio_frame_pb.js'; 6 | import { LoadAudioFilterPluginRequest } from './proto/audio_frame_pb.js'; 7 | 8 | export class AudioFilter { 9 | constructor(moduleId: string, path: string, dependencies: string[] = []) { 10 | const req = new LoadAudioFilterPluginRequest({ 11 | moduleId, 12 | pluginPath: path, 13 | dependencies, 14 | }); 15 | 16 | const res = FfiClient.instance.request({ 17 | message: { 18 | case: 'loadAudioFilterPlugin', 19 | value: req, 20 | }, 21 | }); 22 | 23 | if (res.error) { 24 | throw new Error(`Failed to initialize audio filter: ${res.error}`); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/audio_frame.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import { FfiClient, FfiHandle } from './ffi_client.js'; 5 | import type { OwnedAudioFrameBuffer } from './proto/audio_frame_pb.js'; 6 | import { AudioFrameBufferInfo } from './proto/audio_frame_pb.js'; 7 | 8 | export class AudioFrame { 9 | data: Int16Array; 10 | sampleRate: number; 11 | channels: number; 12 | samplesPerChannel: number; 13 | 14 | // note: if converting from Uint8Array to Int16Array, *do not* use buffer.slice! 15 | // it is marked unstable by Node and can cause undefined behaviour, such as massive chunks of 16 | // noise being added to the end. 17 | // it is recommended to use buffer.subarray instead. 18 | // XXX(nbsp): add this when writing proper docs 19 | constructor(data: Int16Array, sampleRate: number, channels: number, samplesPerChannel: number) { 20 | this.data = data; 21 | this.sampleRate = sampleRate; 22 | this.channels = channels; 23 | this.samplesPerChannel = samplesPerChannel; 24 | } 25 | 26 | static create(sampleRate: number, channels: number, samplesPerChannel: number): AudioFrame { 27 | const data = new Int16Array(channels * samplesPerChannel); 28 | return new AudioFrame(data, sampleRate, channels, samplesPerChannel); 29 | } 30 | 31 | /** @internal */ 32 | static fromOwnedInfo(owned: OwnedAudioFrameBuffer): AudioFrame { 33 | const info = owned.info!; 34 | const len = info.numChannels! * info.samplesPerChannel! * 2; // c_int16 35 | const data = FfiClient.instance.copyBuffer(info.dataPtr!, len); 36 | new FfiHandle(owned.handle!.id!).dispose(); 37 | return new AudioFrame( 38 | new Int16Array(data.buffer), 39 | info.sampleRate!, 40 | info.numChannels!, 41 | info.samplesPerChannel!, 42 | ); 43 | } 44 | 45 | /** @internal */ 46 | protoInfo(): AudioFrameBufferInfo { 47 | return new AudioFrameBufferInfo({ 48 | dataPtr: FfiClient.instance.retrievePtr(new Uint8Array(this.data.buffer)), 49 | sampleRate: this.sampleRate, 50 | numChannels: this.channels, 51 | samplesPerChannel: this.samplesPerChannel, 52 | }); 53 | } 54 | } 55 | 56 | /** 57 | * Combines one or more `rtc.AudioFrame` objects into a single `rtc.AudioFrame`. 58 | * 59 | * This function concatenates the audio data from multiple frames, ensuring that all frames have 60 | * the same sample rate and number of channels. It efficiently merges the data by preallocating the 61 | * necessary memory and copying the frame data without unnecessary reallocations. 62 | * 63 | * @param buffer - a single AudioFrame or list thereof 64 | */ 65 | export const combineAudioFrames = (buffer: AudioFrame | AudioFrame[]): AudioFrame => { 66 | if (!Array.isArray(buffer)) { 67 | return buffer; 68 | } 69 | buffer = buffer as AudioFrame[]; 70 | 71 | if (buffer.length === 0) { 72 | throw new Error('buffer is empty'); 73 | } 74 | 75 | const sampleRate = buffer[0]!.sampleRate; 76 | const channels = buffer[0]!.channels; 77 | 78 | let totalSamplesPerChannel = 0; 79 | for (const frame of buffer) { 80 | if (frame.sampleRate != sampleRate) { 81 | throw new Error(`sample rate mismatch: expected ${sampleRate}, got ${frame.sampleRate}`); 82 | } 83 | 84 | if (frame.channels != channels) { 85 | throw new Error(`channel mismatch: expected ${channels}, got ${frame.channels}`); 86 | } 87 | 88 | totalSamplesPerChannel += frame.samplesPerChannel; 89 | } 90 | 91 | const data = new Int16Array(buffer.map((x) => [...x.data]).flat()); 92 | return new AudioFrame(data, sampleRate, channels, totalSamplesPerChannel); 93 | }; 94 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/audio_resampler.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import { AudioFrame } from './audio_frame.js'; 5 | import { FfiClient, FfiHandle } from './ffi_client.js'; 6 | import type { 7 | FlushSoxResamplerResponse, 8 | NewSoxResamplerResponse, 9 | PushSoxResamplerResponse, 10 | } from './proto/audio_frame_pb.js'; 11 | import { 12 | FlushSoxResamplerRequest, 13 | NewSoxResamplerRequest, 14 | PushSoxResamplerRequest, 15 | SoxQualityRecipe, 16 | SoxResamplerDataType, 17 | } from './proto/audio_frame_pb.js'; 18 | 19 | /** 20 | * Resampler quality. Higher quality settings result in better audio quality but require more 21 | * processing power. 22 | */ 23 | export enum AudioResamplerQuality { 24 | QUICK = SoxQualityRecipe.SOXR_QUALITY_QUICK, 25 | LOW = SoxQualityRecipe.SOXR_QUALITY_LOW, 26 | MEDIUM = SoxQualityRecipe.SOXR_QUALITY_MEDIUM, 27 | HIGH = SoxQualityRecipe.SOXR_QUALITY_HIGH, 28 | VERY_HIGH = SoxQualityRecipe.SOXR_QUALITY_VERYHIGH, 29 | } 30 | 31 | /** 32 | * AudioResampler provides functionality to resample audio data from an input sample rate to 33 | * an output sample rate using the Sox resampling library. It supports multiple channels and 34 | * configurable resampling quality. 35 | */ 36 | export class AudioResampler { 37 | #inputRate: number; 38 | #outputRate: number; 39 | #channels: number; 40 | #ffiHandle: FfiHandle; 41 | 42 | /** 43 | * Initializes a new AudioResampler. 44 | * 45 | * @param inputRate - The sample rate of the input audio data (in Hz). 46 | * @param outputRate - The desired sample rate of the output audio data (in Hz). 47 | * @param channels - The number of audio channels (e.g., 1 for mono, 2 for stereo). Defaults to 1. 48 | * @param quality - The quality setting for the resampler. Defaults to 49 | * `AudioResamplerQuality.MEDIUM`. 50 | */ 51 | constructor( 52 | inputRate: number, 53 | outputRate: number, 54 | channels = 1, 55 | quality = AudioResamplerQuality.MEDIUM, 56 | ) { 57 | this.#inputRate = inputRate; 58 | this.#outputRate = outputRate; 59 | this.#channels = channels; 60 | 61 | const req = new NewSoxResamplerRequest({ 62 | inputRate, 63 | outputRate, 64 | numChannels: channels, 65 | qualityRecipe: quality as number as SoxQualityRecipe, 66 | inputDataType: SoxResamplerDataType.SOXR_DATATYPE_INT16I, 67 | outputDataType: SoxResamplerDataType.SOXR_DATATYPE_INT16I, 68 | flags: 0, 69 | }); 70 | 71 | const res = FfiClient.instance.request({ 72 | message: { 73 | case: 'newSoxResampler', 74 | value: req, 75 | }, 76 | }); 77 | 78 | switch (res.message.case) { 79 | case 'resampler': 80 | this.#ffiHandle = new FfiHandle(res.message.value.handle!.id!); 81 | break; 82 | case 'error': 83 | default: 84 | throw new Error(res.message.value); 85 | } 86 | } 87 | 88 | /** 89 | * Push audio data into the resampler and retrieve any available resampled data. 90 | * 91 | * This method accepts audio data, resamples it according to the configured input and output rates, 92 | * and returns any resampled data that is available after processing the input. 93 | * 94 | * @param data - The audio frame to resample 95 | * 96 | * @returns A list of {@link AudioFrame} objects containing the resampled audio data. The list may 97 | * be empty if no output data is available yet. 98 | */ 99 | push(data: AudioFrame): AudioFrame[] { 100 | const req = new PushSoxResamplerRequest({ 101 | resamplerHandle: this.#ffiHandle.handle, 102 | dataPtr: data.protoInfo().dataPtr, 103 | size: data.data.byteLength, 104 | }); 105 | 106 | const res = FfiClient.instance.request({ 107 | message: { 108 | case: 'pushSoxResampler', 109 | value: req, 110 | }, 111 | }); 112 | 113 | if (res.error) { 114 | throw new Error(res.error); 115 | } 116 | 117 | if (!res.outputPtr) { 118 | return []; 119 | } 120 | 121 | const outputData = FfiClient.instance.copyBuffer(res.outputPtr, res.size!); 122 | return [ 123 | new AudioFrame( 124 | new Int16Array(outputData.buffer), 125 | this.#outputRate, 126 | this.#channels, 127 | Math.trunc(outputData.length / this.#channels / 2), 128 | ), 129 | ]; 130 | } 131 | 132 | /** 133 | * Flush any remaining audio data through the resampler and retrieve the resampled data. 134 | * 135 | * @remarks 136 | * This method should be called when no more input data will be provided to ensure that all 137 | * internal buffers are processed and all resampled data is output. 138 | */ 139 | flush(): AudioFrame[] { 140 | const req = new FlushSoxResamplerRequest({ 141 | resamplerHandle: this.#ffiHandle.handle, 142 | }); 143 | 144 | const res = FfiClient.instance.request({ 145 | message: { 146 | case: 'flushSoxResampler', 147 | value: req, 148 | }, 149 | }); 150 | 151 | if (res.error) { 152 | throw new Error(res.error); 153 | } 154 | 155 | if (!res.outputPtr) { 156 | return []; 157 | } 158 | 159 | const outputData = FfiClient.instance.copyBuffer(res.outputPtr, res.size!); 160 | return [ 161 | new AudioFrame( 162 | new Int16Array(outputData.buffer), 163 | this.#outputRate, 164 | this.#channels, 165 | Math.trunc(outputData.length / this.#channels / 2), 166 | ), 167 | ]; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/audio_source.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import type { AudioFrame } from './audio_frame.js'; 5 | import { FfiClient } from './ffi_client.js'; 6 | import { FfiHandle } from './napi/native.js'; 7 | import type { 8 | AudioSourceInfo, 9 | CaptureAudioFrameCallback, 10 | CaptureAudioFrameResponse, 11 | ClearAudioBufferResponse, 12 | NewAudioSourceResponse, 13 | } from './proto/audio_frame_pb.js'; 14 | import { 15 | AudioSourceType, 16 | CaptureAudioFrameRequest, 17 | ClearAudioBufferRequest, 18 | NewAudioSourceRequest, 19 | } from './proto/audio_frame_pb.js'; 20 | 21 | export class AudioSource { 22 | /** @internal */ 23 | info: AudioSourceInfo; 24 | /** @internal */ 25 | ffiHandle: FfiHandle; 26 | /** @internal */ 27 | lastCapture: number; 28 | /** @internal */ 29 | currentQueueSize: number; 30 | /** @internal */ 31 | release = () => {}; 32 | promise = this.newPromise(); 33 | /** @internal */ 34 | timeout?: ReturnType = undefined; 35 | /** @internal */ 36 | closed = false; 37 | 38 | sampleRate: number; 39 | numChannels: number; 40 | queueSize: number; 41 | 42 | constructor(sampleRate: number, numChannels: number, queueSize = 1000) { 43 | this.sampleRate = sampleRate; 44 | this.numChannels = numChannels; 45 | this.queueSize = queueSize; 46 | 47 | this.lastCapture = 0; 48 | this.currentQueueSize = 0; 49 | 50 | const req = new NewAudioSourceRequest({ 51 | type: AudioSourceType.AUDIO_SOURCE_NATIVE, 52 | sampleRate: sampleRate, 53 | numChannels: numChannels, 54 | queueSizeMs: queueSize, 55 | }); 56 | 57 | const res = FfiClient.instance.request({ 58 | message: { 59 | case: 'newAudioSource', 60 | value: req, 61 | }, 62 | }); 63 | 64 | this.info = res.source!.info!; 65 | this.ffiHandle = new FfiHandle(res.source!.handle!.id!); 66 | } 67 | 68 | get queuedDuration(): number { 69 | return Math.max( 70 | this.currentQueueSize - Number(process.hrtime.bigint() / BigInt(1000000)) + this.lastCapture, 71 | 0, 72 | ); 73 | } 74 | 75 | clearQueue() { 76 | const req = new ClearAudioBufferRequest({ 77 | sourceHandle: this.ffiHandle.handle, 78 | }); 79 | 80 | FfiClient.instance.request({ 81 | message: { 82 | case: 'clearAudioBuffer', 83 | value: req, 84 | }, 85 | }); 86 | 87 | this.release(); 88 | } 89 | 90 | /** @internal */ 91 | async newPromise() { 92 | return new Promise((resolve) => { 93 | this.release = resolve; 94 | }); 95 | } 96 | 97 | async waitForPlayout() { 98 | return this.promise.then(() => { 99 | this.lastCapture = 0; 100 | this.currentQueueSize = 0; 101 | this.promise = this.newPromise(); 102 | this.timeout = undefined; 103 | }); 104 | } 105 | 106 | async captureFrame(frame: AudioFrame) { 107 | if (this.closed) { 108 | throw new Error('AudioSource is closed'); 109 | } 110 | 111 | if (frame.samplesPerChannel === 0) { 112 | return; 113 | } 114 | 115 | const now = Number(process.hrtime.bigint() / BigInt(1000000)); 116 | const elapsed = this.lastCapture === 0 ? 0 : now - this.lastCapture; 117 | const frameDurationMs = (frame.samplesPerChannel / frame.sampleRate) * 1000; 118 | this.currentQueueSize += frameDurationMs - elapsed; 119 | 120 | this.lastCapture = now; 121 | 122 | if (this.timeout) { 123 | clearTimeout(this.timeout); 124 | } 125 | 126 | this.timeout = setTimeout(this.release, this.currentQueueSize); 127 | 128 | const req = new CaptureAudioFrameRequest({ 129 | sourceHandle: this.ffiHandle.handle, 130 | buffer: frame.protoInfo(), 131 | }); 132 | 133 | const res = FfiClient.instance.request({ 134 | message: { case: 'captureAudioFrame', value: req }, 135 | }); 136 | 137 | const cb = await FfiClient.instance.waitFor((ev) => { 138 | return ev.message.case == 'captureAudioFrame' && ev.message.value.asyncId == res.asyncId; 139 | }); 140 | 141 | if (cb.error) { 142 | throw new Error(cb.error); 143 | } 144 | } 145 | 146 | async close() { 147 | this.ffiHandle.dispose(); 148 | this.closed = true; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/audio_stream.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import type { UnderlyingSource } from 'node:stream/web'; 5 | import { AudioFrame } from './audio_frame.js'; 6 | import type { FfiEvent } from './ffi_client.js'; 7 | import { FfiClient, FfiClientEvent, FfiHandle } from './ffi_client.js'; 8 | import type { NewAudioStreamResponse } from './proto/audio_frame_pb.js'; 9 | import { AudioStreamType, NewAudioStreamRequest } from './proto/audio_frame_pb.js'; 10 | import type { Track } from './track.js'; 11 | 12 | export interface AudioStreamOptions { 13 | noiseCancellation?: NoiseCancellationOptions; 14 | sampleRate?: number; 15 | numChannels?: number; 16 | } 17 | 18 | export interface NoiseCancellationOptions { 19 | moduleId: string; 20 | options: Record; 21 | } 22 | 23 | class AudioStreamSource implements UnderlyingSource { 24 | private controller?: ReadableStreamDefaultController; 25 | private ffiHandle: FfiHandle; 26 | private sampleRate: number; 27 | private numChannels: number; 28 | private ncOptions?: NoiseCancellationOptions; 29 | 30 | constructor( 31 | track: Track, 32 | sampleRateOrOptions?: number | AudioStreamOptions, 33 | numChannels?: number, 34 | ) { 35 | if (sampleRateOrOptions !== undefined && typeof sampleRateOrOptions !== 'number') { 36 | this.sampleRate = sampleRateOrOptions.sampleRate ?? 48000; 37 | this.numChannels = sampleRateOrOptions.numChannels ?? 1; 38 | this.ncOptions = sampleRateOrOptions.noiseCancellation; 39 | } else { 40 | this.sampleRate = (sampleRateOrOptions as number) ?? 48000; 41 | this.numChannels = numChannels ?? 1; 42 | } 43 | 44 | const req = new NewAudioStreamRequest({ 45 | type: AudioStreamType.AUDIO_STREAM_NATIVE, 46 | trackHandle: track.ffi_handle.handle, 47 | sampleRate: this.sampleRate, 48 | numChannels: this.numChannels, 49 | ...(this.ncOptions 50 | ? { 51 | audioFilterModuleId: this.ncOptions.moduleId, 52 | audioFilterOptions: JSON.stringify(this.ncOptions.options), 53 | } 54 | : {}), 55 | }); 56 | 57 | const res = FfiClient.instance.request({ 58 | message: { 59 | case: 'newAudioStream', 60 | value: req, 61 | }, 62 | }); 63 | 64 | this.ffiHandle = new FfiHandle(res.stream!.handle!.id!); 65 | 66 | FfiClient.instance.on(FfiClientEvent.FfiEvent, this.onEvent); 67 | } 68 | 69 | private onEvent = (ev: FfiEvent) => { 70 | if (!this.controller) { 71 | throw new Error('Stream controller not initialized'); 72 | } 73 | 74 | if ( 75 | ev.message.case != 'audioStreamEvent' || 76 | ev.message.value.streamHandle != this.ffiHandle.handle 77 | ) { 78 | return; 79 | } 80 | 81 | const streamEvent = ev.message.value.message; 82 | switch (streamEvent.case) { 83 | case 'frameReceived': 84 | const frame = AudioFrame.fromOwnedInfo(streamEvent.value.frame!); 85 | this.controller.enqueue(frame); 86 | break; 87 | case 'eos': 88 | FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent); 89 | this.controller.close(); 90 | break; 91 | } 92 | }; 93 | 94 | start(controller: ReadableStreamDefaultController) { 95 | this.controller = controller; 96 | } 97 | 98 | cancel() { 99 | FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent); 100 | this.ffiHandle.dispose(); 101 | } 102 | } 103 | 104 | export class AudioStream extends ReadableStream { 105 | constructor(track: Track); 106 | constructor(track: Track, sampleRate: number); 107 | constructor(track: Track, sampleRate: number, numChannels: number); 108 | constructor(track: Track, options: AudioStreamOptions); 109 | constructor( 110 | track: Track, 111 | sampleRateOrOptions?: number | AudioStreamOptions, 112 | numChannels?: number, 113 | ) { 114 | super(new AudioStreamSource(track, sampleRateOrOptions, numChannels)); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/data_streams/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | export * from './stream_reader.js'; 6 | export * from './stream_writer.js'; 7 | export type * from './types.js'; 8 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/data_streams/stream_reader.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import { log } from '../log.js'; 5 | import type { DataStream_Chunk } from '../proto/room_pb.js'; 6 | import { bigIntToNumber } from '../utils.js'; 7 | import type { BaseStreamInfo, ByteStreamInfo, TextStreamInfo } from './types.js'; 8 | 9 | abstract class BaseStreamReader { 10 | protected reader: ReadableStream; 11 | 12 | protected totalByteSize?: number; 13 | 14 | protected _info: T; 15 | 16 | protected bytesReceived: number; 17 | 18 | get info() { 19 | return this._info; 20 | } 21 | 22 | constructor(info: T, stream: ReadableStream, totalByteSize?: number) { 23 | this.reader = stream; 24 | this.totalByteSize = totalByteSize; 25 | this._info = info; 26 | this.bytesReceived = 0; 27 | } 28 | 29 | protected abstract handleChunkReceived(chunk: DataStream_Chunk): void; 30 | 31 | onProgress?: (progress: number | undefined) => void; 32 | 33 | abstract readAll(): Promise>; 34 | } 35 | 36 | /** 37 | * A class to read chunks from a ReadableStream and provide them in a structured format. 38 | */ 39 | export class ByteStreamReader extends BaseStreamReader { 40 | protected handleChunkReceived(chunk: DataStream_Chunk) { 41 | this.bytesReceived += chunk.content!.byteLength; 42 | const currentProgress = this.totalByteSize 43 | ? this.bytesReceived / this.totalByteSize 44 | : undefined; 45 | this.onProgress?.(currentProgress); 46 | } 47 | 48 | [Symbol.asyncIterator]() { 49 | const reader = this.reader.getReader(); 50 | 51 | return { 52 | next: async (): Promise> => { 53 | try { 54 | const { done, value } = await reader.read(); 55 | if (done) { 56 | return { done: true, value: undefined as any }; 57 | } else { 58 | this.handleChunkReceived(value); 59 | return { done: false, value: value.content! }; 60 | } 61 | } catch (error) { 62 | log.error('error processing stream update', error); 63 | return { done: true, value: undefined }; 64 | } 65 | }, 66 | 67 | return(): IteratorResult { 68 | reader.releaseLock(); 69 | return { done: true, value: undefined }; 70 | }, 71 | }; 72 | } 73 | 74 | async readAll(): Promise> { 75 | const chunks: Set = new Set(); 76 | for await (const chunk of this) { 77 | chunks.add(chunk); 78 | } 79 | return Array.from(chunks); 80 | } 81 | } 82 | 83 | /** 84 | * A class to read chunks from a ReadableStream and provide them in a structured format. 85 | */ 86 | export class TextStreamReader extends BaseStreamReader { 87 | private receivedChunks: Map; 88 | 89 | /** 90 | * A TextStreamReader instance can be used as an AsyncIterator that returns the entire string 91 | * that has been received up to the current point in time. 92 | */ 93 | constructor( 94 | info: TextStreamInfo, 95 | stream: ReadableStream, 96 | totalChunkCount?: number, 97 | ) { 98 | super(info, stream, totalChunkCount); 99 | this.receivedChunks = new Map(); 100 | } 101 | 102 | protected handleChunkReceived(chunk: DataStream_Chunk) { 103 | const index = bigIntToNumber(chunk.chunkIndex!); 104 | const previousChunkAtIndex = this.receivedChunks.get(index!); 105 | if (previousChunkAtIndex && previousChunkAtIndex.version! > chunk.version!) { 106 | // we have a newer version already, dropping the old one 107 | return; 108 | } 109 | this.receivedChunks.set(index, chunk); 110 | const currentProgress = this.totalByteSize 111 | ? this.receivedChunks.size / this.totalByteSize 112 | : undefined; 113 | this.onProgress?.(currentProgress); 114 | } 115 | 116 | /** 117 | * Async iterator implementation to allow usage of `for await...of` syntax. 118 | * Yields structured chunks from the stream. 119 | * 120 | */ 121 | [Symbol.asyncIterator]() { 122 | const reader = this.reader.getReader(); 123 | const decoder = new TextDecoder(); 124 | 125 | return { 126 | next: async (): Promise> => { 127 | try { 128 | const { done, value } = await reader.read(); 129 | if (done) { 130 | return { done: true, value: undefined }; 131 | } else { 132 | this.handleChunkReceived(value); 133 | return { 134 | done: false, 135 | value: decoder.decode(value.content!), 136 | }; 137 | } 138 | } catch (error) { 139 | log.error('error processing stream update', error); 140 | return { done: true, value: undefined }; 141 | } 142 | }, 143 | 144 | return(): IteratorResult { 145 | reader.releaseLock(); 146 | return { done: true, value: undefined }; 147 | }, 148 | }; 149 | } 150 | 151 | async readAll(): Promise { 152 | let finalString: string = ''; 153 | for await (const chunk of this) { 154 | finalString += chunk; 155 | } 156 | return finalString; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/data_streams/stream_writer.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import type { BaseStreamInfo, ByteStreamInfo, TextStreamInfo } from './types.js'; 5 | 6 | class BaseStreamWriter { 7 | protected writableStream: WritableStream; 8 | 9 | protected defaultWriter: WritableStreamDefaultWriter; 10 | 11 | protected onClose?: () => void; 12 | 13 | readonly info: InfoType; 14 | 15 | constructor(writableStream: WritableStream, info: InfoType, onClose?: () => void) { 16 | this.writableStream = writableStream; 17 | this.defaultWriter = writableStream.getWriter(); 18 | this.onClose = onClose; 19 | this.info = info; 20 | } 21 | 22 | write(chunk: T): Promise { 23 | return this.defaultWriter.write(chunk); 24 | } 25 | 26 | async close() { 27 | await this.defaultWriter.close(); 28 | this.defaultWriter.releaseLock(); 29 | this.onClose?.(); 30 | } 31 | } 32 | 33 | export class TextStreamWriter extends BaseStreamWriter {} 34 | 35 | export class ByteStreamWriter extends BaseStreamWriter {} 36 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/data_streams/types.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import type { DataStream_Chunk, DataStream_Header } from '../proto/room_pb.js'; 5 | import type { ByteStreamReader, TextStreamReader } from './stream_reader.js'; 6 | 7 | export interface StreamController { 8 | header: DataStream_Header; 9 | controller: ReadableStreamDefaultController; 10 | startTime: number; 11 | endTime?: number; 12 | } 13 | 14 | export interface BaseStreamInfo { 15 | streamId: string; 16 | mimeType: string; 17 | topic: string; 18 | timestamp: number; 19 | /** total size in bytes for finite streams and undefined for streams of unknown size */ 20 | totalSize?: number; 21 | attributes?: Record; 22 | } 23 | 24 | export type ByteStreamInfo = BaseStreamInfo & { 25 | name: string; 26 | }; 27 | 28 | export type TextStreamInfo = BaseStreamInfo; 29 | 30 | export interface DataStreamOptions { 31 | topic?: string; 32 | destinationIdentities?: Array; 33 | attributes?: Record; 34 | mimeType?: string; 35 | } 36 | 37 | export interface TextStreamOptions extends DataStreamOptions { 38 | // replyToMessageId?: string; 39 | attachments?: []; 40 | } 41 | 42 | export interface ByteStreamOptions extends DataStreamOptions { 43 | name?: string; 44 | onProgress?: (progress: number) => void; 45 | } 46 | 47 | export type ByteStreamHandler = ( 48 | reader: ByteStreamReader, 49 | participantInfo: { identity: string }, 50 | ) => void; 51 | 52 | export type TextStreamHandler = ( 53 | reader: TextStreamReader, 54 | participantInfo: { identity: string }, 55 | ) => void; 56 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/ffi_client.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import type { PartialMessage } from '@bufbuild/protobuf'; 5 | import type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter'; 6 | import EventEmitter from 'events'; 7 | import { 8 | FfiHandle, 9 | livekitCopyBuffer, 10 | livekitDispose, 11 | livekitFfiRequest, 12 | livekitInitialize, 13 | livekitRetrievePtr, 14 | } from './napi/native.js'; 15 | import { FfiEvent, FfiRequest, FfiResponse } from './proto/ffi_pb.js'; 16 | import { SDK_VERSION } from './version.js'; 17 | 18 | export { FfiHandle, type FfiEvent, type FfiResponse, FfiRequest, livekitDispose as dispose }; 19 | 20 | export type FfiClientCallbacks = { 21 | ffi_event: (event: FfiEvent) => void; 22 | }; 23 | 24 | export enum FfiClientEvent { 25 | FfiEvent = 'ffi_event', 26 | } 27 | 28 | declare global { 29 | // eslint-disable-next-line no-var 30 | var _ffiClientInstance: FfiClient | undefined; 31 | } 32 | 33 | export class FfiClient extends (EventEmitter as new () => TypedEmitter) { 34 | /** @internal */ 35 | static get instance(): FfiClient { 36 | if (!globalThis._ffiClientInstance) { 37 | globalThis._ffiClientInstance = new FfiClient(); 38 | } 39 | return globalThis._ffiClientInstance; 40 | } 41 | 42 | constructor() { 43 | super(); 44 | this.setMaxListeners(0); 45 | 46 | livekitInitialize( 47 | (event_data: Uint8Array) => { 48 | const event = FfiEvent.fromBinary(event_data); 49 | this.emit(FfiClientEvent.FfiEvent, event); 50 | }, 51 | true, 52 | SDK_VERSION, 53 | ); 54 | } 55 | 56 | request(req: PartialMessage): T { 57 | const request = new FfiRequest(req); 58 | const req_data = request.toBinary(); 59 | const res_data = livekitFfiRequest(req_data); 60 | return FfiResponse.fromBinary(res_data).message.value as T; 61 | } 62 | 63 | copyBuffer(ptr: bigint, len: number): Uint8Array { 64 | return livekitCopyBuffer(ptr, len); 65 | } 66 | 67 | retrievePtr(data: Uint8Array): bigint { 68 | return livekitRetrievePtr(data); 69 | } 70 | 71 | async waitFor(predicate: (ev: FfiEvent) => boolean): Promise { 72 | return new Promise((resolve) => { 73 | const listener = (ev: FfiEvent) => { 74 | if (predicate(ev)) { 75 | this.off(FfiClientEvent.FfiEvent, listener); 76 | resolve(ev.message.value as T); 77 | } 78 | }; 79 | this.on(FfiClientEvent.FfiEvent, listener); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | export { AudioFrame, combineAudioFrames } from './audio_frame.js'; 6 | export { AudioResampler, AudioResamplerQuality } from './audio_resampler.js'; 7 | export { AudioSource } from './audio_source.js'; 8 | export { AudioStream } from './audio_stream.js'; 9 | export type { NoiseCancellationOptions } from './audio_stream.js'; 10 | export { AudioFilter } from './audio_filter.js'; 11 | export * from './data_streams/index.js'; 12 | export { E2EEManager, FrameCryptor, KeyProvider } from './e2ee.js'; 13 | export type { E2EEOptions, KeyProviderOptions } from './e2ee.js'; 14 | export { dispose } from './ffi_client.js'; 15 | export { LocalParticipant, Participant, RemoteParticipant } from './participant.js'; 16 | export { EncryptionState, EncryptionType } from './proto/e2ee_pb.js'; 17 | export { DisconnectReason, ParticipantKind } from './proto/participant_pb.js'; 18 | export { 19 | ConnectionQuality, 20 | ConnectionState, 21 | ContinualGatheringPolicy, 22 | DataPacketKind, 23 | IceServer, 24 | IceTransportType, 25 | TrackPublishOptions, 26 | } from './proto/room_pb.js'; 27 | export { StreamState, TrackKind, TrackSource } from './proto/track_pb.js'; 28 | export { VideoBufferType, VideoCodec, VideoRotation } from './proto/video_frame_pb.js'; 29 | export { ConnectError, Room, RoomEvent, type RoomOptions, type RtcConfiguration } from './room.js'; 30 | export { RpcError, type PerformRpcParams, type RpcInvocationData } from './rpc.js'; 31 | export { 32 | LocalAudioTrack, 33 | LocalVideoTrack, 34 | RemoteAudioTrack, 35 | RemoteVideoTrack, 36 | Track, 37 | type AudioTrack, 38 | type LocalTrack, 39 | type RemoteTrack, 40 | type VideoTrack, 41 | } from './track.js'; 42 | export { 43 | LocalTrackPublication, 44 | RemoteTrackPublication, 45 | TrackPublication, 46 | } from './track_publication.js'; 47 | export type { Transcription, TranscriptionSegment } from './transcription.js'; 48 | export type { ChatMessage } from './types.js'; 49 | export { VideoFrame } from './video_frame.js'; 50 | export { VideoSource } from './video_source.js'; 51 | export { VideoStream, type VideoFrameEvent } from './video_stream.js'; 52 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | 3 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 4 | // 5 | // SPDX-License-Identifier: Apache-2.0 6 | 7 | extern crate napi_derive; 8 | 9 | pub mod nodejs; 10 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/log.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import type { LevelWithSilent, LoggerOptions } from 'pino'; 5 | import { pino } from 'pino'; 6 | 7 | const isProduction = process.env.NODE_ENV === 'production'; 8 | 9 | const defaultOptions: LoggerOptions = { name: 'lk-rtc' }; 10 | 11 | const devOptions: LoggerOptions = { 12 | ...defaultOptions, 13 | transport: { 14 | target: 'pino-pretty', 15 | options: { 16 | colorize: true, 17 | }, 18 | }, 19 | }; 20 | 21 | const log = pino(isProduction ? defaultOptions : devOptions); 22 | log.level = isProduction ? 'info' : 'debug'; 23 | 24 | export type LogLevel = LevelWithSilent; 25 | export { log }; 26 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/napi/native.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | 4 | /* auto-generated by NAPI-RS */ 5 | 6 | export declare function livekitInitialize( 7 | callback: (data: Uint8Array) => void, 8 | captureLogs: boolean, 9 | sdkVersion: string, 10 | ): void; 11 | export declare function livekitFfiRequest(data: Uint8Array): Uint8Array; 12 | export declare function livekitRetrievePtr(handle: Uint8Array): bigint; 13 | export declare function livekitCopyBuffer(ptr: bigint, len: number): Uint8Array; 14 | export declare function livekitDispose(): Promise; 15 | export declare class FfiHandle { 16 | constructor(handle: bigint); 17 | dispose(): void; 18 | get handle(): bigint; 19 | } 20 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/napi/native.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | // this file exists to smoothly translate the autogenerated CommonJS code 5 | // to an ES module that plays better with TypeScript. 6 | import native from './native.cjs'; 7 | 8 | export const { 9 | livekitDispose, 10 | livekitInitialize, 11 | livekitCopyBuffer, 12 | livekitRetrievePtr, 13 | livekitFfiRequest, 14 | FfiHandle, 15 | } = native; 16 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/nodejs.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | use livekit_ffi::{proto, server, FFI_SERVER}; 6 | use napi::{ 7 | bindgen_prelude::*, 8 | threadsafe_function::{ 9 | ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, 10 | }, 11 | JsFunction, Status, 12 | }; 13 | use napi_derive::napi; 14 | use prost::Message; 15 | use std::sync::Arc; 16 | 17 | #[napi( 18 | ts_args_type = "callback: (data: Uint8Array) => void, captureLogs: boolean, sdkVersion: string" 19 | )] 20 | fn livekit_initialize(cb: JsFunction, capture_logs: bool, sdk_version: String) { 21 | let tsfn: ThreadsafeFunction = cb 22 | .create_threadsafe_function(0, |ctx: ThreadSafeCallContext| { 23 | let data = ctx.value.encode_to_vec(); 24 | let buf = Uint8Array::new(data); 25 | Ok(vec![buf]) 26 | }) 27 | .unwrap(); 28 | 29 | FFI_SERVER.setup(server::FfiConfig { 30 | callback_fn: Arc::new(move |event| { 31 | let status = tsfn.call(event, ThreadsafeFunctionCallMode::NonBlocking); 32 | if status != Status::Ok { 33 | eprintln!("error calling callback status: {}", status); 34 | } 35 | }), 36 | capture_logs, 37 | sdk: "node".to_string(), 38 | sdk_version, 39 | }); 40 | } 41 | 42 | #[napi] 43 | fn livekit_ffi_request(data: Uint8Array) -> Result { 44 | let data = data.to_vec(); 45 | let res = match proto::FfiRequest::decode(data.as_slice()) { 46 | Ok(res) => res, 47 | Err(err) => { 48 | return Err(Error::from_reason(format!( 49 | "failed to decode request: {}", 50 | err.to_string() 51 | ))); 52 | } 53 | }; 54 | 55 | let res = match server::requests::handle_request(&FFI_SERVER, res.clone()) { 56 | Ok(res) => res, 57 | Err(err) => { 58 | return Err(Error::from_reason(format!( 59 | "failed to handle request: {} ({:?})", 60 | err.to_string(), 61 | res 62 | ))); 63 | } 64 | } 65 | .encode_to_vec(); 66 | Ok(Uint8Array::new(res)) 67 | } 68 | 69 | // FfiHandle must be used instead 70 | //#[napi] 71 | //fn livekit_drop_handle(handle: BigInt) -> bool { 72 | // let (_, handle, _) = handle.get_u64(); 73 | // FFI_SERVER.drop_handle(handle) 74 | //} 75 | 76 | #[napi] 77 | fn livekit_retrieve_ptr(handle: Uint8Array) -> BigInt { 78 | BigInt::from(handle.as_ptr() as u64) 79 | } 80 | 81 | #[napi] 82 | fn livekit_copy_buffer(ptr: BigInt, len: u32) -> Uint8Array { 83 | let (_, ptr, _) = ptr.get_u64(); 84 | let data = unsafe { std::slice::from_raw_parts(ptr as *const u8, len as usize) }; 85 | Uint8Array::with_data_copied(data) 86 | } 87 | 88 | #[napi] 89 | async fn livekit_dispose() { 90 | FFI_SERVER.dispose().await; 91 | } 92 | 93 | #[napi(custom_finalize)] 94 | pub struct FfiHandle { 95 | handle: BigInt, 96 | disposed: bool, 97 | // TODO(theomonnom): add gc pressure memory 98 | } 99 | 100 | #[napi] 101 | impl FfiHandle { 102 | #[napi(constructor)] 103 | pub fn new(handle: BigInt) -> Self { 104 | Self { 105 | handle, 106 | disposed: false, 107 | } 108 | } 109 | 110 | #[napi] 111 | pub fn dispose(&mut self) -> Result<()> { 112 | if self.disposed { 113 | return Ok(()); 114 | } 115 | self.disposed = true; 116 | let (_, handle, _) = self.handle.get_u64(); 117 | if !FFI_SERVER.drop_handle(handle) { 118 | return Err(Error::from_reason("trying to drop an invalid handle")); 119 | } 120 | 121 | Ok(()) 122 | } 123 | 124 | #[napi(getter)] 125 | pub fn handle(&self) -> BigInt { 126 | self.handle.clone() 127 | } 128 | } 129 | 130 | impl ObjectFinalize for FfiHandle { 131 | fn finalize(mut self, env: Env) -> Result<()> { 132 | self.dispose() 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/proto/handle_pb.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 LiveKit, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // @generated by protoc-gen-es v1.10.0 with parameter "target=ts,import_extension=.js" 16 | // @generated from file handle.proto (package livekit.proto, syntax proto2) 17 | /* eslint-disable */ 18 | // @ts-nocheck 19 | 20 | import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; 21 | import { Message, proto2 } from "@bufbuild/protobuf"; 22 | 23 | /** 24 | * # Safety 25 | * The foreign language is responsable for disposing handles 26 | * Forgetting to dispose the handle may lead to memory leaks 27 | * 28 | * Dropping a handle doesn't necessarily mean that the object is destroyed if it is still used 29 | * on the FfiServer (Atomic reference counting) 30 | * 31 | * When refering to a handle without owning it, we just use a uint32 without this message. 32 | * (the variable name is suffixed with "_handle") 33 | * 34 | * @generated from message livekit.proto.FfiOwnedHandle 35 | */ 36 | export class FfiOwnedHandle extends Message { 37 | /** 38 | * @generated from field: required uint64 id = 1; 39 | */ 40 | id?: bigint; 41 | 42 | constructor(data?: PartialMessage) { 43 | super(); 44 | proto2.util.initPartial(data, this); 45 | } 46 | 47 | static readonly runtime: typeof proto2 = proto2; 48 | static readonly typeName = "livekit.proto.FfiOwnedHandle"; 49 | static readonly fields: FieldList = proto2.util.newFieldList(() => [ 50 | { no: 1, name: "id", kind: "scalar", T: 4 /* ScalarType.UINT64 */, req: true }, 51 | ]); 52 | 53 | static fromBinary(bytes: Uint8Array, options?: Partial): FfiOwnedHandle { 54 | return new FfiOwnedHandle().fromBinary(bytes, options); 55 | } 56 | 57 | static fromJson(jsonValue: JsonValue, options?: Partial): FfiOwnedHandle { 58 | return new FfiOwnedHandle().fromJson(jsonValue, options); 59 | } 60 | 61 | static fromJsonString(jsonString: string, options?: Partial): FfiOwnedHandle { 62 | return new FfiOwnedHandle().fromJsonString(jsonString, options); 63 | } 64 | 65 | static equals(a: FfiOwnedHandle | PlainMessage | undefined, b: FfiOwnedHandle | PlainMessage | undefined): boolean { 66 | return proto2.util.equals(FfiOwnedHandle, a, b); 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/rpc.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import type { RpcError as RpcError_Proto } from './proto/rpc_pb.js'; 5 | 6 | /** Parameters for initiating an RPC call */ 7 | export interface PerformRpcParams { 8 | /** The `identity` of the destination participant */ 9 | destinationIdentity: string; 10 | /** The method name to call */ 11 | method: string; 12 | /** The method payload */ 13 | payload: string; 14 | /** Timeout for receiving a response after initial connection (milliseconds). Default: 10000 */ 15 | responseTimeout?: number; 16 | } 17 | 18 | /** 19 | * Data passed to method handler for incoming RPC invocations 20 | */ 21 | export interface RpcInvocationData { 22 | /** 23 | * The unique request ID. Will match at both sides of the call, useful for debugging or logging. 24 | */ 25 | requestId: string; 26 | 27 | /** 28 | * The unique participant identity of the caller. 29 | */ 30 | callerIdentity: string; 31 | 32 | /** 33 | * The payload of the request. User-definable format, typically JSON. 34 | */ 35 | payload: string; 36 | 37 | /** 38 | * The maximum time the caller will wait for a response. 39 | */ 40 | responseTimeout: number; 41 | } 42 | 43 | /** 44 | * Specialized error handling for RPC methods. 45 | * 46 | * Instances of this type, when thrown in a method handler, will have their `message` 47 | * serialized and sent across the wire. The caller will receive an equivalent error on the other side. 48 | * 49 | * Built-in types are included but developers may use any string, with a max length of 256 bytes. 50 | */ 51 | 52 | export class RpcError extends Error { 53 | code: typeof RpcError.ErrorCode | number; 54 | data?: string; 55 | 56 | /** 57 | * Creates an error object with the given code and message, plus an optional data payload. 58 | * 59 | * If thrown in an RPC method handler, the error will be sent back to the caller. 60 | * 61 | * Error codes 1001-1999 are reserved for built-in errors (see RpcError.ErrorCode for their meanings). 62 | */ 63 | constructor(code: number, message: string, data?: string) { 64 | super(message); 65 | this.code = code; 66 | this.message = message; 67 | this.data = data; 68 | } 69 | 70 | static fromProto(proto: RpcError_Proto) { 71 | return new RpcError(proto.code!, proto.message!, proto.data); 72 | } 73 | 74 | toProto() { 75 | return { 76 | code: this.code as number, 77 | message: this.message, 78 | data: this.data, 79 | } as RpcError_Proto; 80 | } 81 | 82 | static ErrorCode = { 83 | APPLICATION_ERROR: 1500, 84 | CONNECTION_TIMEOUT: 1501, 85 | RESPONSE_TIMEOUT: 1502, 86 | RECIPIENT_DISCONNECTED: 1503, 87 | RESPONSE_PAYLOAD_TOO_LARGE: 1504, 88 | SEND_FAILED: 1505, 89 | 90 | UNSUPPORTED_METHOD: 1400, 91 | RECIPIENT_NOT_FOUND: 1401, 92 | REQUEST_PAYLOAD_TOO_LARGE: 1402, 93 | UNSUPPORTED_SERVER: 1403, 94 | } as const; 95 | 96 | /** 97 | * @internal 98 | */ 99 | static ErrorMessage: Record = { 100 | APPLICATION_ERROR: 'Application error in method handler', 101 | CONNECTION_TIMEOUT: 'Connection timeout', 102 | RESPONSE_TIMEOUT: 'Response timeout', 103 | RECIPIENT_DISCONNECTED: 'Recipient disconnected', 104 | RESPONSE_PAYLOAD_TOO_LARGE: 'Response payload too large', 105 | SEND_FAILED: 'Failed to send', 106 | 107 | UNSUPPORTED_METHOD: 'Method not supported at destination', 108 | RECIPIENT_NOT_FOUND: 'Recipient not found', 109 | REQUEST_PAYLOAD_TOO_LARGE: 'Request payload too large', 110 | UNSUPPORTED_SERVER: 'RPC not supported by server', 111 | } as const; 112 | 113 | /** 114 | * Creates an error object from the code, with an auto-populated message. 115 | * 116 | * @internal 117 | */ 118 | static builtIn(key: keyof typeof RpcError.ErrorCode, data?: string): RpcError { 119 | return new RpcError(RpcError.ErrorCode[key], RpcError.ErrorMessage[key], data); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/track.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import type { AudioSource } from './audio_source.js'; 5 | import { FfiClient, FfiHandle } from './ffi_client.js'; 6 | import type { 7 | CreateAudioTrackResponse, 8 | CreateVideoTrackResponse, 9 | OwnedTrack, 10 | StreamState, 11 | TrackInfo, 12 | TrackKind, 13 | } from './proto/track_pb.js'; 14 | import { CreateAudioTrackRequest, CreateVideoTrackRequest } from './proto/track_pb.js'; 15 | import type { VideoSource } from './video_source.js'; 16 | 17 | export abstract class Track { 18 | /** @internal */ 19 | info?: TrackInfo; 20 | 21 | /** @internal */ 22 | ffi_handle: FfiHandle; 23 | 24 | constructor(owned: OwnedTrack) { 25 | this.info = owned.info; 26 | this.ffi_handle = new FfiHandle(owned.handle!.id!); 27 | } 28 | 29 | get sid(): string | undefined { 30 | return this.info?.sid; 31 | } 32 | 33 | get name(): string | undefined { 34 | return this.info?.name; 35 | } 36 | 37 | get kind(): TrackKind | undefined { 38 | return this.info?.kind; 39 | } 40 | 41 | get stream_state(): StreamState | undefined { 42 | return this.info?.streamState; 43 | } 44 | 45 | get muted(): boolean | undefined { 46 | return this.info?.muted; 47 | } 48 | } 49 | 50 | export class LocalAudioTrack extends Track { 51 | constructor(owned: OwnedTrack) { 52 | super(owned); 53 | } 54 | 55 | static createAudioTrack(name: string, source: AudioSource): LocalAudioTrack { 56 | const req = new CreateAudioTrackRequest({ 57 | name: name, 58 | sourceHandle: source.ffiHandle.handle, 59 | }); 60 | 61 | const res = FfiClient.instance.request({ 62 | message: { case: 'createAudioTrack', value: req }, 63 | }); 64 | 65 | return new LocalAudioTrack(res.track!); 66 | } 67 | } 68 | 69 | export class LocalVideoTrack extends Track { 70 | constructor(owned: OwnedTrack) { 71 | super(owned); 72 | } 73 | 74 | static createVideoTrack(name: string, source: VideoSource): LocalVideoTrack { 75 | const req = new CreateVideoTrackRequest({ 76 | name: name, 77 | sourceHandle: source.ffiHandle.handle, 78 | }); 79 | 80 | const res = FfiClient.instance.request({ 81 | message: { case: 'createVideoTrack', value: req }, 82 | }); 83 | 84 | return new LocalVideoTrack(res.track!); 85 | } 86 | } 87 | 88 | export class RemoteVideoTrack extends Track { 89 | constructor(owned: OwnedTrack) { 90 | super(owned); 91 | } 92 | } 93 | 94 | export class RemoteAudioTrack extends Track { 95 | constructor(owned: OwnedTrack) { 96 | super(owned); 97 | } 98 | } 99 | 100 | export type LocalTrack = LocalVideoTrack | LocalAudioTrack; 101 | export type RemoteTrack = RemoteVideoTrack | RemoteAudioTrack; 102 | export type AudioTrack = LocalAudioTrack | RemoteAudioTrack; 103 | export type VideoTrack = LocalVideoTrack | RemoteVideoTrack; 104 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/track_publication.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import { FfiClient } from './ffi_client.js'; 5 | import { FfiHandle } from './napi/native.js'; 6 | import type { EncryptionType } from './proto/e2ee_pb.js'; 7 | import type { SetSubscribedResponse } from './proto/room_pb.js'; 8 | import { SetSubscribedRequest } from './proto/room_pb.js'; 9 | import type { 10 | OwnedTrackPublication, 11 | TrackKind, 12 | TrackPublicationInfo, 13 | TrackSource, 14 | } from './proto/track_pb.js'; 15 | import type { Track } from './track.js'; 16 | 17 | export abstract class TrackPublication { 18 | /** @internal */ 19 | ffiHandle: FfiHandle; 20 | 21 | /** @internal */ 22 | info?: TrackPublicationInfo; 23 | track?: Track; 24 | 25 | constructor(ownedInfo: OwnedTrackPublication) { 26 | this.info = ownedInfo.info; 27 | this.ffiHandle = new FfiHandle(ownedInfo.handle!.id!); 28 | } 29 | 30 | get sid(): string | undefined { 31 | return this.info?.sid; 32 | } 33 | 34 | get name(): string | undefined { 35 | return this.info?.name; 36 | } 37 | 38 | get kind(): TrackKind | undefined { 39 | return this.info?.kind; 40 | } 41 | 42 | get source(): TrackSource | undefined { 43 | return this.info?.source; 44 | } 45 | 46 | get simulcasted(): boolean | undefined { 47 | return this.info?.simulcasted; 48 | } 49 | 50 | get width(): number | undefined { 51 | return this.info?.width; 52 | } 53 | 54 | get height(): number | undefined { 55 | return this.info?.height; 56 | } 57 | 58 | get mimeType(): string | undefined { 59 | return this.info?.mimeType; 60 | } 61 | 62 | get muted(): boolean | undefined { 63 | return this.info?.muted; 64 | } 65 | 66 | get encryptionType(): EncryptionType | undefined { 67 | return this.info?.encryptionType; 68 | } 69 | } 70 | 71 | export class LocalTrackPublication extends TrackPublication { 72 | private firstSubscription: Promise; 73 | private firstSubscriptionResolver: (() => void) | null = null; 74 | 75 | constructor(ownedInfo: OwnedTrackPublication) { 76 | super(ownedInfo); 77 | this.firstSubscription = new Promise((resolve) => { 78 | this.firstSubscriptionResolver = resolve; 79 | }); 80 | } 81 | 82 | async waitForSubscription(): Promise { 83 | await this.firstSubscription; 84 | } 85 | 86 | /** @internal */ 87 | resolveFirstSubscription(): void { 88 | if (this.firstSubscriptionResolver) { 89 | this.firstSubscriptionResolver(); 90 | this.firstSubscriptionResolver = null; 91 | } 92 | } 93 | } 94 | 95 | export class RemoteTrackPublication extends TrackPublication { 96 | subscribed: boolean = false; 97 | 98 | constructor(ownedInfo: OwnedTrackPublication) { 99 | super(ownedInfo); 100 | } 101 | 102 | setSubscribed(subscribed: boolean) { 103 | const req = new SetSubscribedRequest({ 104 | subscribe: subscribed, 105 | publicationHandle: this.ffiHandle.handle, 106 | }); 107 | 108 | FfiClient.instance.request({ 109 | message: { case: 'setSubscribed', value: req }, 110 | }); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/transcription.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | export interface TranscriptionSegment { 5 | id: string; 6 | text: string; 7 | startTime: bigint; 8 | endTime: bigint; 9 | language: string; 10 | final: boolean; 11 | } 12 | 13 | export interface Transcription { 14 | participantIdentity: string; 15 | trackSid: string; 16 | segments: TranscriptionSegment[]; 17 | } 18 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/types.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | export interface ChatMessage { 5 | id: string; 6 | timestamp: number; 7 | message: string; 8 | editTimestamp?: number; 9 | generated?: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/utils.test.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import { describe, expect, it } from 'vitest'; 5 | import { splitUtf8 } from './utils.js'; 6 | 7 | describe('splitUtf8', () => { 8 | it('splits a string into chunks of the given size', () => { 9 | expect(splitUtf8('hello world', 5)).toEqual([ 10 | new TextEncoder().encode('hello'), 11 | new TextEncoder().encode(' worl'), 12 | new TextEncoder().encode('d'), 13 | ]); 14 | }); 15 | 16 | it('splits a string with special characters into chunks of the given size', () => { 17 | expect(splitUtf8('héllo wörld', 5)).toEqual([ 18 | new TextEncoder().encode('héll'), 19 | new TextEncoder().encode('o wö'), 20 | new TextEncoder().encode('rld'), 21 | ]); 22 | }); 23 | 24 | it('splits a string with multi-byte utf8 characters correctly', () => { 25 | expect(splitUtf8('こんにちは世界', 5)).toEqual([ 26 | new TextEncoder().encode('こ'), 27 | new TextEncoder().encode('ん'), 28 | new TextEncoder().encode('に'), 29 | new TextEncoder().encode('ち'), 30 | new TextEncoder().encode('は'), 31 | new TextEncoder().encode('世'), 32 | new TextEncoder().encode('界'), 33 | ]); 34 | }); 35 | 36 | it('handles a string with a single multi-byte utf8 character', () => { 37 | expect(splitUtf8('😊', 5)).toEqual([new TextEncoder().encode('😊')]); 38 | }); 39 | 40 | it('handles a string with mixed single and multi-byte utf8 characters', () => { 41 | expect(splitUtf8('a😊b', 4)).toEqual([ 42 | new TextEncoder().encode('a'), 43 | new TextEncoder().encode('😊'), 44 | new TextEncoder().encode('b'), 45 | ]); 46 | }); 47 | 48 | it('handles an empty string', () => { 49 | expect(splitUtf8('', 5)).toEqual([]); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/utils.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | /** convert bigints to numbers preserving undefined values */ 6 | export function bigIntToNumber( 7 | value: T, 8 | ): T extends bigint ? number : undefined { 9 | return (value !== undefined ? Number(value) : undefined) as T extends bigint ? number : undefined; 10 | } 11 | 12 | /** convert numbers to bigints preserving undefined values */ 13 | export function numberToBigInt( 14 | value: T, 15 | ): T extends number ? bigint : undefined { 16 | return (value !== undefined ? BigInt(value) : undefined) as T extends number ? bigint : undefined; 17 | } 18 | 19 | export function splitUtf8(s: string, n: number): Uint8Array[] { 20 | if (n < 4) { 21 | throw new Error('n must be at least 4 due to utf8 encoding rules'); 22 | } 23 | // adapted from https://stackoverflow.com/a/6043797 24 | const result: Uint8Array[] = []; 25 | let encoded = new TextEncoder().encode(s); 26 | while (encoded.length > n) { 27 | let k = n; 28 | while (k > 0) { 29 | const byte = encoded[k]; 30 | if (byte !== undefined && (byte & 0xc0) !== 0x80) { 31 | break; 32 | } 33 | k--; 34 | } 35 | result.push(encoded.slice(0, k)); 36 | encoded = encoded.slice(k); 37 | } 38 | if (encoded.length > 0) { 39 | result.push(encoded); 40 | } 41 | return result; 42 | } 43 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/video_frame.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import { FfiClient, FfiHandle, FfiRequest } from './ffi_client.js'; 5 | import type { OwnedVideoBuffer, VideoConvertResponse } from './proto/video_frame_pb.js'; 6 | import { 7 | VideoBufferInfo, 8 | VideoBufferInfo_ComponentInfo, 9 | VideoBufferType, 10 | } from './proto/video_frame_pb.js'; 11 | 12 | export class VideoFrame { 13 | data: Uint8Array; 14 | width: number; 15 | height: number; 16 | type: VideoBufferType; 17 | 18 | constructor(data: Uint8Array, width: number, height: number, type: VideoBufferType) { 19 | this.data = data; 20 | this.width = width; 21 | this.height = height; 22 | this.type = type; 23 | } 24 | 25 | /** @internal */ 26 | get dataPtr(): bigint { 27 | return FfiClient.instance.retrievePtr(new Uint8Array(this.data.buffer)); 28 | } 29 | 30 | /** @internal */ 31 | protoInfo(): VideoBufferInfo { 32 | const info = new VideoBufferInfo({ 33 | width: this.width, 34 | height: this.height, 35 | type: this.type, 36 | dataPtr: this.dataPtr, 37 | }); 38 | 39 | switch (this.type) { 40 | case VideoBufferType.ARGB: 41 | case VideoBufferType.RGBA: 42 | case VideoBufferType.ABGR: 43 | case VideoBufferType.BGRA: 44 | info.stride = this.width * 4; 45 | break; 46 | case VideoBufferType.RGB24: 47 | info.stride = this.width * 3; 48 | break; 49 | default: 50 | info.stride = 0; 51 | } 52 | 53 | info.components.push(...getPlaneInfos(this.dataPtr, this.type, this.width, this.height)); 54 | 55 | return info; 56 | } 57 | 58 | /** @internal */ 59 | static fromOwnedInfo(owned: OwnedVideoBuffer): VideoFrame { 60 | const info = owned.info!; 61 | const frame = new VideoFrame( 62 | FfiClient.instance.copyBuffer( 63 | info.dataPtr!, 64 | getPlaneLength(info.type!, info.width!, info.height!), 65 | ), 66 | info.width!, 67 | info.height!, 68 | info.type!, 69 | ); 70 | // Dispose of the handle to prevent memory leaks 71 | new FfiHandle(owned.handle!.id!).dispose(); 72 | return frame; 73 | } 74 | 75 | getPlane(planeNth: number): Uint8Array | void { 76 | const planeInfos = getPlaneInfos(this.dataPtr, this.type, this.width, this.height); 77 | if (planeNth >= planeInfos.length) return; 78 | 79 | const planeInfo = planeInfos[planeNth]!; 80 | return FfiClient.instance.copyBuffer(planeInfo.dataPtr!, planeInfo.size!); 81 | } 82 | 83 | convert(dstType: VideoBufferType, flipY = false): VideoFrame { 84 | const req = new FfiRequest({ 85 | message: { 86 | case: 'videoConvert', 87 | value: { 88 | flipY, 89 | dstType, 90 | buffer: this.protoInfo(), 91 | }, 92 | }, 93 | }); 94 | const resp = FfiClient.instance.request(req); 95 | 96 | switch (resp.message.case) { 97 | case 'buffer': 98 | return VideoFrame.fromOwnedInfo(resp.message.value); 99 | case 'error': 100 | default: 101 | throw resp.message.value; 102 | } 103 | } 104 | } 105 | 106 | const getPlaneLength = (type: VideoBufferType, width: number, height: number): number => { 107 | const chromaWidth = Math.trunc((width + 1) / 2); 108 | const chromaHeight = Math.trunc((height + 1) / 2); 109 | switch (type) { 110 | case VideoBufferType.ARGB: 111 | case VideoBufferType.RGBA: 112 | case VideoBufferType.ABGR: 113 | case VideoBufferType.BGRA: 114 | return width * height * 4; 115 | case VideoBufferType.RGB24: 116 | case VideoBufferType.I444: 117 | return width * height * 3; 118 | case VideoBufferType.I420: 119 | return width * height + chromaWidth * chromaHeight * 2; 120 | case VideoBufferType.I420A: 121 | return width * height * 2 + chromaWidth * chromaWidth * 2; 122 | case VideoBufferType.I422: 123 | return width * height + chromaWidth * height * 2; 124 | case VideoBufferType.I010: 125 | return width * height * 2 + chromaWidth * chromaHeight * 4; 126 | case VideoBufferType.NV12: 127 | return width * height + chromaWidth * chromaWidth * 2; 128 | } 129 | }; 130 | 131 | const getPlaneInfos = ( 132 | dataPtr: bigint, 133 | type: VideoBufferType, 134 | width: number, 135 | height: number, 136 | ): VideoBufferInfo_ComponentInfo[] => { 137 | const chromaWidth = Math.trunc((width + 1) / 2); 138 | const chromaHeight = Math.trunc((height + 1) / 2); 139 | switch (type) { 140 | case VideoBufferType.I420: { 141 | const y = new VideoBufferInfo_ComponentInfo({ dataPtr, stride: width, size: width * height }); 142 | const u = new VideoBufferInfo_ComponentInfo({ 143 | dataPtr: y.dataPtr! + BigInt(y.size!), 144 | stride: chromaWidth, 145 | size: chromaWidth * chromaHeight, 146 | }); 147 | const v = new VideoBufferInfo_ComponentInfo({ 148 | dataPtr: u.dataPtr! + BigInt(u.size!), 149 | stride: chromaWidth, 150 | size: chromaWidth * chromaHeight, 151 | }); 152 | return [y, u, v]; 153 | } 154 | case VideoBufferType.I420A: { 155 | const y = new VideoBufferInfo_ComponentInfo({ dataPtr, stride: width, size: width * height }); 156 | const u = new VideoBufferInfo_ComponentInfo({ 157 | dataPtr: y.dataPtr! + BigInt(y.size!), 158 | stride: chromaWidth, 159 | size: chromaWidth * chromaHeight, 160 | }); 161 | const v = new VideoBufferInfo_ComponentInfo({ 162 | dataPtr: u.dataPtr! + BigInt(u.size!), 163 | stride: chromaWidth, 164 | size: chromaWidth * chromaHeight, 165 | }); 166 | const a = new VideoBufferInfo_ComponentInfo({ 167 | dataPtr: v.dataPtr! + BigInt(v.size!), 168 | stride: width, 169 | size: width * height, 170 | }); 171 | return [y, u, v, a]; 172 | } 173 | case VideoBufferType.I422: { 174 | const y = new VideoBufferInfo_ComponentInfo({ dataPtr, stride: width, size: width * height }); 175 | const u = new VideoBufferInfo_ComponentInfo({ 176 | dataPtr: y.dataPtr! + BigInt(y.size!), 177 | stride: chromaWidth, 178 | size: chromaWidth * height, 179 | }); 180 | const v = new VideoBufferInfo_ComponentInfo({ 181 | dataPtr: u.dataPtr! + BigInt(u.size!), 182 | stride: chromaWidth, 183 | size: chromaWidth * height, 184 | }); 185 | return [y, u, v]; 186 | } 187 | case VideoBufferType.I444: { 188 | const y = new VideoBufferInfo_ComponentInfo({ dataPtr, stride: width, size: width * height }); 189 | const u = new VideoBufferInfo_ComponentInfo({ 190 | dataPtr: y.dataPtr! + BigInt(y.size!), 191 | stride: width, 192 | size: width * height, 193 | }); 194 | const v = new VideoBufferInfo_ComponentInfo({ 195 | dataPtr: u.dataPtr! + BigInt(u.size!), 196 | stride: width, 197 | size: width * height, 198 | }); 199 | return [y, u, v]; 200 | } 201 | case VideoBufferType.I010: { 202 | const y = new VideoBufferInfo_ComponentInfo({ 203 | dataPtr, 204 | stride: width * 2, 205 | size: width * height * 2, 206 | }); 207 | const u = new VideoBufferInfo_ComponentInfo({ 208 | dataPtr: y.dataPtr! + BigInt(y.size!), 209 | stride: chromaWidth * 2, 210 | size: chromaWidth * chromaHeight * 2, 211 | }); 212 | const v = new VideoBufferInfo_ComponentInfo({ 213 | dataPtr: u.dataPtr! + BigInt(u.size!), 214 | stride: chromaWidth * 2, 215 | size: chromaWidth * chromaHeight * 2, 216 | }); 217 | return [y, u, v]; 218 | } 219 | case VideoBufferType.NV12: { 220 | const y = new VideoBufferInfo_ComponentInfo({ dataPtr, stride: width, size: width * height }); 221 | const uv = new VideoBufferInfo_ComponentInfo({ 222 | dataPtr: y.dataPtr! + BigInt(y.size!), 223 | stride: chromaWidth * 2, 224 | size: chromaWidth * chromaHeight * 2, 225 | }); 226 | return [y, uv]; 227 | } 228 | default: 229 | return []; 230 | } 231 | }; 232 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/video_source.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import { FfiClient, FfiHandle } from './ffi_client.js'; 5 | import type { 6 | CaptureVideoFrameResponse, 7 | NewVideoSourceResponse, 8 | VideoSourceInfo, 9 | } from './proto/video_frame_pb.js'; 10 | import { 11 | CaptureVideoFrameRequest, 12 | NewVideoSourceRequest, 13 | VideoRotation, 14 | VideoSourceType, 15 | } from './proto/video_frame_pb.js'; 16 | import type { VideoFrame } from './video_frame.js'; 17 | 18 | export class VideoSource { 19 | /** @internal */ 20 | info?: VideoSourceInfo; 21 | /** @internal */ 22 | ffiHandle: FfiHandle; 23 | /** @internal */ 24 | closed = false; 25 | 26 | width: number; 27 | height: number; 28 | 29 | constructor(width: number, height: number) { 30 | this.width = width; 31 | this.height = height; 32 | 33 | const req = new NewVideoSourceRequest({ 34 | type: VideoSourceType.VIDEO_SOURCE_NATIVE, 35 | resolution: { 36 | width: width, 37 | height: height, 38 | }, 39 | }); 40 | 41 | const res = FfiClient.instance.request({ 42 | message: { 43 | case: 'newVideoSource', 44 | value: req, 45 | }, 46 | }); 47 | 48 | this.info = res.source?.info; 49 | this.ffiHandle = new FfiHandle(res.source!.handle!.id!); 50 | } 51 | 52 | captureFrame( 53 | frame: VideoFrame, 54 | timestampUs = BigInt(0), 55 | rotation = VideoRotation.VIDEO_ROTATION_0, 56 | ) { 57 | if (this.closed) { 58 | throw new Error('VideoSource is closed'); 59 | } 60 | const req = new CaptureVideoFrameRequest({ 61 | sourceHandle: this.ffiHandle.handle, 62 | buffer: frame.protoInfo(), 63 | rotation, 64 | timestampUs, 65 | }); 66 | 67 | FfiClient.instance.request({ 68 | message: { case: 'captureVideoFrame', value: req }, 69 | }); 70 | } 71 | 72 | async close() { 73 | this.ffiHandle.dispose(); 74 | this.closed = true; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/livekit-rtc/src/video_stream.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import type { UnderlyingSource } from 'node:stream/web'; 5 | import type { FfiEvent } from './ffi_client.js'; 6 | import { FfiClient, FfiClientEvent, FfiHandle } from './ffi_client.js'; 7 | import type { NewVideoStreamResponse, VideoRotation } from './proto/video_frame_pb.js'; 8 | import { NewVideoStreamRequest, VideoStreamType } from './proto/video_frame_pb.js'; 9 | import type { Track } from './track.js'; 10 | import { VideoFrame } from './video_frame.js'; 11 | 12 | export type VideoFrameEvent = { 13 | frame: VideoFrame; 14 | timestampUs: bigint; 15 | rotation: VideoRotation; 16 | }; 17 | 18 | class VideoStreamSource implements UnderlyingSource { 19 | private controller?: ReadableStreamDefaultController; 20 | private ffiHandle: FfiHandle; 21 | 22 | constructor(track: Track) { 23 | const req = new NewVideoStreamRequest({ 24 | type: VideoStreamType.VIDEO_STREAM_NATIVE, 25 | trackHandle: track.ffi_handle.handle, 26 | }); 27 | 28 | const res = FfiClient.instance.request({ 29 | message: { 30 | case: 'newVideoStream', 31 | value: req, 32 | }, 33 | }); 34 | 35 | this.ffiHandle = new FfiHandle(res.stream!.handle!.id!); 36 | FfiClient.instance.on(FfiClientEvent.FfiEvent, this.onEvent); 37 | } 38 | 39 | private onEvent = (ev: FfiEvent) => { 40 | if (!this.controller) { 41 | throw new Error('Stream controller not initialized'); 42 | } 43 | 44 | if ( 45 | ev.message.case != 'videoStreamEvent' || 46 | ev.message.value.streamHandle != this.ffiHandle.handle 47 | ) { 48 | return; 49 | } 50 | 51 | const streamEvent = ev.message.value.message; 52 | switch (streamEvent.case) { 53 | case 'frameReceived': 54 | const rotation = streamEvent.value.rotation; 55 | const timestampUs = streamEvent.value.timestampUs; 56 | const frame = VideoFrame.fromOwnedInfo(streamEvent.value.buffer!); 57 | const value = { rotation, timestampUs, frame }; 58 | const videoFrameEvent = { 59 | frame: value.frame, 60 | timestampUs: value.timestampUs!, 61 | rotation: value.rotation!, 62 | }; 63 | this.controller.enqueue(videoFrameEvent); 64 | break; 65 | case 'eos': 66 | FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent); 67 | this.controller.close(); 68 | break; 69 | } 70 | }; 71 | 72 | start(controller: ReadableStreamDefaultController) { 73 | this.controller = controller; 74 | } 75 | 76 | cancel() { 77 | FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent); 78 | this.ffiHandle.dispose(); 79 | } 80 | } 81 | 82 | export class VideoStream extends ReadableStream { 83 | constructor(track: Track) { 84 | super(new VideoStreamSource(track)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/livekit-rtc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist" 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "exclude": ["src/**/*.test.ts", "vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/livekit-rtc/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | import defaults from '../../tsup.config'; 4 | 5 | export default defineConfig({ 6 | ...defaults, 7 | external: [/\.\/.*\.cjs/, /\.\/.*.node/], 8 | }); 9 | -------------------------------------------------------------------------------- /packages/livekit-rtc/vite.config.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | import { defineConfig } from 'vite'; 6 | 7 | export default defineConfig({ 8 | test: { 9 | environment: 'node', 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # LiveKit Server API for JS 8 | 9 | Use this SDK to manage LiveKit rooms and create access tokens from your JavaScript/Node.js backend. 10 | 11 | > [!NOTE] 12 | > This is v2 of the server-sdk-js which runs in NodeJS, Deno and Bun! 13 | > (It theoretically now also runs in every major browser, but that's not recommended due to the security risks involved with exposing your API secrets) 14 | > Read the [migration section](#migrate-from-v1x-to-v2x) below for a detailed overview on what has changed. 15 | 16 | ## Installation 17 | 18 | ### Pnpm 19 | 20 | ``` 21 | pnpm add livekit-server-sdk 22 | ``` 23 | 24 | ### Yarn 25 | 26 | ``` 27 | yarn add livekit-server-sdk 28 | ``` 29 | 30 | ### NPM 31 | 32 | ``` 33 | npm install livekit-server-sdk --save 34 | ``` 35 | 36 | ## Usage 37 | 38 | ### Environment Variables 39 | 40 | You may store credentials in environment variables. If api-key or api-secret is not passed in when creating a `RoomServiceClient` or `AccessToken`, the values in the following env vars will be used: 41 | 42 | - `LIVEKIT_API_KEY` 43 | - `LIVEKIT_API_SECRET` 44 | 45 | ### Creating Access Tokens 46 | 47 | Creating a token for participant to join a room. 48 | 49 | ```typescript 50 | import { AccessToken } from 'livekit-server-sdk'; 51 | 52 | // if this room doesn't exist, it'll be automatically created when the first 53 | // client joins 54 | const roomName = 'name-of-room'; 55 | // identifier to be used for participant. 56 | // it's available as LocalParticipant.identity with livekit-client SDK 57 | const participantName = 'user-name'; 58 | 59 | const at = new AccessToken('api-key', 'secret-key', { 60 | identity: participantName, 61 | }); 62 | at.addGrant({ roomJoin: true, room: roomName }); 63 | 64 | const token = await at.toJwt(); 65 | console.log('access token', token); 66 | ``` 67 | 68 | By default, the token expires after 6 hours. you may override this by passing in `ttl` in the access token options. `ttl` is expressed in seconds (as number) or a string describing a time span [vercel/ms](https://github.com/vercel/ms). eg: '2 days', '10h'. 69 | 70 | ### Permissions in Access Tokens 71 | 72 | It's possible to customize the permissions of each participant: 73 | 74 | ```typescript 75 | const at = new AccessToken('api-key', 'secret-key', { 76 | identity: participantName, 77 | }); 78 | 79 | at.addGrant({ 80 | roomJoin: true, 81 | room: roomName, 82 | canPublish: false, 83 | canSubscribe: true, 84 | }); 85 | ``` 86 | 87 | This will allow the participant to subscribe to tracks, but not publish their own to the room. 88 | 89 | ### Managing Rooms 90 | 91 | `RoomServiceClient` gives you APIs to list, create, and delete rooms. It also requires a pair of api key/secret key to operate. 92 | 93 | ```typescript 94 | import { Room, RoomServiceClient } from 'livekit-server-sdk'; 95 | 96 | const livekitHost = 'https://my.livekit.host'; 97 | const svc = new RoomServiceClient(livekitHost, 'api-key', 'secret-key'); 98 | 99 | // list rooms 100 | svc.listRooms().then((rooms: Room[]) => { 101 | console.log('existing rooms', rooms); 102 | }); 103 | 104 | // create a new room 105 | const opts = { 106 | name: 'myroom', 107 | // timeout in seconds 108 | emptyTimeout: 10 * 60, 109 | maxParticipants: 20, 110 | }; 111 | svc.createRoom(opts).then((room: Room) => { 112 | console.log('room created', room); 113 | }); 114 | 115 | // delete a room 116 | svc.deleteRoom('myroom').then(() => { 117 | console.log('room deleted'); 118 | }); 119 | ``` 120 | 121 | ## Webhooks 122 | 123 | The JS SDK also provides helper functions to decode and verify webhook callbacks. While verification is optional, it ensures the authenticity of the message. See [webhooks guide](https://docs.livekit.io/home/server/webhooks/) for details. 124 | 125 | LiveKit POSTs to webhook endpoints with `Content-Type: application/webhook+json`. Please ensure your server is able to receive POST body with that MIME. 126 | 127 | Check out [example projects](examples) for full examples of webhooks integration. 128 | 129 | ```typescript 130 | import { WebhookReceiver } from 'livekit-server-sdk'; 131 | 132 | const receiver = new WebhookReceiver('apikey', 'apisecret'); 133 | 134 | // In order to use the validator, WebhookReceiver must have access to the raw POSTed string (instead of a parsed JSON object) 135 | // if you are using express middleware, ensure that `express.raw` is used for the webhook endpoint 136 | // app.use(express.raw({type: 'application/webhook+json'})); 137 | 138 | app.post('/webhook-endpoint', async (req, res) => { 139 | // event is a WebhookEvent object 140 | const event = await receiver.receive(req.body, req.get('Authorization')); 141 | }); 142 | ``` 143 | 144 | ## Migrate from v1.x to v2.x 145 | 146 | ### Token generation 147 | 148 | Because the `jsonwebtoken` lib got replaced with `jose`, there are a couple of APIs that are now async, that weren't before: 149 | 150 | ```typescript 151 | const at = new AccessToken('api-key', 'secret-key', { 152 | identity: participantName, 153 | }); 154 | at.addGrant({ roomJoin: true, room: roomName }); 155 | 156 | // v1 157 | // const token = at.toJWT(); 158 | 159 | // v2 160 | const token = await at.toJwt(); 161 | 162 | // v1 163 | // const grants = v.verify(token); 164 | 165 | // v2 166 | const grants = await v.verify(token); 167 | 168 | app.post('/webhook-endpoint', async (req, res) => { 169 | // v1 170 | // const event = receiver.receive(req.body, req.get('Authorization')); 171 | 172 | // v2 173 | const event = await receiver.receive(req.body, req.get('Authorization')); 174 | }); 175 | ``` 176 | 177 | ### Egress API 178 | 179 | Egress request types have been updated from interfaces to classes in the latest version. Additionally, `oneof` fields now require an explicit `case` field to specify the value type. 180 | 181 | For example, to create a RoomComposite Egress: 182 | 183 | ```typescript 184 | // v1 185 | // const fileOutput = { 186 | // fileType: EncodedFileType.MP4, 187 | // filepath: 'livekit-demo/room-composite-test.mp4', 188 | // s3: { 189 | // accessKey: 'aws-access-key', 190 | // secret: 'aws-access-secret', 191 | // region: 'aws-region', 192 | // bucket: 'my-bucket', 193 | // }, 194 | // }; 195 | 196 | // const info = await egressClient.startRoomCompositeEgress('my-room', { 197 | // file: fileOutput, 198 | // }); 199 | 200 | // v2 - current 201 | const fileOutput = new EncodedFileOutput({ 202 | filepath: 'dz/davids-room-test.mp4', 203 | output: { 204 | case: 's3', 205 | value: new S3Upload({ 206 | accessKey: 'aws-access-key', 207 | secret: 'aws-access-secret', 208 | bucket: 'my-bucket', 209 | }), 210 | }, 211 | }); 212 | 213 | const info = await egressClient.startRoomCompositeEgress('my-room', { 214 | file: fileOutput, 215 | }); 216 | ``` 217 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livekit-server-sdk", 3 | "version": "2.13.0", 4 | "description": "Server-side SDK for LiveKit", 5 | "main": "dist/index.js", 6 | "require": "dist/index.cjs", 7 | "types": "dist/index.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/livekit/server-sdk-js.git" 11 | }, 12 | "author": "David Zhao ", 13 | "license": "Apache-2.0", 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "import": { 18 | "types": "./dist/index.d.ts", 19 | "default": "./dist/index.js" 20 | }, 21 | "require": { 22 | "types": "./dist/index.d.cts", 23 | "default": "./dist/index.cjs" 24 | } 25 | } 26 | }, 27 | "files": [ 28 | "dist", 29 | "src" 30 | ], 31 | "scripts": { 32 | "build": "tsup --onSuccess \"tsc --declaration --emitDeclarationOnly\"", 33 | "build:watch": "tsc --watch", 34 | "build-docs": "typedoc", 35 | "changeset": "changeset", 36 | "ci:publish": "pnpm build && changeset publish", 37 | "lint": "eslint src", 38 | "format": "prettier --write src", 39 | "format:check": "prettier --check src", 40 | "test": "vitest --environment node run", 41 | "test:browser": "vitest --environment happy-dom run", 42 | "test:edge": "vitest --environment edge-runtime run" 43 | }, 44 | "dependencies": { 45 | "@bufbuild/protobuf": "^1.7.2", 46 | "@livekit/protocol": "^1.39.0", 47 | "camelcase-keys": "^9.0.0", 48 | "jose": "^5.1.2" 49 | }, 50 | "devDependencies": { 51 | "@changesets/cli": "^2.27.1", 52 | "@edge-runtime/vm": "^5.0.0", 53 | "@livekit/changesets-changelog-github": "^0.0.4", 54 | "@types/node": "^20.10.1", 55 | "happy-dom": "^17.0.0", 56 | "prettier": "^3.0.0", 57 | "tsup": "^8.3.5", 58 | "typedoc": "^0.27.0", 59 | "typescript": "5.7.x", 60 | "vite": "^5.2.9", 61 | "vitest": "^3.0.0" 62 | }, 63 | "engines": { 64 | "node": ">=18" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/src/AccessToken.test.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import { 5 | RoomAgentDispatch, 6 | RoomCompositeEgressRequest, 7 | RoomConfiguration, 8 | RoomEgress, 9 | } from '@livekit/protocol'; 10 | import * as jose from 'jose'; 11 | import { describe, expect, it } from 'vitest'; 12 | import { AccessToken, TokenVerifier } from './AccessToken.js'; 13 | import type { ClaimGrants } from './grants.js'; 14 | 15 | const testApiKey = 'abcdefg'; 16 | const testSecret = 'abababa'; 17 | 18 | describe('encoded tokens are valid', () => { 19 | const t = new AccessToken(testApiKey, testSecret, { 20 | identity: 'me', 21 | name: 'myname', 22 | }); 23 | t.addGrant({ room: 'myroom' }); 24 | const EncodedTestSecret = new TextEncoder().encode(testSecret); 25 | it('can be decoded', async () => { 26 | const { payload } = await jose.jwtVerify(await t.toJwt(), EncodedTestSecret, { 27 | issuer: testApiKey, 28 | }); 29 | 30 | expect(payload).not.toBe(undefined); 31 | }); 32 | 33 | it('has name set', async () => { 34 | const { payload } = await jose.jwtVerify(await t.toJwt(), EncodedTestSecret, { 35 | issuer: testApiKey, 36 | }); 37 | 38 | expect(payload.name).toBe('myname'); 39 | }); 40 | 41 | it('has video grants set', async () => { 42 | const { payload } = await jose.jwtVerify(await t.toJwt(), EncodedTestSecret, { 43 | issuer: testApiKey, 44 | }); 45 | 46 | expect(payload.video).toBeTruthy(); 47 | expect((payload as ClaimGrants).video?.room).toEqual('myroom'); 48 | }); 49 | }); 50 | 51 | describe('identity is required for only join grants', () => { 52 | it('allows empty identity for create', async () => { 53 | const t = new AccessToken(testApiKey, testSecret); 54 | t.addGrant({ roomCreate: true }); 55 | 56 | expect(await t.toJwt()).toBeTruthy(); 57 | }); 58 | it('throws error when identity is not provided for join', async () => { 59 | const t = new AccessToken(testApiKey, testSecret); 60 | t.addGrant({ roomJoin: true }); 61 | 62 | await expect(async () => { 63 | await t.toJwt(); 64 | }).rejects.toThrow(); 65 | }); 66 | }); 67 | 68 | describe('verify token is valid', () => { 69 | it('can decode encoded token', async () => { 70 | const t = new AccessToken(testApiKey, testSecret); 71 | t.sha256 = 'abcdefg'; 72 | t.kind = 'agent'; 73 | t.addGrant({ roomCreate: true }); 74 | t.attributes = { foo: 'bar', live: 'kit' }; 75 | 76 | const v = new TokenVerifier(testApiKey, testSecret); 77 | const decoded = await v.verify(await t.toJwt()); 78 | 79 | expect(decoded).not.toBe(undefined); 80 | expect(decoded.sha256).toEqual('abcdefg'); 81 | expect(decoded.video?.roomCreate).toBeTruthy(); 82 | expect(decoded.kind).toEqual('agent'); 83 | expect(decoded.attributes).toEqual(t.attributes); 84 | }); 85 | }); 86 | 87 | describe('adding grants should not overwrite existing grants', () => { 88 | const EncodedTestSecret = new TextEncoder().encode(testSecret); 89 | 90 | it('should not overwrite existing grants', async () => { 91 | const t = new AccessToken(testApiKey, testSecret, { 92 | identity: 'me', 93 | name: 'myname', 94 | }); 95 | t.addGrant({ roomCreate: true }); 96 | t.addGrant({ roomJoin: true }); 97 | 98 | const { payload }: jose.JWTVerifyResult = await jose.jwtVerify( 99 | await t.toJwt(), 100 | EncodedTestSecret, 101 | { issuer: testApiKey }, 102 | ); 103 | expect(payload.video?.roomCreate).toBeTruthy(); 104 | expect(payload.video?.roomJoin).toBeTruthy(); 105 | }); 106 | }); 107 | 108 | describe('room configuration with agents and egress', () => { 109 | it('should set agents and egress in room configuration', async () => { 110 | const t = new AccessToken(testApiKey, testSecret, { 111 | identity: 'test-identity', 112 | }); 113 | 114 | const roomConfig = new RoomConfiguration({ 115 | name: 'test-room', 116 | maxParticipants: 10, 117 | }); 118 | 119 | const agents: RoomAgentDispatch[] = [ 120 | new RoomAgentDispatch({ 121 | agentName: 'agent1', 122 | metadata: 'metadata-1', 123 | }), 124 | new RoomAgentDispatch({ 125 | agentName: 'agent2', 126 | metadata: 'metadata-2', 127 | }), 128 | ]; 129 | 130 | const egress = new RoomEgress({ 131 | room: new RoomCompositeEgressRequest({ roomName: 'test-room' }), 132 | }); 133 | 134 | roomConfig.agents = agents; 135 | roomConfig.egress = egress; 136 | 137 | t.roomConfig = roomConfig; 138 | 139 | const v = new TokenVerifier(testApiKey, testSecret); 140 | const decoded = await v.verify(await t.toJwt()); 141 | 142 | expect(decoded.roomConfig).toBeDefined(); 143 | expect(decoded.roomConfig?.name).toEqual('test-room'); 144 | expect(decoded.roomConfig?.maxParticipants).toEqual(10); 145 | expect(decoded.roomConfig?.agents).toHaveLength(2); 146 | expect(decoded.roomConfig?.agents?.[0]?.agentName).toEqual('agent1'); 147 | expect(decoded.roomConfig?.agents?.[0]?.metadata).toEqual('metadata-1'); 148 | expect(decoded.roomConfig?.agents?.[1]?.agentName).toEqual('agent2'); 149 | expect(decoded.roomConfig?.agents?.[1]?.metadata).toEqual('metadata-2'); 150 | expect(decoded.roomConfig?.egress?.room?.roomName).toEqual('test-room'); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/src/AccessToken.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import type { RoomConfiguration } from '@livekit/protocol'; 5 | import * as jose from 'jose'; 6 | import type { ClaimGrants, SIPGrant, VideoGrant } from './grants.js'; 7 | import { claimsToJwtPayload } from './grants.js'; 8 | 9 | // 6 hours 10 | const defaultTTL = `6h`; 11 | 12 | const defaultClockToleranceSeconds = 10; 13 | 14 | export interface AccessTokenOptions { 15 | /** 16 | * amount of time before expiration 17 | * expressed in seconds or a string describing a time span zeit/ms. 18 | * eg: '2 days', '10h', or seconds as numeric value 19 | */ 20 | ttl?: number | string; 21 | 22 | /** 23 | * display name for the participant, available as `Participant.name` 24 | */ 25 | name?: string; 26 | 27 | /** 28 | * identity of the user, required for room join tokens 29 | */ 30 | identity?: string; 31 | 32 | /** 33 | * custom metadata to be passed to participants 34 | */ 35 | metadata?: string; 36 | 37 | /** 38 | * custom attributes to be passed to participants 39 | */ 40 | attributes?: Record; 41 | } 42 | 43 | export class AccessToken { 44 | private apiKey: string; 45 | 46 | private apiSecret: string; 47 | 48 | private grants: ClaimGrants; 49 | 50 | identity?: string; 51 | 52 | ttl: number | string; 53 | 54 | /** 55 | * Creates a new AccessToken 56 | * @param apiKey - API Key, can be set in env LIVEKIT_API_KEY 57 | * @param apiSecret - Secret, can be set in env LIVEKIT_API_SECRET 58 | */ 59 | constructor(apiKey?: string, apiSecret?: string, options?: AccessTokenOptions) { 60 | if (!apiKey) { 61 | apiKey = process.env.LIVEKIT_API_KEY; 62 | } 63 | if (!apiSecret) { 64 | apiSecret = process.env.LIVEKIT_API_SECRET; 65 | } 66 | if (!apiKey || !apiSecret) { 67 | throw Error('api-key and api-secret must be set'); 68 | } 69 | // @ts-expect-error we're not including dom lib for the server sdk so document is not defined 70 | else if (typeof document !== 'undefined') { 71 | // check against document rather than window because deno provides window 72 | console.error( 73 | 'You should not include your API secret in your web client bundle.\n\n' + 74 | 'Your web client should request a token from your backend server which should then use ' + 75 | 'the API secret to generate a token. See https://docs.livekit.io/client/connect/', 76 | ); 77 | } 78 | this.apiKey = apiKey; 79 | this.apiSecret = apiSecret; 80 | this.grants = {}; 81 | this.identity = options?.identity; 82 | this.ttl = options?.ttl || defaultTTL; 83 | if (typeof this.ttl === 'number') { 84 | this.ttl = `${this.ttl}s`; 85 | } 86 | if (options?.metadata) { 87 | this.metadata = options.metadata; 88 | } 89 | if (options?.attributes) { 90 | this.attributes = options.attributes; 91 | } 92 | if (options?.name) { 93 | this.name = options.name; 94 | } 95 | } 96 | 97 | /** 98 | * Adds a video grant to this token. 99 | * @param grant - 100 | */ 101 | addGrant(grant: VideoGrant) { 102 | this.grants.video = { ...(this.grants.video ?? {}), ...grant }; 103 | } 104 | 105 | /** 106 | * Adds a SIP grant to this token. 107 | * @param grant - 108 | */ 109 | addSIPGrant(grant: SIPGrant) { 110 | this.grants.sip = { ...(this.grants.sip ?? {}), ...grant }; 111 | } 112 | 113 | get name(): string | undefined { 114 | return this.grants.name; 115 | } 116 | 117 | set name(name: string) { 118 | this.grants.name = name; 119 | } 120 | 121 | get metadata(): string | undefined { 122 | return this.grants.metadata; 123 | } 124 | 125 | /** 126 | * Set metadata to be passed to the Participant, used only when joining the room 127 | */ 128 | set metadata(md: string) { 129 | this.grants.metadata = md; 130 | } 131 | 132 | get attributes(): Record | undefined { 133 | return this.grants.attributes; 134 | } 135 | 136 | set attributes(attrs: Record) { 137 | this.grants.attributes = attrs; 138 | } 139 | 140 | get kind(): string | undefined { 141 | return this.grants.kind; 142 | } 143 | 144 | set kind(kind: string) { 145 | this.grants.kind = kind; 146 | } 147 | 148 | get sha256(): string | undefined { 149 | return this.grants.sha256; 150 | } 151 | 152 | set sha256(sha: string | undefined) { 153 | this.grants.sha256 = sha; 154 | } 155 | 156 | get roomPreset(): string | undefined { 157 | return this.grants.roomPreset; 158 | } 159 | 160 | set roomPreset(preset: string | undefined) { 161 | this.grants.roomPreset = preset; 162 | } 163 | 164 | get roomConfig(): RoomConfiguration | undefined { 165 | return this.grants.roomConfig; 166 | } 167 | 168 | set roomConfig(config: RoomConfiguration | undefined) { 169 | this.grants.roomConfig = config; 170 | } 171 | 172 | /** 173 | * @returns JWT encoded token 174 | */ 175 | async toJwt(): Promise { 176 | // TODO: check for video grant validity 177 | 178 | const secret = new TextEncoder().encode(this.apiSecret); 179 | 180 | const jwt = new jose.SignJWT(claimsToJwtPayload(this.grants)) 181 | .setProtectedHeader({ alg: 'HS256' }) 182 | .setIssuer(this.apiKey) 183 | .setExpirationTime(this.ttl) 184 | .setNotBefore(0); 185 | if (this.identity) { 186 | jwt.setSubject(this.identity); 187 | } else if (this.grants.video?.roomJoin) { 188 | throw Error('identity is required for join but not set'); 189 | } 190 | return jwt.sign(secret); 191 | } 192 | } 193 | 194 | export class TokenVerifier { 195 | private apiKey: string; 196 | 197 | private apiSecret: string; 198 | 199 | constructor(apiKey: string, apiSecret: string) { 200 | this.apiKey = apiKey; 201 | this.apiSecret = apiSecret; 202 | } 203 | 204 | async verify( 205 | token: string, 206 | clockTolerance: string | number = defaultClockToleranceSeconds, 207 | ): Promise { 208 | const secret = new TextEncoder().encode(this.apiSecret); 209 | const { payload } = await jose.jwtVerify(token, secret, { 210 | issuer: this.apiKey, 211 | clockTolerance, 212 | }); 213 | if (!payload) { 214 | throw Error('invalid token'); 215 | } 216 | 217 | return payload as ClaimGrants; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/src/AgentDispatchClient.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import { 5 | AgentDispatch, 6 | CreateAgentDispatchRequest, 7 | DeleteAgentDispatchRequest, 8 | ListAgentDispatchRequest, 9 | ListAgentDispatchResponse, 10 | } from '@livekit/protocol'; 11 | import { ServiceBase } from './ServiceBase.js'; 12 | import { type Rpc, TwirpRpc, livekitPackage } from './TwirpRPC.js'; 13 | 14 | interface CreateDispatchOptions { 15 | // any custom data to send along with the job. 16 | // note: this is different from room and participant metadata 17 | metadata?: string; 18 | } 19 | 20 | const svc = 'AgentDispatchService'; 21 | 22 | /** 23 | * Client to access Agent APIs 24 | */ 25 | export class AgentDispatchClient extends ServiceBase { 26 | private readonly rpc: Rpc; 27 | 28 | /** 29 | * @param host - hostname including protocol. i.e. 'https://.livekit.cloud' 30 | * @param apiKey - API Key, can be set in env var LIVEKIT_API_KEY 31 | * @param secret - API Secret, can be set in env var LIVEKIT_API_SECRET 32 | */ 33 | constructor(host: string, apiKey?: string, secret?: string) { 34 | super(apiKey, secret); 35 | this.rpc = new TwirpRpc(host, livekitPackage); 36 | } 37 | 38 | /** 39 | * Create an explicit dispatch for an agent to join a room. To use explicit 40 | * dispatch, your agent must be registered with an `agentName`. 41 | * @param roomName - name of the room to dispatch to 42 | * @param agentName - name of the agent to dispatch 43 | * @param options - optional metadata to send along with the dispatch 44 | * @returns the dispatch that was created 45 | */ 46 | async createDispatch( 47 | roomName: string, 48 | agentName: string, 49 | options?: CreateDispatchOptions, 50 | ): Promise { 51 | const req = new CreateAgentDispatchRequest({ 52 | room: roomName, 53 | agentName, 54 | metadata: options?.metadata, 55 | }).toJson(); 56 | const data = await this.rpc.request( 57 | svc, 58 | 'CreateDispatch', 59 | req, 60 | await this.authHeader({ roomAdmin: true, room: roomName }), 61 | ); 62 | return AgentDispatch.fromJson(data, { ignoreUnknownFields: true }); 63 | } 64 | 65 | /** 66 | * Delete an explicit dispatch for an agent in a room. 67 | * @param dispatchId - id of the dispatch to delete 68 | * @param roomName - name of the room the dispatch is for 69 | */ 70 | async deleteDispatch(dispatchId: string, roomName: string): Promise { 71 | const req = new DeleteAgentDispatchRequest({ 72 | dispatchId, 73 | room: roomName, 74 | }).toJson(); 75 | await this.rpc.request( 76 | svc, 77 | 'DeleteDispatch', 78 | req, 79 | await this.authHeader({ roomAdmin: true, room: roomName }), 80 | ); 81 | } 82 | 83 | /** 84 | * Get an Agent dispatch by ID 85 | * @param dispatchId - id of the dispatch to get 86 | * @param roomName - name of the room the dispatch is for 87 | * @returns the dispatch that was found, or undefined if not found 88 | */ 89 | async getDispatch(dispatchId: string, roomName: string): Promise { 90 | const req = new ListAgentDispatchRequest({ 91 | dispatchId, 92 | room: roomName, 93 | }).toJson(); 94 | const data = await this.rpc.request( 95 | svc, 96 | 'ListDispatch', 97 | req, 98 | await this.authHeader({ roomAdmin: true, room: roomName }), 99 | ); 100 | const res = ListAgentDispatchResponse.fromJson(data, { ignoreUnknownFields: true }); 101 | if (res.agentDispatches.length === 0) { 102 | return undefined; 103 | } 104 | return res.agentDispatches[0]; 105 | } 106 | 107 | /** 108 | * List all agent dispatches for a room 109 | * @param roomName - name of the room to list dispatches for 110 | * @returns the list of dispatches 111 | */ 112 | async listDispatch(roomName: string): Promise { 113 | const req = new ListAgentDispatchRequest({ 114 | room: roomName, 115 | }).toJson(); 116 | const data = await this.rpc.request( 117 | svc, 118 | 'ListDispatch', 119 | req, 120 | await this.authHeader({ roomAdmin: true, room: roomName }), 121 | ); 122 | const res = ListAgentDispatchResponse.fromJson(data, { ignoreUnknownFields: true }); 123 | return res.agentDispatches; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/src/ServiceBase.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import { AccessToken } from './AccessToken.js'; 5 | import type { SIPGrant, VideoGrant } from './grants.js'; 6 | 7 | /** 8 | * Utilities to handle authentication 9 | */ 10 | export class ServiceBase { 11 | private readonly apiKey?: string; 12 | 13 | private readonly secret?: string; 14 | 15 | private readonly ttl: string; 16 | 17 | /** 18 | * @param apiKey - API Key. 19 | * @param secret - API Secret. 20 | * @param ttl - token TTL 21 | */ 22 | constructor(apiKey?: string, secret?: string, ttl?: string) { 23 | this.apiKey = apiKey; 24 | this.secret = secret; 25 | this.ttl = ttl || '10m'; 26 | } 27 | 28 | async authHeader(grant: VideoGrant, sip?: SIPGrant): Promise> { 29 | const at = new AccessToken(this.apiKey, this.secret, { ttl: this.ttl }); 30 | if (grant) { 31 | at.addGrant(grant); 32 | } 33 | if (sip) { 34 | at.addSIPGrant(sip); 35 | } 36 | return { 37 | Authorization: `Bearer ${await at.toJwt()}`, 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/src/TwirpRPC.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import type { JsonValue } from '@bufbuild/protobuf'; 5 | 6 | // twirp RPC adapter for client implementation 7 | 8 | const defaultPrefix = '/twirp'; 9 | 10 | export const livekitPackage = 'livekit'; 11 | export interface Rpc { 12 | request( 13 | service: string, 14 | method: string, 15 | data: JsonValue, 16 | headers: any, // eslint-disable-line @typescript-eslint/no-explicit-any 17 | timeout?: number, 18 | ): Promise; 19 | } 20 | 21 | export class TwirpError extends Error { 22 | status: number; 23 | code?: string; 24 | metadata?: Record; 25 | 26 | constructor( 27 | name: string, 28 | message: string, 29 | status: number, 30 | code?: string, 31 | metadata?: Record, 32 | ) { 33 | super(message); 34 | this.name = name; 35 | this.status = status; 36 | this.code = code; 37 | this.metadata = metadata; 38 | } 39 | } 40 | 41 | /** 42 | * JSON based Twirp V7 RPC 43 | */ 44 | export class TwirpRpc { 45 | host: string; 46 | 47 | pkg: string; 48 | 49 | prefix: string; 50 | 51 | constructor(host: string, pkg: string, prefix?: string) { 52 | if (host.startsWith('ws')) { 53 | host = host.replace('ws', 'http'); 54 | } 55 | this.host = host; 56 | this.pkg = pkg; 57 | this.prefix = prefix || defaultPrefix; 58 | } 59 | 60 | async request( 61 | service: string, 62 | method: string, 63 | data: any, // eslint-disable-line @typescript-eslint/no-explicit-any 64 | headers: any, // eslint-disable-line @typescript-eslint/no-explicit-any 65 | timeout = 60, 66 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 67 | ): Promise { 68 | const path = `${this.prefix}/${this.pkg}.${service}/${method}`; 69 | const url = new URL(path, this.host); 70 | const init: RequestInit = { 71 | method: 'POST', 72 | headers: { 73 | 'Content-Type': 'application/json;charset=UTF-8', 74 | ...headers, 75 | }, 76 | body: JSON.stringify(data), 77 | }; 78 | 79 | if (timeout) { 80 | init.signal = AbortSignal.timeout(timeout * 1000); 81 | } 82 | 83 | const response = await fetch(url, init); 84 | 85 | if (!response.ok) { 86 | const isJson = response.headers.get('content-type') === 'application/json'; 87 | let errorMessage = 'Unknown internal error'; 88 | let errorCode: string | undefined = undefined; 89 | let metadata: Record | undefined = undefined; 90 | try { 91 | if (isJson) { 92 | const parsedError = (await response.json()) as Record; 93 | if ('msg' in parsedError) { 94 | errorMessage = parsedError.msg; 95 | } 96 | if ('code' in parsedError) { 97 | errorCode = parsedError.code; 98 | } 99 | if ('meta' in parsedError) { 100 | metadata = >parsedError.meta; 101 | } 102 | } else { 103 | errorMessage = await response.text(); 104 | } 105 | } catch (e) { 106 | // parsing went wrong, no op and we keep default error message 107 | console.debug(`Error when trying to parse error message, using defaults`, e); 108 | } 109 | 110 | throw new TwirpError(response.statusText, errorMessage, response.status, errorCode, metadata); 111 | } 112 | const parsedResp = (await response.json()) as Record; 113 | 114 | const camelcaseKeys = await import('camelcase-keys').then((mod) => mod.default); 115 | return camelcaseKeys(parsedResp, { deep: true }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/src/WebhookReceiver.test.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import { describe, expect, it } from 'vitest'; 5 | import { AccessToken } from './AccessToken.js'; 6 | import { WebhookEvent, WebhookReceiver } from './WebhookReceiver.js'; 7 | 8 | const testApiKey = 'abcdefg'; 9 | const testSecret = 'abababa'; 10 | 11 | describe('webhook receiver', () => { 12 | const body = 13 | '{"event":"room_started", "room":{"sid":"RM_TkVjUvAqgzKz", "name":"mytestroom", "emptyTimeout":300, "creationTime":"1628545903", "turnPassword":"ICkSr2rEeslkN6e9bXL4Ji5zzMD5Z7zzr6ulOaxMj6N", "enabledCodecs":[{"mime":"audio/opus"}, {"mime":"video/VP8"}]}}'; 14 | const sha = 'CoEQz1chqJ9bnZRcORddjplkvpjmPujmLTR42DbefYI='; 15 | const t = new AccessToken(testApiKey, testSecret); 16 | t.sha256 = sha; 17 | 18 | const receiver = new WebhookReceiver(testApiKey, testSecret); 19 | 20 | it('should receive and decode WebhookEvent', async () => { 21 | const token = await t.toJwt(); 22 | const event = await receiver.receive(body, token); 23 | expect(event).toBeTruthy(); 24 | expect(event.room?.name).toBe('mytestroom'); 25 | expect(event.event).toBe('room_started'); 26 | }); 27 | }); 28 | 29 | describe('decoding json payload', () => { 30 | it('should allow server to return extra fields', () => { 31 | const obj = { 32 | type: 'room_started', 33 | room: { 34 | sid: 'RM_TkVjUvAqgzKz', 35 | name: 'mytestroom', 36 | }, 37 | extra: 'extra', 38 | }; 39 | 40 | const event = new WebhookEvent(obj); 41 | expect(event).toBeTruthy(); 42 | expect(event.room?.name).toBe('mytestroom'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/src/WebhookReceiver.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import type { BinaryReadOptions, JsonReadOptions, JsonValue } from '@bufbuild/protobuf'; 5 | import { WebhookEvent as ProtoWebhookEvent } from '@livekit/protocol'; 6 | import { TokenVerifier } from './AccessToken.js'; 7 | import { digest } from './crypto/digest.js'; 8 | 9 | export const authorizeHeader = 'Authorize'; 10 | 11 | export class WebhookEvent extends ProtoWebhookEvent { 12 | event: WebhookEventNames = ''; 13 | 14 | static fromBinary(bytes: Uint8Array, options?: Partial): WebhookEvent { 15 | return new WebhookEvent().fromBinary(bytes, options); 16 | } 17 | 18 | static fromJson(jsonValue: JsonValue, options?: Partial): WebhookEvent { 19 | return new WebhookEvent().fromJson(jsonValue, options); 20 | } 21 | 22 | static fromJsonString(jsonString: string, options?: Partial): WebhookEvent { 23 | return new WebhookEvent().fromJsonString(jsonString, options); 24 | } 25 | } 26 | 27 | export type WebhookEventNames = 28 | | 'room_started' 29 | | 'room_finished' 30 | | 'participant_joined' 31 | | 'participant_left' 32 | | 'track_published' 33 | | 'track_unpublished' 34 | | 'egress_started' 35 | | 'egress_updated' 36 | | 'egress_ended' 37 | | 'ingress_started' 38 | | 'ingress_ended' 39 | /** 40 | * @internal 41 | * @remarks only used as a default value, not a valid webhook event 42 | */ 43 | | ''; 44 | 45 | export class WebhookReceiver { 46 | private verifier: TokenVerifier; 47 | 48 | constructor(apiKey: string, apiSecret: string) { 49 | this.verifier = new TokenVerifier(apiKey, apiSecret); 50 | } 51 | 52 | /** 53 | * @param body - string of the posted body 54 | * @param authHeader - `Authorization` header from the request 55 | * @param skipAuth - true to skip auth validation 56 | * @param clockTolerance - How much tolerance to allow for checks against the auth header to be skewed from the claims 57 | * @returns The processed webhook event 58 | */ 59 | async receive( 60 | body: string, 61 | authHeader?: string, 62 | skipAuth: boolean = false, 63 | clockTolerance?: string | number, 64 | ): Promise { 65 | // verify token 66 | if (!skipAuth) { 67 | if (!authHeader) { 68 | throw new Error('authorization header is empty'); 69 | } 70 | const claims = await this.verifier.verify(authHeader, clockTolerance); 71 | // confirm sha 72 | const hash = await digest(body); 73 | const hashDecoded = btoa( 74 | Array.from(new Uint8Array(hash)) 75 | .map((v) => String.fromCharCode(v)) 76 | .join(''), 77 | ); 78 | 79 | if (claims.sha256 !== hashDecoded) { 80 | throw new Error('sha256 checksum of body does not match'); 81 | } 82 | } 83 | 84 | return WebhookEvent.fromJson(JSON.parse(body), { ignoreUnknownFields: true }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/src/crypto/digest.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Use the Web Crypto API if available, otherwise fallback to Node.js crypto 6 | export async function digest(data: string): Promise { 7 | if (globalThis.crypto?.subtle) { 8 | const encoder = new TextEncoder(); 9 | return crypto.subtle.digest('SHA-256', encoder.encode(data)); 10 | } else { 11 | const nodeCrypto = await import('node:crypto'); 12 | return nodeCrypto.createHash('sha256').update(data).digest(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/src/crypto/uuid.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | // Use the Web Crypto API if available, otherwise fallback to Node.js crypto 6 | export async function getRandomBytes(size: number = 16): Promise { 7 | if (globalThis.crypto) { 8 | return crypto.getRandomValues(new Uint8Array(size)); 9 | } else { 10 | const nodeCrypto = await import('node:crypto'); 11 | return nodeCrypto.getRandomValues(new Uint8Array(size)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/src/grants.test.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import { TrackSource } from '@livekit/protocol'; 5 | import { describe, expect, it } from 'vitest'; 6 | import type { ClaimGrants, VideoGrant } from './grants.js'; 7 | import { claimsToJwtPayload } from './grants.js'; 8 | 9 | describe('ClaimGrants are parsed correctly', () => { 10 | it('parses TrackSource correctly to strings', () => { 11 | const grant: VideoGrant = { 12 | canPublishSources: [ 13 | TrackSource.CAMERA, 14 | TrackSource.MICROPHONE, 15 | TrackSource.SCREEN_SHARE, 16 | TrackSource.SCREEN_SHARE_AUDIO, 17 | ], 18 | }; 19 | 20 | const claim: ClaimGrants = { video: grant }; 21 | 22 | const jwtPayload = claimsToJwtPayload(claim); 23 | expect(jwtPayload.video).toBeTypeOf('object'); 24 | expect(jwtPayload.video?.canPublishSources).toEqual([ 25 | 'camera', 26 | 'microphone', 27 | 'screen_share', 28 | 'screen_share_audio', 29 | ]); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/src/grants.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | import type { RoomConfiguration } from '@livekit/protocol'; 5 | import { TrackSource } from '@livekit/protocol'; 6 | import type { JWTPayload } from 'jose'; 7 | 8 | export function trackSourceToString(source: TrackSource) { 9 | switch (source) { 10 | case TrackSource.CAMERA: 11 | return 'camera'; 12 | case TrackSource.MICROPHONE: 13 | return 'microphone'; 14 | case TrackSource.SCREEN_SHARE: 15 | return 'screen_share'; 16 | case TrackSource.SCREEN_SHARE_AUDIO: 17 | return 'screen_share_audio'; 18 | default: 19 | throw new TypeError(`Cannot convert TrackSource ${source} to string`); 20 | } 21 | } 22 | 23 | export function claimsToJwtPayload( 24 | grant: ClaimGrants, 25 | ): JWTPayload & { video?: Record } { 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | const claim: Record = { ...grant }; 28 | // eslint-disable-next-line no-restricted-syntax 29 | if (Array.isArray(claim.video?.canPublishSources)) { 30 | claim.video.canPublishSources = claim.video.canPublishSources.map(trackSourceToString); 31 | } 32 | return claim; 33 | } 34 | 35 | export interface VideoGrant { 36 | /** permission to create a room */ 37 | roomCreate?: boolean; 38 | 39 | /** permission to join a room as a participant, room must be set */ 40 | roomJoin?: boolean; 41 | 42 | /** permission to list rooms */ 43 | roomList?: boolean; 44 | 45 | /** permission to start a recording */ 46 | roomRecord?: boolean; 47 | 48 | /** permission to control a specific room, room must be set */ 49 | roomAdmin?: boolean; 50 | 51 | /** name of the room, must be set for admin or join permissions */ 52 | room?: string; 53 | 54 | /** permissions to control ingress, not specific to any room or ingress */ 55 | ingressAdmin?: boolean; 56 | 57 | /** 58 | * allow participant to publish. If neither canPublish or canSubscribe is set, 59 | * both publish and subscribe are enabled 60 | */ 61 | canPublish?: boolean; 62 | 63 | /** 64 | * TrackSource types that the participant is allowed to publish 65 | * When set, it supersedes CanPublish. Only sources explicitly set here can be published 66 | */ 67 | canPublishSources?: TrackSource[]; 68 | 69 | /** allow participant to subscribe to other tracks */ 70 | canSubscribe?: boolean; 71 | 72 | /** 73 | * allow participants to publish data, defaults to true if not set 74 | */ 75 | canPublishData?: boolean; 76 | 77 | /** 78 | * by default, a participant is not allowed to update its own metadata 79 | */ 80 | canUpdateOwnMetadata?: boolean; 81 | 82 | /** participant isn't visible to others */ 83 | hidden?: boolean; 84 | 85 | /** participant is recording the room, when set, allows room to indicate it's being recorded */ 86 | recorder?: boolean; 87 | 88 | /** participant allowed to connect to LiveKit as Agent Framework worker */ 89 | agent?: boolean; 90 | 91 | /** allow participant to subscribe to metrics */ 92 | canSubscribeMetrics?: boolean; 93 | 94 | /** destination room which this participant can forward to */ 95 | destinationRoom?: string; 96 | } 97 | 98 | export interface SIPGrant { 99 | /** manage sip resources */ 100 | admin?: boolean; 101 | 102 | /** make outbound calls */ 103 | call?: boolean; 104 | } 105 | 106 | /** @internal */ 107 | export interface ClaimGrants extends JWTPayload { 108 | name?: string; 109 | video?: VideoGrant; 110 | sip?: SIPGrant; 111 | kind?: string; 112 | metadata?: string; 113 | attributes?: Record; 114 | sha256?: string; 115 | roomPreset?: string; 116 | roomConfig?: RoomConfiguration; 117 | } 118 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | export { 6 | AliOSSUpload, 7 | AudioCodec, 8 | AutoParticipantEgress, 9 | AutoTrackEgress, 10 | AzureBlobUpload, 11 | DataPacket_Kind, 12 | DirectFileOutput, 13 | EgressInfo, 14 | EgressStatus, 15 | EncodedFileOutput, 16 | EncodedFileType, 17 | EncodingOptions, 18 | EncodingOptionsPreset, 19 | GCPUpload, 20 | ImageCodec, 21 | ImageFileSuffix, 22 | ImageOutput, 23 | IngressAudioEncodingOptions, 24 | IngressAudioEncodingPreset, 25 | IngressAudioOptions, 26 | IngressInfo, 27 | IngressInput, 28 | IngressState, 29 | IngressVideoEncodingOptions, 30 | IngressVideoEncodingPreset, 31 | IngressVideoOptions, 32 | ParticipantEgressRequest, 33 | ParticipantInfo, 34 | ParticipantInfo_State, 35 | ParticipantPermission, 36 | Room, 37 | RoomCompositeEgressRequest, 38 | RoomEgress, 39 | S3Upload, 40 | SIPDispatchRuleInfo, 41 | SIPParticipantInfo, 42 | SIPTrunkInfo, 43 | SegmentedFileOutput, 44 | SegmentedFileProtocol, 45 | StreamOutput, 46 | StreamProtocol, 47 | TrackCompositeEgressRequest, 48 | TrackEgressRequest, 49 | TrackInfo, 50 | TrackSource, 51 | TrackType, 52 | WebEgressRequest, 53 | VideoCodec, 54 | } from '@livekit/protocol'; 55 | export * from './AccessToken.js'; 56 | export * from './AgentDispatchClient.js'; 57 | export * from './EgressClient.js'; 58 | export * from './grants.js'; 59 | export * from './IngressClient.js'; 60 | export * from './RoomServiceClient.js'; 61 | export * from './SipClient.js'; 62 | export * from './WebhookReceiver.js'; 63 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist" 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "exclude": ["src/**/*.test.ts", "vite.config.ts"], 9 | "typedocOptions": { 10 | "entryPoints": ["src/index.ts"], 11 | "excludeInternal": true, 12 | "excludePrivate": true, 13 | "excludeProtected": true, 14 | "excludeExternals": true, 15 | "includeVersion": true, 16 | "name": "LiveKit JS Server SDK", 17 | "out": "docs", 18 | "theme": "default" 19 | }, 20 | "ts-node": { 21 | "esm": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | import defaults from '../../tsup.config'; 4 | 5 | export default defineConfig({ 6 | ...defaults, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/livekit-server-sdk/vite.config.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 LiveKit, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | import { defineConfig } from 'vite'; 6 | 7 | export default defineConfig({ 8 | test: { 9 | environment: 'node', 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'examples/*' 3 | - 'packages/*' 4 | - 'packages/livekit-rtc/npm/*' 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "rangeStrategy": "auto", 5 | "packageRules": [ 6 | { 7 | "schedule": "on the first day of the month", 8 | "matchDepTypes": ["devDependencies"], 9 | "matchUpdateTypes": ["patch", "minor"], 10 | "groupName": "devDependencies (non-major)" 11 | }, 12 | { 13 | "matchPackagePrefixes": ["@livekit", "livekit-"], 14 | "matchUpdateTypes": ["patch", "minor"], 15 | "groupName": "Update LiveKit dependencies (non-major)" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**.ts", "vite.config.js"], 4 | "exclude": [] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015"], 4 | "target": "es2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 5 | "module": "node16" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "sourceMap": true, 9 | "moduleResolution": "node16", 10 | "strict": true /* Enable all strict type-checking options. */, 11 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 12 | "skipLibCheck": false /* Skip type checking of declaration files. */, 13 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 14 | "verbatimModuleSyntax": true, 15 | "isolatedModules": true, 16 | "noUncheckedIndexedAccess": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup'; 2 | 3 | const defaultOptions: Options = { 4 | entry: ['src/**/*.ts', '!src/**/*.test.ts'], 5 | format: ['cjs', 'esm'], 6 | splitting: false, 7 | sourcemap: true, 8 | dts: true, 9 | clean: true, 10 | bundle: false, 11 | target: 'node16', 12 | esbuildOptions: (options, context) => { 13 | if (context.format === 'esm') { 14 | options.packages = 'external'; 15 | } 16 | }, 17 | plugins: [ 18 | { 19 | // https://github.com/egoist/tsup/issues/953#issuecomment-2294998890 20 | // ensuring that all local requires/imports in `.cjs` files import from `.cjs` files. 21 | // require('./path') → require('./path.cjs') in `.cjs` files 22 | // require('../path') → require('../path.cjs') in `.cjs` files 23 | // from './path' → from './path.cjs' in `.cjs` files 24 | // from '../path' → from '../path.cjs' in `.cjs` files 25 | name: 'fix-cjs-imports', 26 | renderChunk(code) { 27 | if (this.format === 'cjs') { 28 | const regexCjs = /require\((?['"])(?\.[^'"]+)\.js['"]\)/g; 29 | const regexDynamic = /import\((?['"])(?\.[^'"]+)\.js['"]\)/g; 30 | const regexEsm = /from(?[\s]*)(?['"])(?\.[^'"]+)\.js['"]/g; 31 | return { 32 | code: code 33 | .replace(regexCjs, 'require($$.cjs$)') 34 | .replace(regexDynamic, 'import($$.cjs$)') 35 | .replace(regexEsm, 'from$$$.cjs$'), 36 | }; 37 | } 38 | }, 39 | }, 40 | ], 41 | }; 42 | export default defaultOptions; 43 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "globalEnv": ["LIVEKIT_URL", "LIVEKIT_API_KEY", "LIVEKIT_API_SECRET", "NODE_ENV"], 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**"] 8 | }, 9 | "artifacts": {}, 10 | "lint": { 11 | "outputs": [] 12 | }, 13 | "api:check": { 14 | "cache": false, 15 | "dependsOn": ["^build"] 16 | }, 17 | "api:update": { 18 | "dependsOn": ["^build"] 19 | } 20 | } 21 | } 22 | --------------------------------------------------------------------------------