diff --git a/.changeset/merge_uniffi_into_ffi.md b/.changeset/merge_uniffi_into_ffi.md new file mode 100644 index 000000000..5b60cbbf0 --- /dev/null +++ b/.changeset/merge_uniffi_into_ffi.md @@ -0,0 +1,43 @@ +--- +livekit-ffi: major +livekit: patch +livekit-api: patch +livekit-capture: patch +livekit-common: patch +livekit-data-stream: patch +livekit-datatrack: patch +livekit-net: patch +livekit-rpc: patch +livekit-signaling: patch +livekit-token-source: patch +--- + +Merge `livekit-uniffi` into `livekit-ffi`. + +There is now one crate, one version and one release tag. The `livekit-uniffi` +crate, its changelog and its `livekit-uniffi/v*` tags are gone; its history is +preserved in git. + +The crate carries two mutually exclusive surfaces, selected by feature: + +- `room-apis` (default) is the existing protobuf / C-ABI surface, with + `livekit` and libwebrtc. Everything that built `livekit-ffi` before builds it + unchanged. +- `core-modules` is the former `livekit-uniffi` surface — access tokens, log + forwarding, data tracks, data streams v2 — and depends on none of `livekit`, + `libwebrtc`, `soxr-sys` or `imgproc`. What previously built `livekit-uniffi` + now builds `-p livekit-ffi --no-default-features --features core-modules`. + +`[package.metadata.platform-features]` in `livekit-ffi/Cargo.toml` records +which surface each downstream package build gets, and CI resolves its build +flags from it. + +The merge itself requires no downstream SDK changes: the UniFFI namespace is +pinned per surface and every published package name is preserved. The one +difference is the native library the crate builds, `liblivekit_uniffi` → +`liblivekit_ffi`, which is loaded by name from the generated bindings and the +Dart build hook and needs no consumer change. The package renames are a +separate change, released alongside this one. + +The UniFFI node package (`@livekit/uniffi`) is not built for now and will be +reintroduced separately; node bindings ship from `livekit-ffi-node-bindings`. diff --git a/.changeset/protocol_thread_schema.md b/.changeset/protocol_thread_schema.md index 6724a26b4..60fa1c080 100644 --- a/.changeset/protocol_thread_schema.md +++ b/.changeset/protocol_thread_schema.md @@ -10,7 +10,6 @@ livekit-ffi: patch livekit-rpc: patch livekit-signaling: patch livekit-token: patch -livekit-uniffi: patch --- `livekit-protocol` exposes the conversation schema of a thread's database, as protocol's Go package diff --git a/.changeset/uniffi_swift_dsyms.md b/.changeset/uniffi_swift_dsyms.md index d224650c3..d1d6afddd 100644 --- a/.changeset/uniffi_swift_dsyms.md +++ b/.changeset/uniffi_swift_dsyms.md @@ -1,5 +1,5 @@ --- -livekit-uniffi: patch +livekit-ffi: patch --- -Embed per-slice `.dSYM` bundles in the Swift `RustLiveKitUniFFI.xcframework` again so Xcode archives and crash symbolication can resolve Rust frames (livekit/client-sdk-swift#1149). +Embed per-slice `.dSYM` bundles in the Swift `RustLiveKitFFI.xcframework` again so Xcode archives and crash symbolication can resolve Rust frames (livekit/client-sdk-swift#1149). diff --git a/.github/scripts/changeset_detect.py b/.github/scripts/changeset_detect.py index 11f7b2581..e985e0973 100755 --- a/.github/scripts/changeset_detect.py +++ b/.github/scripts/changeset_detect.py @@ -228,8 +228,8 @@ def reconcile_knope_config(meta, knope_packages): (e.g. a renamed or removed crate leaving a dangling entry). Note the rule is one-directional: publishable implies knope-managed, but a - knope-managed crate may set `publish = false` (e.g. livekit-ffi / livekit- - uniffi, which CI publishes via wrapper packages rather than `cargo publish`). + knope-managed crate may set `publish = false` (e.g. livekit-ffi, which CI + publishes via wrapper packages rather than `cargo publish`). """ workspace_names = {pkg["name"] for pkg in meta["packages"]} publishable = {pkg["name"] for pkg in meta["packages"] if is_publishable(pkg)} diff --git a/.github/scripts/ffi_features.py b/.github/scripts/ffi_features.py new file mode 100755 index 000000000..85329b11d --- /dev/null +++ b/.github/scripts/ffi_features.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# Copyright 2026 LiveKit, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Resolve cargo build flags for a downstream platform build of livekit-ffi. + +`livekit-ffi` exposes two mutually exclusive FFI surfaces, `room-apis` and +`core-modules`. Which one each downstream package build gets is declared once, +in `[package.metadata.platform-features]` in livekit-ffi/Cargo.toml, and read +back here so a platform cannot silently drift onto the wrong surface. + + $ ffi_features.py swift + --no-default-features --features core-modules + + $ ffi_features.py --list + android --no-default-features --features core-modules + ... + +Every build site that produces an artifact names its platform. Used by +livekit-ffi/Makefile.toml and by the uniffi-* workflows. +""" + +import argparse +import json +import subprocess +import sys + +PACKAGE = "livekit-ffi" +METADATA_KEY = "platform-features" + + +def load_table(): + out = subprocess.run( + ["cargo", "metadata", "--no-deps", "--format-version", "1"], + capture_output=True, + text=True, + check=True, + ).stdout + meta = json.loads(out) + for pkg in meta["packages"]: + if pkg["name"] == PACKAGE: + table = (pkg.get("metadata") or {}).get(METADATA_KEY) + if not table: + sys.exit( + f"{PACKAGE} declares no [package.metadata.{METADATA_KEY}] table" + ) + return table + sys.exit(f"{PACKAGE} not found in cargo metadata") + + +def flags_for(entry, platform): + features = entry.get("features") or [] + if not features: + sys.exit(f"platform '{platform}' declares no features") + + surfaces = {f for f in features if f in ("room-apis", "core-modules")} + if len(surfaces) != 1: + sys.exit( + f"platform '{platform}' must select exactly one of room-apis / " + f"core-modules (got: {sorted(surfaces) or 'none'})" + ) + # Every entry states its whole feature set. Without this, an entry inherits + # whatever `default` happens to list, so a change there moves platforms + # silently -- and for a core-modules entry it would select both surfaces, + # which do not compile together. + if not entry.get("no-default-features"): + sys.exit( + f"platform '{platform}' does not set no-default-features; every " + f"entry must list the whole feature set it wants, so that a change " + f"to the crate's `default` features cannot move it onto another " + f"surface" + ) + + flags = [] + if entry.get("no-default-features"): + flags.append("--no-default-features") + flags += ["--features", ",".join(features)] + return " ".join(flags) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("platform", nargs="?", help="platform key to resolve") + ap.add_argument( + "--list", action="store_true", help="print every platform and its flags" + ) + args = ap.parse_args() + + table = load_table() + + if args.list: + width = max(len(k) for k in table) + for name in sorted(table): + print(f"{name:<{width}} {flags_for(table[name], name)}") + return + + if not args.platform: + ap.error("a platform key is required (or --list)") + + if args.platform not in table: + sys.exit( + f"unknown platform '{args.platform}'. Known: {', '.join(sorted(table))}" + ) + print(flags_for(table[args.platform], args.platform)) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index fd263ce78..abbf8435c 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -172,3 +172,30 @@ jobs: cargo install cargo-ndk # FIXME: wgpu_room does not link on aarch64 cargo ndk --target ${{ matrix.target }} build --release -p livekit --workspace -vv + + # `--workspace` above builds livekit-ffi with its default features, which + # select livekit-ffi/room-apis. core-modules is mutually exclusive with + # it, so it needs its own invocation or nothing would compile the + # core-modules code per-commit across this matrix. (Before the + # livekit-uniffi merge that coverage came free from the separate crate.) + - name: Build (Cargo, livekit-ffi core-modules) + if: ${{ !contains(matrix.target, 'android') }} + shell: bash + run: | + # Not a platform build: this gate only checks that the + # core-modules surface still compiles. Platform builds take their + # flags from [package.metadata.platform-features] in + # livekit-ffi/Cargo.toml. + cargo build --release -p livekit-ffi --target ${{ matrix.target }} \ + --no-default-features --features core-modules + + - name: Build (Android, livekit-ffi core-modules) + if: ${{ contains(matrix.target, 'android') }} + shell: bash + run: | + # Not a platform build: this gate only checks that the + # core-modules surface still compiles. Platform builds take their + # flags from [package.metadata.platform-features] in + # livekit-ffi/Cargo.toml. + cargo ndk --target ${{ matrix.target }} build --release -p livekit-ffi \ + --no-default-features --features core-modules diff --git a/.github/workflows/feature-combinations-curated.yml b/.github/workflows/feature-combinations-curated.yml index 37daf0bf8..0ad6851d3 100644 --- a/.github/workflows/feature-combinations-curated.yml +++ b/.github/workflows/feature-combinations-curated.yml @@ -31,7 +31,7 @@ # new feature is invisible to this job until it is added to that list. # # A crate with no such table is not feature-checked here at all. livekit-ffi -# and livekit-uniffi are the deliberate omissions; see the curated step below. +# is the deliberate omission; see the curated step below. # # Measured cold on a 14-core machine: the full 209-combination powerset takes # 359s; this split takes 242s, a 33% saving. That is what brings the job in @@ -269,7 +269,7 @@ jobs: # crate that declares that table is picked up, so adding a crate to this # half of the job means adding the table and nothing else. # - # livekit-ffi and livekit-uniffi deliberately do NOT declare one. + # livekit-ffi deliberately does NOT declare one. # Measured cold, their 8 combinations cost 103s of a 345s total — 30% of # the budget for two crates, because each TLS change rebuilds livekit # underneath livekit-ffi. diff --git a/.github/workflows/ffi-builds.yml b/.github/workflows/ffi-builds.yml index b40db44dc..576fdfbc3 100644 --- a/.github/workflows/ffi-builds.yml +++ b/.github/workflows/ffi-builds.yml @@ -266,7 +266,7 @@ jobs: # This has to run on a macOS entry. The release profile sets # `strip = "symbols"`, which removes the metadata on Linux while macOS # strips less aggressively -- see the note on `bindgen-dart` in - # livekit-uniffi/Makefile.toml. Do not move this to ffi-linux-x86_64 for + # livekit-ffi/Makefile.toml. Do not move this to ffi-linux-x86_64 for # symmetry; bindgen would silently emit an empty module. - name: Generate Python bindings (UniFFI) if: ${{ matrix.name == 'ffi-macos-arm64' }} @@ -355,6 +355,10 @@ jobs: needs: [get-tag, build] permissions: contents: write + # Only for the `gh workflow run` dispatches in the last step. Scoped to + # this job rather than the workflow: it also grants cancelling and + # re-running workflows and deleting runs, artifacts and caches. + actions: write if: needs.get-tag.outputs.tag_name != '' && inputs.dry_run != true env: GH_TOKEN: ${{ github.token }} @@ -368,7 +372,66 @@ jobs: merge-multiple: true path: ${{ github.workspace }}/ffi-builds - - name: Upload artifacts and publish release + - name: Upload artifacts + env: + TAG: ${{ needs.get-tag.outputs.tag_name }} + run: gh release upload "$TAG" ${{ github.workspace }}/ffi-builds/* + + - name: Create release tag + env: + TAG: ${{ needs.get-tag.outputs.tag_name }} + SHA: ${{ github.sha }} run: | - gh release upload ${{ needs.get-tag.outputs.tag_name }} ${{ github.workspace }}/ffi-builds/* - gh release edit ${{ needs.get-tag.outputs.tag_name }} --draft=false + set -euo pipefail + + # This workflow runs on every push to main and adopts whatever draft + # release it finds, which may be several commits old. Tagging a commit + # whose Cargo.toml says a different version would publish mismatched + # packages, and uniffi-dart-publish.yml would reject it later anyway. + crate_version=$(grep -m1 '^version = ' livekit-ffi/Cargo.toml | cut -d'"' -f2) + if [ "${TAG#livekit-ffi/v}" != "$crate_version" ]; then + echo "::error::tag ${TAG} does not match livekit-ffi version ${crate_version} at ${SHA}" + exit 1 + fi + + existing=$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG}" --jq '.object.sha' 2>/dev/null || true) + if [ -n "$existing" ]; then + # A re-run. Carry on rather than failing: the tag is only needed as + # the ref to publish and dispatch against, and it already exists. + echo "::warning::tag ${TAG} already exists at ${existing}; not re-creating it." + else + gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \ + -f ref="refs/tags/${TAG}" \ + -f sha="${SHA}" + fi + + - name: Publish release + env: + TAG: ${{ needs.get-tag.outputs.tag_name }} + run: gh release edit "$TAG" --draft=false + + # A ref created with GITHUB_TOKEN emits no push event -- GitHub's guard + # against recursive workflow runs -- so the UniFFI package publishes + # cannot be tag-push triggered from here. `workflow_dispatch` is the + # documented exception to that guard: it "always creates workflow runs", + # GITHUB_TOKEN included. + # + # Dispatching *against the tag* is what keeps pub.dev happy. The run then + # carries `ref: refs/tags/`, so its OIDC token has refType "tag", + # which pub.dev's trusted publishing requires alongside the tag matching + # the pattern configured on the package (see the eventName / refType + # checks in dart-lang/pub-dev, app/lib/package/backend.dart). + # + # Last step on purpose: the release is already published by now, so if a + # dispatch fails the FFI artifacts are unaffected and only the UniFFI + # packages need the manual re-dispatch the failure tells you to do. + - name: Dispatch the UniFFI package publishes + env: + TAG: ${{ needs.get-tag.outputs.tag_name }} + run: | + set -euo pipefail + for workflow in uniffi-packages.yml uniffi-dart-publish.yml; do + echo "dispatching ${workflow} at ${TAG}" + gh workflow run "$workflow" --repo "$GITHUB_REPOSITORY" \ + --ref "$TAG" -f tag_name="$TAG" + done diff --git a/.github/workflows/gen-node-proto.yml b/.github/workflows/gen-node-proto.yml index 670746a8d..e91bfe0d0 100644 --- a/.github/workflows/gen-node-proto.yml +++ b/.github/workflows/gen-node-proto.yml @@ -5,11 +5,11 @@ on: branches: - main paths: - - livekit-ffi/** + # Only the protos this job regenerates from. livekit-ffi/** would now fire + # on every core-modules change too, and this job auto-commits. + - livekit-ffi/protocol/** - livekit-ffi-node-bindings/** - - Cargo.lock - - Cargo.toml - - .github/** + - .github/workflows/gen-node-proto.yml workflow_dispatch: workflow_call: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d3eb0d98c..8afa5cc34 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -183,6 +183,20 @@ jobs: cargo test --verbose --target ${{ matrix.target }} -p livekit-net --features native cargo test --verbose --target ${{ matrix.target }} -p livekit-signaling --features native + # These live behind livekit-ffi's non-default `core-modules` feature, so a + # workspace-wide `cargo test` does not reach them. Before the + # livekit-uniffi merge they ran as part of that crate. + - name: Test core modules (data streams v2 / data tracks) + shell: bash + run: | + set -euo pipefail + # Not a platform build: this gate only checks that the + # core-modules surface still passes its tests. Platform builds take their + # flags from [package.metadata.platform-features] in + # livekit-ffi/Cargo.toml. + cargo test --verbose --target ${{ matrix.target }} -p livekit-ffi \ + --no-default-features --features core-modules + - name: Test RPC seam (livekit-rpc) shell: bash run: | diff --git a/.github/workflows/uniffi-android.yml b/.github/workflows/uniffi-android.yml index 29df8acb6..781d7b8f4 100644 --- a/.github/workflows/uniffi-android.yml +++ b/.github/workflows/uniffi-android.yml @@ -1,6 +1,7 @@ name: UniFFI Android package -# Builds livekit-uniffi as an Android AAR and publishes it to Maven Central. +# Builds livekit-ffi/core-modules as an Android AAR and publishes +# it to Maven Central. # # Reusable workflow — called from uniffi-packages.yml after the source draft # release has been resolved. Not triggered directly. @@ -13,7 +14,7 @@ on: required: true type: string tag_name: - description: Source release tag (e.g., livekit-uniffi/v0.0.7) — surfaced in publish logs for traceability + description: Source release tag (e.g., livekit-ffi/v0.13.0) — surfaced in publish logs for traceability required: true type: string dry_run: @@ -24,7 +25,7 @@ on: env: CARGO_TERM_COLOR: always - ANDROID_DIR: livekit-uniffi/support/android + ANDROID_DIR: livekit-ffi/support/android jobs: build-and-publish: @@ -54,7 +55,7 @@ jobs: java-version: 17 - name: Build Android package - working-directory: livekit-uniffi + working-directory: livekit-ffi run: cargo make --profile release android-package - name: Publish to Maven Central diff --git a/.github/workflows/uniffi-cdylib.yml b/.github/workflows/uniffi-cdylib.yml index b5002f136..b8458acf8 100644 --- a/.github/workflows/uniffi-cdylib.yml +++ b/.github/workflows/uniffi-cdylib.yml @@ -1,7 +1,7 @@ name: UniFFI cdylib builds -# Builds the livekit-uniffi dynamic library for every Dart/Flutter target and -# attaches them to the livekit-uniffi release as `build-.zip` (+ a +# Builds the livekit-ffi core-modules dynamic library for every Dart/Flutter +# target and attaches them to the livekit-ffi release as `build-.zip` (+ a # `.sha256` sidecar). The Dart build hook (support/dart/hook/build.dart.tera) # downloads these at consumer build time. # @@ -13,11 +13,11 @@ on: workflow_call: inputs: version: - description: Release version (e.g. 0.1.1) + description: Release version (e.g. 0.13.0) required: true type: string tag_name: - description: Source release tag (e.g. livekit-uniffi/v0.1.1) + description: Source release tag (e.g. livekit-ffi/v0.13.0) required: true type: string dry_run: @@ -99,15 +99,18 @@ jobs: - name: Build (Apple / Windows) if: ${{ matrix.platform == 'apple' || matrix.platform == 'windows' }} shell: bash - run: cargo build --release --target ${{ matrix.target }} -p livekit-uniffi + run: | + set -euo pipefail + FLAGS=$(python3 .github/scripts/ffi_features.py dart) + cargo build --release --target ${{ matrix.target }} -p livekit-ffi $FLAGS # Linux: build inside a manylinux_2_28 container so the shared library # links against glibc 2.28 and loads on the distributions the Dart package # is used on (Debian 12, Ubuntu 22.04, Amazon Linux 2023 and newer). A # native build on the runner would inherit its glibc 2.39 floor. Same # approach as ffi-builds.yml. The arm64 entry runs on a native arm64 - # runner, so no cross toolchain is needed, and livekit-uniffi has no C - # dependencies, so the image's stock gcc is enough. The toolchain version + # runner, so no cross toolchain is needed, and livekit-ffi/core-modules + # has no C dependencies, so the image's stock gcc is enough. The toolchain version # comes from rust-toolchain.toml. - name: Build (Linux) if: ${{ matrix.platform == 'linux' }} @@ -115,17 +118,19 @@ jobs: TARGET: ${{ matrix.target }} BUILD_IMAGE: ${{ matrix.build_image }} run: | - docker run --rm -e TARGET -v "$PWD:/workspace" -w /workspace "$BUILD_IMAGE" bash -euo pipefail -c ' + set -euo pipefail + export FLAGS=$(python3 .github/scripts/ffi_features.py dart) + docker run --rm -e TARGET -e FLAGS -v "$PWD:/workspace" -w /workspace "$BUILD_IMAGE" bash -euo pipefail -c ' curl --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain none export PATH="/root/.cargo/bin:$PATH" if [ "$TARGET" = "aarch64-unknown-linux-gnu" ]; then # .cargo/config.toml forces -fuse-ld=lld for this target to link - # libwebrtc, which livekit-uniffi does not use. A non-empty global + # libwebrtc, which livekit-ffi/core-modules does not use. A non-empty global # RUSTFLAGS overrides that per-target config, selecting the GNU # bfd linker the image provides. export RUSTFLAGS="-C link-arg=-fuse-ld=bfd" fi - cargo build --release --target "$TARGET" -p livekit-uniffi + cargo build --release --target "$TARGET" -p livekit-ffi $FLAGS ' # The container ran as root, so hand the outputs back to the runner user. sudo chown -R "$USER:$USER" target @@ -139,7 +144,10 @@ jobs: - name: Build (Android) if: ${{ matrix.platform == 'android' }} - run: cargo ndk --target ${{ matrix.target }} build --release -p livekit-uniffi + run: | + set -euo pipefail + FLAGS=$(python3 .github/scripts/ffi_features.py dart) + cargo ndk --target ${{ matrix.target }} build --release -p livekit-ffi $FLAGS # macOS/Linux: zip + shasum are available. - name: Package artifact (Unix) @@ -149,8 +157,8 @@ jobs: triple="${{ matrix.target }}" dir="target/${triple}/release" case "$triple" in - *apple*) lib="liblivekit_uniffi.dylib" ;; - *) lib="liblivekit_uniffi.so" ;; + *apple*) lib="liblivekit_ffi.dylib" ;; + *) lib="liblivekit_ffi.so" ;; esac out="build-${triple}.zip" # Zip with the bare library name at the archive root. @@ -165,7 +173,7 @@ jobs: shell: pwsh run: | $triple = "${{ matrix.target }}" - $lib = "target/$triple/release/livekit_uniffi.dll" + $lib = "target/$triple/release/livekit_ffi.dll" $out = "build-$triple.zip" Compress-Archive -Path $lib -DestinationPath $out -Force $hash = (Get-FileHash -Algorithm SHA256 $out).Hash.ToLower() diff --git a/.github/workflows/uniffi-dart-publish.yml b/.github/workflows/uniffi-dart-publish.yml index d2546dc6a..df25a9449 100644 --- a/.github/workflows/uniffi-dart-publish.yml +++ b/.github/workflows/uniffi-dart-publish.yml @@ -1,14 +1,19 @@ name: UniFFI Dart package # Publishes the generated livekit_uniffi Dart package to pub.dev on a -# livekit-uniffi release, together with the native libraries it loads. +# livekit-ffi release, together with the native libraries it loads. # # This is a separate workflow from uniffi-packages.yml because pub.dev's -# automated publishing only accepts workflows triggered by a tag push matching -# the tag pattern configured on the package (livekit-uniffi/v{{version}}); -# a release-event-triggered workflow is rejected. knope-bot creates the tag via -# the API, and App-created events do trigger workflows here (see the note in -# uniffi-packages.yml / #1256). +# automated publishing accepts a run only when its OIDC token says the ref is +# a *tag* matching the pattern configured on the package +# (livekit-ffi/v{{version}}); a release-event-triggered workflow is rejected. +# +# livekit-ffi uses knope's draft-release flow, and the tag ffi-builds.yml +# creates with GITHUB_TOKEN emits no push event (#1256). So the normal trigger +# is ffi-builds.yml dispatching this workflow with `--ref ` once the +# release is published: workflow_dispatch is exempt from GitHub's recursion +# guard, and dispatching at the tag still gives the run refType "tag". The +# push trigger below stays for a tag pushed by hand. # # The package's hook/build.dart downloads build-.zip assets from the # GitHub release at consumer build time, so a pub.dev version whose assets are @@ -17,28 +22,38 @@ name: UniFFI Dart package # same run, and the publish job depends on it, so the package can only be # published once every asset is on the release. # -# Publishing is enabled. A tag push runs the whole pipeline and publishes to -# pub.dev through OIDC (dart-lang/setup-dart exchanges the job token). pub.dev -# accepts that token only because automated publishing is configured on the -# package for livekit/rust-sdks with tag pattern `livekit-uniffi/v{{version}}`; -# if that setting is removed the publish step fails and nothing else changes. -# The package was created by a manual first publish of 0.1.10 (pub.dev only -# automates existing packages), and knope-bot's API-created tag was confirmed -# to fire this push trigger on that release. +# Publishing is enabled. The run publishes to pub.dev through OIDC +# (dart-lang/setup-dart exchanges the job token), and pub.dev accepts that +# token only if all of the following hold on the package's automated-publishing +# settings. Every one of them has to be set up before the first release: +# +# - the package `livekit_ffi` exists. pub.dev only automates packages that +# already exist, so it needs one manual publish first -- this is a new +# package, not a rename of `livekit_uniffi`, whose settings do not carry +# over +# - repository `livekit/rust-sdks`, tag pattern `livekit-ffi/v{{version}}` +# - "Enable publishing from `workflow_dispatch` events" is checked, since +# that is how ffi-builds.yml starts this run +# - "Require GitHub Actions environment" is checked, with `pub.dev` as the +# name, matching the `environment:` on the publish job below +# +# The environment is the real guard: give it a deployment-tag rule for +# `livekit-ffi/v*` in the repository settings, and GitHub refuses to mint a +# token carrying that environment for a run on any other ref. That is enforced +# server-side, so editing this file cannot get around it. # # Recovery: if a run fails, fix the cause and re-run the failed jobs, or # dispatch this workflow with the tag. A dispatch rebuilds and re-attaches the -# assets (with --clobber) and re-validates the package, but never publishes, -# since the publish step only runs for push events. Set dry_run to skip the -# asset upload when only exercising the workflow. +# assets (with --clobber), re-validates, and publishes -- so set dry_run when +# only exercising the workflow. on: push: - tags: ["livekit-uniffi/v*"] + tags: ["livekit-ffi/v*"] workflow_dispatch: inputs: tag_name: - description: "Release tag (e.g. livekit-uniffi/v0.1.9)" + description: "Release tag (e.g. livekit-ffi/v0.13.0)" required: true type: string dry_run: @@ -48,9 +63,9 @@ on: env: # Set to "false" to pause publishing while keeping asset uploads and - # validation. Real publishing also requires a tag-push trigger: publishing - # from workflow_dispatch is left disabled on pub.dev, and the publish step - # below only runs for push events, so dispatch runs stop at the dry run. + # validation. This is a convenience switch only -- what actually constrains + # publishing is the pub.dev configuration and the `pub.dev` environment + # described above, both of which GitHub and pub.dev enforce server-side. PUBLISH_ENABLED: "true" permissions: @@ -75,11 +90,11 @@ jobs: run: | TAG="${INPUT_TAG:-$PUSHED_TAG}" case "$TAG" in - livekit-uniffi/v*) ;; + livekit-ffi/v*) ;; *) echo "Unexpected tag: $TAG"; exit 1 ;; esac echo "tag_name=${TAG}" >> "$GITHUB_OUTPUT" - echo "version=${TAG#livekit-uniffi/v}" >> "$GITHUB_OUTPUT" + echo "version=${TAG#livekit-ffi/v}" >> "$GITHUB_OUTPUT" # Fast fail on a mis-pointed tag before spending runner time on the # cdylib matrix: the tag version must match the crate version the tag @@ -88,16 +103,16 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: ref: ${{ steps.tag.outputs.tag_name }} - sparse-checkout: livekit-uniffi/Cargo.toml + sparse-checkout: livekit-ffi/Cargo.toml sparse-checkout-cone-mode: false - name: Check tag matches crate version env: VERSION: ${{ steps.tag.outputs.version }} run: | - crate_version=$(grep -m1 '^version = ' livekit-uniffi/Cargo.toml | cut -d'"' -f2) + crate_version=$(grep -m1 '^version = ' livekit-ffi/Cargo.toml | cut -d'"' -f2) if [ "$crate_version" != "$VERSION" ]; then - echo "Tag version $VERSION does not match livekit-uniffi crate version $crate_version." >&2 + echo "Tag version $VERSION does not match livekit-ffi crate version $crate_version." >&2 exit 1 fi @@ -119,6 +134,11 @@ jobs: name: Build & publish Dart package needs: [resolve-tag, cdylib] runs-on: ubuntu-latest + # Names the environment in the OIDC token so pub.dev's "Require GitHub + # Actions environment" check passes, and subjects the job to the + # environment's deployment-tag rule. Configure that rule to allow only + # `livekit-ffi/v*`; see the note at the top of this file. + environment: pub.dev permissions: contents: read # Exchanged for a temporary pub.dev token by dart-lang/setup-dart. @@ -169,7 +189,7 @@ jobs: sdk: 3.13.1 - name: Build Dart package (release) - working-directory: livekit-uniffi + working-directory: livekit-ffi run: cargo make --profile release dart-package # pub builds the publish archive from git's file listing, and the @@ -182,8 +202,8 @@ jobs: - name: Stage package outside the work tree run: | rm -rf "$RUNNER_TEMP/livekit_uniffi" - cp -R livekit-uniffi/packages/dart "$RUNNER_TEMP/livekit_uniffi" - if find "$RUNNER_TEMP/livekit_uniffi" \( -name 'liblivekit_uniffi.*' -o -name 'livekit_uniffi.dll' \) | grep -q .; then + cp -R livekit-ffi/packages/dart "$RUNNER_TEMP/livekit_uniffi" + if find "$RUNNER_TEMP/livekit_uniffi" \( -name 'liblivekit_ffi.*' -o -name 'livekit_uniffi.dll' \) | grep -q .; then echo "Staged package contains a local native library; refusing to publish." >&2 exit 1 fi @@ -195,6 +215,6 @@ jobs: dart pub publish --dry-run - name: Publish to pub.dev - if: ${{ env.PUBLISH_ENABLED == 'true' && github.event_name == 'push' }} + if: ${{ env.PUBLISH_ENABLED == 'true' && !inputs.dry_run }} working-directory: ${{ runner.temp }}/livekit_uniffi run: dart pub publish --force diff --git a/.github/workflows/uniffi-dart-test.yml b/.github/workflows/uniffi-dart-test.yml index 5a6c61182..533f55f08 100644 --- a/.github/workflows/uniffi-dart-test.yml +++ b/.github/workflows/uniffi-dart-test.yml @@ -2,7 +2,7 @@ name: UniFFI Dart tests # Builds the Dart package (dev mode: host cdylib + generated bindings) and runs # its test suite, exercising the FFI boundary end-to-end. Scoped to changes that -# can affect the livekit-uniffi Dart bindings. +# can affect the livekit-ffi Dart bindings. on: push: @@ -10,7 +10,13 @@ on: pull_request: branches: ["main"] paths: - - "livekit-uniffi/**" + - "livekit-ffi/src/core_modules/**" + - "livekit-ffi/src/lib.rs" + - "livekit-ffi/src/build_info.rs" + - "livekit-ffi/Cargo.toml" + - "livekit-ffi/uniffi.toml" + - "livekit-ffi/Makefile.toml" + - "livekit-ffi/support/dart/**" - "livekit-api/**" - "livekit-protocol/**" - "Cargo.lock" @@ -68,11 +74,11 @@ jobs: ${{ runner.os }}-cargo-target-dart- - name: Build Dart package - working-directory: livekit-uniffi + working-directory: livekit-ffi run: cargo make dart-package - name: Run Dart tests - working-directory: livekit-uniffi/packages/dart + working-directory: livekit-ffi/packages/dart run: | dart pub get dart test diff --git a/.github/workflows/uniffi-packages.yml b/.github/workflows/uniffi-packages.yml index 862dd772e..0d90b8c42 100644 --- a/.github/workflows/uniffi-packages.yml +++ b/.github/workflows/uniffi-packages.yml @@ -1,26 +1,31 @@ name: UniFFI packages -# Publishes the per-language livekit-uniffi wrapper packages (Swift xcframework → -# livekit/livekit-uniffi-xcframework; Android AAR → Maven Central) when a -# livekit-uniffi release is published. +# Publishes the per-language livekit-ffi core-modules wrapper packages (Swift +# xcframework → livekit/livekit-uniffi-xcframework; Android AAR → Maven +# Central) when a livekit-ffi release tag is pushed. # -# knope publishes the `livekit-uniffi/v*` GitHub release (and its git tag) -# directly on the release merge — no draft — so this reacts to the published -# release event, which fires only for genuine livekit-uniffi releases (the gate -# is free). workflow_dispatch allows a manual re-run with an explicit tag. +# Trigger note: this used to react to `release: published`, which worked only +# because knope-bot (a GitHub App) published the livekit-uniffi release and its +# tag directly. livekit-ffi uses knope's `assets = "marker"` draft flow, and a +# draft carries no tag until it is published — with GITHUB_TOKEN, whose refs +# and releases never start a workflow run (see #1256). So ffi-builds.yml +# dispatches this workflow explicitly once its build matrix is green and the +# release is published; workflow_dispatch is exempt from that recursion guard. +# A run therefore still implies a green 11-target build, the same guarantee the +# release event gave. The push trigger below stays for a tag pushed by hand. # # The Dart package, and the cdylib release assets its build hook downloads, are -# handled by uniffi-dart-publish.yml. That workflow is tag-push triggered, as -# pub.dev requires, and builds the assets itself so publishing can depend on -# them directly. +# handled by uniffi-dart-publish.yml, which reacts to the same tag push +# (pub.dev requires a tag-push-triggered workflow) and builds those assets +# itself so publishing can depend on them directly. on: - release: - types: [published] + push: + tags: ["livekit-ffi/v*"] workflow_dispatch: inputs: tag_name: - description: "Release tag (e.g. livekit-uniffi/v0.1.6)" + description: "Release tag (e.g. livekit-ffi/v0.13.0)" required: true type: string dry_run: @@ -34,23 +39,27 @@ permissions: jobs: resolve-tag: runs-on: ubuntu-latest - # The release event fires for every package's release, so proceed only for - # livekit-uniffi. workflow_dispatch always proceeds (explicit tag). - if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.event.release.tag_name, 'livekit-uniffi/v') }} outputs: tag_name: ${{ steps.get-tag.outputs.tag_name }} version: ${{ steps.get-tag.outputs.version }} steps: - id: get-tag - # TAG is bound through env rather than template-expanded into the + # Tags are bound through env rather than template-expanded into the # script: tag names may contain shell metacharacters (Actions script # injection). env: - TAG: ${{ github.event.release.tag_name || inputs.tag_name }} + PUSHED_TAG: ${{ github.ref_name }} + INPUT_TAG: ${{ inputs.tag_name }} run: | + set -euo pipefail + TAG="${INPUT_TAG:-$PUSHED_TAG}" + case "$TAG" in + livekit-ffi/v*) ;; + *) echo "::error::unexpected tag: $TAG"; exit 1 ;; + esac echo "tag_name=${TAG}" >> "$GITHUB_OUTPUT" - # Strip the "livekit-uniffi/v" prefix to get the package version (e.g. 0.1.6). - echo "version=${TAG#livekit-uniffi/v}" >> "$GITHUB_OUTPUT" + # Strip the "livekit-ffi/v" prefix to get the package version (e.g. 0.13.0). + echo "version=${TAG#livekit-ffi/v}" >> "$GITHUB_OUTPUT" swift: needs: resolve-tag diff --git a/.github/workflows/uniffi-swift.yml b/.github/workflows/uniffi-swift.yml index e13fbc583..4a5fac6f1 100644 --- a/.github/workflows/uniffi-swift.yml +++ b/.github/workflows/uniffi-swift.yml @@ -1,7 +1,8 @@ name: UniFFI Swift package -# Builds livekit-uniffi as a Swift xcframework and publishes it to the -# livekit-uniffi-xcframework hosting repo (draft release + PR). +# Builds livekit-ffi/core-modules as a Swift xcframework and +# publishes it to the livekit-uniffi-xcframework hosting repo +# (draft release + PR). # # Reusable workflow — called from uniffi-packages.yml after the source draft # release has been resolved. Not triggered directly. @@ -14,7 +15,7 @@ on: required: true type: string tag_name: - description: Source release tag (e.g., livekit-uniffi/v0.0.7) — surfaced in PR/release bodies for traceability + description: Source release tag (e.g., livekit-ffi/v0.13.0) — surfaced in PR/release bodies for traceability required: true type: string dry_run: @@ -29,9 +30,9 @@ on: env: CARGO_TERM_COLOR: always HOSTING_REPO: livekit/livekit-uniffi-xcframework - # SPM_NAME must match the value in livekit-uniffi/Makefile.toml [env]. + # SPM_NAME must match the value in livekit-ffi/Makefile.toml [env]. SPM_NAME: LiveKitUniFFI - OUTPUT_DIR: livekit-uniffi/packages/swift/LiveKitUniFFI + OUTPUT_DIR: livekit-ffi/packages/swift/LiveKitUniFFI jobs: build-and-publish: @@ -77,7 +78,7 @@ jobs: aarch64-apple-visionos-sim - name: Build Swift package - working-directory: livekit-uniffi + working-directory: livekit-ffi env: SPM_VERSION: ${{ inputs.version }} SPM_HOSTING_REPO: ${{ env.HOSTING_REPO }} diff --git a/AGENTS.md b/AGENTS.md index 97b61115d..6a69115af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,9 +71,9 @@ When reviewing changed code, search for `Arc::clone`, `.clone()` inside `move` c ## UniFFI integration -Several crates export items to Swift/Kotlin/Node/Python through UniFFI — `livekit-uniffi`, plus `livekit-common`, `livekit-datatrack`, and `livekit-net`, each carrying its own `uniffi.toml`. The Kotlin bindgen has sharp edges that `cargo build`, `cargo test`, and the Swift/Node/Python bindings do **not** catch: they surface only when the generated Kotlin is compiled, and one bad name fails the entire generated file. +Several crates export items to Swift/Kotlin/Python through UniFFI — `livekit-ffi` (its `core-modules` feature), plus `livekit-common`, `livekit-datatrack`, and `livekit-net`, each carrying its own `uniffi.toml`. The Kotlin bindgen has sharp edges that `cargo build`, `cargo test`, and the Swift/Node/Python bindings do **not** catch: they surface only when the generated Kotlin is compiled, and one bad name fails the entire generated file. -- Verify any change to UniFFI-exported API by actually generating and compiling the Kotlin bindings (`cargo make android-package` from `livekit-uniffi/`) — a green `cargo build` proves nothing here +- Verify any change to UniFFI-exported API by actually generating and compiling the Kotlin bindings (`cargo make android-package` from `livekit-ffi/`) — a green `cargo build` proves nothing here - **Never export a method named `close`** - UniFFI gives every object a non-`suspend` `close()` to satisfy `AutoCloseable`. An exported Rust method also called `close` differs from it only by `suspend`, which Kotlin rejects as conflicting overloads — see [mozilla/uniffi-rs#2955](https://github.com/mozilla/uniffi-rs/issues/2955) - Work around it with a Kotlin-only rename, so the Rust source and the Swift/Node/Python bindings keep the original name: @@ -81,17 +81,17 @@ Several crates export items to Swift/Kotlin/Node/Python through UniFFI — `live [bindings.kotlin.rename] "ByteStreamWriter.close" = "close_stream" ``` - - The rename must go in the `uniffi.toml` of the crate that **declares** the item, not in `livekit-uniffi/uniffi.toml`: a rename table only reaches items from the crate that owns it. `WsConnection.close` is renamed in `livekit-net/uniffi.toml` for exactly this reason + - The rename must go in the `uniffi.toml` of the crate that **declares** the item, not in `livekit-ffi/uniffi.toml`: a rename table only reaches items from the crate that owns it. `WsConnection.close` is renamed in `livekit-net/uniffi.toml` for exactly this reason - **Never name a field of an exported enum or record `message`** - For an error variant carrying a `message` field, UniFFI emits a constructor property `message` next to an `override val message` inherited from `Throwable` in one class body, which does not compile — and their types differ (`String` vs `String?`), so they cannot be merged into a single override. See [mozilla/uniffi-rs#2938](https://github.com/mozilla/uniffi-rs/issues/2938), closed without a fix - Unlike `close`, this **cannot** be renamed away: UniFFI keys the rename table by crate name but looks up enum and record members by the item's full module path, so a rename for anything declared in a submodule is silently ignored (method renames use the crate name and do work) - - Name the field `reason` in Rust instead — see `DataStreamError` in `livekit-uniffi/src/data_stream/common.rs` + - Name the field `reason` in Rust instead — see `DataStreamError` in `livekit-ffi/src/core_modules/data_stream/common.rs` - A new crate that exports UniFFI items needs its own `uniffi.toml`, including `omit_checksums = true` under `[bindings.kotlin]` - - The Kotlin checksum test is broken on ARM in every UniFFI release this workspace can use; the full explanation lives in `livekit-uniffi/uniffi.toml` and the root `Cargo.toml` + - The Kotlin checksum test is broken on ARM in every UniFFI release this workspace can use; the full explanation lives in `livekit-ffi/uniffi.toml` and the root `Cargo.toml` - **Never use `uniffi(flat_error)` on an error that foreign code can return** - Flat errors lower (Rust → foreign) but cannot be lifted: the derived `Lift` exists only to satisfy trait bounds, and its `try_read`/`try_lift` are `panic!("Can't lift flat errors")`. In an FFI callback that panic has nowhere to unwind to, so the host process aborts — and nothing catches it earlier, since the build, bindings generation, and Kotlin compilation all pass - Applies to the error type of any `#[uniffi::export(with_foreign)]` trait or callback interface, since foreign code implements the method. `flat_error` is correct only for an error that travels Rust → foreign exclusively, e.g. `PublishError` in `livekit-datatrack/src/local/mod.rs` - - Give a host-thrown error a single `Failed { reason: String }` variant plus `From`, so an undeclared exception surfaces as an error rather than aborting — see `PacketDeliveryError` in `livekit-uniffi/src/data_stream/common.rs` + - Give a host-thrown error a single `Failed { reason: String }` variant plus `From`, so an undeclared exception surfaces as an error rather than aborting — see `PacketDeliveryError` in `livekit-ffi/src/core_modules/data_stream/common.rs` ## Feature combinations @@ -115,7 +115,7 @@ check = [ ] ``` -A crate with no such table is not feature-checked at all. Today eleven crates are `"powerset"`, `livekit` and `livekit-api` are `"curated"`, and `livekit-ffi`/`livekit-uniffi` deliberately declare nothing. +A crate with no such table is not feature-checked at all. Today eleven crates are `"powerset"`, `livekit` and `livekit-api` are `"curated"`, and `livekit-ffi` deliberately declares nothing. - **When adding a workspace crate**, give it `mode = "powerset"` and stop there - Its features are then picked up automatically as they are added, and no CI file changes @@ -138,7 +138,7 @@ A crate with no such table is not feature-checked at all. Today eleven crates ar - The workflow validates the tables before running anything and fails on a typo'd `mode`, a `"curated"` crate with an empty `check`, a `"powerset"` crate carrying a `check` list that would be silently ignored, or either package set resolving to empty. A malformed table fails the job rather than quietly dropping a crate from CI - Keep `--depth 2`. `livekit` alone goes from 67 combinations to 232 at depth 3, and pairwise interactions are where these bugs actually live - A feature that pulls in `openssl-sys` cannot build for the three Android targets — there is no Android OpenSSL to link against. Add it to their `exclude_features` in the workflow matrix rather than trying to make it work; Android ships rustls (see `ffi-builds.yml`). It is honoured by both halves of the job -- `livekit-ffi` and `livekit-uniffi` declare no table on purpose: their combinations cost more than everything else in the job combined, because each TLS change rebuilds `livekit` underneath them. Their shipped configurations are covered by `builds.yml` and `ffi-builds.yml` instead +- `livekit-ffi` declares no table on purpose: its combinations cost more than everything else in the job combined, because each TLS change rebuilds `livekit` underneath it. Its two surfaces are mutually exclusive and cannot be combined anyway; the shipped configurations are declared in `[package.metadata.platform-features]` and covered by `builds.yml`, `ffi-builds.yml` and `uniffi-cdylib.yml` instead ## Documenting changes diff --git a/Cargo.lock b/Cargo.lock index f06645337..202b71d08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -635,7 +635,9 @@ dependencies = [ name = "bindgens" version = "0.1.0" dependencies = [ + "camino", "uniffi", + "uniffi-dart", ] [[package]] @@ -3897,6 +3899,7 @@ dependencies = [ name = "livekit-ffi" version = "0.12.83" dependencies = [ + "async-trait", "bytes", "console-subscriber", "dashmap", @@ -3911,9 +3914,14 @@ dependencies = [ "link-cplusplus", "livekit", "livekit-capture", + "livekit-common", + "livekit-data-stream", + "livekit-datatrack", + "livekit-net", "livekit-protocol", "livekit-token", "log", + "once_cell", "parking_lot", "prost 0.14.4", "prost-build", @@ -3921,6 +3929,7 @@ dependencies = [ "soxr-sys", "thiserror 2.0.19", "tokio", + "tokio-util", "uniffi", "webrtc-sys", "webrtc-sys-build", @@ -4030,30 +4039,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "livekit-uniffi" -version = "0.3.0" -dependencies = [ - "async-trait", - "bytes", - "camino", - "futures-util", - "livekit-common", - "livekit-data-stream", - "livekit-datatrack", - "livekit-net", - "livekit-protocol", - "livekit-token", - "log", - "once_cell", - "prost 0.14.4", - "thiserror 2.0.19", - "tokio", - "tokio-util", - "uniffi", - "uniffi-dart", -] - [[package]] name = "livekit-wakeword" version = "0.1.3" @@ -6493,7 +6478,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7217,7 +7202,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", diff --git a/Cargo.toml b/Cargo.toml index fbf37434c..c3fad36ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ members = [ "livekit-data-stream", "livekit-rpc", "livekit-ffi", - "livekit-uniffi", "livekit-datatrack", "livekit-token", "livekit-token-source", @@ -105,7 +104,7 @@ tokio-stream = "0.1" # build here, because uniffi-dart requires 0.31.2 or later. mozilla/uniffi-rs#2935 corrects both # defects, but no release (as of mid august 2026) contains that change. # -# For this reason, livekit-uniffi sets `omit_checksums` for Kotlin. See livekit-uniffi/uniffi.toml. +# For this reason, livekit-ffi sets `omit_checksums` for Kotlin. See livekit-ffi/uniffi.toml. uniffi = "0.31" # For examples diff --git a/README.md b/README.md index 249030d56..48acc1d41 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Use this SDK to add realtime video, audio and data features to your Rust app. By - `livekit-api`: Server APIs and auth token generation - `livekit`: LiveKit real-time SDK -- `livekit-ffi`: Internal crate, used to generate bindings for other languages +- `livekit-ffi`: Internal crate, used to generate bindings for other languages. Its `room-apis` feature (default) serves the Python/Node/Unity SDKs; its `core-modules` feature serves the Swift/Android/Flutter SDKs over UniFFI - `livekit-protocol`: LiveKit protocol generated code When adding the SDK as a dependency to your project, make sure to add the diff --git a/knope.toml b/knope.toml index bf337b338..c9833ce31 100644 --- a/knope.toml +++ b/knope.toml @@ -145,22 +145,7 @@ versioned_files = [ changelog = "livekit-ffi/CHANGELOG.md" # Assets makes knope create a draft release; CI uploads the actual binaries and publishes. assets = "marker" -scopes = ["ffi", "livekit-ffi"] - -[packages.livekit-uniffi] -# Not in workspace.dependencies (not consumed by other crates here), so no -# root Cargo.toml entry is needed. -versioned_files = [ - "livekit-uniffi/Cargo.toml", - "Cargo.lock", -] -changelog = "livekit-uniffi/CHANGELOG.md" -# No `assets` marker: knope publishes the release + tag directly, and -# uniffi-packages.yml reacts to the published release to build/publish the Swift -# and Android wrapper packages. The Dart/Flutter cdylibs are attached to the -# already-published release by its cdylib job (gh release upload --clobber), so -# the marker stays off; a draft-based flow stranded releases before (#1256). -scopes = ["uniffi", "livekit-uniffi"] +scopes = ["ffi", "livekit-ffi", "uniffi", "livekit-uniffi"] [packages.livekit-wakeword] versioned_files = [ diff --git a/livekit-common/Cargo.toml b/livekit-common/Cargo.toml index eb6f2a78f..9716b722d 100644 --- a/livekit-common/Cargo.toml +++ b/livekit-common/Cargo.toml @@ -14,7 +14,7 @@ uniffi = { workspace = true, features = ["scaffolding-ffi-buffer-fns"], optional [features] # Exposes this crate's shared FFI type registrations. Enabled transitively by -# livekit-uniffi so every component borrows one converter per type. +# livekit-ffi so every component borrows one converter per type. uniffi = ["dep:uniffi", "dep:bytes"] # How CI checks this crate's features, read by diff --git a/livekit-common/uniffi.toml b/livekit-common/uniffi.toml index a516eec17..0b0ffc8b1 100644 --- a/livekit-common/uniffi.toml +++ b/livekit-common/uniffi.toml @@ -1,4 +1,4 @@ [bindings.swift] # Matches the Rust convention used by the other components; the cargo-swift -# fork folds every component's header into the single RustLiveKitUniFFI framework. +# fork folds every component's header into the single RustLiveKitFFI framework. ffi_module_name = "RustLiveKitCommon" diff --git a/livekit-data-stream/AGENTS.md b/livekit-data-stream/AGENTS.md index 96685da44..7340e01ae 100644 --- a/livekit-data-stream/AGENTS.md +++ b/livekit-data-stream/AGENTS.md @@ -9,7 +9,7 @@ - Enables proper integration testing - Enables shared implementation amongst multiple _consumers_: - [`livekit`](../livekit/): Rust client SDK - - [`livekit-uniffi`](../livekit-uniffi/): will eventually power downstream client SDKs such as Swift and Kotlin + - [`livekit-ffi`](../livekit-ffi/) (`core-modules` feature): will eventually power downstream client SDKs such as Swift and Kotlin ## Incoming vs. outgoing split diff --git a/livekit-datatrack/AGENTS.md b/livekit-datatrack/AGENTS.md index e918f0b7c..6a60215c6 100644 --- a/livekit-datatrack/AGENTS.md +++ b/livekit-datatrack/AGENTS.md @@ -9,7 +9,7 @@ - Enables proper integration testing - Enables shared implementation amongst multiple _consumers_: - [`livekit`](../livekit/): Rust client SDK - - [`livekit-uniffi`](../livekit-uniffi/): will eventually power downstream client SDKs such as Swift and Kotlin + - [`livekit-ffi`](../livekit-ffi/) (`core-modules` feature): will eventually power downstream client SDKs such as Swift and Kotlin ## Local vs. remote split diff --git a/livekit-ffi/.gitignore b/livekit-ffi/.gitignore index 7c18ec7e5..dfb0ef9b3 100644 --- a/livekit-ffi/.gitignore +++ b/livekit-ffi/.gitignore @@ -1 +1,3 @@ -WEBRTC_LICENSE.md \ No newline at end of file +WEBRTC_LICENSE.md +generated/ +packages/ diff --git a/livekit-uniffi/AGENTS.md b/livekit-ffi/AGENTS.md similarity index 100% rename from livekit-uniffi/AGENTS.md rename to livekit-ffi/AGENTS.md diff --git a/livekit-ffi/Cargo.toml b/livekit-ffi/Cargo.toml index 532c07b01..04dfe6457 100644 --- a/livekit-ffi/Cargo.toml +++ b/livekit-ffi/Cargo.toml @@ -9,45 +9,112 @@ readme = "README.md" publish = false [features] -default = ["rustls-tls-native-roots"] -native-tls = ["livekit/native-tls"] -native-tls-vendored = ["livekit/native-tls-vendored"] -rustls-tls-native-roots = ["livekit/rustls-tls-native-roots"] -rustls-tls-webpki-roots = ["livekit/rustls-tls-webpki-roots"] -__rustls-tls = ["livekit/__rustls-tls"] +default = ["room-apis", "rustls-tls-native-roots"] + +# There are two mutually exclusive modes that this crate can be configured +# in, `room-apis` and `core-modules`. + +# `room-apis` implements a full, rust owned and managed webrtc stack. This is used by more modern +# client sdks like python, node, unity, and c++. +room-apis = [ + "dep:livekit", + "dep:webrtc-sys", + "dep:soxr-sys", + "dep:imgproc", + "dep:prost", + "dep:dashmap", + "dep:downcast-rs", + "dep:env_logger", + "dep:from_variants", + "dep:lazy_static", + "dep:parking_lot", + "dep:prost-build", + "dep:webrtc-sys-build", + "tokio/full", + "tokio/parking_lot", +] + +# `core-modules` implements a set of smaller utility modules in rust to abstract away business +# logic. This does NOT include `libwebrtc` or `livekit`. +# +# This mode is used by older client sdks which historically were fully implemented in each +# downstream language and is a transitional step to migrating eventually someday to the full +# room-apis mode - swift, android, flutter, etc. +core-modules = [ + "dep:livekit-common", + "dep:livekit-data-stream", + "dep:livekit-datatrack", + "dep:livekit-net", + "dep:livekit-token", + "dep:prost", + "dep:async-trait", + "dep:once_cell", + "dep:tokio-util", + "livekit-common/uniffi", + "livekit-datatrack/uniffi", + "livekit-net/uniffi", + "uniffi/tokio", +] + +# TLS selection. These imply `room-apis` because they only configure `livekit`: +# .github/workflows/ffi-builds.yml builds several targets with +# `--no-default-features --features ""`, which would otherwise select +# neither surface and produce an empty crate. +native-tls = ["room-apis", "livekit/native-tls"] +native-tls-vendored = ["room-apis", "livekit/native-tls-vendored"] +rustls-tls-native-roots = ["room-apis", "livekit/rustls-tls-native-roots"] +rustls-tls-webpki-roots = ["room-apis", "livekit/rustls-tls-webpki-roots"] +__rustls-tls = ["room-apis", "livekit/__rustls-tls"] # Enable tokio-console to debug tasks -tracing = ["tokio/tracing", "console-subscriber"] +tracing = ["room-apis", "tokio/tracing", "dep:console-subscriber"] # Capture sources (livekit-capture): publish tracks from server-side # producers. This enables the capture scaffolding only; enable a # `capture-*` feature to include a specific source. -capture = ["dep:livekit-capture"] +capture = ["room-apis", "dep:livekit-capture"] capture-clock = ["capture", "livekit-capture/source-clock"] capture-gstreamer = ["capture", "livekit-capture/source-gstreamer"] capture-pattern = ["capture", "livekit-capture/source-pattern"] [dependencies] -livekit = { workspace = true } -livekit-capture = { workspace = true, optional = true, default-features = false } -webrtc-sys = { workspace = true } -soxr-sys = { workspace = true } -imgproc = { workspace = true } +# Shared by both surfaces. livekit-protocol = { workspace = true } -tokio = { workspace = true, features = ["full", "parking_lot"] } +uniffi = { workspace = true, features = ["scaffolding-ffi-buffer-fns", "tokio"] } +tokio = { workspace = true, features = ["sync", "rt-multi-thread"] } futures-util = { workspace = true, default-features = false, features = ["sink"] } -parking_lot = { workspace = true, features = ["deadlock_detection"] } -prost = { workspace = true } -lazy_static = { workspace = true } -thiserror = { workspace = true } +bytes = { workspace = true } log = { workspace = true } -dashmap = "6.2" -env_logger = { workspace = true } -downcast-rs = "1.2" +thiserror = { workspace = true } + +# room-apis only. Each of these is referenced solely from the `room_apis` module +# tree, so gating them costs no source changes. +livekit = { workspace = true, optional = true } +livekit-capture = { workspace = true, optional = true, default-features = false } + +# core-modules only. +livekit-common = { workspace = true, optional = true } +livekit-token = { workspace = true, optional = true } +livekit-datatrack = { workspace = true, optional = true } +livekit-net = { workspace = true, optional = true } +livekit-data-stream = { workspace = true, optional = true } +tokio-util = { version = "0.7.18", optional = true } +once_cell = { version = "1.21.3", optional = true } +async-trait = { version = "0.1", optional = true } + +# Referenced from no source file at all: webrtc-sys exists to link static +# libwebrtc, which build.rs wires up under the same feature. +webrtc-sys = { workspace = true, optional = true } +soxr-sys = { workspace = true, optional = true } +imgproc = { workspace = true, optional = true } +prost = { workspace = true, optional = true } +parking_lot = { workspace = true, features = ["deadlock_detection"], optional = true } +lazy_static = { workspace = true, optional = true } +env_logger = { workspace = true, optional = true } +dashmap = { version = "6.2", optional = true } +downcast-rs = { version = "1.2", optional = true } +from_variants = { version = "1.0.2", optional = true } console-subscriber = { workspace = true, features = ["parking_lot"], optional = true } -bytes = { workspace = true } -from_variants = "1.0.2" -uniffi = { workspace = true, features = ["scaffolding-ffi-buffer-fns", "tokio"] } [target.'cfg(target_os = "android")'.dependencies] jni = "0.21.1" @@ -55,12 +122,43 @@ link-cplusplus = { version = "1", features = ["nothing"] } device-info = { workspace = true } [build-dependencies] -prost-build = { workspace = true } -webrtc-sys-build = { workspace = true } +prost-build = { workspace = true, optional = true } +webrtc-sys-build = { workspace = true, optional = true } [dev-dependencies] livekit-token = { workspace = true } serial_test = "3.0" +# Which FFI mode each downstream package build is configured to use: room-apis or core-modules. +# +# This is the single source of truth: CI and Makefile.toml resolve their build +# flags from here via .github/scripts/ffi_features.py, so a platform +# cannot silently drift onto the wrong surface. Every entry turns the default +# features off and lists the whole set it wants, so a change to `default` +# cannot move a platform onto another surface. +# +# $ .github/scripts/ffi_features.py --list +# +# Every build site that produces an artifact names its platform and takes its +# flags from the row below, so adding a bindgen or a package means adding a +# row. The per-commit gates in builds.yml and tests.yml build no artifact for +# any platform and state their flags directly. +# +[package.metadata.platform-features] +# room-apis -- SDKs with no WebRTC stack of their own. These consume the C ABI +# (and, increasingly, UniFFI) out of the livekit-ffi release archives. +python = { no-default-features = true, features = ["room-apis", "rustls-tls-native-roots"] } +node = { no-default-features = true, features = ["room-apis", "rustls-tls-native-roots"] } +unity = { no-default-features = true, features = ["room-apis", "rustls-tls-native-roots"] } +ffi-ios = { no-default-features = true, features = ["room-apis", "rustls-tls-webpki-roots"] } +ffi-android = { no-default-features = true, features = ["room-apis", "rustls-tls-webpki-roots"] } +ffi-windows-arm64 = { no-default-features = true, features = ["room-apis", "native-tls"] } + +# core-modules -- SDKs that already include their own WebRTC stack and consume +# this crate purely over UniFFI. +swift = { no-default-features = true, features = ["core-modules"] } +android = { no-default-features = true, features = ["core-modules"] } +dart = { no-default-features = true, features = ["core-modules"] } + [lib] crate-type = ["lib", "staticlib", "cdylib"] diff --git a/livekit-uniffi/Makefile.toml b/livekit-ffi/Makefile.toml similarity index 88% rename from livekit-uniffi/Makefile.toml rename to livekit-ffi/Makefile.toml index 9e368c3ef..ef24baf1f 100644 --- a/livekit-uniffi/Makefile.toml +++ b/livekit-ffi/Makefile.toml @@ -18,7 +18,6 @@ SUPPORT_DIR = "./support" LIB_NAME = "lib${CARGO_MAKE_CRATE_FS_NAME}" TARGET_DIR = "${CARGO_MAKE_CRATE_TARGET_DIRECTORY}" SPM_NAME = "LiveKitUniFFI" -NPM_NAME = "@livekit/uniffi" DART_PACKAGE_NAME = "livekit_uniffi" # Rendered into pubspec.yaml by dart-generate-manifest. Fail closed: every # profile gets `publish_to: none` (dev packages embed a host dylib and must @@ -29,7 +28,17 @@ PUBSPEC_PUBLISH_TO = "none" # Per-release directory holding the cdylib archives published by # .github/workflows/uniffi-cdylib.yml. Downloaders append # `/v/build-.zip` (and a `.sha256` sidecar). -CDYLIB_DOWNLOAD_BASE_URL = "https://github.com/livekit/rust-sdks/releases/download/livekit-uniffi" +CDYLIB_DOWNLOAD_BASE_URL = "https://github.com/livekit/rust-sdks/releases/download/livekit-ffi" + +# Every cargo invocation below relies on the working directory rather than -p, +# so each one must say which feature set to build. Each task names a platform +# in LK_PLATFORM, and the build resolves its flags from the +# [package.metadata.platform-features] table in Cargo.toml. A new bindgen thus +# needs a row in that table and nothing else. +# +# LK_PLATFORM has to be set on a task that drives the work through `run_task`, +# not on one that `extend`s another: a task's own env is not in scope while its +# dependencies run, and `build` is a dependency. [env.release] PUBSPEC_PUBLISH_TO = "" @@ -50,14 +59,19 @@ script_runner = "@duckscript" script = """ echo "TARGET: ${TARGET}" +echo "PLATFORM: ${LK_PLATFORM}" + +# `set -e` matters: if the resolver fails, the assignment fails and the shell +# stops. Without it the flags would be empty and cargo would quietly build the +# default feature set, which is the wrong surface for most callers. if not ${TIER_3} # Ensure target is installed exec rustup target add ${TARGET} - exec cargo build --release --target ${TARGET} + exec sh -c "set -e; FLAGS=$(python3 ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/.github/scripts/ffi_features.py ${LK_PLATFORM}); cargo build --release $FLAGS --target ${TARGET}" else # Ensure nightly toolchain is installed exec rustup component add rust-src --toolchain "nightly-${CARGO_MAKE_RUST_TARGET_TRIPLE}" - exec cargo +nightly build -Zbuild-std=std,panic_abort --release --target ${TARGET} + exec sh -c "set -e; FLAGS=$(python3 ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/.github/scripts/ffi_features.py ${LK_PLATFORM}); cargo +nightly build -Zbuild-std=std,panic_abort --release $FLAGS --target ${TARGET}" end """ @@ -87,26 +101,29 @@ if [ "${CARGO_MAKE_PROFILE}" = "release" ]; then export RUSTFLAGS="-C force-unwind-tables=no" fi -cargo ndk --target "${TARGET}" build ${release_flag} +# Named here and not in env for the same reason as the profile switch above. +FLAGS=$(python3 "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/.github/scripts/ffi_features.py" android) + +cargo ndk --target "${TARGET}" build ${FLAGS} ${release_flag} """ # MARK: - Build, specific platforms [tasks.build-windows-x64] extend = "build" -env = { TARGET = "x86_64-pc-windows-msvc" } +env = { TARGET = "x86_64-pc-windows-msvc", LK_PLATFORM = "dart" } [tasks.build-windows-aarch64] extend = "build" -env = { TARGET = "aarch64-pc-windows-msvc" } +env = { TARGET = "aarch64-pc-windows-msvc", LK_PLATFORM = "dart" } [tasks.build-linux-aarch64] extend = "build" -env = { TARGET = "aarch64-unknown-linux-gnu" } +env = { TARGET = "aarch64-unknown-linux-gnu", LK_PLATFORM = "dart" } [tasks.build-linux-x64] extend = "build" -env = { TARGET = "x86_64-unknown-linux-gnu" } +env = { TARGET = "x86_64-unknown-linux-gnu", LK_PLATFORM = "dart" } [tasks.build-android-aarch64] private = false @@ -168,9 +185,7 @@ dependencies = ["build", "locate-lib"] script_runner = "@duckscript" script = """ out_dir = join_path ${PACKAGES_DIR} ${LANG} -# `--features cli` builds the bindgen bin (gated off library builds so the -# shipped cdylib/staticlib don't carry uniffi's CLI machinery). -exec cargo run --features cli --bin uniffi-bindgen generate \ +exec cargo run -p bindgens --bin uniffi-bindgen -- generate \ --language ${LANG} \ --out-dir ${out_dir} \ --library ${LIB_PATH} @@ -197,99 +212,15 @@ dependencies = [ [tasks.bindgen-kotlin] private = false -extend = "uniffi-bindgen" -env = { LANG = "kotlin" } +env = { LANG = "kotlin", LK_PLATFORM = "android" } +run_task = "uniffi-bindgen" # MARK: - Python [tasks.bindgen-python] private = false -extend = "uniffi-bindgen" -env = { LANG = "python" } - -# MARK: - Node - -[tasks.node-bindgen] -private = true -dependencies = ["build", "locate-lib"] -env = { CARGO_NET_GIT_FETCH_WITH_CLI = true } -install_crate = { crate_name = "uniffi-bindgen-node", binary = "uniffi-bindgen-node", test_arg = "--help" } -install_crate_args = ["--git", "https://github.com/livekit/uniffi-bindgen-node.git"] -command = "uniffi-bindgen-node" -args = [ - "--out-dir", "${PACKAGES_DIR}/${LANG}", - "--out-dirname-api", "import-meta-url", - "--out-node-version", "^18", - "--out-verbose-logs", - "--crate-name", "${CARGO_MAKE_CRATE_FS_NAME}", - "${LIB_PATH}" -] - -[tasks.node-format] -private = true -command = "npx" -args = [ - "--yes", - "prettier", - "--write", "${PACKAGES_DIR}/${LANG}" -] - -[tasks.node-move-code-into-src] -private = true -script_runner = "@shell" -script = """ -mkdir -p ${PACKAGES_DIR}/${LANG}/src -mv ${PACKAGES_DIR}/${LANG}/*.ts ${PACKAGES_DIR}/${LANG}/src -""" - -[tasks.node-generate-manifest] -private = true -extend = "tera" -args = [ - "--env", - "-t", "${SUPPORT_DIR}/${LANG}/package.json.tera", - "-o", "${PACKAGES_DIR}/${LANG}/package.json", - "${PACKAGES_DIR}/${LANG}/package.json" - # Provide original package.json as input -] - -[tasks.node-copy-support-files] -private = true -script_runner = "@shell" -script = """ -cp ${SUPPORT_DIR}/${LANG}/tsconfig.json ${PACKAGES_DIR}/${LANG}/tsconfig.json -cp ${SUPPORT_DIR}/${LANG}/tsup.config.ts ${PACKAGES_DIR}/${LANG}/tsup.config.ts -""" - -[tasks.node-generate-downloader] -private = true -condition = { profiles = ["release"] } -extend = "tera" -args = ["--env-only", "-t", "${SUPPORT_DIR}/${LANG}/download-lib.ts.tera", "-o", "${PACKAGES_DIR}/${LANG}/download-lib.ts"] - -[tasks.node-link-lib] -private = true -condition = { profiles = ["development"] } -script_runner = "@shell" -script = """ -ln -sf ${LIB_PATH} ${PACKAGES_DIR}/${LANG}/src -""" - -[tasks.node-package-flow] -private = true -dependencies = [ - "node-bindgen", - "node-move-code-into-src", - "node-generate-manifest", - "node-generate-downloader", - "node-copy-support-files", - "node-format", - "node-link-lib" -] - -[tasks.node-package] -env = { LANG = "node" } -run_task = "node-package-flow" +env = { LANG = "python", LK_PLATFORM = "python" } +run_task = "uniffi-bindgen" # MARK: - Swift # @@ -352,8 +283,11 @@ for p in ${SPM_PLATFORMS}; do platform_flags="$platform_flags --platforms $p" done +FLAGS=$(python3 "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/.github/scripts/ffi_features.py" "${LK_PLATFORM}") + cargo swift package \ --accept-all \ +${FLAGS} \ --name ${SPM_NAME} \ $platform_flags \ --lib-type dynamic \ @@ -520,7 +454,7 @@ dependencies = [ [tasks.swift-package] description = "Generate Swift package using cargo-swift" -env = { LANG = "swift" } +env = { LANG = "swift", LK_PLATFORM = "swift" } run_task = "swift-package-flow" # Same flow as `swift-package`, restricted to macos by default — a debug @@ -534,6 +468,7 @@ run_task = "swift-package-flow" [tasks.swift-package-debug.env] LANG = "swift" +LK_PLATFORM = "swift" SPM_PLATFORMS = { value = "macos", condition = { env_not_set = ["SPM_PLATFORMS"] } } [tasks.swift-package-debug] @@ -642,7 +577,7 @@ echo "AAR: ${out_dir}/${ANDROID_AAR_BASENAME}.aar" # only applies once its dependencies run, hence run_task. [tasks.android-bindgen-kotlin] private = true -env = { TARGET = "${CARGO_MAKE_RUST_TARGET_TRIPLE}", CARGO_PROFILE_RELEASE_STRIP = "false" } +env = { TARGET = "${CARGO_MAKE_RUST_TARGET_TRIPLE}", LK_PLATFORM = "android", CARGO_PROFILE_RELEASE_STRIP = "false" } run_task = "bindgen-kotlin" [tasks.android-package-flow] @@ -678,9 +613,6 @@ description = "Build Android AAR with native libs and Kotlin bindings; publish t dependencies = ["android-package-flow"] run_task = "android-publish-maven-local" -# Add other bindgen tasks here, following the same pattern -# as above for the task name (e.g., "bindgen-"). - # MARK: - Dart # uniffi-dart has no standalone CLI, so the `uniffi-bindgen-dart` bin drives its @@ -695,7 +627,7 @@ description = "Generate Dart bindings" # The env must be in scope while the `build` dependency compiles, but a task's # own env only applies after its dependencies have run; so set it on this # wrapper and drive the work through run_task (same pattern as dart-package). -env = { LANG = "dart", CARGO_PROFILE_RELEASE_STRIP = "false" } +env = { LANG = "dart", LK_PLATFORM = "dart", CARGO_PROFILE_RELEASE_STRIP = "false" } run_task = "bindgen-dart-flow" [tasks.bindgen-dart-flow] @@ -704,7 +636,7 @@ dependencies = ["build", "locate-lib"] script_runner = "@duckscript" script = """ out_dir = join_path ${PACKAGES_DIR} ${LANG} -exec cargo run --features dart-bindgen --bin uniffi-bindgen-dart -- \ +exec cargo run -p bindgens --features dart-bindgen --bin uniffi-bindgen-dart -- \ --library ${LIB_PATH} \ --out-dir ${out_dir} """ @@ -814,5 +746,8 @@ dependencies = [ [tasks.dart-package] description = "Build a consumable Dart package with bindings, hook, and (dev) native lib" # See bindgen-dart: keep symbols so library-mode bindgen can read metadata. -env = { LANG = "dart", CARGO_PROFILE_RELEASE_STRIP = "false" } +env = { LANG = "dart", LK_PLATFORM = "dart", CARGO_PROFILE_RELEASE_STRIP = "false" } run_task = "dart-package-flow" + +# Add other bindgen tasks here, following the same pattern +# as above for the task name (e.g., "bindgen-"). diff --git a/livekit-ffi/README.md b/livekit-ffi/README.md index fd4d98b58..edd0bb18c 100644 --- a/livekit-ffi/README.md +++ b/livekit-ffi/README.md @@ -1,9 +1,112 @@ # LiveKit FFI -Foreign function interface (FFI) bindings for LiveKit, used to support the following client SDKs: +Foreign function interface (FFI) bindings for LiveKit. The crate exposes two +**mutually exclusive** surfaces, selected by cargo feature for each sdk build +based on that given sdk's implementation. + +## `room-apis` (default) + +The protobuf / C-ABI room surface, plus a growing UniFFI one. Used by the SDKs +that have no WebRTC stack of their own, so this build embeds the full +[_livekit_](https://crates.io/crates/livekit) + libwebrtc stack: - [Python](https://github.com/livekit/python-sdks) - [NodeJS](https://github.com/livekit/node-sdks) - [Unity](https://github.com/livekit/client-sdk-unity) -This crate is compiled as dynamic library, allowing client languages to invoke APIs from the core [_livekit_](https://crates.io/crates/livekit) crate. +```sh +cargo build -p livekit-ffi --no-default-features --features 'room-apis rustls-tls-native-roots' +# or: cargo build -p livekit-ffi +``` + +Mode specific source lives in `src/room_apis/`. + +## `core-modules` + +Core business logic exposed purely over +[UniFFI](https://mozilla.github.io/uniffi-rs/latest/): access tokens, log +forwarding, data tracks and data streams v2. Used by the SDKs that already ship +their own WebRTC stack. This build therefore depends on none of `livekit`, +`libwebrtc`, `soxr-sys` or `imgproc`, so an application does not ship two +WebRTC stacks: + +- [Swift](https://github.com/livekit/client-sdk-swift) +- [Android](https://github.com/livekit/client-sdk-android) +- [Flutter](https://github.com/livekit/client-sdk-flutter) + +```sh +cargo build -p livekit-ffi --no-default-features --features core-modules +``` + +Mode specific source lives in `src/core_modules/`. + +## Which platform gets which + +`[package.metadata.platform-features]` in [Cargo.toml](./Cargo.toml) is the +single source of truth, and CI resolves its build flags from it: + +```sh +.github/scripts/ffi_features.py --list +``` + +## Things that bite + +- **Never enable both features.** Each pins its own UniFFI namespace via + `setup_scaffolding!`, and a crate may declare only one. A `compile_error!` in + `src/lib.rs` enforces this; another rejects selecting neither. +- **The cargo-make tasks below take no `-p`** and rely on the working + directory, so they need explicit feature flags or they build the default + livekit-ffi/room-apis and all of libwebrtc. `Makefile.toml` resolves those from + the table above — do not write them out by hand. +- **`build.rs` is gated on `room-apis`.** It downloads libwebrtc, configures the + linker, and panics on any target it does not recognise — visionOS, tvOS and + Mac Catalyst among them. That gate is what makes the core-modules Apple + builds possible. +- **Verify UniFFI API changes by compiling the Kotlin bindings** + (`cargo make android-package`). A green `cargo build` proves nothing there; + see the UniFFI section of the root [AGENTS.md](../AGENTS.md) for the specific + naming traps. + +## Tasks + +Binding generation and multi-platform builds are handled by +[_cargo-make_](https://github.com/sagiegurari/cargo-make) — please install it +before proceeding. For the full list see [_Makefile.toml_](./Makefile.toml) or +run `cargo make --list-all-steps`. The most important are summarized below. + +### Swift + +Generate Swift bindings and build a multi-platform XCFramework: + +``` +cargo make swift-package +``` + +For a fast, debuggable variant (macOS-only, unstripped), run +`cargo make swift-package-debug` — see +[support/swift/DEBUGGING.md](./support/swift/DEBUGGING.md). + +See [support/swift/README.md](./support/swift/README.md) for debugging vs. +release modes, consumer integration, and prerequisites (Xcode, Rust Apple +targets). + +### Android + +Build native libraries, Kotlin bindings, and a release AAR: + +``` +cargo make android-package # debug .so in release AAR +cargo make --profile release android-package # release .so (CI / publishing) +cargo make android-package-local # + publish to Maven Local +``` + +See [support/android/README.md](./support/android/README.md) for prerequisites +(Android SDK/NDK). + +### Dart + +``` +cargo make dart-package +``` + +See [support/dart/README.md](./support/dart/README.md). diff --git a/livekit-ffi/build.rs b/livekit-ffi/build.rs index 29158f322..849f44447 100644 --- a/livekit-ffi/build.rs +++ b/livekit-ffi/build.rs @@ -12,93 +12,113 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{env, path::Path}; - -const PROTO_SRC_DIR: &str = "protocol"; +use std::env; fn main() { if env::var("DOCS_RS").is_ok() { return; } - download_webrtc(); - copy_webrtc_license(); - configure_linker(); - generate_protobuf(); + // livekit-ffi/core-modules is pure Rust: no libwebrtc download, no linker + // configuration (`configure_linker` panics on visionOS/tvOS/Mac Catalyst, + // which the Swift package builds) and no protobuf generation. + // + // The whole module is gated rather than each item in it, and it has to be a + // `cfg` rather than a `CARGO_FEATURE_ROOM_APIS` lookup: `prost-build` and + // `webrtc-sys-build` are optional build-dependencies gated on the same + // feature, so under core-modules the paths that this code names do not + // resolve at all. + #[cfg(feature = "room-apis")] + room_apis::run_build_steps(); } -fn download_webrtc() { - let webrtc_dir = webrtc_sys_build::webrtc_dir(); - if !webrtc_dir.exists() { - webrtc_sys_build::download_webrtc().unwrap(); - } -} +/// The build steps that only the room-apis surface needs. +#[cfg(feature = "room-apis")] +mod room_apis { + use std::{env, path::Path}; -/// Copy the webrtc license to `CARGO_MANIFEST_DIR`, used by the FFI release action. -fn copy_webrtc_license() { - let webrtc_dir = webrtc_sys_build::webrtc_dir(); - let license = webrtc_dir.join("LICENSE.md"); - let target_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - let out_file = Path::new(&target_dir).join("WEBRTC_LICENSE.md"); - std::fs::copy(license, out_file).unwrap(); -} + const PROTO_SRC_DIR: &str = "protocol"; -fn configure_linker() { - let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); - match target_os.as_str() { - "windows" => {} - "linux" => { - println!("cargo:rustc-link-lib=static=webrtc"); - } - "android" => { - webrtc_sys_build::configure_jni_symbols().unwrap(); - } - "macos" | "ios" => { - println!("cargo:rustc-link-arg=-ObjC"); - } - _ => { - panic!("Unsupported target, {}", target_os); + pub fn run_build_steps() { + download_webrtc(); + copy_webrtc_license(); + configure_linker(); + generate_protobuf(); + } + + fn download_webrtc() { + let webrtc_dir = webrtc_sys_build::webrtc_dir(); + if !webrtc_dir.exists() { + webrtc_sys_build::download_webrtc().unwrap(); } } -} -fn generate_protobuf() { - let paths: Vec<_> = std::fs::read_dir(PROTO_SRC_DIR) - .expect("Failed to read protobuf source directory") - .map(|entry| entry.unwrap().path()) - .filter(|path| path.extension().is_some_and(|ext| ext == "proto")) - .collect(); - for path in &paths { - println!("cargo:rerun-if-changed={}", path.display()); + /// Copy the webrtc license to `CARGO_MANIFEST_DIR`, used by the FFI release action. + fn copy_webrtc_license() { + let webrtc_dir = webrtc_sys_build::webrtc_dir(); + let license = webrtc_dir.join("LICENSE.md"); + let target_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let out_file = Path::new(&target_dir).join("WEBRTC_LICENSE.md"); + std::fs::copy(license, out_file).unwrap(); } - prost_build::Config::new() - .derive_from_variants("livekit.proto.FfiRequest.message") - .derive_from_variants("livekit.proto.FfiResponse.message") - .derive_from_variants("livekit.proto.FfiEvent.message") - .derive_from_variants("livekit.proto.RoomEvent.message") - .from_variants_skip_field("livekit.proto.RoomEvent.message.room_updated") - .from_variants_skip_field("livekit.proto.RoomEvent.message.moved") - .derive_from_variants("livekit.proto.AudioStreamEvent.message") - .derive_from_variants("livekit.proto.TextStreamReaderEvent.detail") - .derive_from_variants("livekit.proto.ByteStreamReaderEvent.detail") - .derive_from_variants("livekit.proto.DataTrackStreamEvent.detail") - .compile_protos(&paths, &[PROTO_SRC_DIR]) - .expect("Protobuf generation failed"); -} -trait ProstConfigExt { - /// Derive [`from_variants::FromVariants`] on a oneof field's generated enum. - fn derive_from_variants(&mut self, path: impl AsRef) -> &mut Self; + fn configure_linker() { + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); + match target_os.as_str() { + "windows" => {} + "linux" => { + println!("cargo:rustc-link-lib=static=webrtc"); + } + "android" => { + webrtc_sys_build::configure_jni_symbols().unwrap(); + } + "macos" | "ios" => { + println!("cargo:rustc-link-arg=-ObjC"); + } + _ => { + panic!("Unsupported target, {}", target_os); + } + } + } - /// When using [`derive_from_variants`], skip a particular case. This is necessary - /// for oneofs that contain multiple cases with the same message type. - fn from_variants_skip_field(&mut self, path: impl AsRef) -> &mut Self; -} + fn generate_protobuf() { + let paths: Vec<_> = std::fs::read_dir(PROTO_SRC_DIR) + .expect("Failed to read protobuf source directory") + .map(|entry| entry.unwrap().path()) + .filter(|path| path.extension().is_some_and(|ext| ext == "proto")) + .collect(); + for path in &paths { + println!("cargo:rerun-if-changed={}", path.display()); + } + prost_build::Config::new() + .derive_from_variants("livekit.proto.FfiRequest.message") + .derive_from_variants("livekit.proto.FfiResponse.message") + .derive_from_variants("livekit.proto.FfiEvent.message") + .derive_from_variants("livekit.proto.RoomEvent.message") + .from_variants_skip_field("livekit.proto.RoomEvent.message.room_updated") + .from_variants_skip_field("livekit.proto.RoomEvent.message.moved") + .derive_from_variants("livekit.proto.AudioStreamEvent.message") + .derive_from_variants("livekit.proto.TextStreamReaderEvent.detail") + .derive_from_variants("livekit.proto.ByteStreamReaderEvent.detail") + .derive_from_variants("livekit.proto.DataTrackStreamEvent.detail") + .compile_protos(&paths, &[PROTO_SRC_DIR]) + .expect("Protobuf generation failed"); + } + + trait ProstConfigExt { + /// Derive [`from_variants::FromVariants`] on a oneof field's generated enum. + fn derive_from_variants(&mut self, path: impl AsRef) -> &mut Self; -impl ProstConfigExt for prost_build::Config { - fn derive_from_variants(&mut self, path: impl AsRef) -> &mut Self { - self.enum_attribute(path, "#[derive(from_variants::FromVariants)]") + /// When using [`derive_from_variants`], skip a particular case. This is necessary + /// for oneofs that contain multiple cases with the same message type. + fn from_variants_skip_field(&mut self, path: impl AsRef) -> &mut Self; } - fn from_variants_skip_field(&mut self, path: impl AsRef) -> &mut Self { - self.field_attribute(path, "#[from_variants(skip)]") + + impl ProstConfigExt for prost_build::Config { + fn derive_from_variants(&mut self, path: impl AsRef) -> &mut Self { + self.enum_attribute(path, "#[derive(from_variants::FromVariants)]") + } + fn from_variants_skip_field(&mut self, path: impl AsRef) -> &mut Self { + self.field_attribute(path, "#[from_variants(skip)]") + } } } diff --git a/livekit-uniffi/src/access_token.rs b/livekit-ffi/src/core_modules/access_token.rs similarity index 100% rename from livekit-uniffi/src/access_token.rs rename to livekit-ffi/src/core_modules/access_token.rs diff --git a/livekit-uniffi/src/common.rs b/livekit-ffi/src/core_modules/common.rs similarity index 100% rename from livekit-uniffi/src/common.rs rename to livekit-ffi/src/core_modules/common.rs diff --git a/livekit-uniffi/src/data_stream/common.rs b/livekit-ffi/src/core_modules/data_stream/common.rs similarity index 100% rename from livekit-uniffi/src/data_stream/common.rs rename to livekit-ffi/src/core_modules/data_stream/common.rs diff --git a/livekit-uniffi/src/data_stream/incoming.rs b/livekit-ffi/src/core_modules/data_stream/incoming.rs similarity index 100% rename from livekit-uniffi/src/data_stream/incoming.rs rename to livekit-ffi/src/core_modules/data_stream/incoming.rs diff --git a/livekit-uniffi/src/data_stream/mod.rs b/livekit-ffi/src/core_modules/data_stream/mod.rs similarity index 100% rename from livekit-uniffi/src/data_stream/mod.rs rename to livekit-ffi/src/core_modules/data_stream/mod.rs diff --git a/livekit-uniffi/src/data_stream/outgoing.rs b/livekit-ffi/src/core_modules/data_stream/outgoing.rs similarity index 100% rename from livekit-uniffi/src/data_stream/outgoing.rs rename to livekit-ffi/src/core_modules/data_stream/outgoing.rs diff --git a/livekit-uniffi/src/data_stream/polled.rs b/livekit-ffi/src/core_modules/data_stream/polled.rs similarity index 100% rename from livekit-uniffi/src/data_stream/polled.rs rename to livekit-ffi/src/core_modules/data_stream/polled.rs diff --git a/livekit-uniffi/src/data_stream/tests.rs b/livekit-ffi/src/core_modules/data_stream/tests.rs similarity index 100% rename from livekit-uniffi/src/data_stream/tests.rs rename to livekit-ffi/src/core_modules/data_stream/tests.rs diff --git a/livekit-uniffi/src/data_track/common.rs b/livekit-ffi/src/core_modules/data_track/common.rs similarity index 100% rename from livekit-uniffi/src/data_track/common.rs rename to livekit-ffi/src/core_modules/data_track/common.rs diff --git a/livekit-uniffi/src/data_track/local.rs b/livekit-ffi/src/core_modules/data_track/local.rs similarity index 100% rename from livekit-uniffi/src/data_track/local.rs rename to livekit-ffi/src/core_modules/data_track/local.rs diff --git a/livekit-uniffi/src/data_track/mod.rs b/livekit-ffi/src/core_modules/data_track/mod.rs similarity index 100% rename from livekit-uniffi/src/data_track/mod.rs rename to livekit-ffi/src/core_modules/data_track/mod.rs diff --git a/livekit-uniffi/src/data_track/remote.rs b/livekit-ffi/src/core_modules/data_track/remote.rs similarity index 100% rename from livekit-uniffi/src/data_track/remote.rs rename to livekit-ffi/src/core_modules/data_track/remote.rs diff --git a/livekit-uniffi/src/log_forward.rs b/livekit-ffi/src/core_modules/log_forward.rs similarity index 100% rename from livekit-uniffi/src/log_forward.rs rename to livekit-ffi/src/core_modules/log_forward.rs diff --git a/livekit-uniffi/src/lib.rs b/livekit-ffi/src/core_modules/mod.rs similarity index 62% rename from livekit-uniffi/src/lib.rs rename to livekit-ffi/src/core_modules/mod.rs index c93e40cdc..a0b7cf857 100644 --- a/livekit-uniffi/src/lib.rs +++ b/livekit-ffi/src/core_modules/mod.rs @@ -12,6 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! The UniFFI core surface. +//! +//! Business logic that client SDKs adopt incrementally over UniFFI: the Swift, +//! Android and Flutter SDKs already have their own WebRTC stack, so this +//! surface deliberately depends on none of `livekit`, `libwebrtc`, `soxr-sys` +//! or `imgproc` -- which is what lets it build for visionOS, tvOS and Mac +//! Catalyst. +//! +//! Gated behind the `core-modules` feature, which is mutually exclusive with +//! `room-apis`. + /// Data tracks core from [`livekit-datatrack`]. pub mod data_track; @@ -24,15 +35,12 @@ pub mod access_token; /// Forward log messages from Rust. pub mod log_forward; -/// Information about the build such as version. -pub mod build_info; - /// Shared exports and utilities. pub mod common; /// Global async runtime. pub mod runtime; +// Forces livekit-net's UniFFI scaffolding into the cdylib. Nothing in this +// crate names livekit-net by path, so without this its exports go missing. extern crate livekit_net; - -uniffi::setup_scaffolding!(); diff --git a/livekit-uniffi/src/runtime.rs b/livekit-ffi/src/core_modules/runtime.rs similarity index 100% rename from livekit-uniffi/src/runtime.rs rename to livekit-ffi/src/core_modules/runtime.rs diff --git a/livekit-ffi/src/lib.rs b/livekit-ffi/src/lib.rs index 68b400aa5..8c9612e83 100644 --- a/livekit-ffi/src/lib.rs +++ b/livekit-ffi/src/lib.rs @@ -12,43 +12,62 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::borrow::Cow; +// `room-apis` and `core-modules` are two distinct FFI surfaces that ship as +// separate artifacts to different SDKs, and each sets up its own UniFFI +// scaffolding below. Selecting both is always a build-configuration mistake. +#[cfg(all(feature = "room-apis", feature = "core-modules"))] +compile_error!( + "the `room-apis` and `core-modules` features are mutually exclusive. \ + Build livekit-ffi/core-modules with \ + `--no-default-features --features core-modules`." +); -use lazy_static::lazy_static; -use livekit::prelude::*; -use thiserror::Error; +// Both surfaces export `build_version` through the scaffolding below, which +// only exists when a surface is selected. Failing here beats failing inside +// the uniffi macro expansion. +#[cfg(not(any(feature = "room-apis", feature = "core-modules")))] +compile_error!( + "exactly one FFI surface must be selected: enable either `room-apis` \ + (the default) or `core-modules`." +); -mod conversion; +// Each surface owns everything specific to it, including its own types and +// statics; the globs republish it at the crate root. That keeps the public API +// (`livekit_ffi::{cabi, proto, server, FfiError, FFI_SERVER, ...}`) and the +// crate-internal `crate::` paths identical to before the modules existed. +#[cfg(feature = "room-apis")] mod migration; -#[cfg(test)] +#[cfg(all(test, feature = "room-apis"))] mod migration_tests; +#[cfg(feature = "room-apis")] +mod room_apis; +#[cfg(feature = "room-apis")] +pub use room_apis::*; + +// `conversion` is not part of the public API, so the glob above does not reach +// it. Re-export it for this crate, which keeps the `crate::conversion` paths +// that the surface used before it moved into `room_apis`. +#[cfg(feature = "room-apis")] +pub(crate) use room_apis::conversion; + +#[cfg(feature = "core-modules")] +mod core_modules; +#[cfg(feature = "core-modules")] +pub use core_modules::*; + +/// Information about the build such as version. +/// +/// Shared by both surfaces, and unconditional for that reason: a +/// room-apis-only build would otherwise ship a UniFFI component with no +/// exports at all (ffi-builds.yml greps the generated Python for it). pub mod build_info; -pub mod cabi; -pub mod proto; -pub mod server; - -uniffi::setup_scaffolding!(); - -#[derive(Error, Debug)] -pub enum FfiError { - #[error("the server is not configured")] - NotConfigured, - #[error("the server is already initialized")] - AlreadyInitialized, - #[error("room error {0}")] - Room(#[from] RoomError), - #[error("invalid request: {0}")] - InvalidRequest(Cow<'static, str>), -} - -/// # SAFTEY: The "C" callback must be threadsafe and not block -pub type FfiCallbackFn = unsafe extern "C" fn(*const u8, usize); -pub type FfiResult = Result; -pub type FfiHandleId = u64; - -pub const INVALID_HANDLE: FfiHandleId = 0; - -lazy_static! { - pub static ref FFI_SERVER: server::FfiServer = server::FfiServer::default(); -} + +// Must sit at the crate root: this defines `crate::UniFfiTag`, which every +// `#[uniffi::export]` in the surface modules resolves against. Only one call +// may exist per crate, which is why the two surfaces are mutually exclusive; +// they are kept separate so either can pin its own namespace later. +#[cfg(feature = "room-apis")] +uniffi::setup_scaffolding!("livekit_ffi"); +#[cfg(feature = "core-modules")] +uniffi::setup_scaffolding!("livekit_uniffi"); diff --git a/livekit-ffi/src/cabi.rs b/livekit-ffi/src/room_apis/cabi.rs similarity index 100% rename from livekit-ffi/src/cabi.rs rename to livekit-ffi/src/room_apis/cabi.rs diff --git a/livekit-ffi/src/conversion/audio_frame.rs b/livekit-ffi/src/room_apis/conversion/audio_frame.rs similarity index 100% rename from livekit-ffi/src/conversion/audio_frame.rs rename to livekit-ffi/src/room_apis/conversion/audio_frame.rs diff --git a/livekit-ffi/src/conversion/capture.rs b/livekit-ffi/src/room_apis/conversion/capture.rs similarity index 100% rename from livekit-ffi/src/conversion/capture.rs rename to livekit-ffi/src/room_apis/conversion/capture.rs diff --git a/livekit-ffi/src/conversion/data_stream.rs b/livekit-ffi/src/room_apis/conversion/data_stream.rs similarity index 100% rename from livekit-ffi/src/conversion/data_stream.rs rename to livekit-ffi/src/room_apis/conversion/data_stream.rs diff --git a/livekit-ffi/src/conversion/data_track.rs b/livekit-ffi/src/room_apis/conversion/data_track.rs similarity index 100% rename from livekit-ffi/src/conversion/data_track.rs rename to livekit-ffi/src/room_apis/conversion/data_track.rs diff --git a/livekit-ffi/src/conversion/mod.rs b/livekit-ffi/src/room_apis/conversion/mod.rs similarity index 100% rename from livekit-ffi/src/conversion/mod.rs rename to livekit-ffi/src/room_apis/conversion/mod.rs diff --git a/livekit-ffi/src/conversion/participant.rs b/livekit-ffi/src/room_apis/conversion/participant.rs similarity index 100% rename from livekit-ffi/src/conversion/participant.rs rename to livekit-ffi/src/room_apis/conversion/participant.rs diff --git a/livekit-ffi/src/conversion/resampler.rs b/livekit-ffi/src/room_apis/conversion/resampler.rs similarity index 100% rename from livekit-ffi/src/conversion/resampler.rs rename to livekit-ffi/src/room_apis/conversion/resampler.rs diff --git a/livekit-ffi/src/conversion/room.rs b/livekit-ffi/src/room_apis/conversion/room.rs similarity index 100% rename from livekit-ffi/src/conversion/room.rs rename to livekit-ffi/src/room_apis/conversion/room.rs diff --git a/livekit-ffi/src/conversion/stats.rs b/livekit-ffi/src/room_apis/conversion/stats.rs similarity index 100% rename from livekit-ffi/src/conversion/stats.rs rename to livekit-ffi/src/room_apis/conversion/stats.rs diff --git a/livekit-ffi/src/conversion/track.rs b/livekit-ffi/src/room_apis/conversion/track.rs similarity index 100% rename from livekit-ffi/src/conversion/track.rs rename to livekit-ffi/src/room_apis/conversion/track.rs diff --git a/livekit-ffi/src/conversion/video_frame.rs b/livekit-ffi/src/room_apis/conversion/video_frame.rs similarity index 100% rename from livekit-ffi/src/conversion/video_frame.rs rename to livekit-ffi/src/room_apis/conversion/video_frame.rs diff --git a/livekit-ffi/src/room_apis/mod.rs b/livekit-ffi/src/room_apis/mod.rs new file mode 100644 index 000000000..380f635c2 --- /dev/null +++ b/livekit-ffi/src/room_apis/mod.rs @@ -0,0 +1,58 @@ +// Copyright 2025 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! The protobuf / C-ABI room surface. +//! +//! This is the FFI interface used by the Python, Node and Unity SDKs, which +//! have no WebRTC stack of their own: requests arrive as encoded +//! [`proto::FfiRequest`] messages through [`cabi`] and are dispatched by +//! [`server`]. It depends on `livekit` and, through it, on libwebrtc. +//! +//! Gated behind the `room-apis` feature, which is mutually exclusive with +//! `core-modules`. + +use std::borrow::Cow; + +use lazy_static::lazy_static; +use livekit::prelude::*; +use thiserror::Error; + +pub(crate) mod conversion; + +pub mod cabi; +pub mod proto; +pub mod server; + +#[derive(Error, Debug)] +pub enum FfiError { + #[error("the server is not configured")] + NotConfigured, + #[error("the server is already initialized")] + AlreadyInitialized, + #[error("room error {0}")] + Room(#[from] RoomError), + #[error("invalid request: {0}")] + InvalidRequest(Cow<'static, str>), +} + +/// # SAFTEY: The "C" callback must be threadsafe and not block +pub type FfiCallbackFn = unsafe extern "C" fn(*const u8, usize); +pub type FfiResult = Result; +pub type FfiHandleId = u64; + +pub const INVALID_HANDLE: FfiHandleId = 0; + +lazy_static! { + pub static ref FFI_SERVER: server::FfiServer = server::FfiServer::default(); +} diff --git a/livekit-ffi/src/proto.rs b/livekit-ffi/src/room_apis/proto.rs similarity index 100% rename from livekit-ffi/src/proto.rs rename to livekit-ffi/src/room_apis/proto.rs diff --git a/livekit-ffi/src/server/audio_filter_tests.rs b/livekit-ffi/src/room_apis/server/audio_filter_tests.rs similarity index 100% rename from livekit-ffi/src/server/audio_filter_tests.rs rename to livekit-ffi/src/room_apis/server/audio_filter_tests.rs diff --git a/livekit-ffi/src/server/audio_plugin.rs b/livekit-ffi/src/room_apis/server/audio_plugin.rs similarity index 100% rename from livekit-ffi/src/server/audio_plugin.rs rename to livekit-ffi/src/room_apis/server/audio_plugin.rs diff --git a/livekit-ffi/src/server/audio_source.rs b/livekit-ffi/src/room_apis/server/audio_source.rs similarity index 100% rename from livekit-ffi/src/server/audio_source.rs rename to livekit-ffi/src/room_apis/server/audio_source.rs diff --git a/livekit-ffi/src/server/audio_stream.rs b/livekit-ffi/src/room_apis/server/audio_stream.rs similarity index 100% rename from livekit-ffi/src/server/audio_stream.rs rename to livekit-ffi/src/room_apis/server/audio_stream.rs diff --git a/livekit-ffi/src/server/capture.rs b/livekit-ffi/src/room_apis/server/capture.rs similarity index 100% rename from livekit-ffi/src/server/capture.rs rename to livekit-ffi/src/room_apis/server/capture.rs diff --git a/livekit-ffi/src/server/colorcvt/cvtimpl.rs b/livekit-ffi/src/room_apis/server/colorcvt/cvtimpl.rs similarity index 100% rename from livekit-ffi/src/server/colorcvt/cvtimpl.rs rename to livekit-ffi/src/room_apis/server/colorcvt/cvtimpl.rs diff --git a/livekit-ffi/src/server/colorcvt/mod.rs b/livekit-ffi/src/room_apis/server/colorcvt/mod.rs similarity index 100% rename from livekit-ffi/src/server/colorcvt/mod.rs rename to livekit-ffi/src/room_apis/server/colorcvt/mod.rs diff --git a/livekit-ffi/src/server/data_stream.rs b/livekit-ffi/src/room_apis/server/data_stream.rs similarity index 100% rename from livekit-ffi/src/server/data_stream.rs rename to livekit-ffi/src/room_apis/server/data_stream.rs diff --git a/livekit-ffi/src/server/data_track.rs b/livekit-ffi/src/room_apis/server/data_track.rs similarity index 100% rename from livekit-ffi/src/server/data_track.rs rename to livekit-ffi/src/room_apis/server/data_track.rs diff --git a/livekit-ffi/src/server/logger.rs b/livekit-ffi/src/room_apis/server/logger.rs similarity index 100% rename from livekit-ffi/src/server/logger.rs rename to livekit-ffi/src/room_apis/server/logger.rs diff --git a/livekit-ffi/src/server/mod.rs b/livekit-ffi/src/room_apis/server/mod.rs similarity index 100% rename from livekit-ffi/src/server/mod.rs rename to livekit-ffi/src/room_apis/server/mod.rs diff --git a/livekit-ffi/src/server/participant.rs b/livekit-ffi/src/room_apis/server/participant.rs similarity index 100% rename from livekit-ffi/src/server/participant.rs rename to livekit-ffi/src/room_apis/server/participant.rs diff --git a/livekit-ffi/src/server/platform_audio.rs b/livekit-ffi/src/room_apis/server/platform_audio.rs similarity index 100% rename from livekit-ffi/src/server/platform_audio.rs rename to livekit-ffi/src/room_apis/server/platform_audio.rs diff --git a/livekit-ffi/src/server/requests.rs b/livekit-ffi/src/room_apis/server/requests.rs similarity index 100% rename from livekit-ffi/src/server/requests.rs rename to livekit-ffi/src/room_apis/server/requests.rs diff --git a/livekit-ffi/src/server/resampler.rs b/livekit-ffi/src/room_apis/server/resampler.rs similarity index 100% rename from livekit-ffi/src/server/resampler.rs rename to livekit-ffi/src/room_apis/server/resampler.rs diff --git a/livekit-ffi/src/server/room.rs b/livekit-ffi/src/room_apis/server/room.rs similarity index 100% rename from livekit-ffi/src/server/room.rs rename to livekit-ffi/src/room_apis/server/room.rs diff --git a/livekit-ffi/src/server/tests.rs b/livekit-ffi/src/room_apis/server/tests.rs similarity index 100% rename from livekit-ffi/src/server/tests.rs rename to livekit-ffi/src/room_apis/server/tests.rs diff --git a/livekit-ffi/src/server/utils.rs b/livekit-ffi/src/room_apis/server/utils.rs similarity index 100% rename from livekit-ffi/src/server/utils.rs rename to livekit-ffi/src/room_apis/server/utils.rs diff --git a/livekit-ffi/src/server/video_source.rs b/livekit-ffi/src/room_apis/server/video_source.rs similarity index 100% rename from livekit-ffi/src/server/video_source.rs rename to livekit-ffi/src/room_apis/server/video_source.rs diff --git a/livekit-ffi/src/server/video_stream.rs b/livekit-ffi/src/room_apis/server/video_stream.rs similarity index 100% rename from livekit-ffi/src/server/video_stream.rs rename to livekit-ffi/src/room_apis/server/video_stream.rs diff --git a/livekit-uniffi/support/android/.gitignore b/livekit-ffi/support/android/.gitignore similarity index 100% rename from livekit-uniffi/support/android/.gitignore rename to livekit-ffi/support/android/.gitignore diff --git a/livekit-uniffi/support/android/README.md b/livekit-ffi/support/android/README.md similarity index 98% rename from livekit-uniffi/support/android/README.md rename to livekit-ffi/support/android/README.md index a747938d3..40bc37067 100644 --- a/livekit-uniffi/support/android/README.md +++ b/livekit-ffi/support/android/README.md @@ -1,4 +1,4 @@ -# LiveKit UniFFI — Android library +# LiveKit FFI — Android library Gradle Android library module that packages UniFFI Kotlin bindings and native `liblivekit_uniffi.so` binaries into an **AAR**. diff --git a/livekit-uniffi/support/android/build.gradle.kts b/livekit-ffi/support/android/build.gradle.kts similarity index 100% rename from livekit-uniffi/support/android/build.gradle.kts rename to livekit-ffi/support/android/build.gradle.kts diff --git a/livekit-uniffi/support/android/consumer-rules.pro b/livekit-ffi/support/android/consumer-rules.pro similarity index 100% rename from livekit-uniffi/support/android/consumer-rules.pro rename to livekit-ffi/support/android/consumer-rules.pro diff --git a/livekit-uniffi/support/android/gradle.properties b/livekit-ffi/support/android/gradle.properties similarity index 100% rename from livekit-uniffi/support/android/gradle.properties rename to livekit-ffi/support/android/gradle.properties diff --git a/livekit-uniffi/support/android/gradle/wrapper/gradle-wrapper.jar b/livekit-ffi/support/android/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from livekit-uniffi/support/android/gradle/wrapper/gradle-wrapper.jar rename to livekit-ffi/support/android/gradle/wrapper/gradle-wrapper.jar diff --git a/livekit-uniffi/support/android/gradle/wrapper/gradle-wrapper.properties b/livekit-ffi/support/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from livekit-uniffi/support/android/gradle/wrapper/gradle-wrapper.properties rename to livekit-ffi/support/android/gradle/wrapper/gradle-wrapper.properties diff --git a/livekit-uniffi/support/android/gradlew b/livekit-ffi/support/android/gradlew similarity index 100% rename from livekit-uniffi/support/android/gradlew rename to livekit-ffi/support/android/gradlew diff --git a/livekit-uniffi/support/android/gradlew.bat b/livekit-ffi/support/android/gradlew.bat similarity index 100% rename from livekit-uniffi/support/android/gradlew.bat rename to livekit-ffi/support/android/gradlew.bat diff --git a/livekit-uniffi/support/android/proguard-rules.pro b/livekit-ffi/support/android/proguard-rules.pro similarity index 100% rename from livekit-uniffi/support/android/proguard-rules.pro rename to livekit-ffi/support/android/proguard-rules.pro diff --git a/livekit-uniffi/support/android/settings.gradle.kts b/livekit-ffi/support/android/settings.gradle.kts similarity index 100% rename from livekit-uniffi/support/android/settings.gradle.kts rename to livekit-ffi/support/android/settings.gradle.kts diff --git a/livekit-uniffi/support/android/src/main/AndroidManifest.xml b/livekit-ffi/support/android/src/main/AndroidManifest.xml similarity index 100% rename from livekit-uniffi/support/android/src/main/AndroidManifest.xml rename to livekit-ffi/support/android/src/main/AndroidManifest.xml diff --git a/livekit-uniffi/support/android/src/main/jniLibs/arm64-v8a/.gitkeep b/livekit-ffi/support/android/src/main/jniLibs/arm64-v8a/.gitkeep similarity index 100% rename from livekit-uniffi/support/android/src/main/jniLibs/arm64-v8a/.gitkeep rename to livekit-ffi/support/android/src/main/jniLibs/arm64-v8a/.gitkeep diff --git a/livekit-uniffi/support/android/src/main/jniLibs/armeabi-v7a/.gitkeep b/livekit-ffi/support/android/src/main/jniLibs/armeabi-v7a/.gitkeep similarity index 100% rename from livekit-uniffi/support/android/src/main/jniLibs/armeabi-v7a/.gitkeep rename to livekit-ffi/support/android/src/main/jniLibs/armeabi-v7a/.gitkeep diff --git a/livekit-uniffi/support/android/src/main/jniLibs/x86_64/.gitkeep b/livekit-ffi/support/android/src/main/jniLibs/x86_64/.gitkeep similarity index 100% rename from livekit-uniffi/support/android/src/main/jniLibs/x86_64/.gitkeep rename to livekit-ffi/support/android/src/main/jniLibs/x86_64/.gitkeep diff --git a/livekit-uniffi/support/dart/README.md b/livekit-ffi/support/dart/README.md similarity index 100% rename from livekit-uniffi/support/dart/README.md rename to livekit-ffi/support/dart/README.md diff --git a/livekit-uniffi/support/dart/analysis_options.yaml b/livekit-ffi/support/dart/analysis_options.yaml similarity index 100% rename from livekit-uniffi/support/dart/analysis_options.yaml rename to livekit-ffi/support/dart/analysis_options.yaml diff --git a/livekit-uniffi/support/dart/hook/build.dart.tera b/livekit-ffi/support/dart/hook/build.dart.tera similarity index 98% rename from livekit-uniffi/support/dart/hook/build.dart.tera rename to livekit-ffi/support/dart/hook/build.dart.tera index b8c04cedb..684a24b01 100644 --- a/livekit-uniffi/support/dart/hook/build.dart.tera +++ b/livekit-ffi/support/dart/hook/build.dart.tera @@ -1,7 +1,7 @@ // Native Assets build hook for the LiveKit UniFFI Dart bindings. // // The generated bindings call the native library through `@Native` annotations -// bound to the asset id `package:/uniffi:livekit_uniffi`. This hook +// bound to the asset id `package:/uniffi:livekit_ffi`. This hook // resolves the dynamic library for the target and registers that code asset. // // Two modes, chosen automatically: @@ -21,7 +21,7 @@ import 'package:hooks/hooks.dart'; import 'package:http/http.dart' as http; // Matches `cdylib_name` in uniffi.toml and the asset name in the generated Dart. -const _cdylibName = 'livekit_uniffi'; +const _cdylibName = 'livekit_ffi'; const _version = '{{ CARGO_MAKE_CRATE_VERSION }}'; // Per-release directory holding the `build-.zip` assets. const _downloadBase = '{{ CDYLIB_DOWNLOAD_BASE_URL }}'; diff --git a/livekit-uniffi/support/dart/pubspec.yaml.tera b/livekit-ffi/support/dart/pubspec.yaml.tera similarity index 100% rename from livekit-uniffi/support/dart/pubspec.yaml.tera rename to livekit-ffi/support/dart/pubspec.yaml.tera diff --git a/livekit-uniffi/support/dart/test/access_token_test.dart b/livekit-ffi/support/dart/test/access_token_test.dart similarity index 100% rename from livekit-uniffi/support/dart/test/access_token_test.dart rename to livekit-ffi/support/dart/test/access_token_test.dart diff --git a/livekit-uniffi/support/dart/test/build_version_test.dart b/livekit-ffi/support/dart/test/build_version_test.dart similarity index 100% rename from livekit-uniffi/support/dart/test/build_version_test.dart rename to livekit-ffi/support/dart/test/build_version_test.dart diff --git a/livekit-uniffi/support/swift/DEBUGGING.md b/livekit-ffi/support/swift/DEBUGGING.md similarity index 100% rename from livekit-uniffi/support/swift/DEBUGGING.md rename to livekit-ffi/support/swift/DEBUGGING.md diff --git a/livekit-uniffi/support/swift/LiveKitUniFFI.podspec.tera b/livekit-ffi/support/swift/LiveKitUniFFI.podspec.tera similarity index 100% rename from livekit-uniffi/support/swift/LiveKitUniFFI.podspec.tera rename to livekit-ffi/support/swift/LiveKitUniFFI.podspec.tera diff --git a/livekit-uniffi/support/swift/Package.swift.tera b/livekit-ffi/support/swift/Package.swift.tera similarity index 100% rename from livekit-uniffi/support/swift/Package.swift.tera rename to livekit-ffi/support/swift/Package.swift.tera diff --git a/livekit-uniffi/support/swift/Package@swift-6.2.swift.tera b/livekit-ffi/support/swift/Package@swift-6.2.swift.tera similarity index 100% rename from livekit-uniffi/support/swift/Package@swift-6.2.swift.tera rename to livekit-ffi/support/swift/Package@swift-6.2.swift.tera diff --git a/livekit-uniffi/support/swift/PrivacyInfo.xcprivacy b/livekit-ffi/support/swift/PrivacyInfo.xcprivacy similarity index 100% rename from livekit-uniffi/support/swift/PrivacyInfo.xcprivacy rename to livekit-ffi/support/swift/PrivacyInfo.xcprivacy diff --git a/livekit-uniffi/support/swift/README.md b/livekit-ffi/support/swift/README.md similarity index 100% rename from livekit-uniffi/support/swift/README.md rename to livekit-ffi/support/swift/README.md diff --git a/livekit-uniffi/uniffi.toml b/livekit-ffi/uniffi.toml similarity index 90% rename from livekit-uniffi/uniffi.toml rename to livekit-ffi/uniffi.toml index 5e2941caf..e0e2d2081 100644 --- a/livekit-uniffi/uniffi.toml +++ b/livekit-ffi/uniffi.toml @@ -4,7 +4,7 @@ ffi_module_name = "RustLiveKitUniFFI" [bindings.kotlin] android = true package_name = "io.livekit.uniffi" -cdylib_name = "livekit_uniffi" # the name of the so file to be loaded +cdylib_name = "livekit_ffi" # the name of the so file to be loaded # The Kotlin checksum test is off because it fails on ARM devices. # @@ -53,6 +53,8 @@ omit_checksums = true [bindings.dart] # Dart package name; must match the published pub package so the Native Assets -# asset id resolves to `package:livekit_uniffi/uniffi:livekit_uniffi`. +# asset id resolves to `package:livekit_uniffi/uniffi:livekit_ffi`. The package +# name is unchanged by the livekit-uniffi merge; only the library the crate +# builds is now liblivekit_ffi. package_name = "livekit_uniffi" -cdylib_name = "livekit_uniffi" \ No newline at end of file +cdylib_name = "livekit_ffi" \ No newline at end of file diff --git a/livekit-net/Cargo.toml b/livekit-net/Cargo.toml index 6852a18f0..1ceaae5fe 100644 --- a/livekit-net/Cargo.toml +++ b/livekit-net/Cargo.toml @@ -51,7 +51,7 @@ mode = "powerset" async-trait = "0.1" # Optional: scaffolding for the `uniffi` feature. `scaffolding-ffi-buffer-fns` -# matches livekit-datatrack/livekit-uniffi so the ABI is consistent when these +# matches livekit-datatrack/livekit-ffi so the ABI is consistent when these # crates are combined into one cdylib. No runtime feature: the exported traits # are `with_foreign` (host-driven futures) and the exported setters are sync, so # the crate stays runtime-agnostic. diff --git a/livekit-net/uniffi.toml b/livekit-net/uniffi.toml index 9031b7333..ca5a2801e 100644 --- a/livekit-net/uniffi.toml +++ b/livekit-net/uniffi.toml @@ -1,10 +1,10 @@ [bindings.kotlin] # The Kotlin checksum test is off because it fails on ARM devices. For more info, see the similar -# comment in livekit-uniffi/uniffi.toml. +# comment in livekit-ffi/uniffi.toml. omit_checksums = true # Kotlin only, so the Rust source and the Swift/Python/Node bindings keep the original name. Lives -# here rather than in livekit-uniffi/uniffi.toml because uniffi applies a rename table only to +# here rather than in livekit-ffi/uniffi.toml because uniffi applies a rename table only to # items from the crate that owns it. [bindings.kotlin.rename] "WsConnection.close" = "close_connection" diff --git a/livekit-rpc/AGENTS.md b/livekit-rpc/AGENTS.md index 6d2bb2d7a..a1d190b59 100644 --- a/livekit-rpc/AGENTS.md +++ b/livekit-rpc/AGENTS.md @@ -9,7 +9,7 @@ - Enables fast unit testing without linking `libwebrtc` - Enables shared implementation amongst multiple _consumers_: - [`livekit`](../livekit/): Rust client SDK - - [`livekit-uniffi`](../livekit-uniffi/): will eventually power downstream client SDKs such as Swift and Kotlin + - [`livekit-ffi`](../livekit-ffi/) (`core-modules` feature): will eventually power downstream client SDKs such as Swift and Kotlin ## Client vs. server split diff --git a/livekit-uniffi/.gitignore b/livekit-uniffi/.gitignore deleted file mode 100644 index 507686d02..000000000 --- a/livekit-uniffi/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -generated/ -packages/ \ No newline at end of file diff --git a/livekit-uniffi/CHANGELOG.md b/livekit-uniffi/CHANGELOG.md deleted file mode 100644 index 70cc6dbe1..000000000 --- a/livekit-uniffi/CHANGELOG.md +++ /dev/null @@ -1,245 +0,0 @@ -## 0.3.0 (2026-10-12) - -### Breaking Changes - -#### `VideoGrants` gains the `agent` grant and `Claims` the `kind` claim (with - -`AccessToken::with_kind`), which the Go, Python and JS SDKs already carry. An -agent worker's token is `VideoGrants { agent: true }` and a simulated job's -participant token is `kind: "agent"`; neither could be minted from Rust before. -`livekit-uniffi` exposes both: `TokenOptions.kind`, `Claims.kind`, and `agent` -on its `VideoGrants` record. - -**Breaking:** the four grants the server infers when absent -- `can_publish`, -`can_subscribe`, `can_publish_data`, `can_update_own_metadata` -- are now -`Option`, as in the Go and JS SDKs. `None` leaves the decision to the -server, and the new getters (`can_publish()`, `can_subscribe()`, -`can_publish_data()`, `can_update_own_metadata()`) read a token the way the -server does, `can_publish_data` falling back to `can_publish` included. Code -that set these fields writes `Some(..)`; code that read them uses the getters. -The same fields are optional on the `livekit-uniffi` record, and -`livekit-api` re-exports the crate as `livekit_api::access_token`, so both -carry the change. - -Nothing at its default is written into the token any more: unset claims and -grants are omitted, as the server's own `omitempty` grants are. Verification -of existing tokens is unchanged. - -### Fixes - -- In-flight incoming data streams now error with `AbnormalEnd` when the room closes. Before, the stream readers ended cleanly, so `read_all()` returned truncated content as if it were complete. - -## 0.2.2 (2026-10-07) - -### Fixes - -- Increase the local and remote data track event buffers so a burst of track lifecycle events cannot fill the channel and deadlock room disconnect. - -## 0.2.1 (2026-09-24) - -### Fixes - -- Format the generated Swift bindings with swiftformat when building the Swift package. -- Upgrade to prost 0.14 across the whole project - #1447 (@1egoman) - -## 0.2.0 (2026-09-22) - -### Breaking Changes - -#### `VideoGrants` gains the `agent` grant and `Claims` the `kind` claim (with - -`AccessToken::with_kind`), which the Go, Python and JS SDKs already carry. An -agent worker's token is `VideoGrants { agent: true }` and a simulated job's -participant token is `kind: "agent"`; neither could be minted from Rust before. -`livekit-uniffi` exposes both: `TokenOptions.kind`, `Claims.kind`, and `agent` -on its `VideoGrants` record. - -**Breaking:** the four grants the server infers when absent -- `can_publish`, -`can_subscribe`, `can_publish_data`, `can_update_own_metadata` -- are now -`Option`, as in the Go and JS SDKs. `None` leaves the decision to the -server, and the new getters (`can_publish()`, `can_subscribe()`, -`can_publish_data()`, `can_update_own_metadata()`) read a token the way the -server does, `can_publish_data` falling back to `can_publish` included. Code -that set these fields writes `Some(..)`; code that read them uses the getters. -The same fields are optional on the `livekit-uniffi` record, and -`livekit-api` re-exports the crate as `livekit_api::access_token`, so both -carry the change. - -Nothing at its default is written into the token any more: unset claims and -grants are omitted, as the server's own `omitempty` grants are. Verification -of existing tokens is unchanged. - -### Features - -#### `EncryptionError::Failed` and `DecryptionError::Failed` carry a `reason` string and are no longer `flat_error`, - -so a foreign `EncryptionProvider` or `DecryptionProvider` returning an error no longer aborts the process with -"Can't lift flat errors" -- a failed data track decrypt (no E2EE manager, key mismatch, corrupt frame) now -drops the frame and leaves the room connected. - -### Fixes - -- Cleanup unused dependencies -- Fix reliable data channel replay: keep the full retry buffer across resumes, drop duplicate reliable packets, and ignore replayed chunks on uncompressed streams instead of failing with `MissedChunk`. - -#### Report the reconnect reason to the server when resuming. - -Resumes previously sent no reason, so server-side telemetry could not attribute why Rust -clients reconnect — every resume looked like `RR_UNKNOWN`. The engine now records what caused -the episode (signal disconnected, publisher failed, subscriber failed) and reports it on each -resume attempt. The v0 signalling path was also missing the `reconnect_reason` query parameter -entirely, so it would not have been reported even if a reason had been supplied. - -## 0.1.12 (2026-09-10) - -### Fixes - -#### Allow code_assets 2.x in the Dart package and stop re-running its build hook - -The `livekit_uniffi` Dart package capped `code_assets` below 2.0.0, which has since -shipped. Its only breaking change (equality on `OS` and `Architecture`) does not -affect the hook, and the cap would make the package unresolvable next to any -dependency that already requires 2.x. The constraint now allows it, and the hook -was verified against `code_assets 2.0.0` / `hooks 2.2.0`. - -The hook also registered the downloaded library as a dependency. Dependencies are -inputs, so the hooks runner saw a file modified during the build and re-ran the -hook, and the download, once on every fresh build. The registration is removed. - -The hook also wrote every target's library to the same shared path. A universal -macOS build runs the hook once per architecture and then merges the results with -`lipo`, which failed because the second download had overwritten the first. Each -target now gets its own subdirectory. - -## 0.1.11 (2026-09-09) - -### Fixes - -- Re-baseline the iOS and Android binary size budgets, which had fallen behind the shipping binaries and blocked the 0.1.10 release. - -#### Moves the RPC implementation into a new `livekit-rpc` crate, alongside the existing - -`livekit-data-stream` and `livekit-datatrack` crates. - -**Breaking:** the `livekit::rpc` module is gone. The RPC types it held are unchanged and -still re-exported from `livekit::participant` and the prelude, so most code needs no edit; -code that spelled the module out (`use livekit::rpc::RpcError;`) should import from -`livekit::participant` or the prelude instead. `RpcClientManager`, `RpcServerManager` and -`HandleRequestOptions` remain reachable under `livekit::participant` but are now -`#[doc(hidden)]`: they are internal SDK API and were never usable without the (private) -transport trait. - -Within the `livekit` crate itself, RPC types are now imported from `livekit-rpc` directly -rather than through those re-exports. - -The new crate does not depend on `libwebrtc`, so its unit tests run without building WebRTC. -The transport seam that made this possible was already in place; the only change to it is -that `RpcTransport::publish_data` now returns a message-only `RpcTransportError` instead of -`livekit::RoomError`, mirroring `livekit_data_stream::api::SendError`. - -Also fixes four latent bugs found while moving the code: - -- An RPC call to a participant who disconnects mid-call now fails promptly with - `RecipientDisconnected`. Pending calls were never purged on disconnect, so the caller - waited out its full response timeout (15s by default) and got `ResponseTimeout` instead. -- A server reporting a version that is not valid semver no longer panics the calling task. - An unparseable version is no longer treated as evidence that the server is too old. -- A v1 `RpcResponse` carrying a compressed payload, or no value at all, now fails with an - `ApplicationError` instead of resolving the caller with an empty successful response. -- Removed an unguarded `unwrap` when building a v1 response packet, by giving the function - a signature that cannot represent the invalid state. - -Also drops the `semver` dependency from `livekit`, which was only used by the RPC client. - -## 0.1.10 (2026-09-08) - -### Features - -- Removes livekit-runtime and converts this package to be tokio only again - #1375 (@1egoman) - -### Fixes - -- Add data streams v2 to exposed uniffi interface - #1286 (@1egoman) -- Lower the Android UniFFI AAR minSdk from 24 to 21 -- Add the `PASSTHROUGH` encoding preset and remove the unused `UpdateEgressRequest` from the generated protocol -- Add `self_test_http_get` / `self_test_ws_echo` / `has_http_client` / `has_ws_client` UniFFI exports so foreign hosts can exercise the transport seam end-to-end. -- Attach Dart/Flutter cdylib assets to releases and prepare livekit_uniffi for pub.dev publishing -- Update Android's JNA dependency version to 5.19.1 to support 16KB page sizes - -## 0.1.9 (2026-08-25) - -### Fixes - -- differentiate signal connection errors correctly from timeouts - #1234 (@lukasIO) -- Expose `RemoteDataTrack.set_pipeline_options` and `RemoteDataTrackPipelineOptions` (`max_partial_frames`) over UniFFI, matching the JS and Rust SDKs -- Register the `Bytes` UniFFI custom type once in `livekit-common` and borrow it from each component with `uniffi::use_remote_type!`, so the converter is emitted once and the post-generation Swift workaround is no longer needed - -#### Moves access-token generation and verification into a new `livekit-token` crate. - -`livekit_api::access_token::*` continues to resolve to the same types via a -re-export, so no consumer changes are needed. - -Also fixes the `services-tokio` and `services-async` features, which used the -access-token types without declaring the `access-token` feature. Building with -`--no-default-features --features services-tokio` previously failed to compile. - -## 0.1.8 (2026-08-03) - -### Fixes - -- Add a unified `EgressClient::start_egress` that calls the v2 `Egress.StartEgress` RPC with a `StartEgressRequest`, alongside the existing per-type helpers. -- Fix the Android AAR build. Two cargo-make bugs kept `cargo make --profile release android-package` from ever producing a release artifact: the per-arch tasks' `env = { TARGET = ... }` replaced (rather than merged) the parent env map, dropping the `--release` flag, and the `TARGET` they set leaked into the Kotlin bindgen's host build, which then cross-compiled with the host linker. Also raise the Swift and Android size budgets to match the binaries as they stand since the data-track UniFFI surface landed, and check out the released tag rather than the dispatch ref when building the wrapper packages. - -## 0.1.7 (2026-07-29) - -### Fixes - -- Caching of tokio backend reqwest http client - #1285 (@MaxHeimbrock) -- Add data streams v2 - #1192 (@1egoman) - -## 0.1.6 (2026-07-27) - -### Fixes - -- Data tracks schema metadata support. -- Gate the uniffi `cli` feature (clap + bindgen backends) behind an opt-in feature so it is no longer compiled into shipped library builds, shrinking the static archive by ~43 MiB. The dynamic library is byte-unchanged. - #1275 (@jhugman) - -#### Route LiveKit signalling through a pluggable transport (new `livekit-net` crate). - -The signalling WebSocket and the two pre-connect HTTP GETs (validate, region discovery) now go through pluggable transport traits (`WsClient` for the WebSocket, `HttpClient` for request/response) resolved from a process-global registry with independent slots — a consumer can bring only HTTP, or only WebSocket. The new `livekit-net` crate owns the WebSocket/HTTP/TLS stack behind those traits and ships native (tokio / async-std) backends. Native builds are unchanged in behavior. - -**Breaking (`livekit-api`, and `livekit` via `EngineError::Signal`):** - -- `SignalError::WsError` is removed — `tungstenite` is no longer part of the public API. A failed WebSocket handshake now surfaces its HTTP status as `SignalError::Client`/`Server`; transport connection and close failures surface as the new `SignalError::Connection(String)` / `SignalError::Closed` variants (previously all collapsed into `Timeout`). -- `SignalError` is now `#[non_exhaustive]`, and gains a `SignalError::TransportNotConfigured` variant — returned when no transport is registered (host/foreign builds must call `livekit_net::set_ws_client` / `set_http_client` before connecting). This is a permanent configuration error; callers must not retry. -- The signalling WebSocket/HTTP/TLS crates are no longer transitive dependencies of `livekit-api`; TLS features delegate to `livekit-net`. Existing `signal-client-tokio` / `-async` / `-dispatcher` and TLS feature names are unchanged. - -## 0.1.5 (2026-07-14) - -### Features - -- Expose data tracks core functionality - -## 0.1.4 (2026-07-09) - -### Features - -- Add a Dart bindings target. Bumps the crate's UniFFI dependency from 0.30 to 0.31 to match the bindgen. - -## 0.1.3 (2026-06-24) - -### Fixes - -- harden reconnect behaviour - #1148 (@lukasIO) - -## 0.1.2 (2026-06-23) - -### Fixes - -- Upgrade protocol to v1.48.0 - -## 0.1.1 (2026-05-29) - -### Fixes - -- bump protocol to v1.46.4 - #1121 (@lukasIO) diff --git a/livekit-uniffi/Cargo.toml b/livekit-uniffi/Cargo.toml deleted file mode 100644 index d8d946070..000000000 --- a/livekit-uniffi/Cargo.toml +++ /dev/null @@ -1,73 +0,0 @@ -[lib] -crate-type = ["cdylib", "staticlib", "lib"] - -[package] -name = "livekit-uniffi" -version = "0.3.0" -edition.workspace = true -license.workspace = true -description = "Experimental FFI interface using UniFFI" -repository.workspace = true -readme = "README.md" -publish = false - -[dependencies] -livekit-protocol = { workspace = true } -livekit-common = { workspace = true, features = ["uniffi"] } -livekit-token = { workspace = true } -livekit-datatrack = { workspace = true, features = ["uniffi"] } -livekit-net = { workspace = true, features = ["uniffi"] } -livekit-data-stream = { workspace = true } -uniffi = { workspace = true, features = ["scaffolding-ffi-buffer-fns", "tokio"] } -log = { workspace = true } -tokio = { workspace = true, features = ["sync", "rt-multi-thread"] } -tokio-util = "0.7.18" -prost = { workspace = true } -futures-util = { workspace = true, default-features = false, features = ["sink"] } -bytes = { workspace = true } -once_cell = "1.21.3" -thiserror = { workspace = true } -async-trait = "0.1" -# Dart binding generator. Not published to crates.io, so pinned by git rev. The -# rev must target the same uniffi-rs release (0.31) as the `uniffi` dependency -# above, or it cannot read this crate's compiled metadata. -# -# Pinned to upstream main rather than a release: the newest release -# (v0.2.1+v0.31.2) predates the three fixes this crate needs -- custom types -# leaking into FFI signatures (#154), object converter statics colliding with -# Rust methods named `write` (#156), and each crate re-declaring the runtime -# scaffolding, which kept livekit-datatrack's types from crossing into -# livekit-uniffi (#150). Move to a version requirement once a release carries -# them. -uniffi-dart = { git = "https://github.com/Uniffi-Dart/uniffi-dart", rev = "67547701d849c9ef6302f79bb2189bb6075f69fa", optional = true } -camino = { version = "1", optional = true } - -[features] -# Enables the `uniffi-bindgen` bin (uniffi's CLI: clap + the bindings backends). -# Tooling-only — kept off library builds so the shipped cdylib/staticlib don't -# carry the bindgen machinery. Enable it when running the bin: -# `cargo run --features cli --bin uniffi-bindgen generate ...`. -cli = ["uniffi/cli"] -# Enables the `uniffi-bindgen-dart` bin. Not enabled for library builds. -dart-bindgen = ["dep:uniffi-dart", "dep:camino"] - -[build-dependencies] -uniffi = { workspace = true, features = ["build", "scaffolding-ffi-buffer-fns"] } - -[[bin]] -name = "uniffi-bindgen" -path = "bindgen.rs" -required-features = ["cli"] - -[[bin]] -name = "uniffi-bindgen-dart" -path = "bindgen-dart.rs" -required-features = ["dart-bindgen"] - -[profile.release] -opt-level = "z" -lto = true -codegen-units = 1 -panic = "abort" -strip = "symbols" -debug = false diff --git a/livekit-uniffi/README.md b/livekit-uniffi/README.md deleted file mode 100644 index 361a0ab15..000000000 --- a/livekit-uniffi/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# LiveKit UniFFI - -Experimental FFI interface using [UniFFI](https://mozilla.github.io/uniffi-rs/latest/). - -At this stage in development, this interface will not attempt to replace the existing FFI interface defined in [_livekit-ffi_](../livekit-ffi/). Instead, it will focus on exposing core business logic that can be cleanly modularized and adopted by client SDKs incrementally. - -## Functionality exposed - -- [x] Logging -- [x] Access token generation and verification - -## Tasks - -Binding generation and multi-platform builds are handled by [_cargo-make_](https://github.com/sagiegurari/cargo-make)—please install on your system before proceeding. For a full list of available tasks, see [_Makefile.toml_](./Makefile.toml) or run `cargo make --list-all-steps`. The most important tasks are summarized below: - -### Swift - -Generate Swift bindings and build a multi-platform XCFramework: -``` -cargo make swift-package -``` - -For a fast, debuggable variant (macOS-only, unstripped), run `cargo make swift-package-debug` — see [support/swift/DEBUGGING.md](./support/swift/DEBUGGING.md). - -See [support/swift/README.md](./support/swift/README.md) for debugging vs. release modes, consumer integration, and prerequisites (Xcode, Rust Apple targets). - -### Node - -Generate Node bindings: -``` -cargo make node-package -``` - -To test them out, run `cd node_test && npx tsx index.ts` - -### Android - -Build native libraries, Kotlin bindings, and a release AAR: - -``` -cargo make android-package # debug .so in release AAR -cargo make --profile release android-package # release .so (CI / publishing) -cargo make android-package-local # + publish to Maven Local -``` - -See [support/android/README.md](./support/android/README.md) for prerequisites (Android SDK/NDK). diff --git a/livekit-uniffi/bindgen.rs b/livekit-uniffi/bindgen.rs deleted file mode 100644 index 7eb9d1172..000000000 --- a/livekit-uniffi/bindgen.rs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2025 LiveKit, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// This binary is included to allow invoking the UniFFI bindgen CLI from Cargo: -/// `cargo run --bin uniffi-bindgen generate ...` -fn main() { - uniffi::uniffi_bindgen_main() -} diff --git a/livekit-uniffi/node_test/.gitignore b/livekit-uniffi/node_test/.gitignore deleted file mode 100644 index 3c3629e64..000000000 --- a/livekit-uniffi/node_test/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/livekit-uniffi/node_test/index.ts b/livekit-uniffi/node_test/index.ts deleted file mode 100644 index fa973dd67..000000000 --- a/livekit-uniffi/node_test/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2025 LiveKit, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - buildVersion, - logForwardBootstrap, - type ApiCredentials, - tokenGenerate, - tokenVerify, - logForwardReceive, -} from '@livekit/uniffi'; - -async function main() { - // Receive log messages from Rust - logForwardBootstrap("debug"); - - // Print FFI version - console.log(`FFI version: v${buildVersion()}`); - - const credentials: ApiCredentials = { key: "devkey", secret: "secret" }; - - const jwt = tokenGenerate( - { - identity: "some_participant", - roomConfiguration: { - name: "test", - emptyTimeout: 1000, - departureTimeout: 1000, - maxParticipants: 1000, - metadata: "", - minPlayoutDelay: 1000, - maxPlayoutDelay: 1000, - syncStreams: false, - agents: [], - }, - }, - credentials, - ); - console.log("Generated JWT:", jwt); - - const decodedGrants = tokenVerify(jwt, credentials); - console.log("Verified generated JWT:", decodedGrants); - - while (true) { - const message = await logForwardReceive(); - if (!message) { - console.log('Log forwarding ended'); - break; - } - console.log('Log from Rust:', message); - } -} - -if (require.main === module) { - main(); -} diff --git a/livekit-uniffi/node_test/package.json b/livekit-uniffi/node_test/package.json deleted file mode 100644 index bceb4c37b..000000000 --- a/livekit-uniffi/node_test/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "node_test", - "version": "1.0.0", - "main": "index.ts", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "description": "", - "dependencies": { - "@livekit/uniffi": "link:../packages/node" - }, - "devDependencies": { - "@types/node": "^25.0.1", - "tsx": "^4.21.0", - "typescript": "^5.9.3" - } -} diff --git a/livekit-uniffi/node_test/pnpm-lock.yaml b/livekit-uniffi/node_test/pnpm-lock.yaml deleted file mode 100644 index 7e9f6e1cc..000000000 --- a/livekit-uniffi/node_test/pnpm-lock.yaml +++ /dev/null @@ -1,333 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@livekit/uniffi': - specifier: link:../packages/node - version: link:../packages/node - devDependencies: - '@types/node': - specifier: ^25.0.1 - version: 25.9.3 - tsx: - specifier: ^4.21.0 - version: 4.22.4 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - -packages: - - '@esbuild/aix-ppc64@0.28.1': - resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.28.1': - resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.28.1': - resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.28.1': - resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.28.1': - resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.28.1': - resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.28.1': - resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.28.1': - resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.28.1': - resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.28.1': - resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.28.1': - resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.28.1': - resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.28.1': - resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.28.1': - resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.28.1': - resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.28.1': - resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.28.1': - resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.28.1': - resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.28.1': - resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.28.1': - resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.28.1': - resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.28.1': - resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.28.1': - resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.28.1': - resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.28.1': - resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.28.1': - resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@types/node@25.9.3': - resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} - - esbuild@0.28.1: - resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} - engines: {node: '>=18'} - hasBin: true - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - tsx@4.22.4: - resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} - engines: {node: '>=18.0.0'} - hasBin: true - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@7.24.6: - resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} - -snapshots: - - '@esbuild/aix-ppc64@0.28.1': - optional: true - - '@esbuild/android-arm64@0.28.1': - optional: true - - '@esbuild/android-arm@0.28.1': - optional: true - - '@esbuild/android-x64@0.28.1': - optional: true - - '@esbuild/darwin-arm64@0.28.1': - optional: true - - '@esbuild/darwin-x64@0.28.1': - optional: true - - '@esbuild/freebsd-arm64@0.28.1': - optional: true - - '@esbuild/freebsd-x64@0.28.1': - optional: true - - '@esbuild/linux-arm64@0.28.1': - optional: true - - '@esbuild/linux-arm@0.28.1': - optional: true - - '@esbuild/linux-ia32@0.28.1': - optional: true - - '@esbuild/linux-loong64@0.28.1': - optional: true - - '@esbuild/linux-mips64el@0.28.1': - optional: true - - '@esbuild/linux-ppc64@0.28.1': - optional: true - - '@esbuild/linux-riscv64@0.28.1': - optional: true - - '@esbuild/linux-s390x@0.28.1': - optional: true - - '@esbuild/linux-x64@0.28.1': - optional: true - - '@esbuild/netbsd-arm64@0.28.1': - optional: true - - '@esbuild/netbsd-x64@0.28.1': - optional: true - - '@esbuild/openbsd-arm64@0.28.1': - optional: true - - '@esbuild/openbsd-x64@0.28.1': - optional: true - - '@esbuild/openharmony-arm64@0.28.1': - optional: true - - '@esbuild/sunos-x64@0.28.1': - optional: true - - '@esbuild/win32-arm64@0.28.1': - optional: true - - '@esbuild/win32-ia32@0.28.1': - optional: true - - '@esbuild/win32-x64@0.28.1': - optional: true - - '@types/node@25.9.3': - dependencies: - undici-types: 7.24.6 - - esbuild@0.28.1: - optionalDependencies: - '@esbuild/aix-ppc64': 0.28.1 - '@esbuild/android-arm': 0.28.1 - '@esbuild/android-arm64': 0.28.1 - '@esbuild/android-x64': 0.28.1 - '@esbuild/darwin-arm64': 0.28.1 - '@esbuild/darwin-x64': 0.28.1 - '@esbuild/freebsd-arm64': 0.28.1 - '@esbuild/freebsd-x64': 0.28.1 - '@esbuild/linux-arm': 0.28.1 - '@esbuild/linux-arm64': 0.28.1 - '@esbuild/linux-ia32': 0.28.1 - '@esbuild/linux-loong64': 0.28.1 - '@esbuild/linux-mips64el': 0.28.1 - '@esbuild/linux-ppc64': 0.28.1 - '@esbuild/linux-riscv64': 0.28.1 - '@esbuild/linux-s390x': 0.28.1 - '@esbuild/linux-x64': 0.28.1 - '@esbuild/netbsd-arm64': 0.28.1 - '@esbuild/netbsd-x64': 0.28.1 - '@esbuild/openbsd-arm64': 0.28.1 - '@esbuild/openbsd-x64': 0.28.1 - '@esbuild/openharmony-arm64': 0.28.1 - '@esbuild/sunos-x64': 0.28.1 - '@esbuild/win32-arm64': 0.28.1 - '@esbuild/win32-ia32': 0.28.1 - '@esbuild/win32-x64': 0.28.1 - - fsevents@2.3.3: - optional: true - - tsx@4.22.4: - dependencies: - esbuild: 0.28.1 - optionalDependencies: - fsevents: 2.3.3 - - typescript@5.9.3: {} - - undici-types@7.24.6: {} diff --git a/livekit-uniffi/python_test/main.py b/livekit-uniffi/python_test/main.py deleted file mode 100644 index 17ede299a..000000000 --- a/livekit-uniffi/python_test/main.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2025 LiveKit, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import sys -import os - -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'generated', 'python'))) - -import asyncio -from livekit_uniffi import * - -def main(): - # Receive log messages from Rust - log_forward_bootstrap(level=LogForwardFilter.DEBUG) - - # Print FFI version - print(f"FFI version: v{build_version()}") - - credentials = ApiCredentials(key="devkey", secret="secret") - - jwt = token_generate( - options=TokenOptions(room_name="test", identity="some_participant"), - credentials=credentials, - ) - print(f"Generated JWT: {jwt}") - - decoded_grants = token_verify( - token=jwt, - credentials=credentials, - ) - print(f"Verified generated JWT: {decoded_grants}") - - async def receive_log_messages(): - while True: - message = await log_forward_receive() - if message is None: - print("Log forwarding ended") - break - print(f"Log from Rust: {message}") - - asyncio.run(receive_log_messages()) - -if __name__ == "__main__": - main() diff --git a/livekit-uniffi/src/build_info.rs b/livekit-uniffi/src/build_info.rs deleted file mode 100644 index 9ce3697e2..000000000 --- a/livekit-uniffi/src/build_info.rs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2025 LiveKit, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Returns the version specified in the crate's Cargo.toml. -#[uniffi::export] -pub fn build_version() -> String { - env!("CARGO_PKG_VERSION").to_string() -} diff --git a/livekit-uniffi/support/node/download-lib.ts.tera b/livekit-uniffi/support/node/download-lib.ts.tera deleted file mode 100644 index e725a7cfb..000000000 --- a/livekit-uniffi/support/node/download-lib.ts.tera +++ /dev/null @@ -1,127 +0,0 @@ -// This file is automatically generated. Do not edit manually. - -import * as fs from 'node:fs/promises'; -import { join, resolve } from 'node:path'; -import { parseArgs, type ParseArgsOptionsConfig } from 'node:util'; -import { Open } from 'unzipper'; -import fetch from 'node-fetch'; - -const PACKAGE_NAME = "{{ CARGO_MAKE_CRATE_FS_NAME }}"; -const PACKAGE_VERSION = "{{ CARGO_MAKE_CRATE_VERSION }}"; -const DOWNLOAD_BASE_URL = "{{ CDYLIB_DOWNLOAD_BASE_URL }}"; -const OUTPUT_DIR_PATH = resolve("dist/"); - -type TargetPlatform = ReturnType; -function targetPlatform() { - // ref: https://nodejs.org/api/process.html#processplatform - switch (process.platform) { - case 'win32': - return 'pc-windows-msvc'; - case 'darwin': - return 'apple-darwin'; - case 'linux': - return 'unknown-linux-gnu'; - default: - throw new Error(`Platform ${process.platform} is unsupported.`); - } -} - -type TargetArch = ReturnType; - -function targetArch() { - // ref: https://nodejs.org/api/process.html#processarch - switch (process.arch) { - case "arm": - throw new Error(`Arch arm is unsupported. Build for arm64 / aarch64 instead!`); - case 'arm64': - return "aarch64"; - case 'x64': - return "x86_64"; - default: - throw new Error(`Arch ${process.arch} is unsupported.`); - } -} - -function generateLibraryName(platform: TargetPlatform) { - switch (platform) { - case "pc-windows-msvc": - return `${PACKAGE_NAME}.dll`; - case "apple-darwin": - return `lib${PACKAGE_NAME}.dylib`; - case "unknown-linux-gnu": - return `lib${PACKAGE_NAME}.so`; - } -} - -function generateLibraryDownloadUrl( - platform: TargetPlatform, - arch: TargetArch, - version: string, -) { - // ie, https://github.com/livekit/rust-sdks/releases/download/v0.1.1/build-aarch64-apple-darwin.zip - const filename = `build-${arch}-${platform}.zip`; - return `${DOWNLOAD_BASE_URL}/v${version}/${filename}`; -} - -async function doesFileExist(path: string) { - try { - await fs.access(path, fs.constants.F_OK); - return true; - } catch { - return false; - } -} - -async function downloadLib( - platform: TargetPlatform = targetPlatform(), - arch: TargetArch = targetArch(), - version = PACKAGE_VERSION, - output = OUTPUT_DIR_PATH, - skipIfExists = false, -) { - const libName = generateLibraryName(platform); - const libPath = join(output, libName); - - if (skipIfExists && await doesFileExist(libPath)) { - console.log("Lib file exists, skipping download."); - return; - } - const downloadUrl = generateLibraryDownloadUrl(platform, arch, version); - - const response = await fetch(downloadUrl); - if (!response.ok) { - throw new Error( - `Error fetching ${downloadUrl}: ${response.status} ${await response.text()}`, - ); - } - if (!response.body) { - throw new Error("Response body was null"); - } - - const zipFile = await Open.buffer(Buffer.from(await response.arrayBuffer())); - await zipFile.extract({ path: output }); - - if (!(await doesFileExist(libPath))) { - throw new Error( - `Lib file ${libPath} not found after downloading ${downloadUrl} and unzipping the results!`, - ); - } -} - -if (require.main === module && process.env.SKIP_DOWNLOAD_LIB !== 'true') { - const options: ParseArgsOptionsConfig = { - platform: { type: 'string', default: targetPlatform() }, - arch: { type: 'string', default: targetArch() }, - version: { type: 'string', default: PACKAGE_VERSION }, - output: { type: 'string', default: OUTPUT_DIR_PATH }, - "skip-if-exists": { type: 'boolean', short: 's', default: false }, - }; - const { values } = parseArgs({ args: process.argv.slice(2), options }); - downloadLib( - values.platform, - values.arch, - values.version, - values.output, - values["skip-if-exists"], - ); -} diff --git a/livekit-uniffi/support/node/package.json.tera b/livekit-uniffi/support/node/package.json.tera deleted file mode 100644 index 79f31dea3..000000000 --- a/livekit-uniffi/support/node/package.json.tera +++ /dev/null @@ -1,45 +0,0 @@ -{%- if CARGO_MAKE_PROFILE == "development" -%} - {%- set postinstall = "" -%} - {%- set tsup_args = " --onSuccess 'cp -P src/" ~ LIB_NAME ~ "." ~ LIB_EXT ~ " dist/'" -%} -{%- elif CARGO_MAKE_PROFILE == "release" -%} - {%- set postinstall = "tsx download-lib.ts --skip" -%} - {%- set tsup_args = "" -%} -{%- endif -%} - -{ - "name": "{{ NPM_NAME }}", - "version": "{{ CARGO_MAKE_CRATE_VERSION }}", - "main": "dist/index.mjs", - "types": "dist/index.d.ts", - "exports": { - "import": "./dist/index.mjs", - "require": "./dist/index.js" - }, - "files": ["dist"], - "scripts": { - "prepublishOnly": "npm run build", - "build": "tsup{{tsup_args}}", - "dev": "tsup --watch{{tsup_args}}", - "ts:check": "tsc --noEmit" - {%- if postinstall -%}, - "postinstall": "{{postinstall}}" - {%- endif -%} - }, - "engines": { - "node": ">= 18" - }, - - {#- Include dependencies specified by the bindgen -#} - "dependencies": {{ dependencies | json_encode | safe }}, - "devDependencies": { - {% for package_name, package_version in devDependencies %} - "{{package_name | addslashes}}": "{{package_version | addslashes}}", - {%- endfor %} - "tsup": "^8.4.0", - "typescript": "5.9.3", - "node-fetch": "3.3.2", - "unzipper": "0.10.11", - "@types/unzipper": "0.10.11", - "tsx": "4.21.0" - } -} diff --git a/livekit-uniffi/support/node/tsconfig.json b/livekit-uniffi/support/node/tsconfig.json deleted file mode 100644 index 04172bd5f..000000000 --- a/livekit-uniffi/support/node/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, - "module": "ES2020" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, - "outDir": "dist", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "strict": true /* Enable all strict type-checking options. */, - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, - "skipLibCheck": true /* Skip type checking of declaration files. */, - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, - "moduleResolution": "node" - } -} diff --git a/livekit-uniffi/support/node/tsup.config.ts b/livekit-uniffi/support/node/tsup.config.ts deleted file mode 100644 index 8de8bc46c..000000000 --- a/livekit-uniffi/support/node/tsup.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from 'tsup'; - -export default defineConfig({ - entry: ['src/index.ts'], - format: ['cjs', 'esm'], - target: 'node18', - dts: true, - clean: true, - - // Inline ubrn into the built bundle since it doesn't publish a commonjs compatible build - noExternal: ["uniffi-bindgen-react-native"], - - // ref: https://stackoverflow.com/a/75868407 - shims: true, -}); diff --git a/tools/bindgens/Cargo.toml b/tools/bindgens/Cargo.toml index 592a91066..4821bd9bc 100644 --- a/tools/bindgens/Cargo.toml +++ b/tools/bindgens/Cargo.toml @@ -8,8 +8,30 @@ description = "Foreign-binding generator CLIs for the LiveKit Rust crates" publish = false # Tooling crate: each bin is a code-generation CLI shared across the workspace's -# uniffi-exposing crates (livekit-ffi, livekit-uniffi, ...). It has no library -# target — the bindgens read metadata from an already-compiled cdylib via +# uniffi-exposing crates (livekit-ffi, ...). It has no library target beyond +# these bins — they read metadata from an already-compiled cdylib via # `--library`, so they never depend on the crates they generate for. [dependencies] uniffi = { workspace = true, features = ["cli"] } +# Dart binding generator. Not published to crates.io, so pinned by git rev. The +# rev must target the same uniffi-rs release (0.31) as the `uniffi` dependency +# above, or it cannot read the compiled metadata. +# +# Pinned to upstream main rather than a release: the newest release +# (v0.2.1+v0.31.2) predates the three fixes this needs -- custom types leaking +# into FFI signatures (#154), object converter statics colliding with Rust +# methods named `write` (#156), and each crate re-declaring the runtime +# scaffolding, which kept livekit-datatrack's types from crossing over (#150). +# Move to a version requirement once a release carries them. +uniffi-dart = { git = "https://github.com/Uniffi-Dart/uniffi-dart", rev = "67547701d849c9ef6302f79bb2189bb6075f69fa", optional = true } +camino = { version = "1", optional = true } + +[features] +# Enables the `uniffi-bindgen-dart` bin. Off by default so a plain +# `cargo build --workspace` does not fetch or compile the git dependency. +dart-bindgen = ["dep:uniffi-dart", "dep:camino"] + +[[bin]] +name = "uniffi-bindgen-dart" +path = "src/bin/uniffi-bindgen-dart.rs" +required-features = ["dart-bindgen"] diff --git a/livekit-uniffi/bindgen-dart.rs b/tools/bindgens/src/bin/uniffi-bindgen-dart.rs similarity index 100% rename from livekit-uniffi/bindgen-dart.rs rename to tools/bindgens/src/bin/uniffi-bindgen-dart.rs