Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4026257
refactor(livekit-ffi): move the FFI surface into a room_apis module
1egoman Sep 17, 2026
ddb1583
refactor(livekit-ffi): move the crate-root FFI types into room_apis
1egoman Sep 17, 2026
6b83f68
IMPORTANT(livekit-ffi): gate the existing surface behind a room-apis …
1egoman Sep 17, 2026
89d1f56
IMPORTANT(livekit-ffi): skip the libwebrtc build steps without room-apis
1egoman Sep 17, 2026
7b930a1
chore(bindgens): move the Dart bindgen CLI into tools/bindgens
1egoman Sep 17, 2026
8605f2e
refactor(livekit-ffi): move the livekit-uniffi sources into a core_mo…
1egoman Sep 17, 2026
dc8aa81
IMPORTANT(livekit-ffi): add the core-modules feature
1egoman Sep 17, 2026
ba5eb68
IMPORTANT(livekit-ffi): bind the core modules to prost 0.12
1egoman Sep 17, 2026
9a01d68
chore(livekit-ffi): move the livekit-uniffi packaging assets in
1egoman Sep 17, 2026
1561786
chore(livekit-ffi): drop the UniFFI node build
1egoman Sep 17, 2026
991b2b5
chore: remove the livekit-uniffi crate
1egoman Sep 17, 2026
9590c13
feat(ci): declare which feature set each platform builds
1egoman Sep 17, 2026
38c71fe
IMPORTANT(livekit-ffi): build core-modules from the cargo-make packaging
1egoman Sep 17, 2026
867ce51
IMPORTANT(livekit-ffi): follow the cdylib rename in the bindings config
1egoman Sep 17, 2026
94a7ef9
ci: build livekit-ffi/core-modules in the uniffi workflows
1egoman Sep 17, 2026
cbf9b78
IMPORTANT(ci): create the release tag with the repo-writer App
1egoman Sep 17, 2026
20066b6
IMPORTANT(ci): trigger the UniFFI packages from the tag push
1egoman Sep 17, 2026
4f0337c
ci: retarget the Dart publish at livekit-ffi/v*
1egoman Sep 17, 2026
bf05dff
test(ci): keep core-modules compiled and tested per commit
1egoman Sep 17, 2026
c4ae74f
ci: narrow the gen-node-proto trigger
1egoman Sep 17, 2026
441a1aa
docs(livekit-ffi): rewrite the crate README for the two surfaces
1egoman Sep 17, 2026
c72aaf4
docs: update the repo docs and add the merge changeset
1egoman Sep 17, 2026
0b4aa9a
fix(livekit-ffi): make every platform-features entry self-contained
1egoman Sep 18, 2026
d83416d
fix(livekit-ffi): keep crate::conversion resolving after the move
1egoman Sep 18, 2026
99b9379
feat(livekit-ffi): resolve build features per platform in the packaging
1egoman Sep 18, 2026
e3d654e
refactor(ci): state the core-modules flags in the per-commit gates
1egoman Sep 18, 2026
cded005
refactor(ci): remove --surface from platform_features.py
1egoman Sep 18, 2026
c854feb
chore(livekit-ffi): remove the orphaned python test harness
1egoman Sep 18, 2026
99f5e06
fix: adjust changeset to make ci check pass
1egoman Sep 18, 2026
be33b2e
refactor: drop useless comments
1egoman Sep 18, 2026
5da03fe
fix(changeset): cover the remaining dependent crates
1egoman Sep 18, 2026
3aff285
fix(ci): run the core-modules build steps under bash
1egoman Sep 18, 2026
088b773
feat: upgrade to prost 0.14 across the whole project
1egoman Sep 21, 2026
84736d8
docs(livekit-ffi): drop the stale note about a split prost
1egoman Sep 22, 2026
95faa05
docs(livekit-ffi): say why core-modules excludes the WebRTC stack
1egoman Sep 22, 2026
d6cde30
chore(ci): correct the copyright year on the feature resolver
1egoman Sep 22, 2026
64c6dac
refactor(ci): rename platform_features.py to ffi_features.py
1egoman Sep 22, 2026
912b8e8
refactor(livekit-ffi): group the room-apis build steps into a module
1egoman Sep 22, 2026
206699f
fix: port to use pub.dev.workflow_dispatch ci trigger
1egoman Oct 19, 2026
44426a2
fix(changeset): retarget the livekit-uniffi bumps at livekit-ffi
1egoman Oct 19, 2026
273783b
docs(livekit-common): name the renamed Swift framework in the comment
1egoman Oct 19, 2026
bb49119
docs(livekit-ffi): title the Android README after the merged crate
1egoman Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .changeset/merge_uniffi_into_ffi.md
Original file line number Diff line number Diff line change
@@ -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
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
---

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`.
1 change: 0 additions & 1 deletion .changeset/protocol_thread_schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .changeset/uniffi_swift_dsyms.md
Original file line number Diff line number Diff line change
@@ -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).
4 changes: 2 additions & 2 deletions .github/scripts/changeset_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down
119 changes: 119 additions & 0 deletions .github/scripts/ffi_features.py
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
1egoman marked this conversation as resolved.

`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()
27 changes: 27 additions & 0 deletions .github/workflows/builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions .github/workflows/feature-combinations-curated.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
71 changes: 67 additions & 4 deletions .github/workflows/ffi-builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' }}
Expand Down Expand Up @@ -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 }}
Expand All @@ -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/<tag>`, 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
8 changes: 4 additions & 4 deletions .github/workflows/gen-node-proto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
Loading
Loading