Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 54 additions & 11 deletions .github/workflows/build-rtc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,13 @@
with:
submodules: true

- uses: actions/setup-python@v4

- name: Install cibuildwheel
if: runner.os != 'macOS'
run: python3 -m pip install cibuildwheel==2.17.0

- name: Install cibuildwheel on macOS
if: runner.os == 'macOS'
run: python3 -m pip install --break-system-packages cibuildwheel==2.17.0
- uses: actions/setup-python@v5
id: setup-python
with:
python-version: "3.11"

- name: Build wheels
run: python3 -m cibuildwheel --output-dir dist
run: pipx run --python '${{ steps.setup-python.outputs.python-path }}' cibuildwheel==3.3.1 --output-dir dist
env:
CIBW_ARCHS: ${{ matrix.archs }}

Expand Down Expand Up @@ -120,9 +115,57 @@
name: rtc-release-sdist
path: livekit-rtc/dist/*.tar.gz


test:
name: Test (${{ matrix.os }}, Python ${{ matrix.python-version }})
needs: [build_wheels]
strategy:
fail-fast: false
matrix:
include:
# Linux x86_64 tests
- os: ubuntu-latest
python-version: "3.9"
artifact: rtc-release-ubuntu-latest
- os: ubuntu-latest
python-version: "3.10"
artifact: rtc-release-ubuntu-latest
- os: ubuntu-latest
python-version: "3.11"
artifact: rtc-release-ubuntu-latest
- os: ubuntu-latest
python-version: "3.12"
artifact: rtc-release-ubuntu-latest
- os: ubuntu-latest
python-version: "3.13"
artifact: rtc-release-ubuntu-latest
# macOS tests (arm64 runner)
- os: macos-latest
python-version: "3.9"
artifact: rtc-release-macos-latest
- os: macos-latest
python-version: "3.12"
artifact: rtc-release-macos-latest
# Windows tests
- os: windows-latest
python-version: "3.9"
artifact: rtc-release-windows-latest
- os: windows-latest
python-version: "3.12"
artifact: rtc-release-windows-latest
uses: ./.github/workflows/tests.yml
with:
os: ${{ matrix.os }}
python-version: ${{ matrix.python-version }}
artifact-name: ${{ matrix.artifact }}
secrets:
LIVEKIT_URL: ${{ secrets.LIVEKIT_URL }}
LIVEKIT_API_KEY: ${{ secrets.LIVEKIT_API_KEY }}
LIVEKIT_API_SECRET: ${{ secrets.LIVEKIT_API_SECRET }}

publish:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {}
name: Publish RTC release
needs: [build_wheels, make_sdist]
needs: [build_wheels, make_sdist, test]
runs-on: ubuntu-latest
permissions:
id-token: write
Expand Down
137 changes: 116 additions & 21 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,45 +1,140 @@
name: Tests

on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
workflow_call:
inputs:
os:
description: "Runner OS (e.g., ubuntu-latest, macos-latest, windows-latest)"
required: true
type: string
python-version:
description: "Python version to test"
required: true
type: string
artifact-name:
description: "Name of the wheel artifact to download"
required: true
type: string
run-id:
description: "Workflow run ID to download artifacts from (optional, uses current run if not specified)"
required: false
type: string
secrets:
LIVEKIT_URL:
required: true
LIVEKIT_API_KEY:
required: true
LIVEKIT_API_SECRET:
required: true

jobs:
tests:
name: Run tests
runs-on: ubuntu-latest
test:
name: Test (${{ inputs.os }}, Python ${{ inputs.python-version }})
runs-on: ${{ inputs.os }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
submodules: true
lfs: true
- name: Install uv

- uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python-version }}

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"

- name: Install the project
run: uv sync --all-extras --dev
- name: Download livekit-rtc wheel (current run)
if: ${{ inputs.run-id == '' }}
uses: actions/download-artifact@v4
with:
name: ${{ inputs.artifact-name }}
path: rtc-wheel

- uses: actions/setup-python@v6
- name: Download livekit-rtc wheel (from specific run)
if: ${{ inputs.run-id != '' }}
uses: actions/download-artifact@v4
with:
python-version: '3.13'
name: ${{ inputs.artifact-name }}
path: rtc-wheel
run-id: ${{ inputs.run-id }}
github-token: ${{ github.token }}

- name: Select compatible wheel (macOS)
if: runner.os == 'macOS'
id: select-wheel-macos
run: |
# macOS artifacts contain both x86_64 and arm64 wheels, select the right one
WHEEL=$(python3 -c "
import glob
import platform
import sys

wheels = glob.glob('rtc-wheel/*.whl')
machine = platform.machine().lower()

arch_map = {
'x86_64': ['x86_64'],
'arm64': ['arm64'],
}
patterns = arch_map.get(machine, [machine])

for wheel in wheels:
wheel_lower = wheel.lower()
if any(p in wheel_lower for p in patterns):
print(wheel)
sys.exit(0)

print(f'No matching wheel found for {machine}', file=sys.stderr)
sys.exit(1)
")
echo "wheel=$WHEEL" >> $GITHUB_OUTPUT

- name: Run tests
- name: Create venv and install dependencies (Unix)
if: runner.os == 'Linux'
run: |
uv venv .test-venv
source .test-venv/bin/activate
uv pip install rtc-wheel/*.whl ./livekit-api ./livekit-protocol
uv pip install pytest pytest-asyncio numpy matplotlib

- name: Create venv and install dependencies (macOS)
if: runner.os == 'macOS'
run: |
uv venv .test-venv
source .test-venv/bin/activate
uv pip install "${{ steps.select-wheel-macos.outputs.wheel }}"
uv pip install ./livekit-api ./livekit-protocol
uv pip install pytest pytest-asyncio numpy matplotlib

- name: Create venv and install dependencies (Windows)
if: runner.os == 'Windows'
run: |
uv venv .test-venv
$wheel = (Get-ChildItem rtc-wheel\*.whl)[0].FullName
uv pip install --python .test-venv $wheel .\livekit-api .\livekit-protocol
uv pip install --python .test-venv pytest pytest-asyncio numpy matplotlib
shell: pwsh

- name: Run tests (Unix)
if: runner.os != 'Windows'
env:
LIVEKIT_URL: ${{ secrets.LIVEKIT_URL }}
LIVEKIT_API_KEY: ${{ secrets.LIVEKIT_API_KEY }}
LIVEKIT_API_SECRET: ${{ secrets.LIVEKIT_API_SECRET }}
run: |

uv run python ./livekit-rtc/rust-sdks/download_ffi.py --output livekit-rtc/livekit/rtc/resources
uv add ./livekit-rtc ./livekit-api ./livekit-protocol
uv run pytest . --ignore=livekit-rtc/rust-sdks
source .test-venv/bin/activate
pytest tests/

- name: Run tests (Windows)
if: runner.os == 'Windows'
env:
LIVEKIT_URL: ${{ secrets.LIVEKIT_URL }}
LIVEKIT_API_KEY: ${{ secrets.LIVEKIT_API_KEY }}
LIVEKIT_API_SECRET: ${{ secrets.LIVEKIT_API_SECRET }}
run: .test-venv\Scripts\python.exe -m pytest tests/
shell: pwsh

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium test

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
74 changes: 72 additions & 2 deletions livekit-rtc/hatch_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,81 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Custom build hook for platform-specific wheel tagging.

This hook generates py3-none-{platform} wheels because the native FFI libraries
(.so/.dylib/.dll) don't use the Python C API - they're loaded via ctypes at
runtime. This makes them compatible with any Python 3.x version.

Why not use sysconfig.get_platform()?
- On macOS, it returns the Python interpreter's compile-time deployment target,
not the MACOSX_DEPLOYMENT_TARGET from the environment that cibuildwheel sets.

Why not let hatchling infer the tag?
- hatchling doesn't recognize bundled .so/.dylib/.dll as platform-specific
unless we explicitly set pure_python=False and provide the tag.
"""

import os
import platform
import sys

from hatchling.builders.hooks.plugin.interface import BuildHookInterface


class CustomBuildHook(BuildHookInterface):
def initialize(self, version, build_data):
"""Force platform-specific wheel due to native libraries"""
build_data["pure_python"] = False
build_data["infer_tag"] = True
build_data["infer_tag"] = False
build_data["tag"] = f"py3-none-{self._get_platform_tag()}"

def _get_platform_tag(self):
"""Get the wheel platform tag for the current/target platform."""
if sys.platform == "darwin":
return self._get_macos_tag()
elif sys.platform == "linux":
# Return linux tag; cibuildwheel's auditwheel converts to manylinux
return f"linux_{platform.machine()}"
elif sys.platform == "win32":
return f"win_{self._normalize_arch(platform.machine())}"
else:
return f"{platform.system().lower()}_{platform.machine()}"

def _get_macos_tag(self):
"""Build macOS platform tag respecting cross-compilation settings.

cibuildwheel sets MACOSX_DEPLOYMENT_TARGET and ARCHFLAGS when building.
We must use these rather than the host machine's values.
"""
target = os.environ.get("MACOSX_DEPLOYMENT_TARGET")
if not target:
# Fall back to current macOS version (for local dev builds)
target = platform.mac_ver()[0]
parts = target.split(".")
target = f"{parts[0]}.{parts[1] if len(parts) > 1 else '0'}"

version_tag = target.replace(".", "_")
arch = self._get_target_arch()
return f"macosx_{version_tag}_{arch}"

def _get_target_arch(self):
"""Detect target architecture, respecting ARCHFLAGS for cross-compilation.

cibuildwheel sets ARCHFLAGS="-arch arm64" or "-arch x86_64" when
cross-compiling on macOS.
"""
archflags = os.environ.get("ARCHFLAGS", "")
if "-arch arm64" in archflags:
return "arm64"
if "-arch x86_64" in archflags:
return "x86_64"
return self._normalize_arch(platform.machine())

def _normalize_arch(self, arch):
"""Normalize architecture names to wheel tag format."""
return {
"AMD64": "amd64",
"x86_64": "x86_64",
"arm64": "arm64",
"aarch64": "aarch64",
}.get(arch, arch.lower())
21 changes: 11 additions & 10 deletions livekit-rtc/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,16 @@ include = ["/livekit", "/rust-sdks"]

[tool.cibuildwheel]
build = "cp39-*"
skip = "*-musllinux_*" # not supported (libwebrtc is using glibc)

skip = "*-musllinux_*" # not supported (libwebrtc requires glibc)
before-build = "pip install requests && python rust-sdks/download_ffi.py --output livekit/rtc/resources"
# Note: manylinux_2_28 is the default in cibuildwheel 3.x, no explicit config needed

# macOS deployment targets must match the FFI binaries (see rust-sdks/.github/workflows/ffi-builds.yml)
# x86_64 supports macOS 10.15+, arm64 requires macOS 11.0+
[[tool.cibuildwheel.overrides]]
select = "*macosx_x86_64"
environment = { MACOSX_DEPLOYMENT_TARGET = "10.15" }

manylinux-x86_64-image = "manylinux_2_28"
manylinux-i686-image = "manylinux_2_28"
manylinux-aarch64-image = "manylinux_2_28"
manylinux-ppc64le-image = "manylinux_2_28"
manylinux-s390x-image = "manylinux_2_28"
manylinux-pypy_x86_64-image = "manylinux_2_28"
manylinux-pypy_i686-image = "manylinux_2_28"
manylinux-pypy_aarch64-image = "manylinux_2_28"
[[tool.cibuildwheel.overrides]]
select = "*macosx_arm64"
environment = { MACOSX_DEPLOYMENT_TARGET = "11.0" }
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ convention = "google"


[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
addopts = ["--import-mode=importlib", "--ignore=examples"]
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
10 changes: 6 additions & 4 deletions livekit-rtc/tests/test_apm.py → tests/rtc/test_apm.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@

from livekit.rtc import AudioProcessingModule, AudioFrame

# Test fixture directory
FIXTURES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures")


def test_audio_processing():
sample_rate = 48000
num_channels = 1
frames_per_chunk = sample_rate // 100

current_dir = os.path.dirname(os.path.abspath(__file__))
capture_wav = os.path.join(current_dir, "test_echo_capture.wav")
render_wav = os.path.join(current_dir, "test_echo_render.wav")
output_wav = os.path.join(current_dir, "test_processed.wav")
capture_wav = os.path.join(FIXTURES_DIR, "test_echo_capture.wav")
render_wav = os.path.join(FIXTURES_DIR, "test_echo_render.wav")
output_wav = os.path.join(FIXTURES_DIR, "test_processed.wav")

# Initialize APM with echo cancellation enabled
apm = AudioProcessingModule(
Expand Down
File renamed without changes.
File renamed without changes.
Loading
Loading